mirror of
https://github.com/zadam/trilium.git
synced 2026-02-05 14:09:18 +01:00
Compare commits
284 Commits
standalone
...
renovate/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
715f42b6c3 | ||
|
|
199bfc8a37 | ||
|
|
e82ae762f0 | ||
|
|
f90cc9aff7 | ||
|
|
9a2b7fbda1 | ||
|
|
e0b4ebed93 | ||
|
|
ac3b289c9e | ||
|
|
62534e0e93 | ||
|
|
b802c3174c | ||
|
|
aee1a6e1f0 | ||
|
|
46f1cd38e0 | ||
|
|
7f891ef523 | ||
|
|
06af5e15cd | ||
|
|
af89a0a883 | ||
|
|
fa72eb2edb | ||
|
|
c1ea94423b | ||
|
|
1e70d066bd | ||
|
|
ab89f16e7c | ||
|
|
8c848a4cb5 | ||
|
|
e021a54d2d | ||
|
|
70523574b0 | ||
|
|
66e0f1ab19 | ||
|
|
671a05470e | ||
|
|
99eec0c41e | ||
|
|
48d06dcb06 | ||
|
|
e38df0c731 | ||
|
|
0f3f49915e | ||
|
|
416144265b | ||
|
|
fe02871e91 | ||
|
|
bb05aeeaf7 | ||
|
|
846358ccb0 | ||
|
|
dabc779727 | ||
|
|
08fd2ec64b | ||
|
|
c42c06d048 | ||
|
|
e951d60800 | ||
|
|
1c260f5890 | ||
|
|
177aedeaae | ||
|
|
3f6f3d2565 | ||
|
|
38489dbfeb | ||
|
|
ff2a3d6a28 | ||
|
|
2c74697fb1 | ||
|
|
110e9200b9 | ||
|
|
6e792f9735 | ||
|
|
51d8b13a81 | ||
|
|
a05691fd07 | ||
|
|
d25e7915d9 | ||
|
|
1a025dfef3 | ||
|
|
84cc4194aa | ||
|
|
331a56277c | ||
|
|
bc2915adb9 | ||
|
|
703fe9a71b | ||
|
|
6c50664046 | ||
|
|
673cbc97e1 | ||
|
|
3e83766099 | ||
|
|
b453589077 | ||
|
|
654fa18ab1 | ||
|
|
7b0d91534c | ||
|
|
8d4801bb6f | ||
|
|
9e36f4f625 | ||
|
|
d83a824812 | ||
|
|
ca128f2fa9 | ||
|
|
c02642d0f9 | ||
|
|
7340709111 | ||
|
|
56d0383372 | ||
|
|
49d33ea19a | ||
|
|
979fa0359a | ||
|
|
c7381d058a | ||
|
|
5db298f031 | ||
|
|
171d948a00 | ||
|
|
facd56cdf4 | ||
|
|
ba17ec4be4 | ||
|
|
6c163b5479 | ||
|
|
79649805b8 | ||
|
|
220ca8a570 | ||
|
|
348c00f86d | ||
|
|
12b641b522 | ||
|
|
6855bc1de6 | ||
|
|
a9c5b99ae8 | ||
|
|
76f36e2fd3 | ||
|
|
afe710321c | ||
|
|
0d444daaca | ||
|
|
c8a0c9fd23 | ||
|
|
79f07ae923 | ||
|
|
c77e7a568b | ||
|
|
ff9ec2057b | ||
|
|
6a313b99e4 | ||
|
|
8a92370042 | ||
|
|
411a59ec54 | ||
|
|
2d4022044d | ||
|
|
bbc5ebd76b | ||
|
|
e9c90fcde8 | ||
|
|
911f78867f | ||
|
|
5507cc5abc | ||
|
|
0e5aa401ef | ||
|
|
d48473ab87 | ||
|
|
734efaf40c | ||
|
|
f89718d88a | ||
|
|
aa4942a0da | ||
|
|
90bb162a88 | ||
|
|
0382a4b30e | ||
|
|
490d940cd1 | ||
|
|
b090eb9359 | ||
|
|
cb9e67ce84 | ||
|
|
c4d131dd23 | ||
|
|
2667f266bf | ||
|
|
8258936d6c | ||
|
|
e4e7449078 | ||
|
|
11b020e859 | ||
|
|
72d6b83ec5 | ||
|
|
fbe5152cb3 | ||
|
|
6bef01f755 | ||
|
|
f0d4b4a6d9 | ||
|
|
a54fe62643 | ||
|
|
eb4bbd49fb | ||
|
|
ab4f1bd4f4 | ||
|
|
f8b414c354 | ||
|
|
82a21624c3 | ||
|
|
1b212ac720 | ||
|
|
c36ce3ea14 | ||
|
|
841fab77a8 | ||
|
|
fd6f910824 | ||
|
|
ce9ca1917d | ||
|
|
c702fb273c | ||
|
|
4c72d8691b | ||
|
|
35ac5fc514 | ||
|
|
92991cc03c | ||
|
|
76492475e3 | ||
|
|
e2363d860c | ||
|
|
c06a90913a | ||
|
|
e88c0f7326 | ||
|
|
2a4280b5bf | ||
|
|
d3c733c57f | ||
|
|
f91add3cd4 | ||
|
|
90e3f7508a | ||
|
|
5e981da4df | ||
|
|
2ddd5d75fc | ||
|
|
88f509cbb6 | ||
|
|
ca161bc881 | ||
|
|
676ca44cdf | ||
|
|
5d0c91202f | ||
|
|
a166f049d5 | ||
|
|
0dc4692dfc | ||
|
|
996607d096 | ||
|
|
bd907ea008 | ||
|
|
b1573b1f3b | ||
|
|
246849ce94 | ||
|
|
2c2c68261a | ||
|
|
e38a0361bf | ||
|
|
cbf879bd32 | ||
|
|
808625e564 | ||
|
|
c3b4c2f7d4 | ||
|
|
14f521fdd7 | ||
|
|
087831df5a | ||
|
|
6b0542a5bf | ||
|
|
ac57856f00 | ||
|
|
2ab1587df0 | ||
|
|
632aa6e003 | ||
|
|
9142f2df4b | ||
|
|
eea4cbbd6c | ||
|
|
bb31c20282 | ||
|
|
f8ab206744 | ||
|
|
4129b3a96e | ||
|
|
0de704bd3e | ||
|
|
5f20ce87a7 | ||
|
|
ff80154fda | ||
|
|
0d99cf9fb9 | ||
|
|
a5306b2067 | ||
|
|
e9b826e498 | ||
|
|
e7f356b87c | ||
|
|
d2abde714f | ||
|
|
20eaa79079 | ||
|
|
3cb74bb844 | ||
|
|
a72d4f425a | ||
|
|
8f3545624e | ||
|
|
8f6cfe8a04 | ||
|
|
bec7943e05 | ||
|
|
a486f5951e | ||
|
|
e8158aadec | ||
|
|
b6f107b85b | ||
|
|
5abd27f252 | ||
|
|
39648b6df8 | ||
|
|
0a7b2e3304 | ||
|
|
48db6e1756 | ||
|
|
2a38af5db6 | ||
|
|
740b1093d7 | ||
|
|
6b70412f6e | ||
|
|
43f147ec60 | ||
|
|
bf0fc57493 | ||
|
|
325b8b886c | ||
|
|
a02bbdc550 | ||
|
|
4285fd7708 | ||
|
|
96fa6eac44 | ||
|
|
05fa1ef2fb | ||
|
|
13aebc060e | ||
|
|
1aae4098d6 | ||
|
|
3367bb2e5b | ||
|
|
49fc5e1559 | ||
|
|
3a9b448a83 | ||
|
|
a45fb975c0 | ||
|
|
e070fc2f52 | ||
|
|
5f8aac31e1 | ||
|
|
07f2e2eafc | ||
|
|
2b41fb7108 | ||
|
|
81c18f1869 | ||
|
|
61fd2fe87d | ||
|
|
054cb974f1 | ||
|
|
50a19ecd74 | ||
|
|
1232909a3b | ||
|
|
9ba0e076a6 | ||
|
|
ede91c645d | ||
|
|
b6a91723e7 | ||
|
|
fddd73fdb1 | ||
|
|
ffbe8f9dc4 | ||
|
|
f80763ffb4 | ||
|
|
f65aa1b875 | ||
|
|
0c4de9a5e0 | ||
|
|
1822970e23 | ||
|
|
80615d0382 | ||
|
|
c1002ed52a | ||
|
|
83e585ed35 | ||
|
|
0e164b9daa | ||
|
|
1c9f8a2540 | ||
|
|
88ba4451eb | ||
|
|
305f195539 | ||
|
|
fbc6b853ed | ||
|
|
f3bcab813a | ||
|
|
2fa9b2e4dd | ||
|
|
fc756ba46e | ||
|
|
85a82124b1 | ||
|
|
b5cfbc92af | ||
|
|
e4f260e242 | ||
|
|
7c6c0c1fbd | ||
|
|
2f5f4dbebd | ||
|
|
94adc6af95 | ||
|
|
e3ea703f3d | ||
|
|
a14753e5f3 | ||
|
|
2ffd55c2d8 | ||
|
|
ad0ee8da32 | ||
|
|
e5cfe37b17 | ||
|
|
9247dd8b17 | ||
|
|
2f69b1d6e1 | ||
|
|
9c43930e7b | ||
|
|
f18ecfa524 | ||
|
|
b6f7453ed9 | ||
|
|
4a0b77dabb | ||
|
|
de7687b3ab | ||
|
|
85f2ec9d92 | ||
|
|
ed6b8dfc9b | ||
|
|
1539a6eabc | ||
|
|
6424d96703 | ||
|
|
15f6ac8e89 | ||
|
|
f9803a4694 | ||
|
|
13de9975e3 | ||
|
|
a64c4ef66f | ||
|
|
36e85eb3a7 | ||
|
|
20ffe7e082 | ||
|
|
ea1dc58b9a | ||
|
|
cd292ad605 | ||
|
|
03d9a6c0e5 | ||
|
|
fa54a2e67c | ||
|
|
7f2530470d | ||
|
|
a284934136 | ||
|
|
2ca6606508 | ||
|
|
bb1c691b34 | ||
|
|
19d3e1b11c | ||
|
|
e35e64caaa | ||
|
|
a3cf72c76c | ||
|
|
710e95bdee | ||
|
|
d281fb7065 | ||
|
|
e669d5041b | ||
|
|
d8d91451c8 | ||
|
|
5d5947f676 | ||
|
|
8fc889ae08 | ||
|
|
ff46493775 | ||
|
|
a1cb3b8371 | ||
|
|
51131433d3 | ||
|
|
3c8a066f76 | ||
|
|
6856a98d50 | ||
|
|
120b767a68 | ||
|
|
8c0d4cde86 | ||
|
|
280697f2f7 | ||
|
|
0650be664d | ||
|
|
60c61f553a | ||
|
|
022c967781 |
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -42,5 +42,8 @@
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "*", "severity": "warn" }
|
||||
],
|
||||
"cSpell.words": [
|
||||
"Trilium"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.9",
|
||||
"@redocly/cli": "2.15.1",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<body id="trilium-app">
|
||||
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
||||
|
||||
<div id="context-menu-cover"></div>
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
||||
|
||||
<!-- Required to match the PWA's top bar color with the theme -->
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mind-elixir/node-menu": "5.0.1",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@preact/signals": "2.6.1",
|
||||
"@preact/signals": "2.6.2",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
@@ -42,8 +42,8 @@
|
||||
"color": "5.0.3",
|
||||
"debounce": "3.0.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "17.0.0",
|
||||
"force-graph": "1.51.1",
|
||||
"globals": "17.3.0",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "4.0.0",
|
||||
@@ -56,12 +56,12 @@
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.6.1",
|
||||
"mind-elixir": "5.7.1",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.2",
|
||||
"preact": "10.28.3",
|
||||
"react-i18next": "16.5.4",
|
||||
"react-window": "2.2.5",
|
||||
"react-window": "2.2.6",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
@@ -78,7 +78,7 @@
|
||||
"@types/reveal.js": "5.2.2",
|
||||
"@types/tabulator-tables": "6.3.1",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.4.0",
|
||||
"happy-dom": "20.5.0",
|
||||
"lightningcss": "1.31.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.2.0"
|
||||
|
||||
@@ -99,15 +99,22 @@ function initFullScreenDetection(currentWindow: Electron.BrowserWindow) {
|
||||
}
|
||||
|
||||
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||
const material = style.getPropertyValue("--background-material").trim();
|
||||
if (window.glob.platform === "win32") {
|
||||
const material = style.getPropertyValue("--background-material");
|
||||
// TriliumNextTODO: find a nicer way to make TypeScript happy – unfortunately TS did not like Array.includes here
|
||||
const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const;
|
||||
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
|
||||
if (foundBgMaterialOption) {
|
||||
currentWindow.setBackgroundMaterial(foundBgMaterialOption);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.glob.platform === "darwin") {
|
||||
const bgMaterialOptions = [ "popover", "tooltip", "titlebar", "selection", "menu", "sidebar", "header", "sheet", "window", "hud", "fullscreen-ui", "content", "under-window", "under-page" ] as const;
|
||||
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
|
||||
if (foundBgMaterialOption) {
|
||||
currentWindow.setVibrancy(foundBgMaterialOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
72
apps/client/src/layouts/mobile_layout.css
Normal file
72
apps/client/src/layouts/mobile_layout.css
Normal file
@@ -0,0 +1,72 @@
|
||||
span.keyboard-shortcut,
|
||||
kbd {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
padding-inline-start: 0.5em;
|
||||
padding-inline-end: 0.5em;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
.quick-search {
|
||||
margin: 0;
|
||||
}
|
||||
.quick-search .dropdown-menu {
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
/* #region Tree */
|
||||
.tree-wrapper {
|
||||
max-height: 100%;
|
||||
margin-top: 0px;
|
||||
overflow-y: auto;
|
||||
contain: content;
|
||||
padding-inline-start: 10px;
|
||||
}
|
||||
|
||||
.fancytree-title {
|
||||
margin-inline-start: 0.6em !important;
|
||||
}
|
||||
|
||||
.fancytree-node {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
span.fancytree-expander {
|
||||
width: 24px !important;
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander {
|
||||
width: 24px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander:after {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 4px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.tree-wrapper .collapse-tree-button,
|
||||
.tree-wrapper .scroll-to-active-note-button,
|
||||
.tree-wrapper .tree-settings-button {
|
||||
position: fixed;
|
||||
margin-inline-end: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree-wrapper .unhoist-button {
|
||||
font-size: 200%;
|
||||
}
|
||||
/* #endregion */
|
||||
@@ -1,128 +1,41 @@
|
||||
import "./mobile_layout.css";
|
||||
|
||||
import type AppContext from "../components/app_context.js";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import ContentHeader from "../widgets/containers/content_header.js";
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
|
||||
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
|
||||
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
span.keyboard-shortcut,
|
||||
kbd {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
padding-inline-start: 0.5em;
|
||||
padding-inline-end: 0.5em;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
.quick-search {
|
||||
margin: 0;
|
||||
}
|
||||
.quick-search .dropdown-menu {
|
||||
max-width: 350px;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
const FANCYTREE_CSS = `
|
||||
<style>
|
||||
.tree-wrapper {
|
||||
max-height: 100%;
|
||||
margin-top: 0px;
|
||||
overflow-y: auto;
|
||||
contain: content;
|
||||
padding-inline-start: 10px;
|
||||
}
|
||||
|
||||
.fancytree-custom-icon {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.fancytree-title {
|
||||
font-size: 1.5em;
|
||||
margin-inline-start: 0.6em !important;
|
||||
}
|
||||
|
||||
.fancytree-node {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.fancytree-node .fancytree-expander:before {
|
||||
font-size: 2em !important;
|
||||
}
|
||||
|
||||
span.fancytree-expander {
|
||||
width: 24px !important;
|
||||
margin-inline-end: 5px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander {
|
||||
width: 24px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.fancytree-loading span.fancytree-expander:after {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 4px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.tree-wrapper .collapse-tree-button,
|
||||
.tree-wrapper .scroll-to-active-note-button,
|
||||
.tree-wrapper .tree-settings-button {
|
||||
position: fixed;
|
||||
margin-inline-end: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree-wrapper .unhoist-button {
|
||||
font-size: 200%;
|
||||
}
|
||||
</style>`;
|
||||
|
||||
export default class MobileLayout {
|
||||
getRootWidget(appContext: typeof AppContext) {
|
||||
const rootContainer = new RootContainer(true)
|
||||
.setParent(appContext)
|
||||
.class("horizontal-layout")
|
||||
.cssBlock(MOBILE_CSS)
|
||||
.child(new FlexContainer("column").id("mobile-sidebar-container"))
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
@@ -136,7 +49,7 @@ export default class MobileLayout {
|
||||
.css("padding-inline-start", "0")
|
||||
.css("padding-inline-end", "0")
|
||||
.css("contain", "content")
|
||||
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
|
||||
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget()))
|
||||
)
|
||||
.child(
|
||||
new ScreenContainer("detail", "row")
|
||||
@@ -147,23 +60,21 @@ export default class MobileLayout {
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.class("title-row note-split-title")
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(<NoteBadges />)
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
.child(<PromotedAttributes />)
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.contentSized()
|
||||
.child(new ContentHeader()
|
||||
.child(<ReadOnlyNoteInfoBar />)
|
||||
.child(<SharedInfoWidget />)
|
||||
)
|
||||
.child(<InlineTitle />)
|
||||
.child(<NoteTitleActions />)
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||
@@ -171,6 +82,7 @@ export default class MobileLayout {
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(<MobileEditorToolbar />)
|
||||
.child(new FindWidget())
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -179,7 +91,6 @@ export default class MobileLayout {
|
||||
new FlexContainer("column")
|
||||
.contentSized()
|
||||
.id("mobile-bottom-bar")
|
||||
.child(new TabRowWidget().css("height", "40px"))
|
||||
.child(new FlexContainer("row")
|
||||
.class("horizontal")
|
||||
.css("height", "53px")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { h, JSX, render } from "preact";
|
||||
|
||||
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { h, JSX, render } from "preact";
|
||||
|
||||
export interface ContextMenuOptions<T> {
|
||||
x: number;
|
||||
@@ -62,17 +63,17 @@ export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEve
|
||||
|
||||
class ContextMenu {
|
||||
private $widget: JQuery<HTMLElement>;
|
||||
private $cover: JQuery<HTMLElement>;
|
||||
private $cover?: JQuery<HTMLElement>;
|
||||
private options?: ContextMenuOptions<any>;
|
||||
private isMobile: boolean;
|
||||
|
||||
constructor() {
|
||||
this.$widget = $("#context-menu-container");
|
||||
this.$cover = $("#context-menu-cover");
|
||||
this.$widget.addClass("dropend");
|
||||
this.isMobile = utils.isMobile();
|
||||
|
||||
if (this.isMobile) {
|
||||
this.$cover = $("#context-menu-cover");
|
||||
this.$cover.on("click", () => this.hide());
|
||||
} else {
|
||||
$(document).on("click", (e) => this.hide());
|
||||
@@ -91,7 +92,7 @@ class ContextMenu {
|
||||
}
|
||||
|
||||
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
|
||||
this.$cover.addClass("show");
|
||||
this.$cover?.addClass("show");
|
||||
$("body").addClass("context-menu-shown");
|
||||
|
||||
this.$widget.empty();
|
||||
@@ -140,16 +141,14 @@ class ContextMenu {
|
||||
} else {
|
||||
left = this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET;
|
||||
}
|
||||
} else if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
|
||||
// Overflow: right
|
||||
left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
|
||||
} else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
|
||||
// Overflow: left
|
||||
left = CONTEXT_MENU_PADDING;
|
||||
} else {
|
||||
if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
|
||||
// Overflow: right
|
||||
left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
|
||||
} else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
|
||||
// Overflow: left
|
||||
left = CONTEXT_MENU_PADDING;
|
||||
} else {
|
||||
left = this.options.x - CONTEXT_MENU_OFFSET;
|
||||
}
|
||||
left = this.options.x - CONTEXT_MENU_OFFSET;
|
||||
}
|
||||
|
||||
this.$widget
|
||||
@@ -249,7 +248,7 @@ class ContextMenu {
|
||||
if ("uiIcon" in item || "checked" in item) {
|
||||
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
|
||||
if (icon) {
|
||||
$icon.addClass(icon);
|
||||
$icon.addClass([icon, "tn-icon"]);
|
||||
} else {
|
||||
$icon.append(" ");
|
||||
}
|
||||
@@ -261,7 +260,7 @@ class ContextMenu {
|
||||
.append(item.title);
|
||||
|
||||
if ("badges" in item && item.badges) {
|
||||
for (let badge of item.badges) {
|
||||
for (const badge of item.badges) {
|
||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||
|
||||
if (badge.className) {
|
||||
@@ -352,7 +351,7 @@ class ContextMenu {
|
||||
async hide() {
|
||||
this.options?.onHide?.();
|
||||
this.$widget.removeClass("show");
|
||||
this.$cover.removeClass("show");
|
||||
this.$cover?.removeClass("show");
|
||||
$("body").removeClass("context-menu-shown");
|
||||
this.$widget.hide();
|
||||
}
|
||||
|
||||
9
apps/client/src/services/content_renderer.css
Normal file
9
apps/client/src/services/content_renderer.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.rendered-content.no-preview > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 500%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./content_renderer.css";
|
||||
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
|
||||
@@ -71,18 +73,9 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
|
||||
|
||||
$renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
|
||||
} else if (entity instanceof FNote) {
|
||||
$renderedContent
|
||||
.css("display", "flex")
|
||||
.css("flex-direction", "column");
|
||||
$renderedContent.addClass("no-preview");
|
||||
$renderedContent.append(
|
||||
$("<div>")
|
||||
.css("display", "flex")
|
||||
.css("justify-content", "space-around")
|
||||
.css("align-items", "center")
|
||||
.css("height", "100%")
|
||||
.css("font-size", "500%")
|
||||
.css("flex-grow", "1")
|
||||
.append($("<span>").addClass(entity.getIcon()))
|
||||
$("<div>").append($("<span>").addClass(entity.getIcon()))
|
||||
);
|
||||
|
||||
if (entity.type === "webView" && entity.hasLabel("webViewSrc")) {
|
||||
|
||||
@@ -49,7 +49,7 @@ function createClassForColor(colorString: string | null) {
|
||||
return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue");
|
||||
}
|
||||
|
||||
function parseColor(color: string) {
|
||||
export function parseColor(color: string) {
|
||||
try {
|
||||
return Color(color.toLowerCase());
|
||||
} catch (ex) {
|
||||
@@ -77,7 +77,7 @@ function adjustColorLightness(color: ColorInstance, lightThemeMaxLightness: numb
|
||||
}
|
||||
|
||||
/** Returns the hue of the specified color, or undefined if the color is grayscale. */
|
||||
function getHue(color: ColorInstance) {
|
||||
export function getHue(color: ColorInstance) {
|
||||
const hslColor = color.hsl();
|
||||
if (hslColor.saturationl() > 0) {
|
||||
return hslColor.hue();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "./i18n";
|
||||
import options from "./options";
|
||||
import { isMobile } from "./utils";
|
||||
|
||||
export interface ExperimentalFeature {
|
||||
id: string;
|
||||
@@ -21,7 +22,7 @@ let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
|
||||
|
||||
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
|
||||
if (featureId === "new-layout") {
|
||||
return options.is("newLayout");
|
||||
return (isMobile() || options.is("newLayout"));
|
||||
}
|
||||
|
||||
return getEnabledFeatures().has(featureId);
|
||||
@@ -29,7 +30,7 @@ export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId):
|
||||
|
||||
export function getEnabledExperimentalFeatureIds() {
|
||||
const values = [ ...getEnabledFeatures().values() ];
|
||||
if (options.is("newLayout")) {
|
||||
if (isMobile() || options.is("newLayout")) {
|
||||
values.push("new-layout");
|
||||
}
|
||||
return values;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
--bs-body-color: var(--main-text-color) !important;
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--tn-modal-max-height: 90vh;
|
||||
--tn-modal-max-height: 90svh;
|
||||
|
||||
--tree-item-light-theme-max-color-lightness: 50;
|
||||
--tree-item-dark-theme-min-color-lightness: 75;
|
||||
@@ -111,6 +111,7 @@ body.mobile #root-widget.virtual-keyboard-opened #mobile-bottom-bar {
|
||||
}
|
||||
|
||||
#mobile-bottom-bar {
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding-bottom: var(--mobile-bottom-offset);
|
||||
}
|
||||
|
||||
@@ -224,10 +225,6 @@ body.mobile .modal .modal-dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.mobile .modal .modal-content {
|
||||
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
|
||||
}
|
||||
|
||||
.component {
|
||||
contain: size;
|
||||
}
|
||||
@@ -458,7 +455,7 @@ body.desktop .tabulator-popup-container,
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||
.dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||
body.desktop .dropdown-menu .dropdown-toggle,
|
||||
body #context-menu-container .dropdown-item > span,
|
||||
body.mobile .dropdown .dropdown-submenu > span {
|
||||
@@ -466,6 +463,15 @@ body.mobile .dropdown .dropdown-submenu > span {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
body.mobile .dropdown .dropdown-submenu {
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item span.keyboard-shortcut,
|
||||
.dropdown-item *:not(.keyboard-shortcut) > kbd {
|
||||
flex-grow: 1;
|
||||
@@ -1255,7 +1261,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
z-index: 2500;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -1534,7 +1540,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
|
||||
@media (max-width: 991px) {
|
||||
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
|
||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show,
|
||||
body.mobile .dropdown-menu.mobile-bottom-menu.show {
|
||||
--dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
|
||||
position: fixed !important;
|
||||
bottom: var(--dropdown-bottom) !important;
|
||||
@@ -1546,6 +1553,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
|
||||
}
|
||||
|
||||
body.mobile .dropdown-menu.mobile-bottom-menu.show {
|
||||
--dropdown-bottom: 0px;
|
||||
}
|
||||
|
||||
#mobile-sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -1614,6 +1625,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
|
||||
body.mobile .modal-content {
|
||||
overflow-y: auto;
|
||||
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
|
||||
}
|
||||
|
||||
body.mobile .modal-footer {
|
||||
@@ -1669,39 +1681,16 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
#detail-container {
|
||||
background: var(--main-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
body.mobile.force-fixed-tree #mobile-sidebar-wrapper {
|
||||
padding-top: 0;
|
||||
position: static;
|
||||
height: 40vh;
|
||||
width: 100vw;
|
||||
transform: none !important;
|
||||
background-color: var(--left-pane-background-color) !important;
|
||||
border-bottom: 0.5px solid var(--main-border-color);
|
||||
}
|
||||
body.mobile {
|
||||
.modal-dialog {
|
||||
margin: var(--bs-modal-margin);
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree #mobile-sidebar-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree #mobile-sidebar-wrapper .quick-search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree .component > button.bx-sidebar {
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree #mobile-rest-container {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
body.mobile.force-fixed-tree #detail-container {
|
||||
flex-grow: 1;
|
||||
.modal-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2617,14 +2606,14 @@ iframe.print-iframe {
|
||||
}
|
||||
}
|
||||
|
||||
#root-widget.virtual-keyboard-opened .note-split:not(:focus-within) {
|
||||
#root-widget.virtual-keyboard-opened .note-split:not(.active) {
|
||||
max-height: 80px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.desktop .title-row {
|
||||
.title-row {
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
align-items: center;
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
--left-pane-collapsed-border-color: #0009;
|
||||
--left-pane-background-color: #1f1f1f;
|
||||
--left-pane-text-color: #aaaaaa;
|
||||
--left-pane-icon-color: #c5c5c5;
|
||||
--left-pane-item-hover-background: #ffffff0d;
|
||||
--left-pane-item-selected-background: #ffffff25;
|
||||
--left-pane-item-selected-color: #dfdfdf;
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
--left-pane-collapsed-border-color: #0000000d;
|
||||
--left-pane-background-color: #f2f2f2;
|
||||
--left-pane-text-color: #383838;
|
||||
--left-pane-icon-color: currentColor;
|
||||
--left-pane-item-hover-background: rgba(0, 0, 0, 0.032);
|
||||
--left-pane-item-selected-background: white;
|
||||
--left-pane-item-selected-color: black;
|
||||
|
||||
@@ -47,9 +47,14 @@
|
||||
}
|
||||
|
||||
/* The toolbar show / hide button for the current text block */
|
||||
.ck.ck-block-toolbar-button {
|
||||
:root .ck.ck-block-toolbar-button {
|
||||
--ck-color-block-toolbar-button: var(--muted-text-color);
|
||||
--ck-color-button-on-background: transparent;
|
||||
--ck-color-button-on-color: currentColor;
|
||||
--ck-color-button-on-color: var(--ck-editor-toolbar-button-on-color);
|
||||
translate: -40% 0;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
z-index: 1600;
|
||||
}
|
||||
|
||||
:root .ck.ck-toolbar .ck-button:not(.ck-disabled):active,
|
||||
@@ -517,6 +522,10 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck
|
||||
* EDITOR'S CONTENT
|
||||
*/
|
||||
|
||||
.note-detail-editable-text-editor > .ck-placeholder {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
/*
|
||||
* Code Blocks
|
||||
*/
|
||||
|
||||
@@ -156,6 +156,10 @@
|
||||
--preferred-max-content-width: var(--options-card-max-width);
|
||||
}
|
||||
|
||||
.note-split.options .collection-properties {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Create a gap at the top of the option pages */
|
||||
.note-detail-content-widget-content.options>*:first-child {
|
||||
margin-top: var(--options-first-item-top-margin, 1em);
|
||||
|
||||
@@ -40,13 +40,30 @@ body.mobile {
|
||||
|
||||
/* #region Mica */
|
||||
|
||||
/* Quirk: --background-material is read before "theme-supports-background-effects" class
|
||||
* is applied. Apply the matterial even if the theme doesn't support it. */
|
||||
body.background-effects.platform-win32 {
|
||||
/* Quirk: --background-material is read before "theme-supports-background-effects" class
|
||||
* is applied. Apply the matterial even if the theme doesn't support it. */
|
||||
--background-material: tabbed;
|
||||
&.layout-vertical {
|
||||
--background-material: mica;
|
||||
}
|
||||
|
||||
&.layout-horizontal {
|
||||
--background-material: tabbed;
|
||||
}
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects.platform-win32 {
|
||||
body.background-effects.platform-darwin {
|
||||
/** Reference: https://developer.apple.com/documentation/appkit/nsvisualeffectview?preferredLanguage=objc **/
|
||||
&.layout-vertical {
|
||||
--background-material: under-window;
|
||||
}
|
||||
|
||||
&.layout-horizontal {
|
||||
--background-material: hud;
|
||||
}
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects {
|
||||
--launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
|
||||
--launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
|
||||
--launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
|
||||
@@ -56,33 +73,29 @@ body.background-effects.theme-supports-background-effects.platform-win32 {
|
||||
--root-background: transparent;
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32.layout-vertical {
|
||||
--background-material: mica;
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical {
|
||||
body.background-effects.theme-supports-background-effects.layout-vertical {
|
||||
--left-pane-background-color: var(--window-background-color-bgfx);
|
||||
--center-pane-background-color-bgfx: var(--center-pane-vert-layout-background-color-bgfx);
|
||||
--right-pane-background-color: var(--right-pane-background-color-bgfx);
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal {
|
||||
body.background-effects.theme-supports-background-effects.layout-horizontal {
|
||||
--center-pane-background-color-bgfx: var(--center-pane-horiz-layout-background-color-bgfx);
|
||||
--gutter-color: var(--left-pane-background-color);
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects.platform-win32,
|
||||
body.background-effects.theme-supports-background-effects.platform-win32 #root-widget {
|
||||
body.background-effects.theme-supports-background-effects,
|
||||
body.background-effects.theme-supports-background-effects #root-widget {
|
||||
background: var(--window-background-color-bgfx) !important;
|
||||
}
|
||||
|
||||
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal #horizontal-main-container,
|
||||
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical #vertical-main-container {
|
||||
body.background-effects.theme-supports-background-effects.layout-horizontal #horizontal-main-container,
|
||||
body.background-effects.theme-supports-background-effects.layout-vertical #vertical-main-container {
|
||||
background-color: var(--root-background);
|
||||
}
|
||||
|
||||
/* Note split with background effects */
|
||||
body.background-effects.theme-supports-background-effects.platform-win32 #center-pane .note-split.bgfx {
|
||||
body.background-effects.theme-supports-background-effects #center-pane .note-split.bgfx {
|
||||
--note-split-background-color: var(--center-pane-background-color-bgfx);
|
||||
}
|
||||
|
||||
@@ -726,18 +739,12 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
|
||||
transform: translateX(-25%);
|
||||
}
|
||||
|
||||
body.mobile .fancytree-expander::before,
|
||||
body.mobile .fancytree-title,
|
||||
body.mobile .fancytree-node > span {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
body.mobile #mobile-sidebar-container {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
body.mobile:not(.force-fixed-tree) #mobile-sidebar-wrapper {
|
||||
body.mobile #mobile-sidebar-wrapper {
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
border-inline-end: 1px solid var(--subtle-border-color);
|
||||
@@ -756,9 +763,9 @@ body.mobile .fancytree-node > span {
|
||||
|
||||
#left-pane .fancytree-custom-icon {
|
||||
margin-top: 0; /* Use this to align the icon with the tree view item's caption */
|
||||
color: var(--custom-color, var(--left-pane-icon-color));
|
||||
}
|
||||
|
||||
|
||||
#left-pane span.fancytree-active .fancytree-title {
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -1054,7 +1061,7 @@ body.layout-horizontal .tab-row-widget-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.desktop:not(.background-effects.platform-win32) #root-widget.horizontal-layout {
|
||||
body.desktop:not(.background-effects) #root-widget.horizontal-layout {
|
||||
background-color: var(--root-background) !important;
|
||||
}
|
||||
|
||||
@@ -1259,7 +1266,7 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
|
||||
#center-pane .note-split {
|
||||
padding-top: 2px;
|
||||
background-color: var(--note-split-background-color, var(--main-background-color));
|
||||
transition: border-color 250ms ease-in;
|
||||
transition: border-color 150ms ease-out;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
@@ -1309,7 +1316,7 @@ body.mobile .note-title {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
body.desktop .title-row {
|
||||
/* Aligns the "Create new split" button with the note menu button (the three dots button) */
|
||||
padding-inline-end: 3px;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
"widget-render-error": {
|
||||
"title": "فشل عرض عنصر واجهة مستخدم React مخصص"
|
||||
},
|
||||
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك."
|
||||
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك.",
|
||||
"open-script-note": "فتح ملاحظة برمجية",
|
||||
"scripting-error": "خطأ في النص البرمجي المخصص: {{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "أضافة رابط",
|
||||
@@ -37,7 +39,9 @@
|
||||
"search_note": "البحث عن الملاحظة بالاسم",
|
||||
"link_title": "عنوان الرابط",
|
||||
"button_add_link": "اضافة رابط",
|
||||
"help_on_links": "مساعدة حول الارتباطات التشعبية"
|
||||
"help_on_links": "مساعدة حول الارتباطات التشعبية",
|
||||
"link_title_mirrors": "عنوان الرابط يعكس العنوان الحالي للملاحظة",
|
||||
"link_title_arbitrary": "يمكن تغيير عنوان الرابط حسب الرغبة"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "تعديل بادئة الفرع",
|
||||
|
||||
@@ -1782,8 +1782,8 @@
|
||||
"desktop-application": "桌面应用程序",
|
||||
"native-title-bar": "原生标题栏",
|
||||
"native-title-bar-description": "对于 Windows 和 macOS,关闭原生标题栏可使应用程序看起来更紧凑。在 Linux 上,保留原生标题栏可以更好地与系统集成。",
|
||||
"background-effects": "启用背景效果(仅适用于 Windows 11)",
|
||||
"background-effects-description": "Mica 效果为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
|
||||
"background-effects": "启用背景效果",
|
||||
"background-effects-description": "为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
|
||||
"restart-app-button": "重启应用程序以查看更改",
|
||||
"zoom-factor": "缩放系数"
|
||||
},
|
||||
@@ -1802,7 +1802,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "创建一个新的子笔记并将其添加到地图中",
|
||||
"create-child-note-instruction": "单击地图以在该位置创建新笔记,或按 Escape 以取消。",
|
||||
"unable-to-load-map": "无法加载地图。"
|
||||
"unable-to-load-map": "无法加载地图。",
|
||||
"create-child-note-text": "添加标记"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "打开位置",
|
||||
@@ -2117,7 +2118,7 @@
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_title": "背景效果现已推出稳定版本",
|
||||
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。",
|
||||
"background_effects_message": "在 Windows 和 macOS 设备上,背景效果现在已稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。",
|
||||
"background_effects_button": "启用背景效果",
|
||||
"next_theme_title": "试用新 Trilium 主题",
|
||||
"next_theme_message": "当前使用旧版主题,要试用新主题吗?",
|
||||
@@ -2253,5 +2254,12 @@
|
||||
"pages_alt": "第{{pageNumber}}页",
|
||||
"pages_loading": "加载中...",
|
||||
"layers_other": "{{count}} 层"
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "在 {{platform}} 上可用"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_other": "{{count}} 选项卡",
|
||||
"more_options": "更多选项"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1751,8 +1751,8 @@
|
||||
"desktop-application": "Desktop Anwendung",
|
||||
"native-title-bar": "Native Anwendungsleiste",
|
||||
"native-title-bar-description": "In Windows und macOS, sorgt das Deaktivieren der nativen Anwendungsleiste für ein kompakteres Aussehen. Unter Linux, sorgt das Aktivieren der nativen Anwendungsleiste für eine bessere Integration mit anderen Teilen des Systems.",
|
||||
"background-effects": "Hintergrundeffekte aktivieren (nur Windows 11)",
|
||||
"background-effects-description": "Der Mica Effekt fügt einen unscharfen, stylischen Hintergrund in Anwendungsfenstern ein. Dieser erzeugt Tiefe und ein modernes Auftreten. \"Native Titelleiste\" muss deaktiviert sein.",
|
||||
"background-effects": "Hintergrundeffekte aktivieren",
|
||||
"background-effects-description": "Fügt einen unscharfen, stylischen Hintergrund in das Anwendungsfenstern ein. Dies erzeugt Tiefe und ein modernes Auftreten. \"Native Titelleiste\" muss deaktiviert sein.",
|
||||
"restart-app-button": "Anwendung neustarten um Änderungen anzuwenden",
|
||||
"zoom-factor": "Zoomfaktor"
|
||||
},
|
||||
@@ -1771,7 +1771,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Neue Unternotiz anlegen und zur Karte hinzufügen",
|
||||
"create-child-note-instruction": "Auf die Karte klicken, um eine neue Notiz an der Stelle zu erstellen oder Escape drücken um abzubrechen.",
|
||||
"unable-to-load-map": "Karte konnte nicht geladen werden."
|
||||
"unable-to-load-map": "Karte konnte nicht geladen werden.",
|
||||
"create-child-note-text": "Marker hinzufügen"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Ort öffnen",
|
||||
@@ -2133,7 +2134,7 @@
|
||||
"next_theme_message": "Es wird aktuell das alte Design verwendet. Möchten Sie das neue Design ausprobieren?",
|
||||
"next_theme_button": "Teste das neue Design",
|
||||
"background_effects_title": "Hintergrundeffekte sind jetzt zuverlässig nutzbar",
|
||||
"background_effects_message": "Auf Windows-Geräten sind die Hintergrundeffekte nun vollständig stabil. Die Hintergrundeffekte verleihen der Benutzeroberfläche einen Farbakzent, indem der Hintergrund dahinter weichgezeichnet wird. Diese Technik wird auch in anderen Anwendungen wie dem Windows-Explorer eingesetzt.",
|
||||
"background_effects_message": "Auf Windows- und macOS-Geräten sind die Hintergrundeffekte nun stabil. Die Hintergrundeffekte verleihen der Benutzeroberfläche einen Farbakzent, indem der Hintergrund dahinter weichgezeichnet wird.",
|
||||
"background_effects_button": "Aktiviere Hintergrundeffekte",
|
||||
"dismiss": "Ablehnen",
|
||||
"new_layout_title": "Neues Layout",
|
||||
@@ -2267,5 +2268,13 @@
|
||||
"pages_other": "{{count}} Seiten",
|
||||
"pages_alt": "Seite {{pageNumber}}",
|
||||
"pages_loading": "Lädt..."
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Verfügbar auf {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} Tab",
|
||||
"title_other": "{{count}} Tabs",
|
||||
"more_options": "Weitere Optionen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,7 +745,7 @@
|
||||
"button_title": "Export diagram as SVG"
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"create_child_note_title": "Create new child note and add it into this relation map",
|
||||
"create_child_note_title": "Create child note and add it to map",
|
||||
"reset_pan_zoom_title": "Reset pan & zoom to initial coordinates and magnification",
|
||||
"zoom_in_title": "Zoom In",
|
||||
"zoom_out_title": "Zoom Out"
|
||||
@@ -760,7 +760,8 @@
|
||||
"delete_this_note": "Delete this note",
|
||||
"note_revisions": "Note revisions",
|
||||
"error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'",
|
||||
"error_unrecognized_command": "Unrecognized command {{command}}"
|
||||
"error_unrecognized_command": "Unrecognized command {{command}}",
|
||||
"backlinks": "Backlinks"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Change note icon",
|
||||
@@ -1958,8 +1959,8 @@
|
||||
"desktop-application": "Desktop Application",
|
||||
"native-title-bar": "Native title bar",
|
||||
"native-title-bar-description": "For Windows and macOS, keeping the native title bar off makes the application look more compact. On Linux, keeping the native title bar on integrates better with the rest of the system.",
|
||||
"background-effects": "Enable background effects (Windows 11 only)",
|
||||
"background-effects-description": "The Mica effect adds a blurred, stylish background to app windows, creating depth and a modern look. \"Native title bar\" must be disabled.",
|
||||
"background-effects": "Enable background effects",
|
||||
"background-effects-description": "Adds a blurred, stylish background to app windows, creating depth and a modern look. \"Native title bar\" must be disabled.",
|
||||
"restart-app-button": "Restart the application to view the changes",
|
||||
"zoom-factor": "Zoom factor"
|
||||
},
|
||||
@@ -1977,6 +1978,7 @@
|
||||
},
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Create a new child note and add it to the map",
|
||||
"create-child-note-text": "Add marker",
|
||||
"create-child-note-instruction": "Click on the map to create a new note at that location or press Escape to dismiss.",
|
||||
"unable-to-load-map": "Unable to load map."
|
||||
},
|
||||
@@ -2152,7 +2154,7 @@
|
||||
"next_theme_message": "You are currently using the legacy theme, would you like to try the new theme?",
|
||||
"next_theme_button": "Try the new theme",
|
||||
"background_effects_title": "Background effects are now stable",
|
||||
"background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of color to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer.",
|
||||
"background_effects_message": "On Windows and macOS devices, background effects are now stable. The background effects adds a touch of color to the user interface by blurring the background behind it.",
|
||||
"background_effects_button": "Enable background effects",
|
||||
"new_layout_title": "New layout",
|
||||
"new_layout_message": "We’ve introduced a modernized layout for Trilium. The ribbon has been removed and seamlessly integrated into the main interface, with a new status bar and expandable sections (such as promoted attributes) taking over key functions.\n\nThe new layout is enabled by default, and can be temporarily disabled via Options → Appearance.",
|
||||
@@ -2267,5 +2269,13 @@
|
||||
"pages_other": "{{count}} pages",
|
||||
"pages_alt": "Page {{pageNumber}}",
|
||||
"pages_loading": "Loading..."
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Available on {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} tab",
|
||||
"title_other": "{{count}} tabs",
|
||||
"more_options": "More options"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1614,7 +1614,7 @@
|
||||
},
|
||||
"bookmark_switch": {
|
||||
"bookmark": "Marcador",
|
||||
"bookmark_this_note": "Añadir esta nota a marcadores en el panel lateral izquierdo",
|
||||
"bookmark_this_note": "Agregar esta nota a marcadores en el panel lateral izquierdo",
|
||||
"remove_bookmark": "Eliminar marcador"
|
||||
},
|
||||
"editability_select": {
|
||||
@@ -1944,8 +1944,8 @@
|
||||
"desktop-application": "Aplicación de escritorio",
|
||||
"native-title-bar": "Barra de título nativa",
|
||||
"native-title-bar-description": "Para Windows y macOS, quitar la barra de título nativa hace que la aplicación se vea más compacta. En Linux, mantener la barra de título nativa hace que se integre mejor con el resto del sistema.",
|
||||
"background-effects": "Habilitar efectos de fondo (sólo en Windows 11)",
|
||||
"background-effects-description": "El efecto Mica agrega un fondo borroso y elegante a las ventanas de la aplicación, creando profundidad y un aspecto moderno. \"Título nativo de la barra\" debe deshabilitarse.",
|
||||
"background-effects": "Habilitar efectos de fondo",
|
||||
"background-effects-description": "Agrega un fondo borroso y elegante a las ventanas de la aplicación, creando profundidad y un aspecto moderno. \"Título nativo de la barra\" debe deshabilitarse.",
|
||||
"restart-app-button": "Reiniciar la aplicación para ver los cambios",
|
||||
"zoom-factor": "Factor de zoom"
|
||||
},
|
||||
@@ -1964,7 +1964,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Crear una nueva subnota y agregarla al mapa",
|
||||
"create-child-note-instruction": "Dé clic en el mapa para crear una nueva nota en esa ubicación o presione Escape para cancelar.",
|
||||
"unable-to-load-map": "No se puede cargar el mapa."
|
||||
"unable-to-load-map": "No se puede cargar el mapa.",
|
||||
"create-child-note-text": "Agregar marcador"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Abrir ubicación",
|
||||
@@ -2130,7 +2131,7 @@
|
||||
"next_theme_message": "Estás usando actualmente el tema heredado. ¿Te gustaría probar el nuevo tema?",
|
||||
"next_theme_button": "Prueba el nuevo tema",
|
||||
"background_effects_title": "Los efectos de fondo son ahora estables",
|
||||
"background_effects_message": "En los dispositivos Windows, los efectos de fondo ya son totalmente estables. Los efectos de fondo añaden un toque de color a la interfaz de usuario difuminando el fondo que hay detrás. Esta técnica también se utiliza en otras aplicaciones como el Explorador de Windows.",
|
||||
"background_effects_message": "En los dispositivos Windows y macOS, los efectos de fondo ya son estables. Los efectos de fondo añaden un toque de color a la interfaz de usuario difuminando el fondo que hay detrás.",
|
||||
"background_effects_button": "Activar efectos de fondo",
|
||||
"dismiss": "Desestimar",
|
||||
"new_layout_title": "Nuevo diseño",
|
||||
@@ -2281,5 +2282,14 @@
|
||||
"empty_button": "Ocultar el panel",
|
||||
"toggle": "Alternar panel derecho",
|
||||
"custom_widget_go_to_source": "Ir al código fuente"
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Disponible en {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} pestaña",
|
||||
"title_many": "{{count}} pestañas",
|
||||
"title_other": "{{count}} pestañas",
|
||||
"more_options": "Más opciones"
|
||||
}
|
||||
}
|
||||
|
||||
5
apps/client/src/translations/ga/translation.json
Normal file
5
apps/client/src/translations/ga/translation.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"global_menu": {
|
||||
"about": "Maidir le Trilium Notes"
|
||||
}
|
||||
}
|
||||
@@ -1757,8 +1757,8 @@
|
||||
"desktop-application": "デスクトップアプリケーション",
|
||||
"native-title-bar": "ネイティブタイトルバー",
|
||||
"native-title-bar-description": "WindowsとmacOSでは、ネイティブタイトルバーをオフにしておくと、アプリケーションがよりコンパクトに見えます。Linuxでは、ネイティブタイトルバーを表示したままの方が、他のシステムとの統一性が高まります。",
|
||||
"background-effects": "背景効果を有効化(Windows 11のみ)",
|
||||
"background-effects-description": "Mica効果は、アプリのウィンドウにぼかされたスタイリッシュな背景を追加し、奥行きとモダンな外観を演出します。「ネイティブタイトルバー」を無効にする必要があります。",
|
||||
"background-effects": "背景効果を有効化",
|
||||
"background-effects-description": "アプリウィンドウにぼかしの効いたスタイリッシュな背景を追加し、奥行きとモダンな外観を演出します。「ネイティブタイトルバー」を無効にする必要があります。",
|
||||
"restart-app-button": "アプリケーションを再起動して変更を反映",
|
||||
"zoom-factor": "ズーム倍率"
|
||||
},
|
||||
@@ -2008,7 +2008,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "新しい子ノートを作成し、マップに追加する",
|
||||
"create-child-note-instruction": "地図をクリックしてその場所に新しいノートを作成するか、Esc キーを押して閉じます。",
|
||||
"unable-to-load-map": "マップを読み込めません。"
|
||||
"unable-to-load-map": "マップを読み込めません。",
|
||||
"create-child-note-text": "マーカーを追加"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "現在位置を表示",
|
||||
@@ -2044,7 +2045,7 @@
|
||||
"next_theme_message": "現在、レガシーテーマを使用しています。新しいテーマを試してみませんか?",
|
||||
"next_theme_button": "新しいテーマを試す",
|
||||
"background_effects_title": "背景効果が安定しました",
|
||||
"background_effects_message": "Windowsデバイスでは、背景効果が完全に安定しました。背景効果は、背景をぼかすことでユーザーインターフェースに彩りを添えます。この技術は、Windowsエクスプローラーなどの他のアプリケーションでも使用されています。",
|
||||
"background_effects_message": "WindowsおよびmacOSデバイスで、背景効果が安定しました。背景効果は、背景をぼかすことでユーザーインターフェースに彩りを添えます。",
|
||||
"background_effects_button": "背景効果を有効にする",
|
||||
"dismiss": "却下",
|
||||
"new_layout_title": "新しいレイアウト",
|
||||
@@ -2253,5 +2254,12 @@
|
||||
"pages_other": "{{count}} ページ",
|
||||
"pages_alt": "ページ {{pageNumber}}",
|
||||
"pages_loading": "読み込み中..."
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "{{platform}} で利用可能"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_other": "{{count}} タブ",
|
||||
"more_options": "その他のオプション"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1761,8 +1761,8 @@
|
||||
"show-recent-notes": "Afișează notițele recente"
|
||||
},
|
||||
"electron_integration": {
|
||||
"background-effects": "Activează efectele de fundal (doar pentru Windows 11)",
|
||||
"background-effects-description": "Efectul Mica adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.",
|
||||
"background-effects": "Activează efectele de fundal",
|
||||
"background-effects-description": "Adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.",
|
||||
"desktop-application": "Aplicația desktop",
|
||||
"native-title-bar": "Bară de titlu nativă",
|
||||
"native-title-bar-description": "Pentru Windows și macOS, dezactivarea bării de titlu native face aplicația să pară mai compactă. Pe Linux, păstrarea bării integrează mai bine aplicația cu restul sistemului de operare.",
|
||||
@@ -1781,7 +1781,8 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Crează o notiță nouă și adaug-o pe hartă",
|
||||
"unable-to-load-map": "Nu s-a putut încărca harta.",
|
||||
"create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula."
|
||||
"create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula.",
|
||||
"create-child-note-text": "Adaugă marcaj"
|
||||
},
|
||||
"duration": {
|
||||
"days": "zile",
|
||||
@@ -2127,7 +2128,7 @@
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_title": "Efectele de fundal sunt acum stabile",
|
||||
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
|
||||
"background_effects_message": "Pe dispozitive cu Windows și macOS, efectele de fundal sunt stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei.",
|
||||
"background_effects_button": "Activează efectele de fundal",
|
||||
"next_theme_title": "Încercați noua temă Trilium",
|
||||
"next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?",
|
||||
@@ -2281,5 +2282,14 @@
|
||||
"pages_other": "{{count}} de pagini",
|
||||
"pages_alt": "Pagina {{pageNumber}}",
|
||||
"pages_loading": "Încărcare..."
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Disponibil pe {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} tab",
|
||||
"title_few": "{{count}} taburi",
|
||||
"title_other": "{{count}} de taburi",
|
||||
"more_options": "Mai multe opțiuni"
|
||||
}
|
||||
}
|
||||
|
||||
83
apps/client/src/widgets/Backlinks.css
Normal file
83
apps/client/src/widgets/Backlinks.css
Normal file
@@ -0,0 +1,83 @@
|
||||
.tn-backlinks-widget .backlinks-items {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: static;
|
||||
width: unset;
|
||||
|
||||
> li {
|
||||
--border-radius: 8px;
|
||||
|
||||
max-width: 600px;
|
||||
padding: 10px 20px;
|
||||
background: var(--card-background-color);
|
||||
|
||||
& + li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
/* Card header */
|
||||
& > span:first-child {
|
||||
display: block;
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
/* Note path */
|
||||
> small {
|
||||
flex: 100%;
|
||||
order: -1;
|
||||
font-size: .65rem;
|
||||
|
||||
.note-path {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Note icon */
|
||||
> .tn-icon {
|
||||
color: var(--menu-item-icon-color);
|
||||
}
|
||||
|
||||
/* Note title */
|
||||
> a {
|
||||
margin-inline-start: 4px;
|
||||
color: currentColor;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Card content - excerpt */
|
||||
.backlink-excerpt {
|
||||
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
|
||||
display: block;
|
||||
|
||||
margin: 8px 0;
|
||||
border-radius: 4px;
|
||||
background: var(--quick-search-result-content-background);
|
||||
padding: 8px;
|
||||
font-size: .75rem;
|
||||
|
||||
a {
|
||||
background: transparent;
|
||||
color: var(--quick-search-result-highlight-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./Backlinks.css";
|
||||
|
||||
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
|
||||
import { VNode } from "preact";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
@@ -54,21 +56,12 @@ export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
OpenTriliumApiDocsButton,
|
||||
SaveToNoteButton,
|
||||
RelationMapButtons,
|
||||
GeoMapButtons,
|
||||
CopyImageReferenceButton,
|
||||
ExportImageButtons,
|
||||
InAppHelpButton,
|
||||
Backlinks
|
||||
];
|
||||
|
||||
export const MOBILE_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
RefreshBackendLogButton,
|
||||
EditButton,
|
||||
RelationMapButtons,
|
||||
ExportImageButtons,
|
||||
Backlinks
|
||||
];
|
||||
|
||||
/**
|
||||
* Floating buttons that should be hidden in popup editor (Quick edit).
|
||||
*/
|
||||
@@ -98,10 +91,10 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
|
||||
/>;
|
||||
}
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
|
||||
function ToggleReadOnlyButton({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || isSavedSqlite)
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
@@ -243,17 +236,6 @@ function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingB
|
||||
);
|
||||
}
|
||||
|
||||
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
|
||||
const isEnabled = viewType === "geoMap" && !isReadOnly;
|
||||
return isEnabled && (
|
||||
<FloatingButton
|
||||
icon="bx bx-plus-circle"
|
||||
text={t("geo-map.create-child-note-title")}
|
||||
onClick={() => triggerEvent("geoMapCreateChildNote")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const hiddenImageCopyRef = useRef<HTMLDivElement>(null);
|
||||
const isEnabled = (
|
||||
@@ -305,7 +287,7 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
|
||||
|
||||
function InAppHelpButton({ note }: FloatingButtonContext) {
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
const isEnabled = !!helpUrl;
|
||||
const isEnabled = note.type !== "book" && !!helpUrl;
|
||||
|
||||
return isEnabled && (
|
||||
<FloatingButton
|
||||
|
||||
@@ -3,6 +3,29 @@
|
||||
font-family: var(--detail-font-family);
|
||||
font-size: var(--detail-font-size);
|
||||
contain: none;
|
||||
|
||||
&.fixed-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.fixed-note-tree-container {
|
||||
height: 60%;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
|
||||
.tree-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tree {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.prefers-centered-content .note-detail {
|
||||
@@ -12,4 +35,4 @@ body.prefers-centered-content .note-detail {
|
||||
|
||||
.note-detail > * {
|
||||
contain: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "./NoteDetail.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { isValidElement, VNode } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -12,8 +13,9 @@ import { t } from "../services/i18n";
|
||||
import protected_session_holder from "../services/protected_session_holder";
|
||||
import toast from "../services/toast.js";
|
||||
import { dynamicRequire, isElectron, isMobile } from "../services/utils";
|
||||
import NoteTreeWidget from "./note_tree";
|
||||
import { ExtendedNoteType, TYPE_MAPPINGS, TypeWidget } from "./note_types";
|
||||
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import { useLegacyWidget, useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import { NoteListWithLinks } from "./react/NoteList";
|
||||
import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
|
||||
@@ -36,6 +38,7 @@ export default function NoteDetail() {
|
||||
const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
|
||||
const [ activeNoteType, setActiveNoteType ] = useState<ExtendedNoteType>();
|
||||
const widgetRequestId = useRef(0);
|
||||
const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile();
|
||||
|
||||
const props: TypeWidgetProps = {
|
||||
note: note!,
|
||||
@@ -119,13 +122,6 @@ export default function NoteDetail() {
|
||||
}
|
||||
});
|
||||
|
||||
// Fixed tree for launch bar config on mobile.
|
||||
useEffect(() => {
|
||||
if (!isMobile) return;
|
||||
const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot";
|
||||
document.body.classList.toggle("force-fixed-tree", hasFixedTree);
|
||||
}, [ note ]);
|
||||
|
||||
// Handle toast notifications.
|
||||
useEffect(() => {
|
||||
if (!isElectron()) return;
|
||||
@@ -215,8 +211,13 @@ export default function NoteDetail() {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
|
||||
class={clsx("component note-detail", {
|
||||
"full-height": isFullHeight,
|
||||
"fixed-tree": hasFixedTree
|
||||
})}
|
||||
>
|
||||
{hasFixedTree && <FixedTree />}
|
||||
|
||||
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
|
||||
return <NoteDetailWrapper
|
||||
Element={Element}
|
||||
@@ -231,6 +232,12 @@ export default function NoteDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
function FixedTree() {
|
||||
const { noteContext } = useNoteContext();
|
||||
const [ treeEl ] = useLegacyWidget(() => new NoteTreeWidget(), { noteContext });
|
||||
return <div class="fixed-note-tree-container">{treeEl}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a single note type widget, in order to keep it in the DOM even after the user has switched away to another note type. This allows faster loading of the same note type again. The properties are cached, so that they are updated only
|
||||
* while the widget is visible, to avoid rendering in the background. When not visible, the DOM element is simply hidden.
|
||||
|
||||
@@ -5,6 +5,7 @@ import clsx from "clsx";
|
||||
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
|
||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../components/note_context";
|
||||
import FAttribute from "../entities/fattribute";
|
||||
import FNote from "../entities/fnote";
|
||||
import { Attribute } from "../services/attribute_parser";
|
||||
@@ -40,8 +41,8 @@ type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | J
|
||||
type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
|
||||
|
||||
export default function PromotedAttributes() {
|
||||
const { note, componentId } = useNoteContext();
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
|
||||
const { note, componentId, noteContext } = useNoteContext();
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
|
||||
return <PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />;
|
||||
}
|
||||
|
||||
@@ -74,12 +75,12 @@ export function PromotedAttributesContent({ note, componentId, cells, setCells }
|
||||
*
|
||||
* The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell.
|
||||
*/
|
||||
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
|
||||
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string, noteContext: NoteContext | undefined): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
|
||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||
const [ cells, setCells ] = useState<Cell[]>();
|
||||
|
||||
function refresh() {
|
||||
if (!note || viewType === "table") {
|
||||
if (!note || viewType === "table" || noteContext?.viewScope?.viewMode !== "default") {
|
||||
setCells([]);
|
||||
return;
|
||||
}
|
||||
@@ -124,7 +125,7 @@ export function usePromotedAttributeData(note: FNote | null | undefined, compone
|
||||
setCells(cells);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note, viewType ]);
|
||||
useEffect(refresh, [ note, viewType, noteContext ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
|
||||
refresh();
|
||||
|
||||
@@ -29,7 +29,6 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
const isVerticalLayout = !isHorizontalLayout;
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
||||
const isMobileLocal = isMobile();
|
||||
const logoRef = useRef<SVGSVGElement>(null);
|
||||
useStaticTooltip(logoRef);
|
||||
|
||||
@@ -44,8 +43,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
</div>}
|
||||
</>}
|
||||
noDropdownListStyle
|
||||
onShown={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover") : undefined}
|
||||
onHidden={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover") : undefined}
|
||||
mobileBackdrop
|
||||
>
|
||||
|
||||
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
|
||||
@@ -107,8 +105,7 @@ function BrowserOnlyOptions() {
|
||||
|
||||
function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
|
||||
return <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem disabled>Development Options</FormListItem>
|
||||
<FormListHeader text="Development Options"></FormListHeader>
|
||||
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={dropStart}>
|
||||
{experimentalFeatures.map((feature) => (
|
||||
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.note-list-widget {
|
||||
min-height: 0;
|
||||
max-width: var(--max-content-width); /* Inherited from .note-split */
|
||||
|
||||
|
||||
overflow: auto;
|
||||
contain: none !important;
|
||||
}
|
||||
@@ -11,10 +11,6 @@ body.prefers-centered-content .note-list-widget:not(.full-height) {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.note-list-widget .note-list {
|
||||
padding-block: 10px;
|
||||
}
|
||||
|
||||
.note-list-widget.full-height,
|
||||
.note-list-widget.full-height .note-list-widget-content {
|
||||
height: 100%;
|
||||
@@ -25,8 +21,12 @@ body.prefers-centered-content .note-list-widget:not(.full-height) {
|
||||
}
|
||||
|
||||
/* #region Pagination */
|
||||
.note-list-pager span.current-page {
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
.note-list-pager {
|
||||
font-size: 1rem;
|
||||
|
||||
span.current-page {
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
/* #endregion */
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
position: relative;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
overflow-x: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
--card-font-size: 0.9em;
|
||||
--card-line-height: 1.2;
|
||||
@@ -19,8 +20,10 @@ body.mobile .board-view {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
padding: 1em;
|
||||
padding-inline: 12px;
|
||||
padding-block: 4px;
|
||||
align-items: flex-start;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.board-view-container .board-column {
|
||||
@@ -352,4 +355,4 @@ body.mobile .board-view-container .board-column {
|
||||
font-size: 0.9em;
|
||||
max-width: 200px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import "./index.css";
|
||||
import { ColumnMap, getBoardData } from "./data";
|
||||
|
||||
import { createContext, TargetedKeyboardEvent } from "preact";
|
||||
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { t } from "../../../services/i18n";
|
||||
import toast from "../../../services/toast";
|
||||
import CollectionProperties from "../../note_bars/CollectionProperties";
|
||||
import FormTextArea from "../../react/FormTextArea";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import { useNoteLabelBoolean, useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import { t } from "../../../services/i18n";
|
||||
import Api from "./api";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import { createContext, TargetedKeyboardEvent } from "preact";
|
||||
import { onWheelHorizontalScroll } from "../../widget_utils";
|
||||
import Column from "./column";
|
||||
import BoardApi from "./api";
|
||||
import FormTextArea from "../../react/FormTextArea";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import NoteAutocomplete from "../../react/NoteAutocomplete";
|
||||
import toast from "../../../services/toast";
|
||||
import { onWheelHorizontalScroll } from "../../widget_utils";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import Api from "./api";
|
||||
import BoardApi from "./api";
|
||||
import Column from "./column";
|
||||
import { ColumnMap, getBoardData } from "./data";
|
||||
|
||||
export interface BoardViewData {
|
||||
columns?: BoardColumnData[];
|
||||
@@ -145,7 +148,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
||||
const insertBefore = mouseX < columnMiddle;
|
||||
|
||||
// Calculate the target position
|
||||
let targetIndex = insertBefore ? index : index + 1;
|
||||
const targetIndex = insertBefore ? index : index + 1;
|
||||
|
||||
setColumnDropPosition(targetIndex);
|
||||
}, [draggedColumn]);
|
||||
@@ -159,15 +162,14 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
||||
}, [draggedColumn, columnDropPosition, handleColumnDrop]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="board-view"
|
||||
onWheel={onWheelHorizontalScroll}
|
||||
>
|
||||
<div className="board-view">
|
||||
<CollectionProperties note={parentNote} />
|
||||
<BoardViewContext.Provider value={boardViewContext}>
|
||||
{byColumn && columns && <div
|
||||
className="board-view-container"
|
||||
onDragOver={handleColumnDragOver}
|
||||
onDrop={handleContainerDrop}
|
||||
onWheel={onWheelHorizontalScroll}
|
||||
>
|
||||
{columns.map((column, index) => (
|
||||
<>
|
||||
@@ -194,7 +196,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
|
||||
</div>}
|
||||
</BoardViewContext.Provider>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMode: boolean }) {
|
||||
@@ -218,26 +220,26 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
|
||||
tabIndex={300}
|
||||
>
|
||||
{!isCreatingNewColumn
|
||||
? <>
|
||||
<Icon icon="bx bx-plus" />{" "}
|
||||
{t("board_view.add-column")}
|
||||
</>
|
||||
: (
|
||||
<TitleEditor
|
||||
placeholder={t("board_view.add-column-placeholder")}
|
||||
save={async (columnName) => {
|
||||
const created = await api.addNewColumn(columnName);
|
||||
if (!created) {
|
||||
toast.showMessage(t("board_view.column-already-exists"), undefined, "bx bx-duplicate");
|
||||
}
|
||||
}}
|
||||
dismiss={() => setIsCreatingNewColumn(false)}
|
||||
isNewItem
|
||||
mode={isInRelationMode ? "relation" : "normal"}
|
||||
/>
|
||||
)}
|
||||
? <>
|
||||
<Icon icon="bx bx-plus" />{" "}
|
||||
{t("board_view.add-column")}
|
||||
</>
|
||||
: (
|
||||
<TitleEditor
|
||||
placeholder={t("board_view.add-column-placeholder")}
|
||||
save={async (columnName) => {
|
||||
const created = await api.addNewColumn(columnName);
|
||||
if (!created) {
|
||||
toast.showMessage(t("board_view.column-already-exists"), undefined, "bx bx-duplicate");
|
||||
}
|
||||
}}
|
||||
dismiss={() => setIsCreatingNewColumn(false)}
|
||||
isNewItem
|
||||
mode={isInRelationMode ? "relation" : "normal"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
|
||||
@@ -302,26 +304,26 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NoteAutocomplete
|
||||
inputRef={inputRef}
|
||||
noteId={currentValue ?? ""}
|
||||
opts={{
|
||||
hideAllButtons: true,
|
||||
allowCreatingNotes: true
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
dismiss();
|
||||
}
|
||||
}}
|
||||
onBlur={() => dismiss()}
|
||||
noteIdChanged={(newValue) => {
|
||||
save(newValue);
|
||||
dismiss();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NoteAutocomplete
|
||||
inputRef={inputRef}
|
||||
noteId={currentValue ?? ""}
|
||||
opts={{
|
||||
hideAllButtons: true,
|
||||
allowCreatingNotes: true
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
dismiss();
|
||||
}
|
||||
}}
|
||||
onBlur={() => dismiss()}
|
||||
noteIdChanged={(newValue) => {
|
||||
save(newValue);
|
||||
dismiss();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,16 @@
|
||||
outline: 0;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 10px;
|
||||
padding: 0;
|
||||
|
||||
@media (max-width: 991px) {
|
||||
padding: 0;
|
||||
|
||||
th {
|
||||
font-weight: normal;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-view a,
|
||||
@@ -43,6 +52,7 @@
|
||||
--fc-border-color: var(--main-border-color);
|
||||
--fc-neutral-bg-color: var(--launcher-pane-background-color);
|
||||
--fc-list-event-hover-bg-color: var(--left-pane-item-hover-background);
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.calendar-container .fc-list-sticky .fc-list-day > * {
|
||||
@@ -59,33 +69,35 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* #region Header */
|
||||
.calendar-view .calendar-header {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.calendar-view .collection-properties {
|
||||
.center-container {
|
||||
justify-content: center;
|
||||
|
||||
.calendar-view .calendar-header .btn {
|
||||
min-width: unset !important;
|
||||
}
|
||||
.title {
|
||||
min-width: 150px;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-view .calendar-header > .title {
|
||||
flex-grow: 1;
|
||||
font-size: 1.3rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
>div {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
body.desktop:not(.zen):not(.experimental-feature-new-layout) .calendar-view .calendar-header {
|
||||
padding-block-start: 4px;
|
||||
padding-inline-end: 5em;
|
||||
}
|
||||
.right-container {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.search-result-widget-content .calendar-view .calendar-header {
|
||||
padding-inline-end: unset !important;
|
||||
.center-container {
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
min-width: 110px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Events */
|
||||
|
||||
@@ -93,7 +105,7 @@ body.desktop:not(.zen):not(.experimental-feature-new-layout) .calendar-view .cal
|
||||
* week, month, year views
|
||||
*/
|
||||
|
||||
.calendar-container a.fc-event {
|
||||
.calendar-container a.fc-event {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ import dialog from "../../../services/dialog";
|
||||
import froca from "../../../services/froca";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { isMobile } from "../../../services/utils";
|
||||
import CollectionProperties from "../../note_bars/CollectionProperties";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import Button, { ButtonGroup } from "../../react/Button";
|
||||
import Dropdown from "../../react/Dropdown";
|
||||
import { FormListItem } from "../../react/FormList";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
|
||||
import { ParentComponent } from "../../react/react_utils";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||
@@ -41,24 +44,28 @@ const CALENDAR_VIEWS = [
|
||||
{
|
||||
type: "timeGridWeek",
|
||||
name: t("calendar.week"),
|
||||
icon: "bx bx-calendar-week",
|
||||
previousText: t("calendar.week_previous"),
|
||||
nextText: t("calendar.week_next")
|
||||
},
|
||||
{
|
||||
type: "dayGridMonth",
|
||||
name: t("calendar.month"),
|
||||
icon: "bx bx-calendar",
|
||||
previousText: t("calendar.month_previous"),
|
||||
nextText: t("calendar.month_next")
|
||||
},
|
||||
{
|
||||
type: "multiMonthYear",
|
||||
name: t("calendar.year"),
|
||||
icon: "bx bx-layer",
|
||||
previousText: t("calendar.year_previous"),
|
||||
nextText: t("calendar.year_next")
|
||||
},
|
||||
{
|
||||
type: "listMonth",
|
||||
name: t("calendar.list"),
|
||||
icon: "bx bx-list-ol",
|
||||
previousText: t("calendar.month_previous"),
|
||||
nextText: t("calendar.month_next")
|
||||
}
|
||||
@@ -140,7 +147,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
|
||||
return (plugins &&
|
||||
<div className="calendar-view" ref={containerRef} tabIndex={100}>
|
||||
<CalendarHeader calendarRef={calendarRef} />
|
||||
<CalendarCollectionProperties note={note} calendarRef={calendarRef} />
|
||||
<Calendar
|
||||
events={eventBuilder}
|
||||
calendarRef={calendarRef}
|
||||
@@ -169,28 +176,67 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarHeader({ calendarRef }: { calendarRef: RefObject<FullCalendar> }) {
|
||||
function CalendarCollectionProperties({ note, calendarRef }: {
|
||||
note: FNote;
|
||||
calendarRef: RefObject<FullCalendar>;
|
||||
}) {
|
||||
const { title, viewType: currentViewType } = useOnDatesSet(calendarRef);
|
||||
const currentViewData = CALENDAR_VIEWS.find(v => calendarRef.current && v.type === currentViewType);
|
||||
const isMobileLocal = isMobile();
|
||||
|
||||
return (
|
||||
<div className="calendar-header">
|
||||
<span className="title">{title}</span>
|
||||
<CollectionProperties
|
||||
note={note}
|
||||
centerChildren={<>
|
||||
<ActionButton icon="bx bx-chevron-left" text={currentViewData?.previousText ?? ""} onClick={() => calendarRef.current?.prev()} />
|
||||
<span className="title">{title}</span>
|
||||
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} onClick={() => calendarRef.current?.next()} />
|
||||
<Button text={t("calendar.today")} onClick={() => calendarRef.current?.today()} />
|
||||
{isMobileLocal && <MobileCalendarViewSwitcher calendarRef={calendarRef} />}
|
||||
</>}
|
||||
rightChildren={<>
|
||||
{!isMobileLocal && <DesktopCalendarViewSwitcher calendarRef={calendarRef} />}
|
||||
</>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopCalendarViewSwitcher({ calendarRef }: { calendarRef: RefObject<FullCalendar> }) {
|
||||
const { viewType: currentViewType } = useOnDatesSet(calendarRef);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup>
|
||||
{CALENDAR_VIEWS.map(viewData => (
|
||||
<Button
|
||||
key={viewData.type}
|
||||
text={viewData.name}
|
||||
className={currentViewType === viewData.type ? "active" : ""}
|
||||
onClick={() => calendarRef.current?.changeView(viewData.type)}
|
||||
/>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
<Button text={t("calendar.today")} onClick={() => calendarRef.current?.today()} />
|
||||
<ButtonGroup>
|
||||
<ActionButton icon="bx bx-chevron-left" text={currentViewData?.previousText ?? ""} frame onClick={() => calendarRef.current?.prev()} />
|
||||
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileCalendarViewSwitcher({ calendarRef }: { calendarRef: RefObject<FullCalendar> }) {
|
||||
const { viewType: currentViewType } = useOnDatesSet(calendarRef);
|
||||
const currentViewTypeData = CALENDAR_VIEWS.find(view => view.type === currentViewType);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
text={currentViewTypeData?.name}
|
||||
>
|
||||
{CALENDAR_VIEWS.map(viewData => (
|
||||
<FormListItem
|
||||
key={viewData.type}
|
||||
selected={currentViewType === viewData.type}
|
||||
icon={viewData.icon}
|
||||
onClick={() => calendarRef.current?.changeView(viewData.type)}
|
||||
>{viewData.name}</FormListItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .collection-properties {
|
||||
position: relative;
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
||||
.geo-map-container {
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import Map from "./map";
|
||||
import "./index.css";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
|
||||
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
|
||||
|
||||
import { divIcon, GPXOptions, LatLng, LeafletMouseEvent } from "leaflet";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import Marker, { GpxTrack } from "./marker";
|
||||
import froca from "../../../services/froca";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
||||
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import { createNewNote, moveMarker } from "./api";
|
||||
import openContextMenu, { openMapContextMenu } from "./context_menu";
|
||||
import toast from "../../../services/toast";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import branches from "../../../services/branches";
|
||||
import froca from "../../../services/froca";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import branches from "../../../services/branches";
|
||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSlider } from "../../react/TouchBar";
|
||||
import toast from "../../../services/toast";
|
||||
import CollectionProperties from "../../note_bars/CollectionProperties";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { ButtonOrActionButton } from "../../react/Button";
|
||||
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
|
||||
import { ParentComponent } from "../../react/react_utils";
|
||||
import TouchBar, { TouchBarButton, TouchBarSlider } from "../../react/TouchBar";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { createNewNote, moveMarker } from "./api";
|
||||
import openContextMenu, { openMapContextMenu } from "./context_menu";
|
||||
import Map from "./map";
|
||||
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
|
||||
import Marker, { GpxTrack } from "./marker";
|
||||
|
||||
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
|
||||
const DEFAULT_ZOOM = 2;
|
||||
@@ -50,7 +55,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
useEffect(() => { froca.getNotes(noteIds).then(setNotes) }, [ noteIds ]);
|
||||
useEffect(() => { froca.getNotes(noteIds).then(setNotes); }, [ noteIds ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!note) return;
|
||||
@@ -60,7 +65,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
|
||||
// Note creation.
|
||||
useTriliumEvent("geoMapCreateChildNote", () => {
|
||||
toast.showPersistent({
|
||||
toast.showPersistent({
|
||||
icon: "plus",
|
||||
id: "geo-new-note",
|
||||
title: "New note",
|
||||
@@ -130,6 +135,19 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
|
||||
return (
|
||||
<div className={`geo-view ${state === State.NewNote ? "placing-note" : ""}`}>
|
||||
<CollectionProperties
|
||||
note={note}
|
||||
rightChildren={<>
|
||||
<ToggleReadOnlyButton note={note} />
|
||||
<ButtonOrActionButton
|
||||
icon="bx bx-plus"
|
||||
text={t("geo-map.create-child-note-text")}
|
||||
title={t("geo-map.create-child-note-title")}
|
||||
triggerCommand="geoMapCreateChildNote"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</>}
|
||||
/>
|
||||
{ coordinates !== undefined && zoom !== undefined && <Map
|
||||
apiRef={apiRef} containerRef={containerRef}
|
||||
coordinates={coordinates}
|
||||
@@ -151,6 +169,16 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleReadOnlyButton({ note }: { note: FNote }) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
|
||||
return <ActionButton
|
||||
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
|
||||
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
|
||||
onClick={() => setReadOnly(!isReadOnly)}
|
||||
/>;
|
||||
}
|
||||
|
||||
function NoteWrapper({ note, isReadOnly }: { note: FNote, isReadOnly: boolean }) {
|
||||
const mime = useNoteProperty(note, "mime");
|
||||
const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE);
|
||||
@@ -204,7 +232,7 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
|
||||
onDragged={editable ? onDragged : undefined}
|
||||
onClick={!editable ? onClick : undefined}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
/>;
|
||||
}
|
||||
|
||||
function NoteGpxTrack({ note }: { note: FNote }) {
|
||||
@@ -238,7 +266,7 @@ function NoteGpxTrack({ note }: { note: FNote }) {
|
||||
color: note.getLabelValue("color") ?? "blue"
|
||||
}
|
||||
}), [ color, iconClass ]);
|
||||
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />
|
||||
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />;
|
||||
}
|
||||
|
||||
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
|
||||
@@ -292,5 +320,5 @@ function GeoMapTouchBar({ state, map }: { state: State, map: L.Map | null | unde
|
||||
enabled={state === State.Normal}
|
||||
/>
|
||||
</TouchBar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import attribute_renderer from "../../../services/attribute_renderer";
|
||||
import content_renderer from "../../../services/content_renderer";
|
||||
import { t } from "../../../services/i18n";
|
||||
import link from "../../../services/link";
|
||||
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
|
||||
import CollectionProperties from "../../note_bars/CollectionProperties";
|
||||
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean, useNoteProperty } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import NoteLink from "../../react/NoteLink";
|
||||
import { ViewModeProps } from "../interface";
|
||||
@@ -19,11 +20,18 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
|
||||
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
|
||||
const { pageNotes, ...pagination } = usePagination(note, noteIds);
|
||||
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const hasCollectionProperties = [ "book", "search" ].includes(noteType ?? "");
|
||||
|
||||
return (
|
||||
<div class="note-list list-view">
|
||||
<CollectionProperties
|
||||
note={note}
|
||||
centerChildren={<Pager {...pagination} />}
|
||||
/>
|
||||
|
||||
{ noteIds.length > 0 && <div class="note-list-wrapper">
|
||||
<Pager {...pagination} />
|
||||
{!hasCollectionProperties && <Pager {...pagination} />}
|
||||
|
||||
<div class="note-list-container use-tn-links">
|
||||
{pageNotes?.map(childNote => (
|
||||
@@ -45,11 +53,18 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
|
||||
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
|
||||
const { pageNotes, ...pagination } = usePagination(note, noteIds);
|
||||
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const hasCollectionProperties = [ "book", "search" ].includes(noteType ?? "");
|
||||
|
||||
return (
|
||||
<div class="note-list grid-view">
|
||||
<CollectionProperties
|
||||
note={note}
|
||||
centerChildren={<Pager {...pagination} />}
|
||||
/>
|
||||
|
||||
<div class="note-list-wrapper">
|
||||
<Pager {...pagination} />
|
||||
{!hasCollectionProperties && <Pager {...pagination} />}
|
||||
|
||||
<div class="note-list-container use-tn-links">
|
||||
{pageNotes?.map(childNote => (
|
||||
@@ -140,7 +155,7 @@ function NoteAttributes({ note }: { note: FNote }) {
|
||||
return <span className="note-list-attributes" ref={ref} />;
|
||||
}
|
||||
|
||||
function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
|
||||
export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
|
||||
note: FNote;
|
||||
trim?: boolean;
|
||||
noChildrenList?: boolean;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
.presentation-button-bar {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
|
||||
.floating-buttons-children {
|
||||
top: 0;
|
||||
}
|
||||
.presentation-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.presentation-container {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { ViewModeMedia, ViewModeProps } from "../interface";
|
||||
import "./index.css";
|
||||
|
||||
import { RefObject } from "preact";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
import Reveal from "reveal.js";
|
||||
import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw";
|
||||
import slideCustomStylesheet from "./slidejs.css?raw";
|
||||
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
|
||||
import ShadowDom from "../../react/ShadowDom";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import "./index.css";
|
||||
import { RefObject } from "preact";
|
||||
|
||||
import { openInCurrentNoteContext } from "../../../components/note_context";
|
||||
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { t } from "../../../services/i18n";
|
||||
import CollectionProperties from "../../note_bars/CollectionProperties";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
|
||||
import ShadowDom from "../../react/ShadowDom";
|
||||
import { ViewModeMedia, ViewModeProps } from "../interface";
|
||||
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
|
||||
import slideCustomStylesheet from "./slidejs.css?raw";
|
||||
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
|
||||
|
||||
export default function PresentationView({ note, noteIds, media, onReady, onProgressChanged }: ViewModeProps<{}>) {
|
||||
const [ presentation, setPresentation ] = useState<PresentationModel>();
|
||||
@@ -51,14 +54,17 @@ export default function PresentationView({ note, noteIds, media, onReady, onProg
|
||||
|
||||
if (media === "screen") {
|
||||
return (
|
||||
<>
|
||||
<div class="presentation-view">
|
||||
<CollectionProperties
|
||||
note={note}
|
||||
rightChildren={<ButtonOverlay containerRef={containerRef} api={api} />}
|
||||
/>
|
||||
<ShadowDom
|
||||
className="presentation-container"
|
||||
containerRef={containerRef}
|
||||
>{content}</ShadowDom>
|
||||
<ButtonOverlay containerRef={containerRef} api={api} />
|
||||
</>
|
||||
)
|
||||
</div>
|
||||
);
|
||||
} else if (media === "print") {
|
||||
// Printing needs a query parameter that is read by Reveal.js.
|
||||
const url = new URL(window.location.href);
|
||||
@@ -108,42 +114,34 @@ function ButtonOverlay({ containerRef, api }: { containerRef: RefObject<HTMLDivE
|
||||
}, [ api ]);
|
||||
|
||||
return (
|
||||
<div className="presentation-button-bar">
|
||||
<div className="floating-buttons-children">
|
||||
<ActionButton
|
||||
className="floating-button"
|
||||
icon="bx bx-edit"
|
||||
text={t("presentation_view.edit-slide")}
|
||||
noIconActionClass
|
||||
onClick={e => {
|
||||
const currentSlide = api?.getCurrentSlide();
|
||||
const noteId = getNoteIdFromSlide(currentSlide);
|
||||
<>
|
||||
<ActionButton
|
||||
icon="bx bx-edit"
|
||||
text={t("presentation_view.edit-slide")}
|
||||
onClick={e => {
|
||||
const currentSlide = api?.getCurrentSlide();
|
||||
const noteId = getNoteIdFromSlide(currentSlide);
|
||||
|
||||
if (noteId) {
|
||||
openInCurrentNoteContext(e, noteId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
if (noteId) {
|
||||
openInCurrentNoteContext(e, noteId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
className="floating-button"
|
||||
icon="bx bx-grid-horizontal"
|
||||
text={t("presentation_view.slide-overview")}
|
||||
active={isOverviewActive}
|
||||
noIconActionClass
|
||||
onClick={() => api?.toggleOverview()}
|
||||
/>
|
||||
<ActionButton
|
||||
icon="bx bx-grid-horizontal"
|
||||
text={t("presentation_view.slide-overview")}
|
||||
active={isOverviewActive}
|
||||
onClick={() => api?.toggleOverview()}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
className="floating-button"
|
||||
icon="bx bx-fullscreen"
|
||||
text={t("presentation_view.start-presentation")}
|
||||
noIconActionClass
|
||||
onClick={() => containerRef.current?.requestFullscreen()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<ActionButton
|
||||
icon="bx bx-fullscreen"
|
||||
text={t("presentation_view.start-presentation")}
|
||||
onClick={() => containerRef.current?.requestFullscreen()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Presentation({ presentation, setApi } : { presentation: PresentationModel, setApi: (api: Reveal.Api | undefined) => void }) {
|
||||
@@ -179,7 +177,7 @@ function Presentation({ presentation, setApi } : { presentation: PresentationMod
|
||||
api.destroy();
|
||||
setRevealApi(undefined);
|
||||
setApi(undefined);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -191,19 +189,19 @@ function Presentation({ presentation, setApi } : { presentation: PresentationMod
|
||||
<div className="slides">
|
||||
{presentation.slides?.map(slide => {
|
||||
if (!slide.verticalSlides) {
|
||||
return <Slide key={slide.noteId} slide={slide} />
|
||||
} else {
|
||||
return (
|
||||
<section>
|
||||
<Slide key={slide.noteId} slide={slide} />
|
||||
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
|
||||
</section>
|
||||
);
|
||||
return <Slide key={slide.noteId} slide={slide} />;
|
||||
}
|
||||
return (
|
||||
<section>
|
||||
<Slide key={slide.noteId} slide={slide} />
|
||||
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
|
||||
</section>
|
||||
);
|
||||
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
position: relative;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 0 5px 0 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tabulator-tableholder {
|
||||
height: unset !important;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { TableData } from "./rows";
|
||||
import { useLegacyWidget } from "../../react/hooks";
|
||||
import Tabulator from "./tabulator";
|
||||
import { Tabulator as VanillaTabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule, Options, RowComponent} from 'tabulator-tables';
|
||||
import { useContextMenu } from "./context_menu";
|
||||
import { ParentComponent } from "../../react/react_utils";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { t } from "../../../services/i18n";
|
||||
import Button from "../../react/Button";
|
||||
import "./index.css";
|
||||
import useRowTableEditing from "./row_editing";
|
||||
import useColTableEditing from "./col_editing";
|
||||
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
|
||||
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { DataTreeModule, EditModule, FormatModule, FrozenColumnsModule, InteractionModule, MoveColumnsModule, MoveRowsModule, Options, PersistenceModule, ResizeColumnsModule, RowComponent,SortModule, Tabulator as VanillaTabulator} from 'tabulator-tables';
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import SpacedUpdate from "../../../services/spaced_update";
|
||||
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
|
||||
import CollectionProperties from "../../note_bars/CollectionProperties";
|
||||
import { ButtonOrActionButton } from "../../react/Button";
|
||||
import { useLegacyWidget } from "../../react/hooks";
|
||||
import { ParentComponent } from "../../react/react_utils";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import useColTableEditing from "./col_editing";
|
||||
import { useContextMenu } from "./context_menu";
|
||||
import useData, { TableConfig } from "./data";
|
||||
import useRowTableEditing from "./row_editing";
|
||||
import { TableData } from "./rows";
|
||||
import Tabulator from "./tabulator";
|
||||
|
||||
export default function TableView({ note, noteIds, notePath, viewConfig, saveConfig }: ViewModeProps<TableConfig>) {
|
||||
const tabulatorRef = useRef<VanillaTabulator>(null);
|
||||
@@ -36,7 +38,7 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
|
||||
dataTreeChildIndent: 20,
|
||||
dataTreeExpandElement: `<button class="tree-expand"><span class="bx bx-chevron-right"></span></button>`,
|
||||
dataTreeCollapseElement: `<button class="tree-collapse"><span class="bx bx-chevron-down"></span></button>`
|
||||
}
|
||||
};
|
||||
}, [ hasChildren ]);
|
||||
|
||||
const rowFormatter = useCallback((row: RowComponent) => {
|
||||
@@ -46,6 +48,16 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
|
||||
|
||||
return (
|
||||
<div className="table-view">
|
||||
<CollectionProperties
|
||||
note={note}
|
||||
rightChildren={note.type !== "search" &&
|
||||
<>
|
||||
<ButtonOrActionButton triggerCommand="addNewRow" icon="bx bx-plus" text={t("table_view.new-row")} />
|
||||
<ButtonOrActionButton triggerCommand="addNewTableColumn" icon="bx bx-carousel" text={t("table_view.new-column")} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{rowData !== undefined && persistenceProps && (
|
||||
<>
|
||||
<Tabulator
|
||||
@@ -54,7 +66,6 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
|
||||
columns={columnDefs ?? []}
|
||||
data={rowData}
|
||||
modules={[ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ]}
|
||||
footerElement={<TableFooter note={note} />}
|
||||
events={{
|
||||
...contextMenuEvents,
|
||||
...rowEditingEvents
|
||||
@@ -67,24 +78,11 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
|
||||
rowFormatter={rowFormatter}
|
||||
{...dataTreeProps}
|
||||
/>
|
||||
<TableFooter note={note} />
|
||||
</>
|
||||
)}
|
||||
{attributeDetailWidgetEl}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ note }: { note: FNote }) {
|
||||
return (note.type !== "search" &&
|
||||
<div className="tabulator-footer">
|
||||
<div className="tabulator-footer-contents">
|
||||
<Button triggerCommand="addNewRow" icon="bx bx-plus" text={t("table_view.new-row")} />
|
||||
{" "}
|
||||
<Button triggerCommand="addNewTableColumn" icon="bx bx-carousel" text={t("table_view.new-column")} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function usePersistence(viewConfig: TableConfig | null | undefined, saveConfig: (newConfig: TableConfig) => void) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { EventData } from "../../components/app_context.js";
|
||||
import { LOCALES } from "@triliumnext/commons";
|
||||
import { readCssVar } from "../../utils/css-var.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import options from "../../services/options.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
import { EventData } from "../../components/app_context.js";
|
||||
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
|
||||
import options from "../../services/options.js";
|
||||
import utils, { isMobile } from "../../services/utils.js";
|
||||
import { readCssVar } from "../../utils/css-var.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
|
||||
/**
|
||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||
@@ -88,7 +89,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
}
|
||||
|
||||
#setBackdropEffects() {
|
||||
const enabled = options.is("backdropEffectsEnabled");
|
||||
const enabled = options.is("backdropEffectsEnabled") && !isMobile();
|
||||
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
// Supports background effects
|
||||
|
||||
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
|
||||
.asBoolean(false);
|
||||
.asBoolean(false);
|
||||
|
||||
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
.scrolling-container {
|
||||
--content-margin-inline: 24px;
|
||||
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
position: relative;
|
||||
|
||||
> .inline-title,
|
||||
> .note-detail > .note-detail-editable-text,
|
||||
> .note-list-widget:not(.full-height) {
|
||||
padding-inline: 24px;
|
||||
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor,
|
||||
> .note-list-widget:not(.full-height) .note-list-wrapper {
|
||||
margin-inline: var(--content-margin-inline);
|
||||
}
|
||||
|
||||
> .inline-title {
|
||||
padding-inline: var(--content-margin-inline);
|
||||
}
|
||||
|
||||
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor {
|
||||
overflow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.note-split.type-code:not(.mime-text-x-sqlite) {
|
||||
|
||||
@@ -91,8 +91,9 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-editable-text {
|
||||
padding: 0 1em;
|
||||
.modal.popup-editor-dialog .note-detail-editable-text-editor {
|
||||
margin: 0 28px;
|
||||
overflow: visible; /* Allow selection rectangle to go outside of the editor area */
|
||||
}
|
||||
|
||||
.modal.popup-editor-dialog .note-detail-file {
|
||||
|
||||
@@ -5,13 +5,15 @@ import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import tree from "../../services/tree";
|
||||
import utils from "../../services/utils";
|
||||
import NoteList from "../collections/NoteList";
|
||||
import FloatingButtons from "../FloatingButtons";
|
||||
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
|
||||
import { DESKTOP_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
|
||||
import NoteBadges from "../layout/NoteBadges";
|
||||
import NoteIcon from "../note_icon";
|
||||
import NoteTitleWidget from "../note_title";
|
||||
import NoteDetail from "../NoteDetail";
|
||||
@@ -23,8 +25,6 @@ import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
|
||||
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
|
||||
import FormattingToolbar from "../ribbon/FormattingToolbar";
|
||||
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
|
||||
import NoteBadges from "../layout/NoteBadges";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function PopupEditor() {
|
||||
const [ noteContext, setNoteContext ] = useState(new NoteContext("_popup-editor"));
|
||||
const isMobile = utils.isMobile();
|
||||
const items = useMemo(() => {
|
||||
const baseItems = isMobile ? MOBILE_FLOATING_BUTTONS : DESKTOP_FLOATING_BUTTONS;
|
||||
const baseItems = isMobile ? [] : DESKTOP_FLOATING_BUTTONS;
|
||||
return baseItems.filter(item => !POPUP_HIDDEN_FLOATING_BUTTONS.includes(item));
|
||||
}, [ isMobile ]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import appContext from "../../components/app_context";
|
||||
import { t } from "../../services/i18n";
|
||||
import options from "../../services/options";
|
||||
import utils from "../../services/utils";
|
||||
import utils, { isMac } from "../../services/utils";
|
||||
|
||||
/**
|
||||
* A "call-to-action" is an interactive message for the user, generally to present new features.
|
||||
@@ -41,10 +41,6 @@ export interface CallToAction {
|
||||
}[];
|
||||
}
|
||||
|
||||
function isNextTheme() {
|
||||
return [ "next", "next-light", "next-dark" ].includes(options.get("theme"));
|
||||
}
|
||||
|
||||
const CALL_TO_ACTIONS: CallToAction[] = [
|
||||
{
|
||||
id: "new_layout",
|
||||
@@ -63,7 +59,7 @@ const CALL_TO_ACTIONS: CallToAction[] = [
|
||||
id: "background_effects",
|
||||
title: t("call_to_action.background_effects_title"),
|
||||
message: t("call_to_action.background_effects_message"),
|
||||
enabled: () => false,
|
||||
enabled: () => (isMac() && !options.is("backgroundEffects")),
|
||||
buttons: [
|
||||
{
|
||||
text: t("call_to_action.background_effects_button"),
|
||||
@@ -78,7 +74,7 @@ const CALL_TO_ACTIONS: CallToAction[] = [
|
||||
id: "next_theme",
|
||||
title: t("call_to_action.next_theme_title"),
|
||||
message: t("call_to_action.next_theme_message"),
|
||||
enabled: () => !isNextTheme(),
|
||||
enabled: () => ![ "next", "next-light", "next-dark" ].includes(options.get("theme")),
|
||||
buttons: [
|
||||
{
|
||||
text: t("call_to_action.next_theme_button"),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useLayoutEffect, useState } from "preact/hooks";
|
||||
import FNote from "../../entities/fnote";
|
||||
import froca from "../../services/froca";
|
||||
import { isDesktop, isMobile } from "../../services/utils";
|
||||
import TabSwitcher from "../mobile_widgets/TabSwitcher";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { onWheelHorizontalScroll } from "../widget_utils";
|
||||
import BookmarkButtons from "./BookmarkButtons";
|
||||
@@ -97,6 +98,8 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
return <QuickSearchLauncherWidget />;
|
||||
case "aiChatLauncher":
|
||||
return <AiChatButton launcherNote={note} />;
|
||||
case "mobileTabSwitcher":
|
||||
return <TabSwitcher />;
|
||||
default:
|
||||
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from "clsx";
|
||||
import { createContext } from "preact";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
@@ -18,12 +19,12 @@ export interface LauncherNoteProps {
|
||||
launcherNote: FNote;
|
||||
}
|
||||
|
||||
export function LaunchBarActionButton(props: Omit<ActionButtonProps, "className" | "noIconActionClass" | "titlePosition">) {
|
||||
export function LaunchBarActionButton({ className, ...props }: Omit<ActionButtonProps, "noIconActionClass" | "titlePosition">) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
className="button-widget launcher-button"
|
||||
className={clsx("button-widget launcher-button", className)}
|
||||
noIconActionClass
|
||||
titlePosition={isHorizontalLayout ? "bottom" : "right"}
|
||||
{...props}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Tooltip } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { ComponentChild } from "preact";
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import type React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import "./NoteTitleActions.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import CollectionProperties from "../note_bars/CollectionProperties";
|
||||
import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail";
|
||||
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
|
||||
import SimpleBadge from "../react/Badge";
|
||||
import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible";
|
||||
import { useNoteContext, useNoteLabel, useNoteProperty, useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
|
||||
import NoteLink, { NewNoteLink } from "../react/NoteLink";
|
||||
import { NewNoteLink } from "../react/NoteLink";
|
||||
import { useEditedNotes } from "../ribbon/EditedNotesTab";
|
||||
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
|
||||
import NoteTypeSwitcher from "./NoteTypeSwitcher";
|
||||
|
||||
export default function NoteTitleActions() {
|
||||
const { note, ntxId, componentId, noteContext } = useNoteContext();
|
||||
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
|
||||
const { note, ntxId, componentId, noteContext, viewScope } = useNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
|
||||
return (
|
||||
<div className="title-actions">
|
||||
<PromotedAttributes note={note} componentId={componentId} noteContext={noteContext} />
|
||||
{noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />}
|
||||
{!isHiddenNote && note && noteType === "book" && <CollectionProperties note={note} />}
|
||||
<EditedNotes />
|
||||
<NoteTypeSwitcher />
|
||||
{viewScope?.viewMode === "default" && <NoteTypeSwitcher />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,7 +44,7 @@ function PromotedAttributes({ note, componentId, noteContext }: {
|
||||
componentId: string,
|
||||
noteContext: NoteContext | undefined
|
||||
}) {
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
|
||||
const [ expanded, setExpanded ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -91,68 +91,6 @@
|
||||
.note-paths-widget {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.note-path-intro {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.note-path-list {
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
/* Note path card */
|
||||
li {
|
||||
--border-radius: 6px;
|
||||
|
||||
position: relative;
|
||||
background: var(--card-background-color);
|
||||
padding: 8px 20px 8px 25px;
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
& + li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Current path arrow */
|
||||
&.path-current::before {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
content: "\ee8f";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
bottom: 0;
|
||||
font-family: "boxicons";
|
||||
font-size: .75em;
|
||||
color: var(--menu-item-icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Note path segment */
|
||||
a {
|
||||
margin-inline: 2px;
|
||||
padding-inline: 2px;
|
||||
color: currentColor;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
|
||||
/* The last segment of the note path */
|
||||
&.basename {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.backlinks-widget > .dropdown-menu {
|
||||
@@ -160,84 +98,6 @@
|
||||
|
||||
max-height: 60vh;
|
||||
overflow-y: scroll;
|
||||
|
||||
/* Backlink card */
|
||||
li {
|
||||
--border-radius: 8px;
|
||||
|
||||
max-width: 600px;
|
||||
padding: 10px 20px;
|
||||
background: var(--card-background-color);
|
||||
|
||||
& + li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
/* Card header */
|
||||
& > span:first-child {
|
||||
display: block;
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
/* Note path */
|
||||
> small {
|
||||
flex: 100%;
|
||||
order: -1;
|
||||
font-size: .65rem;
|
||||
|
||||
.note-path {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Note icon */
|
||||
> .tn-icon {
|
||||
color: var(--menu-item-icon-color);
|
||||
}
|
||||
|
||||
/* Note title */
|
||||
> a {
|
||||
margin-inline-start: 4px;
|
||||
color: currentColor;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Card content - excerpt */
|
||||
& > span:nth-child(2) > div {
|
||||
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
|
||||
display: block;
|
||||
|
||||
margin: 8px 0;
|
||||
border-radius: 4px;
|
||||
background: var(--quick-search-result-content-background);
|
||||
padding: 8px;
|
||||
font-size: .75rem;
|
||||
|
||||
a {
|
||||
background: transparent;
|
||||
color: var(--quick-search-result-highlight-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function StatusBar() {
|
||||
similarNotesShown: activePane === "similar-notes",
|
||||
setSimilarNotesShown: (shown) => setActivePane(shown && "similar-notes")
|
||||
};
|
||||
const isHiddenNote = note?.isInHiddenSubtree();
|
||||
const isHiddenNote = note?.isHiddenCompletely();
|
||||
|
||||
return (
|
||||
<div className="status-bar">
|
||||
@@ -300,7 +300,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) {
|
||||
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
return (note && count > 0 &&
|
||||
<StatusBarDropdown
|
||||
className="backlinks-badge backlinks-widget"
|
||||
className="backlinks-badge backlinks-widget tn-backlinks-widget"
|
||||
icon="bx bx-link"
|
||||
text={t("status_bar.backlinks", { count })}
|
||||
title={t("status_bar.backlinks_title", { count })}
|
||||
|
||||
139
apps/client/src/widgets/mobile_widgets/TabSwitcher.css
Normal file
139
apps/client/src/widgets/mobile_widgets/TabSwitcher.css
Normal file
@@ -0,0 +1,139 @@
|
||||
#launcher-container .mobile-tab-switcher {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: attr(data-tab-count);
|
||||
font-family: var(--main-font-family);
|
||||
font-size: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.modal.tab-bar-modal {
|
||||
.modal-dialog {
|
||||
min-height: 85vh;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1em;
|
||||
|
||||
@media (min-width: 850px) {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.tab-card {
|
||||
background: var(--card-background-color);
|
||||
border-radius: 1em;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.with-hue {
|
||||
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
|
||||
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
outline: 4px solid var(--more-accented-background-color);
|
||||
background: var(--card-background-hover-color);
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 0.4em 0.5em;
|
||||
border-bottom: 1px solid rgba(150, 150, 150, 0.1);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
color: var(--custom-color, inherit);
|
||||
flex-shrink: 0;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 1px solid rgba(150, 150, 150, 0.1);
|
||||
}
|
||||
|
||||
>.tn-icon {
|
||||
margin-inline-end: 0.4em;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
font-size: 0.9em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.icon-action {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-preview {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 0.5em;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
|
||||
&.type-text {
|
||||
padding: 10px;
|
||||
--ck-content-todo-list-checkmark-size: 8px;
|
||||
|
||||
p { margin-bottom: 0.2em;}
|
||||
hr { margin-block: 0.1em; height: 1px; }
|
||||
h2 { font-size: 1.20em; }
|
||||
h3 { font-size: 1.15em; }
|
||||
h4 { font-size: 1.10em; }
|
||||
h5 { font-size: 1.05em}
|
||||
h6 { font-size: 1em; }
|
||||
ul, ol { margin: 0 }
|
||||
}
|
||||
|
||||
&.type-book,
|
||||
&.type-contentWidget,
|
||||
&.type-search,
|
||||
&.type-empty,
|
||||
&.type-relationMap,
|
||||
&.type-launcher,
|
||||
&.tab-preview-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25em;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
font-size: 500%;
|
||||
}
|
||||
}
|
||||
|
||||
&.with-split {
|
||||
.preview-placeholder {
|
||||
font-size: 250%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
.tn-link {
|
||||
color: var(--main-text-color);
|
||||
width: 40%;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
267
apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx
Normal file
267
apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import "./TabSwitcher.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ComponentChild } from "preact";
|
||||
import { createPortal, Fragment } from "preact/compat";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import contextMenu from "../../menus/context_menu";
|
||||
import { getHue, parseColor } from "../../services/css_class_manager";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import type { ViewMode, ViewScope } from "../../services/link";
|
||||
import { NoteContent } from "../collections/legacy/ListOrGridView";
|
||||
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
|
||||
import { ICON_MAPPINGS } from "../note_bars/CollectionProperties";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import LinkButton from "../react/LinkButton";
|
||||
import Modal from "../react/Modal";
|
||||
|
||||
const VIEW_MODE_ICON_MAPPINGS: Record<Exclude<ViewMode, "default">, string> = {
|
||||
source: "bx bx-code",
|
||||
"contextual-help": "bx bx-help-circle",
|
||||
"note-map": "bx bxs-network-chart",
|
||||
attachments: "bx bx-paperclip",
|
||||
};
|
||||
|
||||
export default function TabSwitcher() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const mainNoteContexts = useMainNoteContexts();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LaunchBarActionButton
|
||||
className="mobile-tab-switcher"
|
||||
icon="bx bx-rectangle"
|
||||
text="Tabs"
|
||||
onClick={() => setShown(true)}
|
||||
data-tab-count={mainNoteContexts.length > 99 ? "∞" : mainNoteContexts.length}
|
||||
/>
|
||||
{createPortal(<TabBarModal mainNoteContexts={mainNoteContexts} shown={shown} setShown={setShown} />, document.body)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TabBarModal({ mainNoteContexts, shown, setShown }: {
|
||||
mainNoteContexts: NoteContext[];
|
||||
shown: boolean;
|
||||
setShown: (newValue: boolean) => void;
|
||||
}) {
|
||||
const [ fullyShown, setFullyShown ] = useState(false);
|
||||
const selectTab = useCallback((noteContextToActivate: NoteContext) => {
|
||||
appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId);
|
||||
setShown(false);
|
||||
}, [ setShown ]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="tab-bar-modal"
|
||||
size="xl"
|
||||
title={t("mobile_tab_switcher.title", { count: mainNoteContexts.length})}
|
||||
show={shown}
|
||||
onShown={() => setFullyShown(true)}
|
||||
customTitleBarButtons={[
|
||||
{
|
||||
iconClassName: "bx bx-dots-vertical-rounded",
|
||||
title: t("mobile_tab_switcher.more_options"),
|
||||
onClick(e) {
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{ title: t("tab_row.new_tab"), command: "openNewTab", uiIcon: "bx bx-plus" },
|
||||
{ title: t("tab_row.reopen_last_tab"), command: "reopenLastTab", uiIcon: "bx bx-undo", enabled: appContext.tabManager.recentlyClosedTabs.length !== 0 },
|
||||
{ kind: "separator" },
|
||||
{ title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-trash destructive-action-icon" },
|
||||
],
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (command) {
|
||||
appContext.triggerCommand(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
]}
|
||||
footer={<>
|
||||
<LinkButton
|
||||
text={t("tab_row.new_tab")}
|
||||
onClick={() => {
|
||||
appContext.triggerCommand("openNewTab");
|
||||
setShown(false);
|
||||
}}
|
||||
/>
|
||||
</>}
|
||||
scrollable
|
||||
onHidden={() => {
|
||||
setShown(false);
|
||||
setFullyShown(false);
|
||||
}}
|
||||
>
|
||||
<TabBarModelContent mainNoteContexts={mainNoteContexts} selectTab={selectTab} shown={fullyShown} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function TabBarModelContent({ mainNoteContexts, selectTab, shown }: {
|
||||
mainNoteContexts: NoteContext[];
|
||||
shown: boolean;
|
||||
selectTab: (noteContextToActivate: NoteContext) => void;
|
||||
}) {
|
||||
const activeNoteContext = useActiveNoteContext();
|
||||
const tabRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
// Scroll to active tab.
|
||||
useEffect(() => {
|
||||
if (!shown || !activeNoteContext?.ntxId) return;
|
||||
const correspondingEl = tabRefs.current[activeNoteContext.ntxId];
|
||||
requestAnimationFrame(() => {
|
||||
correspondingEl?.scrollIntoView();
|
||||
});
|
||||
}, [ activeNoteContext, shown ]);
|
||||
|
||||
return (
|
||||
<div className="tabs">
|
||||
{mainNoteContexts.map((noteContext) => (
|
||||
<Tab
|
||||
key={noteContext.ntxId}
|
||||
noteContext={noteContext}
|
||||
activeNtxId={activeNoteContext.ntxId}
|
||||
selectTab={selectTab}
|
||||
containerRef={el => (tabRefs.current[noteContext.ntxId ?? ""] = el)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
|
||||
containerRef: (el: HTMLDivElement | null) => void;
|
||||
noteContext: NoteContext;
|
||||
selectTab: (noteContextToActivate: NoteContext) => void;
|
||||
activeNtxId: string | null | undefined;
|
||||
}) {
|
||||
const { note } = noteContext;
|
||||
const colorClass = note?.getColorClass() || '';
|
||||
const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext);
|
||||
const subContexts = noteContext.getSubContexts();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={clsx("tab-card", {
|
||||
active: noteContext.ntxId === activeNtxId,
|
||||
"with-hue": workspaceTabBackgroundColorHue !== undefined,
|
||||
"with-split": subContexts.length > 1
|
||||
})}
|
||||
onClick={() => selectTab(noteContext)}
|
||||
style={{
|
||||
"--bg-hue": workspaceTabBackgroundColorHue
|
||||
}}
|
||||
>
|
||||
{subContexts.map(subContext => (
|
||||
<Fragment key={subContext.ntxId}>
|
||||
<TabHeader noteContext={subContext} colorClass={colorClass} />
|
||||
<TabPreviewContent note={subContext.note} viewScope={subContext.viewScope} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabHeader({ noteContext, colorClass }: { noteContext: NoteContext, colorClass: string }) {
|
||||
const iconClass = useNoteIcon(noteContext.note);
|
||||
const [ navigationTitle, setNavigationTitle ] = useState<string | null>(null);
|
||||
|
||||
// Manage the title for read-only notes
|
||||
useEffect(() => {
|
||||
noteContext?.getNavigationTitle().then(setNavigationTitle);
|
||||
}, [noteContext]);
|
||||
|
||||
return (
|
||||
<header className={colorClass}>
|
||||
{noteContext.note && <Icon icon={iconClass} />}
|
||||
<span className="title">{navigationTitle ?? noteContext.note?.title ?? t("tab_row.new_tab")}</span>
|
||||
{noteContext.isMainContext() && <ActionButton
|
||||
icon="bx bx-x"
|
||||
text={t("tab_row.close_tab")}
|
||||
onClick={(e) => {
|
||||
// We are closing a tab, so we need to prevent propagation for click (activate tab).
|
||||
e.stopPropagation();
|
||||
appContext.tabManager.removeNoteContext(noteContext.ntxId);
|
||||
}}
|
||||
/>}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function TabPreviewContent({ note, viewScope }: {
|
||||
note: FNote | null,
|
||||
viewScope: ViewScope | undefined
|
||||
}) {
|
||||
let el: ComponentChild;
|
||||
let isPlaceholder = true;
|
||||
|
||||
if (!note) {
|
||||
el = <PreviewPlaceholder icon="bx bx-plus" />;
|
||||
} else if (note.type === "book") {
|
||||
el = <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
|
||||
} else if (viewScope?.viewMode && viewScope.viewMode !== "default") {
|
||||
el = <PreviewPlaceholder icon={VIEW_MODE_ICON_MAPPINGS[viewScope?.viewMode ?? ""] ?? "bx bx-empty"} />;
|
||||
} else {
|
||||
el = <NoteContent
|
||||
note={note}
|
||||
highlightedTokens={undefined}
|
||||
trim
|
||||
includeArchivedNotes={false}
|
||||
/>;
|
||||
isPlaceholder = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("tab-preview", `type-${note?.type ?? "empty"}`, { "tab-preview-placeholder": isPlaceholder })}>{el}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewPlaceholder({ icon}: {
|
||||
icon: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="preview-placeholder">
|
||||
<Icon icon={icon} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getWorkspaceTabBackgroundColorHue(noteContext: NoteContext) {
|
||||
if (!noteContext.hoistedNoteId) return;
|
||||
const hoistedNote = froca.getNoteFromCache(noteContext.hoistedNoteId);
|
||||
if (!hoistedNote) return;
|
||||
|
||||
const workspaceTabBackgroundColor = hoistedNote.getWorkspaceTabBackgroundColor();
|
||||
if (!workspaceTabBackgroundColor) return;
|
||||
|
||||
try {
|
||||
const parsedColor = parseColor(workspaceTabBackgroundColor);
|
||||
if (!parsedColor) return;
|
||||
return getHue(parsedColor);
|
||||
} catch (e) {
|
||||
// Colors are non-critical, simply ignore.
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
function useMainNoteContexts() {
|
||||
const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts());
|
||||
|
||||
useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved" ] , () => {
|
||||
setNoteContexts(appContext.tabManager.getMainNoteContexts());
|
||||
});
|
||||
|
||||
return noteContexts;
|
||||
}
|
||||
@@ -1,74 +1,136 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
import appContext, { CommandMappings } from "../../components/app_context";
|
||||
import contextMenu, { MenuItem } from "../../menus/context_menu";
|
||||
import branches from "../../services/branches";
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { createPortal, useRef, useState } from "preact/compat";
|
||||
|
||||
import FNote, { NotePathRecord } from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import note_create from "../../services/note_create";
|
||||
import tree from "../../services/tree";
|
||||
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import BasicWidget from "../basic_widget";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useNoteContext } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import { NoteContextMenu } from "../ribbon/NoteActions";
|
||||
import NoteActionsCustom from "../ribbon/NoteActionsCustom";
|
||||
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
|
||||
|
||||
export default function MobileDetailMenu() {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const dropdownRef = useRef<BootstrapDropdown | null>(null);
|
||||
const { note, noteContext, parentComponent, ntxId, viewScope, hoistedNoteId } = useNoteContext();
|
||||
const subContexts = noteContext?.getMainContext().getSubContexts() ?? [];
|
||||
const isMainContext = noteContext?.isMainContext();
|
||||
const [ backlinksModalShown, setBacklinksModalShown ] = useState(false);
|
||||
const [ notePathsModalShown, setNotePathsModalShown ] = useState(false);
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
|
||||
function closePane() {
|
||||
// Wait first for the context menu to be dismissed, otherwise the backdrop stays on.
|
||||
requestAnimationFrame(() => {
|
||||
parentComponent.triggerCommand("closeThisNoteSplit", { ntxId });
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
icon="bx bx-dots-vertical-rounded"
|
||||
text=""
|
||||
onClick={(e) => {
|
||||
const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId();
|
||||
if (!ntxId) return;
|
||||
<div style={{ contain: "none" }}>
|
||||
{note ? (
|
||||
<NoteContextMenu
|
||||
dropdownRef={dropdownRef}
|
||||
note={note} noteContext={noteContext}
|
||||
extraItems={<>
|
||||
<div className="form-list-row">
|
||||
<div className="form-list-col">
|
||||
<FormListItem
|
||||
icon="bx bx-link"
|
||||
onClick={() => setBacklinksModalShown(true)}
|
||||
disabled={backlinksCount === 0}
|
||||
>{t("status_bar.backlinks", { count: backlinksCount })}</FormListItem>
|
||||
</div>
|
||||
<div className="form-list-col">
|
||||
<FormListItem
|
||||
icon="bx bx-directions"
|
||||
onClick={() => setNotePathsModalShown(true)}
|
||||
disabled={(sortedNotePaths?.length ?? 0) <= 1}
|
||||
>{t("status_bar.note_paths", { count: sortedNotePaths?.length })}</FormListItem>
|
||||
</div>
|
||||
</div>
|
||||
<FormDropdownDivider />
|
||||
|
||||
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
|
||||
const subContexts = noteContext.getMainContext().getSubContexts();
|
||||
const isMainContext = noteContext?.isMainContext();
|
||||
const note = noteContext.note;
|
||||
{noteContext && ntxId && <NoteActionsCustom note={note} noteContext={noteContext} ntxId={ntxId} />}
|
||||
<FormListItem
|
||||
onClick={() => noteContext?.notePath && note_create.createNote(noteContext.notePath)}
|
||||
icon="bx bx-plus"
|
||||
>{t("mobile_detail_menu.insert_child_note")}</FormListItem>
|
||||
{subContexts.length < 2 && <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
onClick={(e) => {
|
||||
// We have to manually manage the hide because otherwise the old note context gets activated.
|
||||
e.stopPropagation();
|
||||
dropdownRef.current?.hide();
|
||||
parentComponent.triggerCommand("openNewNoteSplit", { ntxId });
|
||||
}}
|
||||
icon="bx bx-dock-right"
|
||||
>{t("create_pane_button.create_new_split")}</FormListItem>
|
||||
</>}
|
||||
{!isMainContext && <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
icon="bx bx-x"
|
||||
onClick={closePane}
|
||||
>{t("close_pane_button.close_this_pane")}</FormListItem>
|
||||
</>}
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
icon="bx bx-x"
|
||||
onClick={closePane}
|
||||
text={t("close_pane_button.close_this_pane")}
|
||||
/>
|
||||
)}
|
||||
|
||||
const items: (MenuItem<keyof CommandMappings>)[] = [
|
||||
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
|
||||
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
|
||||
{ kind: "separator" },
|
||||
{ title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" },
|
||||
{ kind: "separator" },
|
||||
subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" },
|
||||
!isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" }
|
||||
].filter(i => !!i) as MenuItem<keyof CommandMappings>[];
|
||||
|
||||
const lastItem = items.at(-1);
|
||||
if (lastItem && "kind" in lastItem && lastItem.kind === "separator") {
|
||||
items.pop();
|
||||
}
|
||||
|
||||
contextMenu.show<keyof CommandMappings>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
} else if (command === "delete") {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
throw new Error("Cannot get note path to delete.");
|
||||
}
|
||||
|
||||
const branchId = await tree.getBranchIdFromUrl(notePath);
|
||||
|
||||
if (!branchId) {
|
||||
throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath }));
|
||||
}
|
||||
|
||||
if (await branches.deleteNotes([branchId]) && parentComponent) {
|
||||
parentComponent.triggerCommand("setActiveScreen", { screen: "tree" });
|
||||
}
|
||||
} else if (command && parentComponent) {
|
||||
parentComponent.triggerCommand(command, { ntxId });
|
||||
}
|
||||
},
|
||||
forcePositionOnMobile: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
{createPortal((
|
||||
<>
|
||||
<BacklinksModal note={note} modalShown={backlinksModalShown} setModalShown={setBacklinksModalShown} />
|
||||
<NotePathsModal note={note} modalShown={notePathsModalShown} notePath={noteContext?.notePath} sortedNotePaths={sortedNotePaths} setModalShown={setNotePathsModalShown} />
|
||||
</>
|
||||
), document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, setModalShown: (shown: boolean) => void }) {
|
||||
return (
|
||||
<Modal
|
||||
className="backlinks-modal tn-backlinks-widget"
|
||||
size="md"
|
||||
title={t("mobile_detail_menu.backlinks")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
<ul className="backlinks-items">
|
||||
{note && <BacklinksList note={note} />}
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined, setModalShown: (shown: boolean) => void }) {
|
||||
return (
|
||||
<Modal
|
||||
className="note-paths-modal"
|
||||
size="md"
|
||||
title={t("note_paths.title")}
|
||||
show={modalShown}
|
||||
onHidden={() => setModalShown(false)}
|
||||
>
|
||||
{note && (
|
||||
<NotePathsWidget
|
||||
sortedNotePaths={sortedNotePaths}
|
||||
currentNotePath={notePath}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.collection-properties {
|
||||
padding: 0;
|
||||
padding: 0.55em 12px;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
align-items: center;
|
||||
@@ -14,7 +14,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
>div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.center-container {
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.right-container {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
button.btn {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
flex-wrap: wrap;
|
||||
padding: 0.55em 1em;
|
||||
|
||||
>div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import "./CollectionProperties.css";
|
||||
|
||||
import { t } from "i18next";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useContext, useRef } from "preact/hooks";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { getHelpUrlForNote } from "../../services/in_app_help";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useTriliumEvent } from "../react/hooks";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useNoteProperty, useTriliumEvent } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
||||
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
|
||||
|
||||
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
grid: "bx bxs-grid",
|
||||
list: "bx bx-list-ul",
|
||||
calendar: "bx bx-calendar",
|
||||
@@ -28,15 +26,26 @@ const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
presentation: "bx bx-rectangle"
|
||||
};
|
||||
|
||||
export default function CollectionProperties({ note }: { note: FNote }) {
|
||||
export default function CollectionProperties({ note, centerChildren, rightChildren }: {
|
||||
note: FNote;
|
||||
centerChildren?: ComponentChildren;
|
||||
rightChildren?: ComponentChildren;
|
||||
}) {
|
||||
const [ viewType, setViewType ] = useViewType(note);
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
|
||||
return (
|
||||
return ([ "book", "search" ].includes(noteType ?? "") &&
|
||||
<div className="collection-properties">
|
||||
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
|
||||
<ViewOptions note={note} viewType={viewType} />
|
||||
<div className="spacer" />
|
||||
<HelpButton note={note} />
|
||||
<div className="left-container">
|
||||
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
|
||||
<ViewOptions note={note} viewType={viewType} />
|
||||
</div>
|
||||
<div className="center-container">
|
||||
{centerChildren}
|
||||
</div>
|
||||
<div className="right-container">
|
||||
{rightChildren}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -222,15 +231,3 @@ function CheckBoxPropertyView({ note, property }: { note: FNote, property: Check
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function HelpButton({ note }: { note: FNote }) {
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
|
||||
return (helpUrl && (
|
||||
<ActionButton
|
||||
icon="bx bx-help-circle"
|
||||
onClick={(() => openInAppHelpFromUrl(helpUrl))}
|
||||
text={t("help-button.title")}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -117,3 +117,35 @@ body.experimental-feature-new-layout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.mobile .modal.icon-switcher {
|
||||
.modal-dialog {
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: unset;
|
||||
transform: unset;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .filter-row {
|
||||
padding: 0.25em var(--bs-modal-padding) 0.5em var(--bs-modal-padding);
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-list {
|
||||
margin: auto;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
|
||||
span {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,24 @@ import { IconRegistry } from "@triliumnext/commons";
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { t } from "i18next";
|
||||
import { CSSProperties, RefObject } from "preact";
|
||||
import { CSSProperties } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import type React from "react";
|
||||
import { CellComponentProps, Grid } from "react-window";
|
||||
|
||||
import FNote from "../entities/fnote";
|
||||
import attributes from "../services/attributes";
|
||||
import server from "../services/server";
|
||||
import { isDesktop, isMobile } from "../services/utils";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import Dropdown from "./react/Dropdown";
|
||||
import { FormDropdownDivider, FormListItem } from "./react/FormList";
|
||||
import FormTextBox from "./react/FormTextBox";
|
||||
import { useNoteContext, useNoteLabel, useStaticTooltip } from "./react/hooks";
|
||||
import { useNoteContext, useNoteLabel, useStaticTooltip, useWindowSize } from "./react/hooks";
|
||||
import Modal from "./react/Modal";
|
||||
|
||||
const ICON_SIZE = isMobile() ? 56 : 48;
|
||||
|
||||
interface IconToCountCache {
|
||||
iconClassToCountMap: Record<string, number>;
|
||||
@@ -36,6 +42,10 @@ export default function NoteIcon() {
|
||||
setIcon(note?.getIcon());
|
||||
}, [ note, iconClass, workspaceIconClass ]);
|
||||
|
||||
if (isMobile()) {
|
||||
return <MobileNoteIconSwitcher note={note} icon={icon} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="note-icon-widget"
|
||||
@@ -47,16 +57,47 @@ export default function NoteIcon() {
|
||||
hideToggleArrow
|
||||
disabled={viewScope?.viewMode !== "default"}
|
||||
>
|
||||
{ note && <NoteIconList note={note} dropdownRef={dropdownRef} /> }
|
||||
{ note && <NoteIconList note={note} onHide={() => dropdownRef?.current?.hide()} columnCount={12} /> }
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteIconList({ note, dropdownRef }: {
|
||||
note: FNote,
|
||||
dropdownRef: RefObject<BootstrapDropdown>;
|
||||
function MobileNoteIconSwitcher({ note, icon }: {
|
||||
note: FNote | null | undefined;
|
||||
icon: string | null | undefined;
|
||||
}) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const { windowWidth } = useWindowSize();
|
||||
|
||||
return (note &&
|
||||
<div className="note-icon-widget">
|
||||
<ActionButton
|
||||
className="note-icon"
|
||||
icon={icon ?? "bx bx-empty"}
|
||||
text={t("note_icon.change_note_icon")}
|
||||
onClick={() => setModalShown(true)}
|
||||
/>
|
||||
|
||||
{createPortal((
|
||||
<Modal
|
||||
title={t("note_icon.change_note_icon")}
|
||||
size="xl"
|
||||
show={modalShown} onHidden={() => setModalShown(false)}
|
||||
className="icon-switcher note-icon-widget"
|
||||
scrollable
|
||||
>
|
||||
<NoteIconList note={note} onHide={() => setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} />
|
||||
</Modal>
|
||||
), document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteIconList({ note, onHide, columnCount }: {
|
||||
note: FNote;
|
||||
onHide: () => void;
|
||||
columnCount: number;
|
||||
}) {
|
||||
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||
const iconListRef = useRef<HTMLDivElement>(null);
|
||||
const [ search, setSearch ] = useState<string>();
|
||||
const [ filterByPrefix, setFilterByPrefix ] = useState<string | null>(null);
|
||||
@@ -72,53 +113,22 @@ function NoteIconList({ note, dropdownRef }: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="filter-row">
|
||||
<span>{t("note_icon.search")}</span>
|
||||
<FormTextBox
|
||||
inputRef={searchBoxRef}
|
||||
type="text"
|
||||
name="icon-search"
|
||||
placeholder={ filterByPrefix
|
||||
? t("note_icon.search_placeholder_filtered", {
|
||||
number: filteredIcons.length ?? 0,
|
||||
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
|
||||
})
|
||||
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
|
||||
currentValue={search} onChange={setSearch}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{getIconLabels(note).length > 0 && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ActionButton
|
||||
icon="bx bx-reset"
|
||||
text={t("note_icon.reset-default")}
|
||||
onClick={() => {
|
||||
if (!note) return;
|
||||
for (const label of getIconLabels(note)) {
|
||||
attributes.removeAttributeById(note.noteId, label.attributeId);
|
||||
}
|
||||
dropdownRef?.current?.hide();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{glob.iconRegistry.sources.length > 0 && <Dropdown
|
||||
buttonClassName="bx bx-filter-alt"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
iconAction
|
||||
title={t("note_icon.filter")}
|
||||
>
|
||||
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
|
||||
</Dropdown>}
|
||||
</div>
|
||||
<FilterRow
|
||||
note={note}
|
||||
filterByPrefix={filterByPrefix}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setFilterByPrefix={setFilterByPrefix}
|
||||
filteredIcons={filteredIcons}
|
||||
onHide={onHide}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="icon-list"
|
||||
ref={iconListRef}
|
||||
style={{
|
||||
width: (columnCount * ICON_SIZE + 10),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Make sure we are not clicking on something else than a button.
|
||||
const clickedTarget = e.target as HTMLElement;
|
||||
@@ -129,18 +139,19 @@ function NoteIconList({ note, dropdownRef }: {
|
||||
const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass";
|
||||
attributes.setLabel(note.noteId, attributeToSet, iconClass);
|
||||
}
|
||||
dropdownRef?.current?.hide();
|
||||
onHide();
|
||||
}}
|
||||
>
|
||||
{filteredIcons.length ? (
|
||||
<Grid
|
||||
columnCount={12}
|
||||
columnWidth={48}
|
||||
rowCount={Math.ceil(filteredIcons.length / 12)}
|
||||
rowHeight={48}
|
||||
columnCount={columnCount}
|
||||
columnWidth={ICON_SIZE}
|
||||
rowCount={Math.ceil(filteredIcons.length / columnCount)}
|
||||
rowHeight={ICON_SIZE}
|
||||
cellComponent={IconItemCell}
|
||||
cellProps={{
|
||||
filteredIcons
|
||||
filteredIcons,
|
||||
columnCount
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -151,12 +162,97 @@ function NoteIconList({ note, dropdownRef }: {
|
||||
);
|
||||
}
|
||||
|
||||
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{
|
||||
function FilterRow({ note, filterByPrefix, search, setSearch, setFilterByPrefix, filteredIcons, onHide }: {
|
||||
note: FNote;
|
||||
filterByPrefix: string | null;
|
||||
search: string | undefined;
|
||||
setSearch: (value: string | undefined) => void;
|
||||
setFilterByPrefix: (value: string | null) => void;
|
||||
filteredIcons: IconWithName[];
|
||||
}>): React.JSX.Element {
|
||||
const iconIndex = rowIndex * 12 + columnIndex;
|
||||
onHide: () => void;
|
||||
}) {
|
||||
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||
const hasCustomIcon = getIconLabels(note).length > 0;
|
||||
|
||||
function resetToDefaultIcon() {
|
||||
if (!note) return;
|
||||
for (const label of getIconLabels(note)) {
|
||||
attributes.removeAttributeById(note.noteId, label.attributeId);
|
||||
}
|
||||
onHide();
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="filter-row">
|
||||
<span>{t("note_icon.search")}</span>
|
||||
<FormTextBox
|
||||
inputRef={searchBoxRef}
|
||||
type="text"
|
||||
name="icon-search"
|
||||
placeholder={ filterByPrefix
|
||||
? t("note_icon.search_placeholder_filtered", {
|
||||
number: filteredIcons.length ?? 0,
|
||||
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
|
||||
})
|
||||
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
|
||||
currentValue={search} onChange={setSearch}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{isDesktop()
|
||||
? <>
|
||||
{hasCustomIcon && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ActionButton
|
||||
icon="bx bx-reset"
|
||||
text={t("note_icon.reset-default")}
|
||||
onClick={resetToDefaultIcon}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{<Dropdown
|
||||
buttonClassName="bx bx-filter-alt"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
iconAction
|
||||
title={t("note_icon.filter")}
|
||||
>
|
||||
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
|
||||
</Dropdown>}
|
||||
</> : (
|
||||
<Dropdown
|
||||
buttonClassName="bx bx-dots-vertical-rounded"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
iconAction
|
||||
dropdownContainerClassName="mobile-bottom-menu"
|
||||
>
|
||||
{hasCustomIcon && <>
|
||||
<FormListItem
|
||||
icon="bx bx-reset"
|
||||
onClick={resetToDefaultIcon}
|
||||
disabled={!hasCustomIcon}
|
||||
>{t("note_icon.reset-default")}</FormListItem>
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
|
||||
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons, columnCount }: CellComponentProps<{
|
||||
filteredIcons: IconWithName[];
|
||||
columnCount: number;
|
||||
}>) {
|
||||
const iconIndex = rowIndex * columnCount + columnIndex;
|
||||
const iconData = filteredIcons[iconIndex] as IconWithName | undefined;
|
||||
if (!iconData) return <></>;
|
||||
if (!iconData) return <></> as React.ReactElement;
|
||||
|
||||
const { id, terms, iconPack } = iconData;
|
||||
return (
|
||||
@@ -166,7 +262,7 @@ function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellCompo
|
||||
title={t("note_icon.icon_tooltip", { name: terms?.[0] ?? id, iconPack })}
|
||||
style={style as CSSProperties}
|
||||
/>
|
||||
);
|
||||
) as React.ReactElement;
|
||||
}
|
||||
|
||||
function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {
|
||||
@@ -183,7 +279,7 @@ function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {
|
||||
checked={filterByPrefix === "bx"}
|
||||
onClick={() => setFilterByPrefix("bx")}
|
||||
>{t("note_icon.filter-default")}</FormListItem>
|
||||
<FormDropdownDivider />
|
||||
{glob.iconRegistry.sources.length > 1 && <FormDropdownDivider />}
|
||||
|
||||
{glob.iconRegistry.sources.map(({ prefix, name, icon }) => (
|
||||
prefix !== "bx" && <FormListItem
|
||||
|
||||
@@ -109,4 +109,29 @@ body.experimental-feature-new-layout {
|
||||
--input-focus-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile .title-row {
|
||||
.icon-action:not(.note-icon) {
|
||||
--icon-button-size: 45px;
|
||||
--icon-button-icon-ratio: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.note-badges {
|
||||
margin-inline: 0.5em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.note-icon-widget {
|
||||
margin-inline: 0.5em;
|
||||
|
||||
.note-icon {
|
||||
--icon-button-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#left-pane .tree-wrapper {
|
||||
.tree-wrapper {
|
||||
.note-indicator-icon.subtree-hidden-badge {
|
||||
font-family: inherit !important;
|
||||
margin-inline: 0.5em;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import { useStaticTooltip } from "./hooks";
|
||||
import keyboard_actions from "../../services/keyboard_actions";
|
||||
import { HTMLAttributes } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu"> {
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import keyboard_actions from "../../services/keyboard_actions";
|
||||
import { useStaticTooltip } from "./hooks";
|
||||
|
||||
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu" | "style"> {
|
||||
text: string;
|
||||
titlePosition?: "top" | "right" | "bottom" | "left";
|
||||
icon: string;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { ComponentChildren, RefObject } from "preact";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { memo } from "preact/compat";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import { isDesktop } from "../../services/utils";
|
||||
import ActionButton from "./ActionButton";
|
||||
import Icon from "./Icon";
|
||||
|
||||
export interface ButtonProps {
|
||||
@@ -78,7 +81,7 @@ export function ButtonGroup({ children }: { children: ComponentChildren }) {
|
||||
<div className="btn-group" role="group">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function SplitButton({ text, icon, children, ...restProps }: {
|
||||
@@ -103,7 +106,17 @@ export function SplitButton({ text, icon, children, ...restProps }: {
|
||||
{children}
|
||||
</ul>
|
||||
</ButtonGroup>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function ButtonOrActionButton(props: {
|
||||
text: string;
|
||||
icon: string;
|
||||
} & Pick<ButtonProps, "onClick" | "triggerCommand" | "disabled" | "title">) {
|
||||
if (isDesktop()) {
|
||||
return <Button {...props} />;
|
||||
}
|
||||
return <ActionButton {...props} />;
|
||||
}
|
||||
|
||||
export default Button;
|
||||
|
||||
@@ -17,21 +17,34 @@
|
||||
.collapsible-body {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.fully-expanded {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible-inner-body {
|
||||
padding-top: 0.5em;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
.collapsible-title .arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.collapsible-inner-body {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.with-transition {
|
||||
.collapsible-body {
|
||||
transition: height 250ms ease-in;
|
||||
}
|
||||
|
||||
.collapsible-inner-body {
|
||||
transition: opacity 250ms ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
|
||||
const { height } = useElementSize(innerRef) ?? {};
|
||||
const contentId = useUniqueName();
|
||||
const [ transitionEnabled, setTransitionEnabled ] = useState(false);
|
||||
const [ fullyExpanded, setFullyExpanded ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -35,6 +36,21 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded) {
|
||||
if (transitionEnabled) {
|
||||
const timeout = setTimeout(() => {
|
||||
setFullyExpanded(true);
|
||||
}, 250);
|
||||
return () => clearTimeout(timeout);
|
||||
} else {
|
||||
setFullyExpanded(true);
|
||||
}
|
||||
} else {
|
||||
setFullyExpanded(false);
|
||||
}
|
||||
}, [expanded, transitionEnabled])
|
||||
|
||||
return (
|
||||
<div className={clsx("collapsible", className, {
|
||||
expanded,
|
||||
@@ -53,7 +69,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
|
||||
<div
|
||||
id={contentId}
|
||||
ref={bodyRef}
|
||||
className="collapsible-body"
|
||||
className={clsx("collapsible-body", {"fully-expanded": fullyExpanded})}
|
||||
style={{ height: expanded ? height : "0" }}
|
||||
aria-hidden={!expanded}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ComponentChildren, HTMLAttributes } from "preact";
|
||||
import { CSSProperties, HTMLProps } from "preact/compat";
|
||||
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { isMobile } from "../../services/utils";
|
||||
import { useTooltip, useUniqueName } from "./hooks";
|
||||
|
||||
type DataAttributes = {
|
||||
@@ -32,9 +33,10 @@ export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "c
|
||||
dropdownRef?: MutableRef<BootstrapDropdown | null>;
|
||||
titlePosition?: "top" | "right" | "bottom" | "left";
|
||||
titleOptions?: Partial<Tooltip.Options>;
|
||||
mobileBackdrop?: boolean;
|
||||
}
|
||||
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions }: DropdownProps) {
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions, mobileBackdrop }: DropdownProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const dropdownContainerRef = useRef<HTMLUListElement | null>(null);
|
||||
@@ -74,12 +76,18 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
|
||||
setShown(true);
|
||||
externalOnShown?.();
|
||||
hideTooltip();
|
||||
}, [ hideTooltip ]);
|
||||
if (mobileBackdrop && isMobile()) {
|
||||
document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover");
|
||||
}
|
||||
}, [ hideTooltip, mobileBackdrop ]);
|
||||
|
||||
const onHidden = useCallback(() => {
|
||||
setShown(false);
|
||||
externalOnHidden?.();
|
||||
}, []);
|
||||
if (mobileBackdrop && isMobile()) {
|
||||
document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover");
|
||||
}
|
||||
}, [ mobileBackdrop ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
@@ -3,8 +3,9 @@ import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import ActionButton, { ActionButtonProps } from "./ActionButton";
|
||||
import Button, { ButtonProps } from "./Button";
|
||||
import { FormListItem, FormListItemOpts } from "./FormList";
|
||||
|
||||
interface FormFileUploadProps {
|
||||
export interface FormFileUploadProps {
|
||||
name?: string;
|
||||
onChange: (files: FileList | null) => void;
|
||||
multiple?: boolean;
|
||||
@@ -75,3 +76,25 @@ export function FormFileUploadActionButton({ onChange, ...buttonProps }: Omit<Ac
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link FormFileUploadButton}, but uses an {@link FormListItem} instead of a normal {@link Button}.
|
||||
* @param param the change listener for the file upload and the properties for the button.
|
||||
*/
|
||||
export function FormFileUploadFormListItem({ onChange, children, ...buttonProps }: Omit<FormListItemOpts, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormListItem
|
||||
{...buttonProps}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>{children}</FormListItem>
|
||||
<FormFileUpload
|
||||
inputRef={inputRef}
|
||||
hidden
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,3 +27,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-list-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1em;
|
||||
|
||||
.form-list-col {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface FormListBadge {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface FormListItemOpts {
|
||||
export interface FormListItemOpts {
|
||||
children: ComponentChildren;
|
||||
icon?: string;
|
||||
value?: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import { HTMLAttributes } from "preact";
|
||||
|
||||
interface IconProps extends Pick<HTMLAttributes<HTMLSpanElement>, "className" | "onClick"> {
|
||||
interface IconProps extends Pick<HTMLAttributes<HTMLSpanElement>, "className" | "onClick" | "title"> {
|
||||
icon?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useMemo } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ComponentChildren } from "preact";
|
||||
import type { CSSProperties, RefObject } from "preact/compat";
|
||||
import { openDialog } from "../../services/dialog";
|
||||
import { Modal as BootstrapModal } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { ComponentChildren, CSSProperties, RefObject } from "preact";
|
||||
import { memo } from "preact/compat";
|
||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||
|
||||
import { openDialog } from "../../services/dialog";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useSyncedRef } from "./hooks";
|
||||
|
||||
interface CustomTitleBarButton {
|
||||
title: string;
|
||||
iconClassName: string;
|
||||
onClick: () => void;
|
||||
onClick: (e: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export interface ModalProps {
|
||||
@@ -80,7 +80,7 @@ export interface ModalProps {
|
||||
noFocus?: boolean;
|
||||
}
|
||||
|
||||
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
||||
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
||||
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
||||
const modalInstanceRef = useRef<BootstrapModal>();
|
||||
const elementToFocus = useRef<Element | null>();
|
||||
@@ -116,7 +116,7 @@ export default function Modal({ children, className, size, title, customTitleBar
|
||||
focus: !noFocus
|
||||
}).then(($widget) => {
|
||||
modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
modalInstanceRef.current?.hide();
|
||||
}
|
||||
@@ -159,13 +159,12 @@ export default function Modal({ children, className, size, title, customTitleBar
|
||||
|
||||
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
|
||||
<button type="button"
|
||||
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
|
||||
title={titleBarButton.title}
|
||||
onClick={titleBarButton.onClick}>
|
||||
</button>
|
||||
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
|
||||
title={titleBarButton.title}
|
||||
onClick={titleBarButton.onClick} />
|
||||
))}
|
||||
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")} />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { RefObject } from "preact";
|
||||
import { ComponentChildren, RefObject } from "preact";
|
||||
import { useContext, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
@@ -22,7 +22,7 @@ import MovePaneButton from "../buttons/move_pane_button";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useSyncedRef, useTriliumEvent, useTriliumOption } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
|
||||
import NoteActionsCustom from "./NoteActionsCustom";
|
||||
@@ -63,8 +63,13 @@ function RevisionsButton({ note }: { note: FNote }) {
|
||||
|
||||
type ItemToFocus = "basic-properties";
|
||||
|
||||
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||
const dropdownRef = useRef<BootstrapDropdown>(null);
|
||||
export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: externalDropdownRef }: {
|
||||
note: FNote,
|
||||
noteContext?: NoteContext,
|
||||
extraItems?: ComponentChildren;
|
||||
dropdownRef?: RefObject<BootstrapDropdown>;
|
||||
}) {
|
||||
const dropdownRef = useSyncedRef<BootstrapDropdown>(externalDropdownRef, null);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const noteType = useNoteProperty(note, "type") ?? "";
|
||||
const [viewType] = useNoteLabel(note, "viewType");
|
||||
@@ -99,12 +104,15 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
||||
dropdownRef={dropdownRef}
|
||||
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
|
||||
className="note-actions"
|
||||
dropdownContainerClassName="mobile-bottom-menu"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
iconAction
|
||||
onHidden={() => itemToFocusRef.current = null }
|
||||
mobileBackdrop
|
||||
>
|
||||
{extraItems}
|
||||
|
||||
{isReadOnly && <>
|
||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||
@@ -277,7 +285,7 @@ function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?:
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
|
||||
export function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
|
||||
return <FormListItem
|
||||
icon={icon}
|
||||
title={title}
|
||||
|
||||
3
apps/client/src/widgets/ribbon/NoteActionsCustom.css
Normal file
3
apps/client/src/widgets/ribbon/NoteActionsCustom.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body.mobile .note-actions-custom:not(:empty) {
|
||||
margin-bottom: calc(var(--bs-dropdown-divider-margin-y) * 2);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./NoteActionsCustom.css";
|
||||
|
||||
import { NoteType } from "@triliumnext/commons";
|
||||
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -7,11 +9,12 @@ import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { getHelpUrlForNote } from "../../services/in_app_help";
|
||||
import { downloadFileNote, openNoteExternally } from "../../services/open";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { isMobile, openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import { buildSaveSqlToNoteHandler } from "../FloatingButtonsDefinitions";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { FormFileUploadActionButton } from "../react/FormFileUpload";
|
||||
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
|
||||
import { FormFileUploadActionButton, FormFileUploadFormListItem, FormFileUploadProps } from "../react/FormFileUpload";
|
||||
import { FormListItem } from "../react/FormList";
|
||||
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab";
|
||||
@@ -32,6 +35,8 @@ interface NoteActionsCustomInnerProps extends NoteActionsCustomProps {
|
||||
viewType: ViewTypeOptions | null | undefined;
|
||||
}
|
||||
|
||||
const cachedIsMobile = isMobile();
|
||||
|
||||
/**
|
||||
* Part of {@link NoteActions} on the new layout, but are rendered with a slight spacing
|
||||
* from the rest of the note items and the buttons differ based on the note type.
|
||||
@@ -115,7 +120,7 @@ function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps
|
||||
onChange: (files: FileList | null) => void;
|
||||
}) {
|
||||
return (
|
||||
<FormFileUploadActionButton
|
||||
<NoteActionWithFileUpload
|
||||
icon="bx bx-folder-open"
|
||||
text={t("image_properties.upload_new_revision")}
|
||||
disabled={!note.isContentAvailable()}
|
||||
@@ -125,8 +130,8 @@ function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps
|
||||
}
|
||||
|
||||
function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
|
||||
return (
|
||||
<ActionButton
|
||||
return (!cachedIsMobile &&
|
||||
<NoteAction
|
||||
icon="bx bx-link-external"
|
||||
text={t("file_properties.open")}
|
||||
disabled={note.isProtected}
|
||||
@@ -137,7 +142,7 @@ function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
|
||||
|
||||
function DownloadFileButton({ note, parentComponent, ntxId }: NoteActionsCustomInnerProps) {
|
||||
return (
|
||||
<ActionButton
|
||||
<NoteAction
|
||||
icon="bx bx-download"
|
||||
text={t("file_properties.download")}
|
||||
disabled={!note.isContentAvailable()}
|
||||
@@ -149,7 +154,7 @@ function DownloadFileButton({ note, parentComponent, ntxId }: NoteActionsCustomI
|
||||
//#region Floating buttons
|
||||
function CopyReferenceToClipboardButton({ ntxId, noteType, parentComponent }: NoteActionsCustomInnerProps) {
|
||||
return (["mermaid", "canvas", "mindMap", "image"].includes(noteType) &&
|
||||
<ActionButton
|
||||
<NoteAction
|
||||
text={t("image_properties.copy_reference_to_clipboard")}
|
||||
icon="bx bx-copy"
|
||||
onClick={() => parentComponent?.triggerEvent("copyImageReferenceToClipboard", { ntxId })}
|
||||
@@ -161,7 +166,7 @@ function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, not
|
||||
const isEnabled = (note.noteId === "_backendLog" || noteType === "render") && isDefaultViewMode;
|
||||
|
||||
return (isEnabled &&
|
||||
<ActionButton
|
||||
<NoteAction
|
||||
text={t("backend_log.refresh")}
|
||||
icon="bx bx-refresh"
|
||||
onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })}
|
||||
@@ -170,11 +175,11 @@ function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, not
|
||||
}
|
||||
|
||||
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
const isShown = note.type === "mermaid" && note.isContentAvailable() && isDefaultViewMode;
|
||||
const isShown = note.type === "mermaid" && !cachedIsMobile && note.isContentAvailable() && isDefaultViewMode;
|
||||
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
|
||||
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
|
||||
|
||||
return isShown && <ActionButton
|
||||
return isShown && <NoteAction
|
||||
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
|
||||
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
|
||||
onClick={() => setSplitEditorOrientation(upcomingOrientation)}
|
||||
@@ -182,13 +187,13 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N
|
||||
/>;
|
||||
}
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || isSavedSqlite)
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <ActionButton
|
||||
return isEnabled && <NoteAction
|
||||
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
|
||||
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
|
||||
onClick={() => setReadOnly(!isReadOnly)}
|
||||
@@ -197,7 +202,7 @@ function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActions
|
||||
|
||||
function RunActiveNoteButton({ noteMime }: NoteActionsCustomInnerProps) {
|
||||
const isEnabled = noteMime.startsWith("application/javascript") || noteMime === "text/x-sqlite;schema=trilium";
|
||||
return isEnabled && <ActionButton
|
||||
return isEnabled && <NoteAction
|
||||
icon="bx bx-play"
|
||||
text={t("code_buttons.execute_button_title")}
|
||||
triggerCommand="runActiveNote"
|
||||
@@ -218,7 +223,7 @@ function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
|
||||
}
|
||||
});
|
||||
|
||||
return isEnabled && <ActionButton
|
||||
return isEnabled && <NoteAction
|
||||
icon="bx bx-save"
|
||||
text={t("code_buttons.save_to_note_button_title")}
|
||||
onClick={buildSaveSqlToNoteHandler(note)}
|
||||
@@ -227,19 +232,19 @@ function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
|
||||
|
||||
function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) {
|
||||
const isEnabled = noteMime.startsWith("application/javascript;env=");
|
||||
return isEnabled && <ActionButton
|
||||
return isEnabled && <NoteAction
|
||||
icon="bx bx-help-circle"
|
||||
text={t("code_buttons.trilium_api_docs_button_title")}
|
||||
onClick={() => openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
|
||||
/>;
|
||||
}
|
||||
|
||||
function InAppHelpButton({ note, noteType }: NoteActionsCustomInnerProps) {
|
||||
function InAppHelpButton({ note }: NoteActionsCustomInnerProps) {
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
const isEnabled = !!helpUrl && (noteType !== "book");
|
||||
const isEnabled = !!helpUrl;
|
||||
|
||||
return isEnabled && (
|
||||
<ActionButton
|
||||
<NoteAction
|
||||
icon="bx bx-help-circle"
|
||||
text={t("help-button.title")}
|
||||
onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)}
|
||||
@@ -247,16 +252,9 @@ function InAppHelpButton({ note, noteType }: NoteActionsCustomInnerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function AddChildButton({ parentComponent, noteType, viewType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) {
|
||||
if (noteType === "book" && viewType === "geoMap") {
|
||||
return <ActionButton
|
||||
icon="bx bx-plus-circle"
|
||||
text={t("geo-map.create-child-note-title")}
|
||||
onClick={() => parentComponent.triggerEvent("geoMapCreateChildNote", { ntxId })}
|
||||
disabled={isReadOnly}
|
||||
/>;
|
||||
} else if (noteType === "relationMap") {
|
||||
return <ActionButton
|
||||
function AddChildButton({ parentComponent, noteType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) {
|
||||
if (noteType === "relationMap") {
|
||||
return <NoteAction
|
||||
icon="bx bx-folder-plus"
|
||||
text={t("relation_map_buttons.create_child_note_title")}
|
||||
onClick={() => parentComponent.triggerEvent("relationMapCreateChildNote", { ntxId })}
|
||||
@@ -265,3 +263,19 @@ function AddChildButton({ parentComponent, noteType, viewType, ntxId, isReadOnly
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
function NoteAction({ text, ...props }: Pick<ActionButtonProps, "text" | "icon" | "disabled" | "triggerCommand"> & {
|
||||
onClick?: ((e: MouseEvent) => void) | undefined;
|
||||
}) {
|
||||
return (cachedIsMobile
|
||||
? <FormListItem {...props}>{text}</FormListItem>
|
||||
: <ActionButton text={text} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function NoteActionWithFileUpload({ text, ...props }: Pick<ActionButtonProps, "text" | "icon" | "disabled" | "triggerCommand"> & Pick<FormFileUploadProps, "onChange">) {
|
||||
return (cachedIsMobile
|
||||
? <FormFileUploadFormListItem {...props}>{text}</FormFileUploadFormListItem>
|
||||
: <FormFileUploadActionButton text={text} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
63
apps/client/src/widgets/ribbon/NotePathsTab.css
Normal file
63
apps/client/src/widgets/ribbon/NotePathsTab.css
Normal file
@@ -0,0 +1,63 @@
|
||||
body.experimental-feature-new-layout .note-paths-widget {
|
||||
.note-path-intro {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.note-path-list {
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
/* Note path card */
|
||||
li {
|
||||
--border-radius: 6px;
|
||||
|
||||
position: relative;
|
||||
background: var(--card-background-color);
|
||||
padding: 8px 20px 8px 25px;
|
||||
|
||||
&:first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
& + li {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Current path arrow */
|
||||
&.path-current::before {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
content: "\ee8f";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
bottom: 0;
|
||||
font-family: "boxicons";
|
||||
font-size: .75em;
|
||||
color: var(--menu-item-icon-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Note path segment */
|
||||
a {
|
||||
margin-inline: 2px;
|
||||
padding-inline: 2px;
|
||||
color: currentColor;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
|
||||
/* The last segment of the note path */
|
||||
&.basename {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import "./NotePathsTab.css";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import FNote, { NotePathRecord } from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import LinkButton from "../react/LinkButton";
|
||||
import clsx from "clsx";
|
||||
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { joinElements, ParentComponent } from "../react/react_utils";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
|
||||
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
@@ -20,6 +21,7 @@ export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
|
||||
sortedNotePaths: NotePathRecord[] | undefined;
|
||||
currentNotePath?: string | null | undefined;
|
||||
}) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
return (
|
||||
<div class="note-paths-widget">
|
||||
<>
|
||||
@@ -39,7 +41,7 @@ export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
|
||||
|
||||
<LinkButton
|
||||
text={t("note_paths.clone_button")}
|
||||
triggerCommand="cloneNoteIdsTo"
|
||||
onClick={() => parentComponent?.triggerCommand("cloneNoteIdsTo")}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
@@ -112,9 +114,9 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
<li class={classes}>
|
||||
{joinElements(fullNotePaths.map((notePath, index, arr) => (
|
||||
<NoteLink key={notePath}
|
||||
className={clsx({"basename": (index === arr.length - 1)})}
|
||||
notePath={notePath}
|
||||
noPreview />
|
||||
className={clsx({"basename": (index === arr.length - 1)})}
|
||||
notePath={notePath}
|
||||
noPreview />
|
||||
)), NOTE_PATH_TITLE_SEPARATOR)}
|
||||
|
||||
{icons.map(({ icon, title }) => (
|
||||
|
||||
@@ -7,7 +7,6 @@ import appContext from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import server from "../../services/server";
|
||||
@@ -16,7 +15,6 @@ import tree from "../../services/tree";
|
||||
import { getErrorMessage } from "../../services/utils";
|
||||
import ws from "../../services/ws";
|
||||
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
|
||||
import CollectionProperties from "../note_bars/CollectionProperties";
|
||||
import Button from "../react/Button";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormListHeader, FormListItem } from "../react/FormList";
|
||||
@@ -26,8 +24,6 @@ import { ParentComponent } from "../react/react_utils";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabContext, "note" | "ntxId" | "hidden">) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
|
||||
@@ -115,11 +111,6 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
|
||||
defaultValue={defaultValue}
|
||||
/>;
|
||||
})}
|
||||
|
||||
{isNewLayout && <tr className="view-options">
|
||||
<td className="title-column">{t("search_definition.view_options")}</td>
|
||||
<td><CollectionProperties note={note} /></td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
<BulkActionsList note={note} />
|
||||
<tbody className="search-actions">
|
||||
|
||||
@@ -459,7 +459,7 @@ body.experimental-feature-new-layout {
|
||||
gap: var(--button-gap);
|
||||
|
||||
&> button:last-of-type {
|
||||
margin-right: 1em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import PlatformIndicator from "./components/PlatformIndicator";
|
||||
import RadioWithIllustration from "./components/RadioWithIllustration";
|
||||
import RelatedSettings from "./components/RelatedSettings";
|
||||
|
||||
@@ -174,13 +175,13 @@ function LayoutIllustration({ isNewLayout }: { isNewLayout?: boolean }) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="title-bar">
|
||||
<Icon icon="bx bx-leaf" />
|
||||
<span className="title">Title</span>
|
||||
<Icon icon="bx bx-dock-right" />
|
||||
<div>
|
||||
<div className="title-bar">
|
||||
<Icon icon="bx bx-leaf" />
|
||||
<span className="title">Title</span>
|
||||
<Icon icon="bx bx-dock-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNewLayout && <div className="ribbon">
|
||||
@@ -192,7 +193,7 @@ function LayoutIllustration({ isNewLayout }: { isNewLayout?: boolean }) {
|
||||
</div>
|
||||
|
||||
<div className="ribbon-body">
|
||||
<div className="ribbon-body-content"></div>
|
||||
<div className="ribbon-body-content" />
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
@@ -356,7 +357,11 @@ function ElectronIntegration() {
|
||||
|
||||
<FormGroup name="background-effects" description={t("electron_integration.background-effects-description")}>
|
||||
<FormCheckbox
|
||||
label={t("electron_integration.background-effects")}
|
||||
label={<>
|
||||
{t("electron_integration.background-effects")}
|
||||
{" "}
|
||||
<PlatformIndicator windows="11" mac />
|
||||
</>}
|
||||
currentValue={backgroundEffects} onChange={setBackgroundEffects}
|
||||
disabled={nativeTitleBarVisible}
|
||||
/>
|
||||
@@ -383,10 +388,10 @@ function Performance() {
|
||||
currentValue={shadowsEnabled} onChange={setShadowsEnabled}
|
||||
/>
|
||||
|
||||
<FormCheckbox
|
||||
{!isMobile() && <FormCheckbox
|
||||
label={t("ui-performance.enable-backdrop-effects")}
|
||||
currentValue={backdropEffectsEnabled} onChange={setBackdropEffectsEnabled}
|
||||
/>
|
||||
/>}
|
||||
|
||||
{isElectron() && <SmoothScrollEnabledOption />}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.platform-indicator {
|
||||
display: inline-flex;
|
||||
gap: 0.25em;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import "./PlatformIndicator.css";
|
||||
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
import { t } from "../../../../services/i18n";
|
||||
import { useStaticTooltip } from "../../../react/hooks";
|
||||
import Icon from "../../../react/Icon";
|
||||
|
||||
interface PlatformIndicatorProps {
|
||||
windows?: boolean | "11";
|
||||
mac: boolean;
|
||||
}
|
||||
|
||||
export default function PlatformIndicator({ windows, mac }: PlatformIndicatorProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
useStaticTooltip(containerRef, {
|
||||
selector: "span",
|
||||
animation: false,
|
||||
title() { return this.title; },
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="platform-indicator">
|
||||
{windows && <Icon
|
||||
icon="bx bxl-windows"
|
||||
title={t("platform_indicator.available_on", { platform: windows === "11" ? "Windows 11" : "Windows" })}
|
||||
/>}
|
||||
{mac && <Icon
|
||||
icon="bx bxl-apple"
|
||||
title={t("platform_indicator.available_on", { platform: "macOS" })}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import type React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import search from "../../../services/search";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { isElectron } from "../../../services/utils";
|
||||
import Button from "../../react/Button";
|
||||
import FormText from "../../react/FormText";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import TimeSelector from "./components/TimeSelector";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
||||
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import search from "../../../services/search";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import FormSelect from "../../react/FormSelect";
|
||||
import { isElectron } from "../../../services/utils";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import TimeSelector from "./components/TimeSelector";
|
||||
|
||||
export default function OtherSettings() {
|
||||
return (
|
||||
@@ -31,7 +33,7 @@ export default function OtherSettings() {
|
||||
<ShareSettings />
|
||||
<NetworkSettings />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SearchEngineSettings() {
|
||||
@@ -82,7 +84,7 @@ function SearchEngineSettings() {
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TrayOptionsSettings() {
|
||||
@@ -97,7 +99,7 @@ function TrayOptionsSettings() {
|
||||
onChange={trayEnabled => setDisableTray(!trayEnabled)}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NoteErasureTimeout() {
|
||||
@@ -105,13 +107,13 @@ function NoteErasureTimeout() {
|
||||
<OptionsSection title={t("note_erasure_timeout.note_erasure_timeout_title")}>
|
||||
<FormText>{t("note_erasure_timeout.note_erasure_description")}</FormText>
|
||||
<FormGroup name="erase-entities-after" label={t("note_erasure_timeout.erase_notes_after")}>
|
||||
<TimeSelector
|
||||
name="erase-entities-after"
|
||||
<TimeSelector
|
||||
name="erase-entities-after"
|
||||
optionValueId="eraseEntitiesAfterTimeInSeconds" optionTimeScaleId="eraseEntitiesAfterTimeScale"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormText>{t("note_erasure_timeout.manual_erasing_description")}</FormText>
|
||||
|
||||
|
||||
<Button
|
||||
text={t("note_erasure_timeout.erase_deleted_notes_now")}
|
||||
onClick={() => {
|
||||
@@ -121,7 +123,7 @@ function NoteErasureTimeout() {
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentErasureTimeout() {
|
||||
@@ -145,7 +147,7 @@ function AttachmentErasureTimeout() {
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionSnapshotInterval() {
|
||||
@@ -165,7 +167,7 @@ function RevisionSnapshotInterval() {
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function RevisionSnapshotLimit() {
|
||||
@@ -176,7 +178,7 @@ function RevisionSnapshotLimit() {
|
||||
<FormText>{t("revisions_snapshot_limit.note_revisions_snapshot_limit_description")}</FormText>
|
||||
|
||||
<FormGroup name="revision-snapshot-number-limit">
|
||||
<FormTextBoxWithUnit
|
||||
<FormTextBoxWithUnit
|
||||
type="number" min={-1}
|
||||
currentValue={revisionSnapshotNumberLimit}
|
||||
unit={t("revisions_snapshot_limit.snapshot_number_limit_unit")}
|
||||
@@ -197,7 +199,7 @@ function RevisionSnapshotLimit() {
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function HtmlImportTags() {
|
||||
@@ -236,7 +238,7 @@ function HtmlImportTags() {
|
||||
onClick={() => setAllowedHtmlTags(SANITIZER_DEFAULT_ALLOWED_TAGS)}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ShareSettings() {
|
||||
@@ -246,8 +248,8 @@ function ShareSettings() {
|
||||
return (
|
||||
<OptionsSection title={t("share.title")}>
|
||||
<FormGroup name="redirectBareDomain" description={t("share.redirect_bare_domain_description")}>
|
||||
<FormCheckbox
|
||||
label={t(t("share.redirect_bare_domain"))}
|
||||
<FormCheckbox
|
||||
label={t(t("share.redirect_bare_domain"))}
|
||||
currentValue={redirectBareDomain}
|
||||
onChange={async value => {
|
||||
if (value) {
|
||||
@@ -264,17 +266,17 @@ function ShareSettings() {
|
||||
}
|
||||
setRedirectBareDomain(value);
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="showLoginInShareTheme" description={t("share.show_login_link_description")}>
|
||||
<FormCheckbox
|
||||
<FormCheckbox
|
||||
label={t("share.show_login_link")}
|
||||
currentValue={showLogInShareTheme} onChange={setShowLogInShareTheme}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkSettings() {
|
||||
@@ -288,5 +290,5 @@ function NetworkSettings() {
|
||||
currentValue={checkForUpdates} onChange={setCheckForUpdates}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { normalizeMimeTypeForCKEditor, type OptionNames } from "@triliumnext/com
|
||||
import { Themes } from "@triliumnext/highlightjs";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import type React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useNoteContext, useTriliumEvent } from "../../react/hooks";
|
||||
import "./mobile_editor_toolbar.css";
|
||||
import { isIOS } from "../../../services/utils";
|
||||
|
||||
import { CKTextEditor, ClassicEditor } from "@triliumnext/ckeditor5";
|
||||
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { isIOS } from "../../../services/utils";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteProperty, useTriliumEvent } from "../../react/hooks";
|
||||
|
||||
interface MobileEditorToolbarProps {
|
||||
inPopupEditor?: boolean;
|
||||
@@ -17,17 +19,13 @@ interface MobileEditorToolbarProps {
|
||||
export default function MobileEditorToolbar({ inPopupEditor }: MobileEditorToolbarProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { note, noteContext, ntxId } = useNoteContext();
|
||||
const [ shouldDisplay, setShouldDisplay ] = useState(false);
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const { isReadOnly } = useIsNoteReadOnly(note, noteContext);
|
||||
const shouldDisplay = noteType === "text" && isReadOnly === false;
|
||||
const [ dropdownActive, setDropdownActive ] = useState(false);
|
||||
|
||||
usePositioningOniOS(!inPopupEditor, containerRef);
|
||||
|
||||
useEffect(() => {
|
||||
noteContext?.isReadOnly().then(isReadOnly => {
|
||||
setShouldDisplay(note?.type === "text" && !isReadOnly);
|
||||
});
|
||||
}, [ note ]);
|
||||
|
||||
// Attach the toolbar from the CKEditor.
|
||||
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
|
||||
if (eventNtxId !== ntxId || !containerRef.current) return;
|
||||
@@ -62,15 +60,15 @@ export default function MobileEditorToolbar({ inPopupEditor }: MobileEditorToolb
|
||||
|
||||
return (
|
||||
<div className={`classic-toolbar-outer-container ${!shouldDisplay ? "hidden-ext" : "visible"} ${isIOS() ? "ios" : ""}`}>
|
||||
<div ref={containerRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`}></div>
|
||||
<div ref={containerRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef<HTMLDivElement | null>) {
|
||||
const adjustPosition = useCallback(() => {
|
||||
if (!wrapperRef.current) return;
|
||||
let bottom = window.innerHeight - (window.visualViewport?.height || 0);
|
||||
const bottom = window.innerHeight - (window.visualViewport?.height || 0);
|
||||
wrapperRef.current.style.bottom = `${bottom}px`;
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "40.0.0",
|
||||
"electron": "40.1.0",
|
||||
"@electron-forge/cli": "7.11.1",
|
||||
"@electron-forge/maker-deb": "7.11.1",
|
||||
"@electron-forge/maker-dmg": "7.11.1",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@triliumnext/desktop": "workspace:*",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "40.0.0",
|
||||
"electron": "40.1.0",
|
||||
"fs-extra": "11.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -17,17 +17,17 @@ test("Can drag tabs around", async ({ page, context }) => {
|
||||
await app.addNewTab();
|
||||
await app.addNewTab();
|
||||
|
||||
let tab = app.getTab(0);
|
||||
let tab = await app.getTab(0);
|
||||
|
||||
// Drag the first tab at the end
|
||||
await tab.dragTo(app.getTab(2), { targetPosition: { x: 50, y: 0 } });
|
||||
await tab.dragTo(await app.getTab(2), { targetPosition: { x: 50, y: 0 } });
|
||||
|
||||
tab = app.getTab(2);
|
||||
tab = await app.getTab(2);
|
||||
await expect(tab).toContainText(NOTE_TITLE);
|
||||
|
||||
// Drag the tab to the left
|
||||
await tab.dragTo(app.getTab(0), { targetPosition: { x: 50, y: 0 } });
|
||||
await expect(app.getTab(0)).toContainText(NOTE_TITLE);
|
||||
await tab.dragTo(await app.getTab(0), { targetPosition: { x: 50, y: 0 } });
|
||||
await expect(await app.getTab(0)).toContainText(NOTE_TITLE);
|
||||
});
|
||||
|
||||
test("Can drag tab to new window", async ({ page, context }) => {
|
||||
@@ -36,7 +36,7 @@ test("Can drag tab to new window", async ({ page, context }) => {
|
||||
|
||||
await app.closeAllTabs();
|
||||
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
|
||||
const tab = app.getTab(0);
|
||||
const tab = await app.getTab(0);
|
||||
await expect(tab).toContainText(NOTE_TITLE);
|
||||
|
||||
const popupPromise = page.waitForEvent("popup");
|
||||
@@ -75,14 +75,14 @@ test("Tabs are restored in right order", async ({ page, context }) => {
|
||||
await expect(app.getActiveTab()).toContainText("Mermaid");
|
||||
|
||||
// Select the mid one.
|
||||
await app.getTab(1).click();
|
||||
await (await app.getTab(1)).click();
|
||||
await expect(app.noteTreeActiveNote).toContainText("Text notes");
|
||||
|
||||
// Refresh the page and check the order.
|
||||
await app.goto( { preserveTabs: true });
|
||||
await expect(app.getTab(0)).toContainText("Code notes");
|
||||
await expect(app.getTab(1)).toContainText("Text notes");
|
||||
await expect(app.getTab(2)).toContainText("Mermaid");
|
||||
await expect(await app.getTab(0)).toContainText("Code notes");
|
||||
await expect(await app.getTab(1)).toContainText("Text notes");
|
||||
await expect(await app.getTab(2)).toContainText("Mermaid");
|
||||
|
||||
// Check the note tree has the right active node.
|
||||
await expect(app.noteTreeActiveNote).toContainText("Text notes");
|
||||
@@ -118,7 +118,7 @@ test("Search works when dismissing a tab", async ({ page, context }) => {
|
||||
await app.addNewTab();
|
||||
await app.goToNoteInNewTab("Sample mindmap");
|
||||
|
||||
await app.getTab(0).click();
|
||||
await (await app.getTab(0)).click();
|
||||
await app.openAndClickNoteActionMenu("Search in note");
|
||||
await expect(app.findAndReplaceWidget.first()).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export default class App {
|
||||
readonly currentNoteSplitTitle: Locator;
|
||||
readonly currentNoteSplitContent: Locator;
|
||||
readonly sidebar: Locator;
|
||||
private isMobile: boolean = false;
|
||||
|
||||
constructor(page: Page, context: BrowserContext) {
|
||||
this.page = page;
|
||||
@@ -43,6 +44,8 @@ export default class App {
|
||||
}
|
||||
|
||||
async goto({ url, isMobile, preserveTabs }: GotoOpts = {}) {
|
||||
this.isMobile = !!isMobile;
|
||||
|
||||
await this.context.addCookies([
|
||||
{
|
||||
url: BASE_URL,
|
||||
@@ -59,7 +62,7 @@ export default class App {
|
||||
|
||||
// Wait for the page to load.
|
||||
if (url === "/") {
|
||||
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||
await expect(this.page.locator(".tree", { hasText: "Trilium Integration Test" })).toBeVisible();
|
||||
if (!preserveTabs) {
|
||||
await this.closeAllTabs();
|
||||
}
|
||||
@@ -76,14 +79,19 @@ export default class App {
|
||||
const suggestionSelector = resultsSelector.locator(".aa-suggestion")
|
||||
.nth(1); // Select the second one (best candidate), as the first one is "Create a new note"
|
||||
await expect(suggestionSelector).toContainText(noteTitle);
|
||||
suggestionSelector.click();
|
||||
await suggestionSelector.click();
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
await this.page.locator(".launcher-button.bx-cog").click();
|
||||
}
|
||||
|
||||
getTab(tabIndex: number) {
|
||||
async getTab(tabIndex: number) {
|
||||
if (this.isMobile) {
|
||||
await this.launcherBar.locator(".mobile-tab-switcher").click();
|
||||
return this.page.locator(".modal.tab-bar-modal .tab-card").nth(tabIndex);
|
||||
}
|
||||
|
||||
return this.tabBar.locator(".note-tab-wrapper").nth(tabIndex);
|
||||
}
|
||||
|
||||
@@ -97,7 +105,8 @@ export default class App {
|
||||
async closeAllTabs() {
|
||||
await this.triggerCommand("closeAllTabs");
|
||||
// Page in Playwright is not updated somehow, need to click on the tab to make sure it's rendered
|
||||
await this.getTab(0).click();
|
||||
const tab = await this.getTab(0);
|
||||
await tab.click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -337,6 +337,155 @@ paths:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/notes/{noteId}/revisions:
|
||||
parameters:
|
||||
- name: noteId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
get:
|
||||
description: Returns all revisions for a note identified by its ID
|
||||
operationId: getNoteRevisions
|
||||
responses:
|
||||
"200":
|
||||
description: list of revisions
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Revision"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/notes/{noteId}/attachments:
|
||||
parameters:
|
||||
- name: noteId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
get:
|
||||
description: Returns all attachments for a note identified by its ID
|
||||
operationId: getNoteAttachments
|
||||
responses:
|
||||
"200":
|
||||
description: list of attachments
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Attachment"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/notes/{noteId}/undelete:
|
||||
parameters:
|
||||
- name: noteId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
post:
|
||||
description: Restore a deleted note. The note must be deleted and must have at least one undeleted parent.
|
||||
operationId: undeleteNote
|
||||
responses:
|
||||
"200":
|
||||
description: note restored successfully
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/notes/history:
|
||||
get:
|
||||
description: Returns recent changes including note creations, modifications, and deletions
|
||||
operationId: getNoteHistory
|
||||
parameters:
|
||||
- name: ancestorNoteId
|
||||
in: query
|
||||
required: false
|
||||
description: Limit changes to a subtree identified by this note ID. Defaults to "root" (all notes).
|
||||
schema:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
responses:
|
||||
"200":
|
||||
description: list of recent changes
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/RecentChange"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/revisions/{revisionId}:
|
||||
parameters:
|
||||
- name: revisionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
get:
|
||||
description: Returns a revision identified by its ID
|
||||
operationId: getRevisionById
|
||||
responses:
|
||||
"200":
|
||||
description: revision response
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Revision"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/revisions/{revisionId}/content:
|
||||
parameters:
|
||||
- name: revisionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
get:
|
||||
description: Returns revision content identified by its ID
|
||||
operationId: getRevisionContent
|
||||
responses:
|
||||
"200":
|
||||
description: revision content response
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/branches:
|
||||
post:
|
||||
description: >
|
||||
@@ -1186,3 +1335,93 @@ components:
|
||||
type: string
|
||||
description: Human readable error, potentially with more details,
|
||||
example: Note 'evnnmvHTCgIn' is protected and cannot be modified through ETAPI
|
||||
Revision:
|
||||
type: object
|
||||
description: Revision represents a snapshot of note's title and content at some point in the past.
|
||||
properties:
|
||||
revisionId:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
readOnly: true
|
||||
noteId:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
readOnly: true
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
text,
|
||||
code,
|
||||
render,
|
||||
file,
|
||||
image,
|
||||
search,
|
||||
relationMap,
|
||||
book,
|
||||
noteMap,
|
||||
mermaid,
|
||||
webView,
|
||||
shortcut,
|
||||
doc,
|
||||
contentWidget,
|
||||
launcher,
|
||||
]
|
||||
mime:
|
||||
type: string
|
||||
isProtected:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
title:
|
||||
type: string
|
||||
blobId:
|
||||
type: string
|
||||
description: ID of the blob object which effectively serves as a content hash
|
||||
dateLastEdited:
|
||||
$ref: "#/components/schemas/LocalDateTime"
|
||||
readOnly: true
|
||||
dateCreated:
|
||||
$ref: "#/components/schemas/LocalDateTime"
|
||||
readOnly: true
|
||||
utcDateLastEdited:
|
||||
$ref: "#/components/schemas/UtcDateTime"
|
||||
readOnly: true
|
||||
utcDateCreated:
|
||||
$ref: "#/components/schemas/UtcDateTime"
|
||||
readOnly: true
|
||||
utcDateModified:
|
||||
$ref: "#/components/schemas/UtcDateTime"
|
||||
readOnly: true
|
||||
contentLength:
|
||||
type: integer
|
||||
format: int32
|
||||
readOnly: true
|
||||
RecentChange:
|
||||
type: object
|
||||
description: Represents a recent change event (creation, modification, or deletion).
|
||||
properties:
|
||||
noteId:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
readOnly: true
|
||||
title:
|
||||
type: string
|
||||
description: Title at the time of the change (may be "[protected]" for protected notes)
|
||||
current_title:
|
||||
type: string
|
||||
description: Current title of the note (may be "[protected]" for protected notes)
|
||||
current_isDeleted:
|
||||
type: boolean
|
||||
description: Whether the note is currently deleted
|
||||
current_deleteId:
|
||||
type: string
|
||||
description: Delete ID if the note is deleted
|
||||
current_isProtected:
|
||||
type: boolean
|
||||
description: Whether the note is protected
|
||||
utcDate:
|
||||
$ref: "#/components/schemas/UtcDateTime"
|
||||
description: UTC timestamp of the change
|
||||
date:
|
||||
$ref: "#/components/schemas/LocalDateTime"
|
||||
description: Local timestamp of the change
|
||||
canBeUndeleted:
|
||||
type: boolean
|
||||
description: Whether the note can be undeleted (only present for deleted notes)
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"sucrase": "3.35.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@anthropic-ai/sdk": "0.72.1",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@electron/remote": "2.1.3",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/express-partial-content": "workspace:*",
|
||||
@@ -83,7 +83,7 @@
|
||||
"debounce": "3.0.0",
|
||||
"debug": "4.4.3",
|
||||
"ejs": "4.0.1",
|
||||
"electron": "40.0.0",
|
||||
"electron": "40.1.0",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
|
||||
82
apps/server/spec/etapi/get-note-attachments.spec.ts
Normal file
82
apps/server/spec/etapi/get-note-attachments.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Application } from "express";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { createNote, login } from "./utils.js";
|
||||
import config from "../../src/services/config.js";
|
||||
|
||||
let app: Application;
|
||||
let token: string;
|
||||
|
||||
const USER = "etapi";
|
||||
let createdNoteId: string;
|
||||
let createdAttachmentId: string;
|
||||
|
||||
describe("etapi/get-note-attachments", () => {
|
||||
beforeAll(async () => {
|
||||
config.General.noAuthentication = false;
|
||||
const buildApp = (await (import("../../src/app.js"))).default;
|
||||
app = await buildApp();
|
||||
token = await login(app);
|
||||
|
||||
createdNoteId = await createNote(app, token);
|
||||
|
||||
// Create an attachment for the note
|
||||
const response = await supertest(app)
|
||||
.post(`/etapi/attachments`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.send({
|
||||
"ownerId": createdNoteId,
|
||||
"role": "file",
|
||||
"mime": "text/plain",
|
||||
"title": "test-attachment.txt",
|
||||
"content": "test content",
|
||||
"position": 10
|
||||
});
|
||||
createdAttachmentId = response.body.attachmentId;
|
||||
expect(createdAttachmentId).toBeTruthy();
|
||||
});
|
||||
|
||||
it("gets attachments for a note", async () => {
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes/${createdNoteId}/attachments`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
|
||||
const attachment = response.body[0];
|
||||
expect(attachment).toHaveProperty("attachmentId", createdAttachmentId);
|
||||
expect(attachment).toHaveProperty("ownerId", createdNoteId);
|
||||
expect(attachment).toHaveProperty("role", "file");
|
||||
expect(attachment).toHaveProperty("mime", "text/plain");
|
||||
expect(attachment).toHaveProperty("title", "test-attachment.txt");
|
||||
expect(attachment).toHaveProperty("position", 10);
|
||||
expect(attachment).toHaveProperty("blobId");
|
||||
expect(attachment).toHaveProperty("dateModified");
|
||||
expect(attachment).toHaveProperty("utcDateModified");
|
||||
expect(attachment).toHaveProperty("contentLength");
|
||||
});
|
||||
|
||||
it("returns empty array for note with no attachments", async () => {
|
||||
// Create a new note without any attachments
|
||||
const newNoteId = await createNote(app, token, "Note without attachments");
|
||||
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes/${newNoteId}/attachments`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent note", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/notes/nonexistentnote/attachments")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
|
||||
});
|
||||
});
|
||||
77
apps/server/spec/etapi/get-note-revisions.spec.ts
Normal file
77
apps/server/spec/etapi/get-note-revisions.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Application } from "express";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { createNote, login } from "./utils.js";
|
||||
import config from "../../src/services/config.js";
|
||||
|
||||
let app: Application;
|
||||
let token: string;
|
||||
|
||||
const USER = "etapi";
|
||||
let createdNoteId: string;
|
||||
|
||||
describe("etapi/get-note-revisions", () => {
|
||||
beforeAll(async () => {
|
||||
config.General.noAuthentication = false;
|
||||
const buildApp = (await (import("../../src/app.js"))).default;
|
||||
app = await buildApp();
|
||||
token = await login(app);
|
||||
createdNoteId = await createNote(app, token);
|
||||
|
||||
// Create a revision by updating the note content
|
||||
await supertest(app)
|
||||
.put(`/etapi/notes/${createdNoteId}/content`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.set("Content-Type", "text/plain")
|
||||
.send("Updated content for revision")
|
||||
.expect(204);
|
||||
|
||||
// Force create a revision
|
||||
await supertest(app)
|
||||
.post(`/etapi/notes/${createdNoteId}/revision`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(204);
|
||||
});
|
||||
|
||||
it("gets revisions for a note", async () => {
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes/${createdNoteId}/revisions`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
|
||||
const revision = response.body[0];
|
||||
expect(revision).toHaveProperty("revisionId");
|
||||
expect(revision).toHaveProperty("noteId", createdNoteId);
|
||||
expect(revision).toHaveProperty("type");
|
||||
expect(revision).toHaveProperty("mime");
|
||||
expect(revision).toHaveProperty("title");
|
||||
expect(revision).toHaveProperty("isProtected");
|
||||
expect(revision).toHaveProperty("blobId");
|
||||
expect(revision).toHaveProperty("utcDateCreated");
|
||||
});
|
||||
|
||||
it("returns empty array for note with no revisions", async () => {
|
||||
// Create a new note without any revisions
|
||||
const newNoteId = await createNote(app, token, "Brand new content");
|
||||
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes/${newNoteId}/revisions`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
// New notes may or may not have revisions depending on settings
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent note", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/notes/nonexistentnote/revisions")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
|
||||
});
|
||||
});
|
||||
71
apps/server/spec/etapi/get-revision.spec.ts
Normal file
71
apps/server/spec/etapi/get-revision.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Application } from "express";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { createNote, login } from "./utils.js";
|
||||
import config from "../../src/services/config.js";
|
||||
|
||||
let app: Application;
|
||||
let token: string;
|
||||
|
||||
const USER = "etapi";
|
||||
let createdNoteId: string;
|
||||
let revisionId: string;
|
||||
|
||||
describe("etapi/get-revision", () => {
|
||||
beforeAll(async () => {
|
||||
config.General.noAuthentication = false;
|
||||
const buildApp = (await (import("../../src/app.js"))).default;
|
||||
app = await buildApp();
|
||||
token = await login(app);
|
||||
createdNoteId = await createNote(app, token, "Initial content");
|
||||
|
||||
// Update content to create a revision
|
||||
await supertest(app)
|
||||
.put(`/etapi/notes/${createdNoteId}/content`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.set("Content-Type", "text/plain")
|
||||
.send("Updated content")
|
||||
.expect(204);
|
||||
|
||||
// Force create a revision
|
||||
await supertest(app)
|
||||
.post(`/etapi/notes/${createdNoteId}/revision`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(204);
|
||||
|
||||
// Get the revision ID
|
||||
const revisionsResponse = await supertest(app)
|
||||
.get(`/etapi/notes/${createdNoteId}/revisions`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(revisionsResponse.body.length).toBeGreaterThan(0);
|
||||
revisionId = revisionsResponse.body[0].revisionId;
|
||||
});
|
||||
|
||||
it("gets revision metadata by ID", async () => {
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/revisions/${revisionId}`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty("revisionId", revisionId);
|
||||
expect(response.body).toHaveProperty("noteId", createdNoteId);
|
||||
expect(response.body).toHaveProperty("type", "text");
|
||||
expect(response.body).toHaveProperty("mime", "text/html");
|
||||
expect(response.body).toHaveProperty("title", "Hello");
|
||||
expect(response.body).toHaveProperty("isProtected", false);
|
||||
expect(response.body).toHaveProperty("blobId");
|
||||
expect(response.body).toHaveProperty("utcDateCreated");
|
||||
expect(response.body).toHaveProperty("utcDateModified");
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent revision", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/revisions/nonexistentrevision")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.code).toStrictEqual("REVISION_NOT_FOUND");
|
||||
});
|
||||
});
|
||||
94
apps/server/spec/etapi/note-history.spec.ts
Normal file
94
apps/server/spec/etapi/note-history.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Application } from "express";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { createNote, login } from "./utils.js";
|
||||
import config from "../../src/services/config.js";
|
||||
|
||||
let app: Application;
|
||||
let token: string;
|
||||
|
||||
const USER = "etapi";
|
||||
let createdNoteId: string;
|
||||
|
||||
describe("etapi/note-history", () => {
|
||||
beforeAll(async () => {
|
||||
config.General.noAuthentication = false;
|
||||
const buildApp = (await (import("../../src/app.js"))).default;
|
||||
app = await buildApp();
|
||||
token = await login(app);
|
||||
|
||||
// Create a note to ensure there's some history
|
||||
createdNoteId = await createNote(app, token, "History test content");
|
||||
|
||||
// Create a revision to ensure history has entries
|
||||
await supertest(app)
|
||||
.post(`/etapi/notes/${createdNoteId}/revision`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(204);
|
||||
});
|
||||
|
||||
it("gets recent changes history", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/notes/history")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that history entries have expected properties
|
||||
const entry = response.body[0];
|
||||
expect(entry).toHaveProperty("noteId");
|
||||
expect(entry).toHaveProperty("title");
|
||||
expect(entry).toHaveProperty("utcDate");
|
||||
expect(entry).toHaveProperty("date");
|
||||
expect(entry).toHaveProperty("current_isDeleted");
|
||||
expect(entry).toHaveProperty("current_isProtected");
|
||||
});
|
||||
|
||||
it("filters history by ancestor note", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/notes/history?ancestorNoteId=root")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
// All results should be descendants of root (which is everything)
|
||||
});
|
||||
|
||||
it("returns empty array for non-existent ancestor", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/notes/history?ancestorNoteId=nonexistentancestor")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
// Should be empty since no notes are descendants of a non-existent note
|
||||
expect(response.body.length).toBe(0);
|
||||
});
|
||||
|
||||
it("includes canBeUndeleted for deleted notes", async () => {
|
||||
// Create and delete a note
|
||||
const noteToDeleteId = await createNote(app, token, "Note to delete for history test");
|
||||
|
||||
await supertest(app)
|
||||
.delete(`/etapi/notes/${noteToDeleteId}`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(204);
|
||||
|
||||
// Check history - deleted note should appear with canBeUndeleted property
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/notes/history")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
const deletedEntry = response.body.find(
|
||||
(entry: any) => entry.noteId === noteToDeleteId && entry.current_isDeleted === true
|
||||
);
|
||||
|
||||
// Deleted entries should have canBeUndeleted property
|
||||
if (deletedEntry) {
|
||||
expect(deletedEntry).toHaveProperty("canBeUndeleted");
|
||||
}
|
||||
});
|
||||
});
|
||||
64
apps/server/spec/etapi/revision-content.spec.ts
Normal file
64
apps/server/spec/etapi/revision-content.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Application } from "express";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { createNote, login } from "./utils.js";
|
||||
import config from "../../src/services/config.js";
|
||||
|
||||
let app: Application;
|
||||
let token: string;
|
||||
|
||||
const USER = "etapi";
|
||||
let createdNoteId: string;
|
||||
let revisionId: string;
|
||||
|
||||
describe("etapi/revision-content", () => {
|
||||
beforeAll(async () => {
|
||||
config.General.noAuthentication = false;
|
||||
const buildApp = (await (import("../../src/app.js"))).default;
|
||||
app = await buildApp();
|
||||
token = await login(app);
|
||||
createdNoteId = await createNote(app, token, "Initial revision content");
|
||||
|
||||
// Update content to ensure we have content in the revision
|
||||
await supertest(app)
|
||||
.put(`/etapi/notes/${createdNoteId}/content`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.set("Content-Type", "text/plain")
|
||||
.send("Content after first update")
|
||||
.expect(204);
|
||||
|
||||
// Force create a revision
|
||||
await supertest(app)
|
||||
.post(`/etapi/notes/${createdNoteId}/revision`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(204);
|
||||
|
||||
// Get the revision ID
|
||||
const revisionsResponse = await supertest(app)
|
||||
.get(`/etapi/notes/${createdNoteId}/revisions`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(revisionsResponse.body.length).toBeGreaterThan(0);
|
||||
revisionId = revisionsResponse.body[0].revisionId;
|
||||
});
|
||||
|
||||
it("gets revision content", async () => {
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/revisions/${revisionId}/content`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/text\/html/);
|
||||
expect(response.text).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent revision content", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/revisions/nonexistentrevision/content")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.code).toStrictEqual("REVISION_NOT_FOUND");
|
||||
});
|
||||
});
|
||||
103
apps/server/spec/etapi/undelete-note.spec.ts
Normal file
103
apps/server/spec/etapi/undelete-note.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Application } from "express";
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { login } from "./utils.js";
|
||||
import config from "../../src/services/config.js";
|
||||
import { randomInt } from "crypto";
|
||||
|
||||
let app: Application;
|
||||
let token: string;
|
||||
|
||||
const USER = "etapi";
|
||||
|
||||
describe("etapi/undelete-note", () => {
|
||||
beforeAll(async () => {
|
||||
config.General.noAuthentication = false;
|
||||
const buildApp = (await (import("../../src/app.js"))).default;
|
||||
app = await buildApp();
|
||||
token = await login(app);
|
||||
});
|
||||
|
||||
it("undeletes a deleted note", async () => {
|
||||
// Create a note
|
||||
const noteId = `testNote${randomInt(10000)}`;
|
||||
await supertest(app)
|
||||
.post("/etapi/create-note")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.send({
|
||||
"noteId": noteId,
|
||||
"parentNoteId": "root",
|
||||
"title": "Note to delete and restore",
|
||||
"type": "text",
|
||||
"content": "Content to restore"
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
// Verify note exists
|
||||
await supertest(app)
|
||||
.get(`/etapi/notes/${noteId}`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
// Delete the note
|
||||
await supertest(app)
|
||||
.delete(`/etapi/notes/${noteId}`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(204);
|
||||
|
||||
// Verify note is deleted (should return 404)
|
||||
await supertest(app)
|
||||
.get(`/etapi/notes/${noteId}`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(404);
|
||||
|
||||
// Undelete the note
|
||||
const response = await supertest(app)
|
||||
.post(`/etapi/notes/${noteId}/undelete`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
|
||||
// Verify note is restored
|
||||
const restoredResponse = await supertest(app)
|
||||
.get(`/etapi/notes/${noteId}`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(restoredResponse.body.title).toStrictEqual("Note to delete and restore");
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent note", async () => {
|
||||
const response = await supertest(app)
|
||||
.post("/etapi/notes/nonexistentnote/undelete")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
|
||||
});
|
||||
|
||||
it("returns 400 when trying to undelete a non-deleted note", async () => {
|
||||
// Create a note
|
||||
const noteId = `testNote${randomInt(10000)}`;
|
||||
await supertest(app)
|
||||
.post("/etapi/create-note")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.send({
|
||||
"noteId": noteId,
|
||||
"parentNoteId": "root",
|
||||
"title": "Note not deleted",
|
||||
"type": "text",
|
||||
"content": "Content"
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
// Try to undelete a note that isn't deleted
|
||||
const response = await supertest(app)
|
||||
.post(`/etapi/notes/${noteId}/undelete`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.code).toStrictEqual("NOTE_NOT_DELETED");
|
||||
});
|
||||
});
|
||||
@@ -354,6 +354,13 @@
|
||||
<td>Hides the <a class="reference-link" href="#root/_help_0ESUbbAxVnoK">Note List</a> for
|
||||
that particular note.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>subtreeHidden</code>
|
||||
</td>
|
||||
<td>Hides all child notes of this note from the tree, displaying a badge with
|
||||
the count of hidden children. Children remain accessible via search or
|
||||
direct links.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>printLandscape</code>
|
||||
</td>
|
||||
@@ -372,6 +379,11 @@
|
||||
<a
|
||||
class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>map:*</code>
|
||||
</td>
|
||||
<td>Defines specific options for the <a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>calendar:*</code>
|
||||
</td>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user