mirror of
https://github.com/zadam/trilium.git
synced 2026-01-21 14:52:19 +01:00
Compare commits
216 Commits
standalone
...
renovate/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da6294ef2 | ||
|
|
60c789b6c7 | ||
|
|
f83d95136d | ||
|
|
f96ed0af26 | ||
|
|
1539664026 | ||
|
|
8aff775d0e | ||
|
|
94248eafe9 | ||
|
|
02335bba3f | ||
|
|
dad9578b83 | ||
|
|
c043788b09 | ||
|
|
e5bc416b46 | ||
|
|
c97f52da36 | ||
|
|
1661c3292a | ||
|
|
4e80c07630 | ||
|
|
d43309947e | ||
|
|
c7cc702c4a | ||
|
|
c304753ffc | ||
|
|
da59c14231 | ||
|
|
e0b3e41c9e | ||
|
|
5d1a63bce0 | ||
|
|
84cf4ef4a3 | ||
|
|
ec4b6f0a90 | ||
|
|
60dbdbeb71 | ||
|
|
418a546583 | ||
|
|
e6380b87b6 | ||
|
|
1d3d214101 | ||
|
|
a38067560b | ||
|
|
eac7235199 | ||
|
|
4c55e857b8 | ||
|
|
e33950e000 | ||
|
|
fc0ccbfcf5 | ||
|
|
4a82bbb035 | ||
|
|
57d894e765 | ||
|
|
97dfad419c | ||
|
|
bfc521fdc0 | ||
|
|
197fa90176 | ||
|
|
0844914e11 | ||
|
|
c376b0bbe2 | ||
|
|
4491086c55 | ||
|
|
791697369d | ||
|
|
28d0bfd229 | ||
|
|
8182a04eae | ||
|
|
711828d6b4 | ||
|
|
69e88c1d9f | ||
|
|
748b87da9a | ||
|
|
94dca4cd87 | ||
|
|
7179701e0f | ||
|
|
af5061646c | ||
|
|
9c4163ad3a | ||
|
|
46c3f5296a | ||
|
|
ebadcfd844 | ||
|
|
b7d4947462 | ||
|
|
a599526dea | ||
|
|
c4f166fe12 | ||
|
|
a8ae91aa3b | ||
|
|
82b3692acb | ||
|
|
432c054b68 | ||
|
|
dcd8bfa255 | ||
|
|
c287a2ae97 | ||
|
|
8fdadb3798 | ||
|
|
3fce4fc66c | ||
|
|
d83d7ed106 | ||
|
|
1b812f1886 | ||
|
|
56fcc7adcc | ||
|
|
fb0c7359f1 | ||
|
|
4c4e5b85e9 | ||
|
|
476247beb5 | ||
|
|
2c87f609f3 | ||
|
|
bc79ff6845 | ||
|
|
f10373d54f | ||
|
|
630d16b722 | ||
|
|
769f3db21c | ||
|
|
c6896a4b33 | ||
|
|
7c18025098 | ||
|
|
6ae74b3181 | ||
|
|
2ecfbbf284 | ||
|
|
781de9a1fb | ||
|
|
6972a4b901 | ||
|
|
52ed1750ac | ||
|
|
9010e0b1ce | ||
|
|
5053e74447 | ||
|
|
f294276849 | ||
|
|
0740788cc8 | ||
|
|
9bac07ce62 | ||
|
|
3d8289d394 | ||
|
|
5a60fdad8a | ||
|
|
62cca5a96b | ||
|
|
74548d638e | ||
|
|
e0ccf30f4f | ||
|
|
d2c6081537 | ||
|
|
bd933f2c4c | ||
|
|
1d898d618e | ||
|
|
11c8e5b3b2 | ||
|
|
fe4c3ffecb | ||
|
|
334a96186e | ||
|
|
34b2df705b | ||
|
|
5a7fc1c8b6 | ||
|
|
fabab6abb1 | ||
|
|
0c9c20c0c5 | ||
|
|
67cc1113b1 | ||
|
|
3aacd255f4 | ||
|
|
ccfda21413 | ||
|
|
46c88506cc | ||
|
|
51157e1979 | ||
|
|
bfb6d975ff | ||
|
|
aa01bc1457 | ||
|
|
5600f1b7b1 | ||
|
|
a169db807c | ||
|
|
f40348daff | ||
|
|
f63042ef87 | ||
|
|
d148c9d1c6 | ||
|
|
f72929ca13 | ||
|
|
cc3e3ca4d4 | ||
|
|
8fad664a6d | ||
|
|
f1946c1386 | ||
|
|
ea8bd0136f | ||
|
|
4a3f72ae50 | ||
|
|
c944762ef6 | ||
|
|
f6924d7fda | ||
|
|
3a0880fcd6 | ||
|
|
df62dc87b2 | ||
|
|
d42679315e | ||
|
|
2a19be5ab6 | ||
|
|
33bbe994d7 | ||
|
|
04c598caea | ||
|
|
3577688bf9 | ||
|
|
e0439655df | ||
|
|
84f944f78a | ||
|
|
022b6df959 | ||
|
|
9e3e92669f | ||
|
|
35b96a71fc | ||
|
|
4b78de6726 | ||
|
|
4771e02909 | ||
|
|
03dffdb65f | ||
|
|
859a3948cd | ||
|
|
161aa625e6 | ||
|
|
28fd945e80 | ||
|
|
748fb0bf05 | ||
|
|
98e1d0afd9 | ||
|
|
9c61ce1835 | ||
|
|
c3a5705be0 | ||
|
|
9d0fa9f7ca | ||
|
|
cc4ceb975e | ||
|
|
8a6495a0bd | ||
|
|
1d95392d22 | ||
|
|
93dd08d629 | ||
|
|
f466367c4d | ||
|
|
924d495d2e | ||
|
|
0b5c1b648b | ||
|
|
03ec39f52a | ||
|
|
f3ae1bf12c | ||
|
|
6d61cf41d9 | ||
|
|
333679523b | ||
|
|
1cd3a361f6 | ||
|
|
7405627663 | ||
|
|
2c4fb6c0d0 | ||
|
|
3e284208ef | ||
|
|
42b7f4c795 | ||
|
|
d445209eeb | ||
|
|
ed1bf17add | ||
|
|
4800f2a172 | ||
|
|
e7ff364c01 | ||
|
|
79ca299726 | ||
|
|
9d7ba48a6a | ||
|
|
9329665919 | ||
|
|
f6821bce03 | ||
|
|
a7aedf93ab | ||
|
|
4ffcf01452 | ||
|
|
618459d353 | ||
|
|
6436c16449 | ||
|
|
1bec457004 | ||
|
|
3e541e37fe | ||
|
|
a41b78d36f | ||
|
|
79c50f3b4c | ||
|
|
0f54b01cdd | ||
|
|
41f2b03711 | ||
|
|
0be76f982c | ||
|
|
5deb277672 | ||
|
|
3d15c9e94c | ||
|
|
1363f94621 | ||
|
|
abdcd6cc0c | ||
|
|
dc8abed2f3 | ||
|
|
892c2cd838 | ||
|
|
2796b29138 | ||
|
|
05620a129f | ||
|
|
cb11955a44 | ||
|
|
c3623a15fb | ||
|
|
0273c64bbf | ||
|
|
5b37140ffa | ||
|
|
fb4d63b049 | ||
|
|
015e41e792 | ||
|
|
8e47f33329 | ||
|
|
ad4a8ec5f4 | ||
|
|
56356f9c61 | ||
|
|
abda0f9111 | ||
|
|
c2a9d21198 | ||
|
|
59ffa1fa93 | ||
|
|
d056185368 | ||
|
|
251c1f6471 | ||
|
|
9efca9827e | ||
|
|
f5e2129ad4 | ||
|
|
6c8e6f2429 | ||
|
|
9b4b1a393e | ||
|
|
028334407c | ||
|
|
b93540b40d | ||
|
|
9e7eba5eab | ||
|
|
a5e8c8f573 | ||
|
|
644cc27fa7 | ||
|
|
72d9e846b7 | ||
|
|
6595fd9c10 | ||
|
|
2dd541e1d0 | ||
|
|
8157ef5e74 | ||
|
|
0185dd0d18 | ||
|
|
142ed42d90 | ||
|
|
a1c0314334 | ||
|
|
3ecdcd9ea0 |
30
.github/workflows/i18n.yml
vendored
Normal file
30
.github/workflows/i18n.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Internationalization
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "weblate:*"
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/client/src/translations/**"
|
||||
- ".github/workflows/i18n.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
i18n-check:
|
||||
name: Check i18n translations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Check translations
|
||||
run: pnpm tsx scripts/translation/check-translation-coverage.ts
|
||||
2
.github/workflows/website.yml
vendored
2
.github/workflows/website.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --filter website --frozen-lockfile
|
||||
run: pnpm install --filter website --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Build the website
|
||||
run: pnpm website:build
|
||||
|
||||
11
README.md
11
README.md
@@ -165,6 +165,17 @@ pnpm install
|
||||
pnpm edit-docs:edit-docs
|
||||
```
|
||||
|
||||
Alternatively, if you have Nix installed:
|
||||
```shell
|
||||
# Run directly
|
||||
nix run .#edit-docs
|
||||
|
||||
# Or install to your profile
|
||||
nix profile install .#edit-docs
|
||||
trilium-edit-docs
|
||||
```
|
||||
|
||||
|
||||
### Building the Executable
|
||||
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
|
||||
```shell
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.4",
|
||||
"@redocly/cli": "2.14.5",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.3",
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "17.0.0",
|
||||
"i18next": "25.7.3",
|
||||
"i18next": "25.7.4",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"jsplumb": "2.15.6",
|
||||
"katex": "0.16.27",
|
||||
@@ -56,11 +56,11 @@
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.5.0",
|
||||
"mind-elixir": "5.6.1",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.2",
|
||||
"react-i18next": "16.5.1",
|
||||
"react-i18next": "16.5.3",
|
||||
"react-window": "2.2.5",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
@@ -78,9 +78,9 @@
|
||||
"@types/reveal.js": "5.2.2",
|
||||
"@types/tabulator-tables": "6.3.1",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.0.11",
|
||||
"happy-dom": "20.3.3",
|
||||
"lightningcss": "1.30.2",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
"vite-plugin-static-copy": "3.1.5"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||
import { SqlExecuteResponse } from "@triliumnext/commons";
|
||||
import type { NativeImage, TouchBar } from "electron";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
|
||||
@@ -410,7 +410,7 @@ type EventMappings = {
|
||||
addNewLabel: CommandData;
|
||||
addNewRelation: CommandData;
|
||||
sqlQueryResults: CommandData & {
|
||||
results: SqlExecuteResults;
|
||||
response: SqlExecuteResponse;
|
||||
};
|
||||
readOnlyTemporarilyDisabled: {
|
||||
noteContext: NoteContext;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import utils from "../services/utils.js";
|
||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
|
||||
import bundleService from "../services/bundle.js";
|
||||
import dateNoteService from "../services/date_notes.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import linkService from "../services/link.js";
|
||||
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||
import server from "../services/server.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import utils from "../services/utils.js";
|
||||
import ws from "../services/ws.js";
|
||||
import appContext, { type NoteCommandData } from "./app_context.js";
|
||||
import Component from "./component.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import ws from "../services/ws.js";
|
||||
import bundleService from "../services/bundle.js";
|
||||
import froca from "../services/froca.js";
|
||||
import linkService from "../services/link.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
|
||||
export default class Entrypoints extends Component {
|
||||
constructor() {
|
||||
@@ -187,13 +188,8 @@ export default class Entrypoints extends Component {
|
||||
} else if (note.mime.endsWith("env=backend")) {
|
||||
await server.post(`script/run/${note.noteId}`);
|
||||
} else if (note.mime === "text/x-sqlite;schema=trilium") {
|
||||
const resp = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
|
||||
}
|
||||
|
||||
await appContext.triggerEvent("sqlQueryResults", { ntxId: ntxId, results: resp.results });
|
||||
const response = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
||||
await appContext.triggerEvent("sqlQueryResults", { ntxId, response });
|
||||
}
|
||||
|
||||
toastService.showMessage(t("entrypoints.note-executed"));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MIME_TYPES_DICT } from "@triliumnext/commons";
|
||||
import { getNoteIcon } from "@triliumnext/commons";
|
||||
|
||||
import cssClassManager from "../services/css_class_manager.js";
|
||||
import type { Froca } from "../services/froca-interface.js";
|
||||
@@ -13,25 +13,6 @@ import type { AttributeType, default as FAttribute } from "./fattribute.js";
|
||||
const LABEL = "label";
|
||||
const RELATION = "relation";
|
||||
|
||||
export const NOTE_TYPE_ICONS = {
|
||||
file: "bx bx-file",
|
||||
image: "bx bx-image",
|
||||
code: "bx bx-code",
|
||||
render: "bx bx-extension",
|
||||
search: "bx bx-file-find",
|
||||
relationMap: "bx bxs-network-chart",
|
||||
book: "bx bx-book",
|
||||
noteMap: "bx bxs-network-chart",
|
||||
mermaid: "bx bx-selection",
|
||||
canvas: "bx bx-pen",
|
||||
webView: "bx bx-globe-alt",
|
||||
launcher: "bx bx-link",
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap",
|
||||
aiChat: "bx bx-bot"
|
||||
};
|
||||
|
||||
/**
|
||||
* There are many different Note types, some of which are entirely opaque to the
|
||||
* end user. Those types should be used only for checking against, they are
|
||||
@@ -582,32 +563,18 @@ export default class FNote {
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return `tn-icon ${this.#getIconInternal()}`;
|
||||
}
|
||||
|
||||
#getIconInternal() {
|
||||
const iconClassLabels = this.getLabels("iconClass");
|
||||
const workspaceIconClass = this.getWorkspaceIconClass();
|
||||
|
||||
if (iconClassLabels && iconClassLabels.length > 0) {
|
||||
return iconClassLabels[0].value;
|
||||
} else if (workspaceIconClass) {
|
||||
return workspaceIconClass;
|
||||
} else if (this.noteId === "root") {
|
||||
return "bx bx-home-alt-2";
|
||||
}
|
||||
if (this.noteId === "_share") {
|
||||
return "bx bx-share-alt";
|
||||
} else if (this.type === "text") {
|
||||
if (this.isFolder()) {
|
||||
return "bx bx-folder";
|
||||
}
|
||||
return "bx bx-note";
|
||||
} else if (this.type === "code") {
|
||||
const correspondingMimeType = MIME_TYPES_DICT.find(m => m.mime === this.mime);
|
||||
return correspondingMimeType?.icon ?? NOTE_TYPE_ICONS.code;
|
||||
}
|
||||
return NOTE_TYPE_ICONS[this.type];
|
||||
const icon = getNoteIcon({
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
|
||||
workspaceIconClass,
|
||||
isFolder: this.isFolder.bind(this)
|
||||
});
|
||||
return `tn-icon ${icon}`;
|
||||
}
|
||||
|
||||
getColorClass() {
|
||||
|
||||
@@ -16,6 +16,17 @@ async function initJQuery() {
|
||||
const $ = (await import("jquery")).default;
|
||||
window.$ = $;
|
||||
window.jQuery = $;
|
||||
|
||||
// Polyfill removed jQuery methods for autocomplete.js compatibility
|
||||
($ as any).isArray = Array.isArray;
|
||||
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
|
||||
($ as any).isPlainObject = function(obj: any) {
|
||||
if (obj == null || typeof obj !== 'object') { return false; }
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
if (proto === null) { return true; }
|
||||
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
|
||||
return typeof Ctor === 'function' && Ctor === Object;
|
||||
};
|
||||
}
|
||||
|
||||
async function setupGlob() {
|
||||
@@ -39,22 +50,25 @@ async function loadBootstrapCss() {
|
||||
}
|
||||
|
||||
function loadStylesheets() {
|
||||
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
|
||||
const cssToLoad: string[] = [];
|
||||
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
|
||||
cssToLoad.push(`api/fonts`);
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
|
||||
if (themeCssUrl) {
|
||||
cssToLoad.push(themeCssUrl);
|
||||
if (device !== "print") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
|
||||
cssToLoad.push(`api/fonts`);
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
|
||||
if (themeCssUrl) {
|
||||
cssToLoad.push(themeCssUrl);
|
||||
}
|
||||
if (themeUseNextAsBase === "next") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
|
||||
} else if (themeUseNextAsBase === "next-dark") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
|
||||
} else if (themeUseNextAsBase === "next-light") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
|
||||
}
|
||||
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
|
||||
}
|
||||
if (themeUseNextAsBase === "next") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
|
||||
} else if (themeUseNextAsBase === "next-dark") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
|
||||
} else if (themeUseNextAsBase === "next-light") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
|
||||
}
|
||||
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
|
||||
|
||||
for (const href of cssToLoad) {
|
||||
const linkEl = document.createElement("link");
|
||||
@@ -91,10 +105,17 @@ function setBodyAttributes() {
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
if (glob.device === "mobile") {
|
||||
await import("./mobile.js");
|
||||
} else {
|
||||
await import("./desktop.js");
|
||||
switch (glob.device) {
|
||||
case "mobile":
|
||||
await import("./mobile.js");
|
||||
break;
|
||||
case "print":
|
||||
await import("./print.js");
|
||||
break;
|
||||
case "desktop":
|
||||
default:
|
||||
await import("./desktop.js");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,6 @@ import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
import RightPanelContainer from "../widgets/sidebar/RightPanelContainer.jsx";
|
||||
import SqlResults from "../widgets/sql_result.js";
|
||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||
@@ -163,11 +161,9 @@ export default class DesktopLayout {
|
||||
.child(<SharedInfo />)
|
||||
)
|
||||
.optChild(!isNewLayout, <PromotedAttributes />)
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(<NoteDetail />)
|
||||
.child(<NoteList media="screen" />)
|
||||
.child(<SearchResult />)
|
||||
.child(<SqlResults />)
|
||||
.child(<ScrollPadding />)
|
||||
)
|
||||
.child(<ApiLog />)
|
||||
|
||||
@@ -29,7 +29,9 @@ async function main() {
|
||||
const froca = (await import("./services/froca")).default;
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
render(<App note={note} noteId={noteId} />, document.body);
|
||||
const bodyWrapper = document.createElement("div");
|
||||
render(<App note={note} noteId={noteId} />, bodyWrapper);
|
||||
document.body.appendChild(bodyWrapper);
|
||||
}
|
||||
|
||||
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
|
||||
|
||||
@@ -8,6 +8,17 @@ async function loadBootstrap() {
|
||||
}
|
||||
}
|
||||
|
||||
// Polyfill removed jQuery methods for autocomplete.js compatibility
|
||||
($ as any).isArray = Array.isArray;
|
||||
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
|
||||
($ as any).isPlainObject = function(obj: any) {
|
||||
if (obj == null || typeof obj !== 'object') { return false; }
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
if (proto === null) { return true; }
|
||||
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
|
||||
return typeof Ctor === 'function' && Ctor === Object;
|
||||
};
|
||||
|
||||
(window as any).$ = $;
|
||||
(window as any).jQuery = $;
|
||||
await loadBootstrap();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import shortcuts, { isIMEComposing, keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
|
||||
// Mock utils module
|
||||
vi.mock("./utils.js", () => ({
|
||||
@@ -100,6 +101,20 @@ describe("shortcuts", () => {
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should match letter keys using code when key is a special character (macOS Alt behavior)", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' as the key, but code is still 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent("å", "KeyA");
|
||||
expect(keyMatches(macOSAltAEvent, "a")).toBe(true);
|
||||
|
||||
// Option + H produces '˙'
|
||||
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH");
|
||||
expect(keyMatches(macOSAltHEvent, "h")).toBe(true);
|
||||
|
||||
// Option + S produces 'ß'
|
||||
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS");
|
||||
expect(keyMatches(macOSAltSEvent, "s")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchesShortcut", () => {
|
||||
@@ -200,6 +215,33 @@ describe("shortcuts", () => {
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should match Alt+letter shortcuts on macOS where key is a special character", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' but code remains 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent({
|
||||
key: "å",
|
||||
code: "KeyA",
|
||||
altKey: true
|
||||
});
|
||||
expect(matchesShortcut(macOSAltAEvent, "alt+a")).toBe(true);
|
||||
|
||||
// Option/Alt + H produces '˙'
|
||||
const macOSAltHEvent = createKeyboardEvent({
|
||||
key: "˙",
|
||||
code: "KeyH",
|
||||
altKey: true
|
||||
});
|
||||
expect(matchesShortcut(macOSAltHEvent, "alt+h")).toBe(true);
|
||||
|
||||
// Combined with Ctrl: Ctrl+Alt+S where Alt produces 'ß'
|
||||
const macOSCtrlAltSEvent = createKeyboardEvent({
|
||||
key: "ß",
|
||||
code: "KeyS",
|
||||
ctrlKey: true,
|
||||
altKey: true
|
||||
});
|
||||
expect(matchesShortcut(macOSCtrlAltSEvent, "ctrl+alt+s")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bindGlobalShortcut", () => {
|
||||
|
||||
@@ -213,8 +213,11 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
}
|
||||
|
||||
// For letter keys, use the physical key code for consistency
|
||||
// On macOS, Option/Alt key produces special characters, so we must use e.code
|
||||
if (key.length === 1 && key >= 'a' && key <= 'z') {
|
||||
return e.key.toLowerCase() === key.toLowerCase();
|
||||
// e.code is like "KeyA", "KeyB", etc.
|
||||
const expectedCode = `Key${key.toUpperCase()}`;
|
||||
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
|
||||
// For regular keys, check both key and code as fallback
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
--row-moving-background-color: var(--accented-background-color);
|
||||
--row-text-color: var(--main-text-color);
|
||||
--row-delimiter-color: var(--more-accented-background-color);
|
||||
|
||||
|
||||
--cell-horiz-padding-size: 8px;
|
||||
--cell-vert-padding-size: 8px;
|
||||
|
||||
|
||||
--cell-editable-hover-outline-color: var(--main-border-color);
|
||||
--cell-read-only-text-color: var(--muted-text-color);
|
||||
|
||||
|
||||
--cell-editing-border-color: var(--main-border-color);
|
||||
--cell-editing-border-width: 2px;
|
||||
--cell-editing-background-color: var(--ck-color-selector-focused-cell-background);
|
||||
@@ -40,10 +40,42 @@
|
||||
border-bottom: var(--col-header-bottom-border);
|
||||
background: var(--col-header-background-color);
|
||||
color: var(--col-header-text-color);
|
||||
}
|
||||
font-weight: normal;
|
||||
|
||||
.tabulator .tabulator-col-content {
|
||||
padding: 8px 4px !important;
|
||||
.tabulator-col.tabulator-range-highlight {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tabulator-col-content {
|
||||
padding: 0 !important;
|
||||
|
||||
.tabulator-col-title-holder {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
&:has(.tabulator-header-filter) {
|
||||
.tabulator-col-title-holder {
|
||||
padding: 4px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator-header-filter {
|
||||
background: var(--main-background-color);
|
||||
padding: 2px 1px;
|
||||
|
||||
input {
|
||||
background: var(--main-background-color);
|
||||
color: var(--main-text-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
@@ -80,7 +112,6 @@
|
||||
|
||||
.tabulator-tableholder {
|
||||
padding-top: 10px;
|
||||
height: unset !important; /* Don't extend on the full height */
|
||||
}
|
||||
|
||||
/* Rows */
|
||||
@@ -99,6 +130,14 @@
|
||||
border-top: none;
|
||||
border-bottom: 1px solid var(--row-delimiter-color);
|
||||
color: var(--row-text-color);
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.tabulator-range-highlight > .tabulator-cell.tabulator-frozen {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-row-odd {
|
||||
@@ -120,11 +159,14 @@
|
||||
margin-inline-end: var(--cell-editing-border-width);
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
|
||||
.tabulator-row .tabulator-cell {
|
||||
border-inline-end-color: transparent;
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
|
||||
border-inline-end-color: var(--main-border-color);
|
||||
}
|
||||
|
||||
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
|
||||
color: var(--cell-read-only-text-color);
|
||||
}
|
||||
@@ -174,10 +216,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabulator .tabulator-footer {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
/* Context menus */
|
||||
|
||||
.tabulator-popup-container {
|
||||
@@ -192,8 +230,27 @@
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
:root .tabulator .tabulator-footer {
|
||||
border-top: unset;
|
||||
background: transparent;
|
||||
color: var(--main-text-color);
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tabulator-page {
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--hover-item-border-color);
|
||||
color: var(--button-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
background: var(--button-background-color);
|
||||
color: var(--input-text-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1604,7 +1604,9 @@
|
||||
"clone-indicator-tooltip": "此笔记有 {{- count}} 个父级: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "此笔记已克隆(1 个额外的父级:{{- parent}})",
|
||||
"subtree-hidden-tooltip_other": "从树中隐藏的 {{count}} 篇子笔记",
|
||||
"subtree-hidden-moved-title": "已添加到 {{title}}"
|
||||
"subtree-hidden-moved-title": "已添加到 {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "此集合隐藏其树中的子笔记。",
|
||||
"subtree-hidden-moved-description-other": "子笔记隐藏于此笔记的树中。"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此窗口置顶"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Über Trilium Notes",
|
||||
"title": "Über Trilium Notizen",
|
||||
"homepage": "Startseite:",
|
||||
"app_version": "App-Version:",
|
||||
"db_version": "DB-Version:",
|
||||
@@ -26,7 +26,12 @@
|
||||
"widget-list-error": {
|
||||
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
|
||||
},
|
||||
"open-script-note": "Script-Notiz öffnen"
|
||||
"open-script-note": "Script-Notiz öffnen",
|
||||
"widget-render-error": {
|
||||
"title": "Eine externe React Integration konnte nicht dargestellt werden"
|
||||
},
|
||||
"widget-missing-parent": "Der externen Integration fehlt die erforderliche Eigenschaft '{{property}}'\n\nFalls dieses Skript ohne UI-Element ausgeführt werden soll, benutze stattdessen '#run=frontendStartup'.",
|
||||
"scripting-error": "Benutzerdefinierter Skriptfehler: {{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Link hinzufügen",
|
||||
@@ -210,7 +215,7 @@
|
||||
"modalTitle": "Infonachricht",
|
||||
"closeButton": "Schließen",
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "In die Zwischenablage kopieren"
|
||||
"copy_to_clipboard": "In Zwischenablage kopieren"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Suche im Volltext",
|
||||
@@ -698,8 +703,8 @@
|
||||
"export_as_image_png": "PNG (Raster)",
|
||||
"export_as_image_svg": "SVG (Vektor)",
|
||||
"note_map": "Notizen Karte",
|
||||
"view_revisions": "Notizrevisionen",
|
||||
"advanced": "Erweitert"
|
||||
"view_revisions": "Änderungshistorie...",
|
||||
"advanced": "Fortgeschritten"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
|
||||
@@ -792,7 +797,8 @@
|
||||
"expand_all_levels": "Alle Ebenen erweitern",
|
||||
"expand_tooltip": "Erweitert die direkten Unterelemente dieser Sammlung (eine Ebene tiefer). Für weitere Optionen auf den Pfeil rechts klicken.",
|
||||
"expand_first_level": "Direkte Unterelemente erweitern",
|
||||
"expand_nth_level": "{{depth}} Ebenen erweitern"
|
||||
"expand_nth_level": "{{depth}} Ebenen erweitern",
|
||||
"hide_child_notes": "Unterknoten im Baum ausblenden"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
|
||||
@@ -805,7 +811,7 @@
|
||||
"file_type": "Dateityp",
|
||||
"file_size": "Dateigröße",
|
||||
"download": "Herunterladen",
|
||||
"open": "Offen",
|
||||
"open": "Extern öffnen",
|
||||
"upload_new_revision": "Neue Revision hochladen",
|
||||
"upload_success": "Neue Dateirevision wurde hochgeladen.",
|
||||
"upload_failed": "Das Hochladen einer neuen Dateirevision ist fehlgeschlagen.",
|
||||
@@ -903,7 +909,7 @@
|
||||
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
|
||||
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
|
||||
"actions_executed": "Aktionen wurden ausgeführt.",
|
||||
"view_options": "Anzeigeoptionen:"
|
||||
"view_options": "Optionen anzeigen:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Ähnliche Notizen",
|
||||
@@ -1009,9 +1015,9 @@
|
||||
"auto-detect-language": "Automatisch erkannt",
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht.",
|
||||
"editor_crashed_title": "Der Text Editor ist abgestürzt",
|
||||
"editor_crashed_content": "Ihr Inhalt wurde erfolgreich wiederhergestellt, aber einzelne Ihrer letzten Änderungen waren möglicherweise noch nicht gespeichert.",
|
||||
"editor_crashed_details_button": "Zeige mehr Details…",
|
||||
"editor_crashed_details_intro": "Falls Sie diesen Fehler mehrmals sehen, melden Sie dies auf GitHub mit den folgenden Informationen.",
|
||||
"editor_crashed_content": "Ihr Inhalt wurde erfolgreich wiederhergestellt, aber kürzlich gemachte Änderungen wurden unter Umständen nicht gespeichert.",
|
||||
"editor_crashed_details_button": "Mehr Details anzeigen...",
|
||||
"editor_crashed_details_intro": "Falls dieser Fehler häufiger auftritt, ziehen Sie in Betracht uns diesen über GitHub zu melden, indem Sie die folgenden Informationen bereitstellen.",
|
||||
"editor_crashed_details_title": "Technische Informationen"
|
||||
},
|
||||
"empty": {
|
||||
@@ -1412,7 +1418,7 @@
|
||||
"will_be_deleted_in": "Dieser Anhang wird in {{time}} automatisch gelöscht",
|
||||
"will_be_deleted_soon": "Dieser Anhang wird bald automatisch gelöscht",
|
||||
"deletion_reason": ", da der Anhang nicht im Inhalt der Notiz verlinkt ist. Um das Löschen zu verhindern, füge den Anhangslink wieder in den Inhalt ein oder wandel den Anhang in eine Notiz um.",
|
||||
"role_and_size": "Rolle: {{role}}, Größe: {{size}}",
|
||||
"role_and_size": "Rolle: {{role}}, Größe: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "Anhangslink in die Zwischenablage kopiert.",
|
||||
"unrecognized_role": "Unbekannte Anhangsrolle „{{role}}“."
|
||||
},
|
||||
@@ -1463,10 +1469,13 @@
|
||||
"import-into-note": "In Notiz importieren",
|
||||
"apply-bulk-actions": "Massenaktionen anwenden",
|
||||
"converted-to-attachments": "{{count}} Notizen wurden als Anhang konvertiert.",
|
||||
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest?",
|
||||
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest? Diese Operation wird nur auf Bildnotizes angewandt. Andere Notizen werden übersprungen.",
|
||||
"open-in-popup": "Schnellbearbeitung",
|
||||
"archive": "Archiviere",
|
||||
"unarchive": "Entarchivieren"
|
||||
"unarchive": "Entarchivieren",
|
||||
"open-in-a-new-window": "In neuem Fenster öffnen",
|
||||
"hide-subtree": "Teilbaum ausblenden",
|
||||
"show-subtree": "Teilbaum anzeigen"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "Diese Notiz ist öffentlich geteilt auf {{- link}}.",
|
||||
@@ -1556,7 +1565,16 @@
|
||||
"create-child-note": "Unternotiz anlegen",
|
||||
"unhoist": "Fokus verlassen",
|
||||
"toggle-sidebar": "Seitenleiste ein-/ausblenden",
|
||||
"dropping-not-allowed": "Ablegen von Notizen an dieser Stelle ist nicht zulässig."
|
||||
"dropping-not-allowed": "Ablegen von Notizen an dieser Stelle ist nicht zulässig.",
|
||||
"clone-indicator-tooltip": "Diese Notiz hat {{- count}} Elterknoten: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "Diese Notiz ist geklont (1 weiterer Elternknoten: {{- parent}})",
|
||||
"shared-indicator-tooltip": "Diese Notiz ist öffentlich einsehbar",
|
||||
"shared-indicator-tooltip-with-url": "Diese Notiz ist unter {{- url}} öffentlich einsehbar",
|
||||
"subtree-hidden-tooltip_one": "{{count}} Unterknoten, der im Baum ausgeblendet ist",
|
||||
"subtree-hidden-tooltip_other": "{{count}} Unterknoten, die im Baum ausgeblendet sind",
|
||||
"subtree-hidden-moved-title": "Zu {{title}} hinzugefügt",
|
||||
"subtree-hidden-moved-description-collection": "Diese Sammlung blendet ihre Unternotizem im Baum aus.",
|
||||
"subtree-hidden-moved-description-other": "Diese Sammlung blendet ihre Unterknoten im Baum aus."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Dieses Fenster immer oben halten"
|
||||
@@ -1567,7 +1585,9 @@
|
||||
"printing_pdf": "PDF-Export läuft…",
|
||||
"print_report_title": "Druckreport",
|
||||
"print_report_collection_details_button": "Details anzeigen",
|
||||
"print_report_collection_details_ignored_notes": "Ignorierte Notizen"
|
||||
"print_report_collection_details_ignored_notes": "Ignorierte Notizen",
|
||||
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt ist oder geschützt ist.",
|
||||
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt sind oder geschützt sind."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "Titel der Notiz hier eingeben…",
|
||||
@@ -1720,7 +1740,8 @@
|
||||
"open_note_in_new_tab": "Notiz in neuen Tab öffnen",
|
||||
"open_note_in_new_split": "Notiz in neuen geteilten Tab öffnen",
|
||||
"open_note_in_new_window": "Notiz in neuen Fenster öffnen",
|
||||
"open_note_in_popup": "Schnellbearbeitung"
|
||||
"open_note_in_popup": "Schnellbearbeitung",
|
||||
"open_note_in_other_split": "Notiz in neuer Spalte öffnen"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "Desktop Anwendung",
|
||||
@@ -1988,8 +2009,9 @@
|
||||
"unknown_widget": "Unbekanntes Widget für '{{id}}'."
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "Nicht gesetzt",
|
||||
"configure-languages": "Konfiguriere Sprachen..."
|
||||
"not_set": "Keine Sprache ausgewählt",
|
||||
"configure-languages": "Konfiguriere Sprachen...",
|
||||
"help-on-languages": "Zu Übersetzungen beitragen..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "Inhaltssprachen",
|
||||
@@ -2007,7 +2029,8 @@
|
||||
"button_title": "Exportiere Diagramm als PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "Das Diagramm konnte als PNG nicht exportiert werden."
|
||||
"export_to_png": "Das Diagramm konnte als PNG nicht exportiert werden.",
|
||||
"export_to_svg": "Das Diagramm konnte nicht als SVG exportiert werden."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Aussehen",
|
||||
@@ -2055,7 +2078,7 @@
|
||||
"book_properties_config": {
|
||||
"hide-weekends": "Wochenenden ausblenden",
|
||||
"display-week-numbers": "Zeige Kalenderwoche",
|
||||
"map-style": "Kartenstil:",
|
||||
"map-style": "Kartenstil",
|
||||
"max-nesting-depth": "Maximale Verschachtelungstiefe:",
|
||||
"raster": "Raster",
|
||||
"vector_light": "Vektor (Hell)",
|
||||
@@ -2108,14 +2131,20 @@
|
||||
"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_button": "Aktiviere Hintergrundeffekte",
|
||||
"dismiss": "Ablehnen"
|
||||
"dismiss": "Ablehnen",
|
||||
"new_layout_title": "Neues Layout",
|
||||
"new_layout_message": "Wir haben ein modernisiertes Layout für Trilium eingeführt. Die Multifunktionsleiste wurde entfernt und als neue Statusanzeige und ausklappbaren Sektionen (wie hervorgehobenen Attributen), welche Schlüsselfunktionen übernehmen, nahtlos in das Hauptinterface integriert.\n\nDas neue Layout ist standardmäßig aktiviert und kann temporär in Optionen → Anzeige deaktiviert werden.",
|
||||
"new_layout_button": "Mehr Informationen"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Ähnliche Einstellungen"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Farbschema für Code-Blöcke in Textnotizen",
|
||||
"related_code_notes": "Farbschema für Code-Notizen"
|
||||
"related_code_notes": "Farbschema für Code-Notizen",
|
||||
"ui": "Benutzeroberfläche",
|
||||
"ui_old_layout": "Altes Layout",
|
||||
"ui_new_layout": "Neues Layout"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
@@ -2151,6 +2180,88 @@
|
||||
"experimental_features": {
|
||||
"title": "Experimentelle Optionen",
|
||||
"disclaimer": "Diese Optionen sind experimentell und können Instabilitäten verursachen. Achtsam zu verwenden.",
|
||||
"new_layout_name": "Neues Layout"
|
||||
"new_layout_name": "Neues Layout",
|
||||
"new_layout_description": "Probiere das neue Layout für eine modernere Darstellung und verbesserte Benutzbarkeit aus. Kann sich in Zukunft stark ändern."
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "Bei der Kommunikation mit dem Server ist ein Fehler aufgetreten",
|
||||
"unknown_http_error_content": "Statuscode: {{statusCode}}\nURL: {{method}} {{url}}\nNachricht: {{message}}",
|
||||
"traefik_blocks_requests": "Der Traefik Reverse-Proxy hat ein fatales Update bekommen, welche die Kommunikation mit dem Server stört."
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "Zur vorherigen Notiz zurück kehren",
|
||||
"go-forward": "Zur nächsten Notiz"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"hoisted_badge": "Gehoben",
|
||||
"hoisted_badge_title": "Abgesenkt",
|
||||
"workspace_badge": "Arbeitsfläche",
|
||||
"scroll_to_top_title": "Zum Anfang der Notiz springen",
|
||||
"create_new_note": "Neue Unternotiz erstellen",
|
||||
"empty_hide_archived_notes": "Archivierte Notizen ausblenden"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "Nicht Änderbar",
|
||||
"read_only_explicit_description": "Diese Notiz wurde händisch als nicht änderbar markiert.\nKlicke hier um sie temporär zu bearbeiten.",
|
||||
"read_only_auto": "Automatisch nicht änderbar",
|
||||
"read_only_auto_description": "Diese Notiz wurde automatisch aus Leistungsgründen als nicht änderbar markiert. Dieses automatische Limit kann in den Einstellungen angepasst werden.\n\nKlicke hier, um sie temporär zu bearbeiten.",
|
||||
"read_only_temporarily_disabled": "Temporär bearbeitbar",
|
||||
"read_only_temporarily_disabled_description": "Diese Notiz ist aktuell bearbeitbar, ist aber normalerweise nicht änderbar. Sobald du zu einer anderen Notiz navigierst, kehrt diese Notiz in ihren Normalzustand zurück.\n\nKlicke hier, um die Notiz wieder nicht änderbar zu machen.",
|
||||
"shared_publicly": "Öffentlich geteilt",
|
||||
"shared_locally": "Lokal geteilt",
|
||||
"shared_copy_to_clipboard": "Link in die Zwischenablage kopieren",
|
||||
"shared_open_in_browser": "Link öffnen",
|
||||
"shared_unshare": "Teilen aufheben",
|
||||
"clipped_note": "Internetschnellverweis",
|
||||
"clipped_note_description": "Diese Notiz wurde von {{url}} übernommen.\n\nKlicke hier, um zum Ursprung zu gehen.",
|
||||
"execute_script": "Skript ausführen",
|
||||
"execute_script_description": "Diese Notiz ist eine Skriptnotiz. Klicke hier, um das Skript auszuführen.",
|
||||
"execute_sql": "SQL ausführen",
|
||||
"execute_sql_description": "Diese Notiz ist eine SQL-Notiz. Klicke hier, um die SQL-Abfrage auszuführen.",
|
||||
"save_status_saved": "Gespeichert",
|
||||
"save_status_saving": "Speichern...",
|
||||
"save_status_unsaved": "Nicht gespeichert",
|
||||
"save_status_error": "Speichern fehlgeschlagen",
|
||||
"save_status_saving_tooltip": "Änderungen werden gespeichert.",
|
||||
"save_status_unsaved_tooltip": "Es gibt ungespeicherte Änderungen, welche gleich automatisch gespeichert werden.",
|
||||
"save_status_error_tooltip": "Beim speichern der Notiz ist ein Fehler aufgetreten. Wenn möglich, versuche die Notiz woandershin zu kopieren und die Applikation neu zu laden."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Inhaltssprache ändern",
|
||||
"note_info_title": "Notizinfo anzeigen (z.B.: Datum, Notizgröße)",
|
||||
"backlinks_one": "{{count}} Rücklink",
|
||||
"backlinks_other": "{{count}} Rücklinks",
|
||||
"backlinks_title_one": "Rücklink anzeigen",
|
||||
"backlinks_title_other": "Rücklinks anzeigen",
|
||||
"attachments_one": "{{count}} Anhang",
|
||||
"attachments_other": "{{count}} Anhänge",
|
||||
"attachments_title_one": "Anhang in einem neuen Tab öffnen",
|
||||
"attachments_title_other": "Anhänge in einem neuen Tab öffnen",
|
||||
"attributes_one": "{{count}} Eigenschaft",
|
||||
"attributes_other": "{{count}} Eigenschaften",
|
||||
"attributes_title": "Eigene und gererbte Eigenschaften",
|
||||
"note_paths_one": "{{count}} Pfad",
|
||||
"note_paths_other": "{{count}} Pfade",
|
||||
"note_paths_title": "Notizpfade",
|
||||
"code_note_switcher": "Sprachmodus ändern"
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "Notizeigenschaften"
|
||||
},
|
||||
"right_pane": {
|
||||
"empty_message": "Für diese Notiz gibt es nichts anzuzeigen",
|
||||
"empty_button": "Anzeige ausblenden",
|
||||
"toggle": "Rechte Anzeige umschalten",
|
||||
"custom_widget_go_to_source": "Zum Ursprungscode"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} Anhang",
|
||||
"attachments_other": "{{count}} Anhänge",
|
||||
"layers_one": "{{count}} Ebene",
|
||||
"layers_other": "{{count}} Ebenen",
|
||||
"pages_one": "{{count}} Seite",
|
||||
"pages_other": "{{count}} Seiten",
|
||||
"pages_alt": "Seite {{pageNumber}}",
|
||||
"pages_loading": "Laden..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +69,8 @@
|
||||
"clear-color": "Clear note colour",
|
||||
"set-color": "Set note colour",
|
||||
"set-custom-color": "Set custom note colour"
|
||||
},
|
||||
"about": {
|
||||
"title": "About Trilium Notes"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1815,7 +1815,11 @@
|
||||
"configure_launchbar": "Configure Launchbar"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "No rows have been returned for this query"
|
||||
"not_executed": "The query has not been executed yet.",
|
||||
"no_rows": "No rows have been returned for this query",
|
||||
"failed": "SQL query execution has failed",
|
||||
"statement_result": "Statement result",
|
||||
"execute_now": "Execute now"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tables"
|
||||
|
||||
@@ -60,5 +60,23 @@
|
||||
},
|
||||
"include_note": {
|
||||
"label_note": "Notat"
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Ledetekst",
|
||||
"ok": "OK",
|
||||
"defaultTitle": "Ledetekst"
|
||||
},
|
||||
"info": {
|
||||
"closeButton": "Lukk",
|
||||
"okButton": "OK"
|
||||
},
|
||||
"markdown_import": {
|
||||
"import_button": "Importer"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Lukk"
|
||||
},
|
||||
"recent_changes": {
|
||||
"undelete_link": "gjenopprett"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,7 +772,10 @@
|
||||
"filter-default": "Icons default",
|
||||
"no_results": "Não foram encontrados icons.",
|
||||
"search_placeholder_filtered": "Procurar {{number}} icons no {{name}}",
|
||||
"icon_tooltip": "{{name}}\nPacote de icons: {{iconPack}}"
|
||||
"icon_tooltip": "{{name}}\nPacote de icons: {{iconPack}}",
|
||||
"search_placeholder_one": "Procurar {{number}} icon nos {{count}} pacotes",
|
||||
"search_placeholder_many": "Procurar {{number}} icons em {{count}} pacotes",
|
||||
"search_placeholder_other": "Procurar {{number}} icons nos {{count}} pacotes"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Tipo da nota",
|
||||
@@ -799,7 +802,8 @@
|
||||
"expand_nth_level": "Expandir {{depth}} níveis",
|
||||
"expand_all_levels": "Expandir todos os níveis",
|
||||
"include_archived_notes": "Mostrar notas arquivadas",
|
||||
"expand_tooltip": "Expande a direcção dos descendentes desta colecção (um nível). Para mais opções, carregar na seta à direita."
|
||||
"expand_tooltip": "Expande a direcção dos descendentes desta colecção (um nível). Para mais opções, carregar na seta à direita.",
|
||||
"hide_child_notes": "Esconder notas descendentes na árvore"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "Ainda não há nenhuma nota editada neste dia…",
|
||||
@@ -1017,7 +1021,9 @@
|
||||
"editor_crashed_title": "O editor de texto quebrou",
|
||||
"editor_crashed_details_button": "Ver mais detalhes...",
|
||||
"editor_crashed_details_title": "Informação técnica",
|
||||
"editor_crashed_details_intro": "Se teve este erro várias vezes, considerer reportar no GitHub disponibilizando a informação abaixo."
|
||||
"editor_crashed_details_intro": "Se teve este erro várias vezes, considerer reportar no GitHub disponibilizando a informação abaixo.",
|
||||
"editor_crashed_content": "O seu conteudo foi recuperado com sucesso, mas alguns das alterações mais recentes podem não ter sido gravadas.",
|
||||
"keeps-crashing": "Componente de edição a rebentar continuamente. Por favor tentar reiniciar Trilium. Se o problema persistir, considere abrir um bug report."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Abra uma nota a digitar o título da nota no campo abaixo ou escolha uma nota na árvore.",
|
||||
@@ -1145,7 +1151,8 @@
|
||||
"title": "Largura do Conteúdo",
|
||||
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em ecrãs largos.",
|
||||
"max_width_label": "Largura máxima do conteúdo",
|
||||
"max_width_unit": "pixels"
|
||||
"max_width_unit": "pixels",
|
||||
"centerContent": "Manter conteúdo centrado"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barra de Título Nativa (requer recarregar a app)",
|
||||
@@ -1177,7 +1184,9 @@
|
||||
"title": "Desempenho",
|
||||
"enable-motion": "Ativar transições e animações",
|
||||
"enable-shadows": "Ativar sombras",
|
||||
"enable-backdrop-effects": "Ativar efeitos de fundo para menus, popups e painéis"
|
||||
"enable-backdrop-effects": "Ativar efeitos de fundo para menus, popups e painéis",
|
||||
"enable-smooth-scroll": "Activar deslocamento suave",
|
||||
"app-restart-required": "(é necessário reiniciar a aplicação para aplicar as alterações)"
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "Não iniciado",
|
||||
@@ -1336,7 +1345,10 @@
|
||||
"title": "Editor"
|
||||
},
|
||||
"code_mime_types": {
|
||||
"title": "Tipos MIME disponíveis no dropdown"
|
||||
"title": "Tipos MIME disponíveis no dropdown",
|
||||
"tooltip_syntax_highlighting": "Destaque de sintaxe",
|
||||
"tooltip_code_block_syntax": "Blocos de código nas notas de texto",
|
||||
"tooltip_code_note_syntax": "Notas de código"
|
||||
},
|
||||
"vim_key_bindings": {
|
||||
"use_vim_keybindings_in_code_notes": "Atribuições de teclas do Vim",
|
||||
@@ -1456,7 +1468,13 @@
|
||||
"min-days-in-first-week": "Mínimo de dias da primeira semana",
|
||||
"first-week-info": "Primeira semana que contenha a primeira Quinta-feira do ano é baseado na <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
|
||||
"first-week-warning": "Alterar as opções de primeira semana pode causar duplicidade nas Notas Semanais existentes e estas Notas não serão atualizadas de acordo.",
|
||||
"formatting-locale": "Formato de data e número"
|
||||
"formatting-locale": "Formato de data e número",
|
||||
"tuesday": "Terça-feira",
|
||||
"wednesday": "Quarta-feira",
|
||||
"thursday": "Quinta-feira",
|
||||
"friday": "Sexta-feira",
|
||||
"saturday": "Sábado",
|
||||
"formatting-locale-auto": "Baseado na linguagem da aplicação"
|
||||
},
|
||||
"backup": {
|
||||
"automatic_backup": "Backup automático",
|
||||
@@ -1549,7 +1567,8 @@
|
||||
"oauth_description_warning": "Para ativar o OAuth/OpenID, precisa definir a URL base do OAuth/OpenID, o client ID e o client secret no ficheiro config.ini e reiniciar a aplicação. Se quiser configurar via variáveis de ambiente, defina TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID e TRILIUM_OAUTH_CLIENT_SECRET.",
|
||||
"oauth_user_account": "Conta do Utilizador: ",
|
||||
"oauth_user_email": "E-mail do Utilizador: ",
|
||||
"oauth_user_not_logged_in": "Não está logado!"
|
||||
"oauth_user_not_logged_in": "Não está logado!",
|
||||
"oauth_missing_vars": "Configurações em falta: {{-variables}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"keyboard_shortcuts": "Atalhos de Teclado",
|
||||
@@ -1649,7 +1668,12 @@
|
||||
"apply-bulk-actions": "Aplicar ações em massa",
|
||||
"converted-to-attachments": "{{count}} notas foram convertidas em anexos.",
|
||||
"convert-to-attachment-confirm": "Tem certeza que deseja converter as notas selecionadas em anexos das suas notas-pai?",
|
||||
"open-in-popup": "Edição rápida"
|
||||
"open-in-popup": "Edição rápida",
|
||||
"open-in-a-new-window": "Abrir numa nova janela",
|
||||
"archive": "Arquivar",
|
||||
"unarchive": "Retirar do arquivo",
|
||||
"hide-subtree": "Esconder sub-árvore",
|
||||
"show-subtree": "Mostrar sub-árvore"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "Esta nota é partilhada publicamente em {{- link}}.",
|
||||
@@ -1710,7 +1734,13 @@
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Lista de Destaques",
|
||||
"options": "Opções"
|
||||
"options": "Opções",
|
||||
"no_highlights": "Sem destaques encontrados.",
|
||||
"menu_configure": "Configurar lista de destaques...",
|
||||
"modal_title": "Configurar list de destaques",
|
||||
"title_with_count_one": "{{count}} destaque",
|
||||
"title_with_count_many": "{{count}} destaques",
|
||||
"title_with_count_other": "{{count}} destaques"
|
||||
},
|
||||
"quick-search": {
|
||||
"placeholder": "Pesquisa rápida",
|
||||
@@ -1733,16 +1763,43 @@
|
||||
"refresh-saved-search-results": "Atualizar resultados de pesquisa gravados",
|
||||
"create-child-note": "Criar nota filha",
|
||||
"unhoist": "Desafixar",
|
||||
"toggle-sidebar": "Alternar barra lateral"
|
||||
"toggle-sidebar": "Alternar barra lateral",
|
||||
"dropping-not-allowed": "Largar notas nesta localização não é permitida",
|
||||
"clone-indicator-tooltip": "Esta nota tem {{- count}} ascendentes: {{- parents}}",
|
||||
"shared-indicator-tooltip": "Esta nota está partilhada publicamente",
|
||||
"shared-indicator-tooltip-with-url": "Esta nota está partilhada publicamente em: {{- url}}",
|
||||
"subtree-hidden-moved-title": "Adicionar ao {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "Esta colecção esconde as notas descendentes na árvore.",
|
||||
"subtree-hidden-moved-description-other": "Notas descendentes estão escondidades na árvore para esta nota.",
|
||||
"subtree-hidden-tooltip_one": "{{count}} nota descendentes escondidas da árvore",
|
||||
"subtree-hidden-tooltip_many": "{{count}} notas descendentes escondidas da árvore",
|
||||
"subtree-hidden-tooltip_other": "{{count}} notas descendentes escondidas da árvore",
|
||||
"clone-indicator-tooltip-single": "Esta nota está clonada (1 additional parent: {{- parent}})"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Manter Janela no Topo"
|
||||
},
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Não foi possível encontrar typeWidget para o tipo '{{type}}'"
|
||||
"could_not_find_typewidget": "Não foi possível encontrar typeWidget para o tipo '{{type}}'",
|
||||
"print_report_collection_details_button": "Ver detalhes",
|
||||
"printing": "Impressão em progresso...",
|
||||
"printing_pdf": "Exportação PDF em progresso...",
|
||||
"print_report_title": "Imprimir relatório",
|
||||
"print_report_collection_details_ignored_notes": "Ignorar notas",
|
||||
"print_report_collection_content_one": "{{count}} nota na colecção não pode ser impressa porque não é suportado ou está protegida.",
|
||||
"print_report_collection_content_many": "{{count}} notas na colecção não podem ser impressas porque não é suportado ou estão protegidas.",
|
||||
"print_report_collection_content_other": "{{count}} notas na colecção não podem ser impressas porque não é suportado ou estão protegidas."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "digite o título da nota aqui..."
|
||||
"placeholder": "digite o título da nota aqui...",
|
||||
"promoted_attributes": "Atributos destacados",
|
||||
"created_on": "Criado em <Value />",
|
||||
"last_modified": "Modificado em <Value />",
|
||||
"note_type_switcher_label": "Alterar de {{type}} para:",
|
||||
"note_type_switcher_others": "Outro tipo de nota",
|
||||
"note_type_switcher_templates": "Template",
|
||||
"note_type_switcher_collection": "Colecção",
|
||||
"edited_notes": "Notas editadas neste dia"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Nenhuma nota encontrada para os parâmetros de pesquisa digitados.",
|
||||
@@ -1771,7 +1828,8 @@
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Tabela de Conteúdos",
|
||||
"options": "Opções"
|
||||
"options": "Opções",
|
||||
"no_headings": "Sem cabeçalhos."
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "O ficheiro <code class=\"file-path\"></code> foi modificado pela última vez em <span class=\"file-last-modified\"></span>.",
|
||||
@@ -1814,7 +1872,9 @@
|
||||
"ws": {
|
||||
"sync-check-failed": "A verificação de sincronização falhou!",
|
||||
"consistency-checks-failed": "A verificação de consistência falhou! Veja os logs para pormenores.",
|
||||
"encountered-error": "Encontrado o erro \"{{message}}\", verifique o console."
|
||||
"encountered-error": "Encontrado o erro \"{{message}}\", verifique o console.",
|
||||
"lost-websocket-connection-title": "Perdida conexão com o servidor",
|
||||
"lost-websocket-connection-message": "Verifique a configuração da proxy inversa (e.g. nginx ou Apache) para assegurar conexões WebSocket estão permitidas e não bloqueadas."
|
||||
},
|
||||
"hoisted_note": {
|
||||
"confirm_unhoisting": "A nota solicitada '{{requestedNote}}' está fora da árvore da nota fixada '{{hoistedNote}}' e precisa desafixar para aceder a nota. Quer prosseguir e desafixar?"
|
||||
@@ -1870,7 +1930,8 @@
|
||||
"copy-link": "Copiar ligação",
|
||||
"paste": "Colar",
|
||||
"paste-as-plain-text": "Colar como texto sem formatação",
|
||||
"search_online": "Pesquisar por \"{{term}}\" com {{searchEngine}}"
|
||||
"search_online": "Pesquisar por \"{{term}}\" com {{searchEngine}}",
|
||||
"search_in_trilium": "A procurar \"{{term}}\" no Trilium"
|
||||
},
|
||||
"image_context_menu": {
|
||||
"copy_reference_to_clipboard": "Copiar referência para a área de transferência",
|
||||
@@ -1880,7 +1941,8 @@
|
||||
"open_note_in_new_tab": "Abrir nota em nova guia",
|
||||
"open_note_in_new_split": "Abrir nota em nova divisão",
|
||||
"open_note_in_new_window": "Abrir nota em nova janela",
|
||||
"open_note_in_popup": "Edição rápida"
|
||||
"open_note_in_popup": "Edição rápida",
|
||||
"open_note_in_other_split": "Abrir nota noutro separador"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "Aplicação Desktop",
|
||||
@@ -1888,7 +1950,8 @@
|
||||
"native-title-bar-description": "Para Windows e macOS, manter a barra de título nativa desativada faz a aplicação parecer mais compacta. No Linux, manter a barra de título nativa ativada faz a aplicação se integrar melhor com o restante do sistema.",
|
||||
"background-effects": "Ativar efeitos de fundo (apenas Windows 11)",
|
||||
"restart-app-button": "Reiniciar a aplicação para ver as alterações",
|
||||
"zoom-factor": "Fator de Zoom"
|
||||
"zoom-factor": "Fator de Zoom",
|
||||
"background-effects-description": "O Mica adiciona um desfoque, fundo estiloso as janelas da aplicação, criando uma profundidade e aspecto moderno. \"Barra de titulo nativa\" deve estar inactiva."
|
||||
},
|
||||
"note_autocomplete": {
|
||||
"search-for": "Pesquisar por \"{{term}}\"",
|
||||
@@ -1948,7 +2011,8 @@
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "Não atribuído",
|
||||
"configure-languages": "Configurar idiomas..."
|
||||
"configure-languages": "Configurar idiomas...",
|
||||
"help-on-languages": "Ajuda nas linguagens de conteúdos..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "Idiomas do conteúdo",
|
||||
@@ -1966,7 +2030,8 @@
|
||||
"button_title": "Exportar diagrama como PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "O diagrama não pôde ser exportado como PNG."
|
||||
"export_to_png": "O diagrama não pôde ser exportado como PNG.",
|
||||
"export_to_svg": "O diagrama não pode ser exportado para SVG."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Aparência",
|
||||
@@ -1985,7 +2050,11 @@
|
||||
"editorfeatures": {
|
||||
"title": "Recursos",
|
||||
"emoji_completion_enabled": "Ativar auto-completar de Emoji",
|
||||
"note_completion_enabled": "Ativar auto-completar de notas"
|
||||
"note_completion_enabled": "Ativar auto-completar de notas",
|
||||
"emoji_completion_description": "Se activo, emojis podem ser facilmente inseridos em texto ao pressionar `:`, seguido do nome de um emoji.",
|
||||
"note_completion_description": "Se activo, links para notas podem ser criadas ao escrever `@` seguido do titulo de uma nota.",
|
||||
"slash_commands_enabled": "Activar comentários simples",
|
||||
"slash_commands_description": "Se activo, editar comandos como inserir quebras de linha ou cabeçalhos podem ser activado/inactivado ao escrever `/`."
|
||||
},
|
||||
"table_view": {
|
||||
"new-row": "Nova linha",
|
||||
@@ -2027,7 +2096,16 @@
|
||||
"delete-column": "Apagar coluna",
|
||||
"delete-column-confirmation": "Tem certeza que deseja apagar esta coluna? O atributo correspondente também será apagado de todas as notas abaixo desta coluna.",
|
||||
"new-item": "Novo elemento",
|
||||
"add-column": "Adicionar Coluna"
|
||||
"add-column": "Adicionar Coluna",
|
||||
"delete-note": "Apagar nota...",
|
||||
"remove-from-board": "Remover do quadro",
|
||||
"archive-note": "Arquivar nota",
|
||||
"new-item-placeholder": "Inserir titulo da nota...",
|
||||
"add-column-placeholder": "Inserir nome da coluna...",
|
||||
"edit-note-title": "Clicar para editar o titulo da nota",
|
||||
"unarchive-note": "Remover nota do arquivo",
|
||||
"edit-column-title": "Click para editar titulo da coluna",
|
||||
"column-already-exists": "Esta coluna já existe no quadro."
|
||||
},
|
||||
"command_palette": {
|
||||
"tree-action-name": "Árvore: {{name}}",
|
||||
@@ -2058,16 +2136,146 @@
|
||||
"background_effects_title": "Efeitos de fundo estão estáveis agora",
|
||||
"background_effects_message": "Em dispositivos Windows, efeitos de fundo estão estáveis agora. Os efeitos de fundo adicionam um toque de cor à interface do utilizador borrando o plano de fundo atrás dela. Esta técnica também é usada noutras aplicações como o Windows Explorer.",
|
||||
"background_effects_button": "Ativar os efeitos de fundo",
|
||||
"dismiss": "Dispensar"
|
||||
"dismiss": "Dispensar",
|
||||
"new_layout_title": "Novo titulo do layout",
|
||||
"new_layout_button": "Mais informação",
|
||||
"new_layout_message": "Estamos a introduzir um layout modernizado para o Trilium. A faixa foi removida e está integrada na interface principal, com uma nota barra de estado e secções expansíveis (como as propriedades próprias) a tomar papéis principais.\n\nO novo layout está activo por defeito, e pode ser temporáriamente disabilidade em Opções → Aparência."
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Configurações relacionadas"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Esquema de cores para blocos de código em notas de texto",
|
||||
"related_code_notes": "Esquema de cores para notas de código"
|
||||
"related_code_notes": "Esquema de cores para notas de código",
|
||||
"ui": "Interface do utilizador",
|
||||
"ui_old_layout": "Layout antigo",
|
||||
"ui_new_layout": "Nova aparência"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "Opções experimentais",
|
||||
"new_layout_name": "Novo layout",
|
||||
"new_layout_description": "Experimente o novo layout para um aspecto moderno e melhor estabilidade. Sujeito a grandes alterações nas próximas publicações.",
|
||||
"disclaimer": "Estas opções são experimentais e podem causar instabilidade. Usar com cuidado."
|
||||
},
|
||||
"read-only-info": {
|
||||
"read-only-note": "Actualmente a ver em modo de leitura.",
|
||||
"edit-note": "Editar nota",
|
||||
"auto-read-only-note": "Esta nota está a ser mostrada em modo de leitura para um carregamento mais rápido."
|
||||
},
|
||||
"presentation_view": {
|
||||
"edit-slide": "Editar este slide",
|
||||
"start-presentation": "Iniciar apresentação",
|
||||
"slide-overview": "Alternar visão geral dos slides"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "Apagar nota..."
|
||||
},
|
||||
"pagination": {
|
||||
"page_title": "Página {{startIndex}} - {{endIndex}}",
|
||||
"total_notes": "{{count}} notas"
|
||||
},
|
||||
"collections": {
|
||||
"rendering_error": "Sem possíbilidade de mostrar conteúdos devido a um erro."
|
||||
},
|
||||
"note-color": {
|
||||
"clear-color": "Remover cor da nota",
|
||||
"set-color": "Atribuir cor da nota",
|
||||
"set-custom-color": "Afectar cor personalizada da nota"
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Alterar para editor completo"
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "Erro na comunicação com servidor",
|
||||
"unknown_http_error_content": "Código de estado: {{statusCode}}\nURL: {{method}} {{url}}\nMessagem: {{message}}",
|
||||
"traefik_blocks_requests": "Se está a usar o Traefik, este introduz uma alteração que afecta a comunicação com o servidor."
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "Ir para a nota anterior",
|
||||
"go-forward": "Ir para nota seguinte"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"hoisted_badge": "Içado",
|
||||
"workspace_badge": "Área de trabalho",
|
||||
"scroll_to_top_title": "Saltar para o início da nota",
|
||||
"create_new_note": "Criar nova nota descendente",
|
||||
"empty_hide_archived_notes": "Esconder notas arquivadas",
|
||||
"hoisted_badge_title": "Retirar de içado"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "Modo de leitura",
|
||||
"read_only_auto": "Modo de leitura automático",
|
||||
"read_only_temporarily_disabled": "Editável temporáriamente",
|
||||
"read_only_auto_description": "Esta nota foi automaticamente colocada em modo de leitura por razões de performance. Este limite automatico é ajustável nas configurações.\n\nClicar para editar temporáriamente.",
|
||||
"read_only_temporarily_disabled_description": "Esta nota está editável, mas normalmente está em modo de leitura. A nova vai regressar para mode de leitura assim que navegar para outra nota.\n\nClicar para reactivar o modo de leitura.",
|
||||
"read_only_explicit_description": "Esta nota foi manualmente colocada em modo de leitura.\nClicar para editar temporáriamente.",
|
||||
"shared_publicly": "Partilhado publicamente",
|
||||
"shared_locally": "Partilhado localmente",
|
||||
"shared_copy_to_clipboard": "Copiar link para a área de transferência",
|
||||
"shared_open_in_browser": "Abrir link no browser",
|
||||
"shared_unshare": "Remover partilha",
|
||||
"clipped_note_description": "Esta nota foi retirar do {{url}}.\n\nClicar para navegar no código fonte da página.",
|
||||
"clipped_note": "Web clipe",
|
||||
"execute_script": "Correr script",
|
||||
"execute_script_description": "Esta nota é uma nota de script. Clicar para executar o script.",
|
||||
"execute_sql": "Correr SQL",
|
||||
"execute_sql_description": "Esta nota é uma nota de SQL. Clicar para executar script SQL.",
|
||||
"save_status_saved": "Guardar",
|
||||
"save_status_saving": "A guardar...",
|
||||
"save_status_unsaved": "Não gravado",
|
||||
"save_status_error": "Gravar falhou",
|
||||
"save_status_saving_tooltip": "Alterações estão a ser guardadas",
|
||||
"save_status_unsaved_tooltip": "Existem alterações não guardadas. Serão guardadas automaticamente em breve.",
|
||||
"save_status_error_tooltip": "Ocorreu um erro ao guardar a nota. Se possível, tente copiar os conteúdos da nota para outro local e reiniciar a aplicação."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Alterar lingua do conteúdo",
|
||||
"note_info_title": "Ver informação da nota (e.g., datas, tamanho da nota)",
|
||||
"backlinks_one": "{{count}} backlink",
|
||||
"backlinks_many": "{{count}} backlinks",
|
||||
"backlinks_other": "{{count}} backlinks",
|
||||
"backlinks_title_one": "Ver backlink",
|
||||
"backlinks_title_many": "Ver backlinks",
|
||||
"backlinks_title_other": "Ver backlinks",
|
||||
"attachments_one": "{{count}} anexo",
|
||||
"attachments_many": "{{count}} anexos",
|
||||
"attachments_other": "{{count}} anexos",
|
||||
"attachments_title_one": "Ver anexo num novo separador",
|
||||
"attachments_title_many": "Ver anexos num novo separador",
|
||||
"attachments_title_other": "Ver anexos num novo separador",
|
||||
"attributes_one": "{{count}} atributo",
|
||||
"attributes_many": "{{count}} atributos",
|
||||
"attributes_other": "{{count}} atributos",
|
||||
"attributes_title": "Atributos próprios e herdados",
|
||||
"note_paths_one": "{{count}} caminho",
|
||||
"note_paths_many": "{{count}} caminhos",
|
||||
"note_paths_other": "{{count}} caminhos",
|
||||
"note_paths_title": "Caminhos da nota",
|
||||
"code_note_switcher": "Alterar modo de linguagem"
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "Atributos da nota"
|
||||
},
|
||||
"right_pane": {
|
||||
"empty_message": "Nada para mostrar nesta nota",
|
||||
"empty_button": "Esconder painél",
|
||||
"toggle": "Alterar painel direito",
|
||||
"custom_widget_go_to_source": "Ir para código fonte"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} anexo pdf",
|
||||
"attachments_many": "{{count}} anexos pdf",
|
||||
"attachments_other": "{{count}} anexos pdf",
|
||||
"layers_one": "{{count}} camada",
|
||||
"layers_many": "{{count}} camadas",
|
||||
"layers_other": "{{count}} camadas",
|
||||
"pages_one": "{{count}} página",
|
||||
"pages_many": "{{count}} páginas",
|
||||
"pages_other": "{{count}} páginas",
|
||||
"pages_alt": "Página {{pageNumber}}",
|
||||
"pages_loading": "A carregar..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import Component from "../components/component";
|
||||
import NoteContext from "../components/note_context";
|
||||
import FNote from "../entities/fnote";
|
||||
import attributes from "../services/attributes";
|
||||
import { isExperimentalFeatureEnabled } from "../services/experimental_features";
|
||||
import froca from "../services/froca";
|
||||
import { t } from "../services/i18n";
|
||||
import { copyImageReferenceToClipboard } from "../services/image";
|
||||
@@ -101,7 +100,8 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
|
||||
@@ -265,9 +265,13 @@ function useNoteInfo() {
|
||||
const [ note, setNote ] = useState<FNote | null | undefined>();
|
||||
const [ type, setType ] = useState<ExtendedNoteType>();
|
||||
const [ mime, setMime ] = useState<string>();
|
||||
const refreshIdRef = useRef(0);
|
||||
|
||||
function refresh() {
|
||||
const refreshId = ++refreshIdRef.current;
|
||||
|
||||
getExtendedWidgetType(actualNote, noteContext).then(type => {
|
||||
if (refreshId !== refreshIdRef.current) return;
|
||||
setNote(actualNote);
|
||||
setType(type);
|
||||
setMime(actualNote?.mime);
|
||||
@@ -318,6 +322,8 @@ export async function getExtendedWidgetType(note: FNote | null | undefined, note
|
||||
resultingType = "noteMap";
|
||||
} else if (type === "text" && (await noteContext?.isReadOnly())) {
|
||||
resultingType = "readOnlyText";
|
||||
} else if (note.isTriliumSqlite()) {
|
||||
resultingType = "sqlConsole";
|
||||
} else if ((type === "code" || type === "mermaid") && (await noteContext?.isReadOnly())) {
|
||||
resultingType = "readOnlyCode";
|
||||
} else if (type === "text") {
|
||||
@@ -342,9 +348,8 @@ export function checkFullHeight(noteContext: NoteContext | undefined, type: Exte
|
||||
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
const isBackendNote = noteContext?.noteId === "_backendLog";
|
||||
const isSqlNote = noteContext.note?.mime === "text/x-sqlite;schema=trilium";
|
||||
const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight;
|
||||
return (!noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|
||||
return (!noteContext?.hasNoteList() && isFullHeightNoteType)
|
||||
|| noteContext?.viewScope?.viewMode === "attachments"
|
||||
|| isBackendNote;
|
||||
}
|
||||
@@ -358,8 +363,8 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) {
|
||||
});
|
||||
}
|
||||
|
||||
function handlePrintReport(printReport: PrintReport) {
|
||||
if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) {
|
||||
function handlePrintReport(printReport?: PrintReport) {
|
||||
if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) {
|
||||
toast.showPersistent({
|
||||
id: "print-report",
|
||||
icon: "bx bx-collection",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./NoteList.css";
|
||||
|
||||
import { WebSocketMessage } from "@triliumnext/commons";
|
||||
import { VNode } from "preact";
|
||||
import { Component, VNode } from "preact";
|
||||
import { lazy, Suspense } from "preact/compat";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
@@ -120,7 +120,9 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa
|
||||
}
|
||||
|
||||
const ComponentToRender = viewType && props && isEnabled && (
|
||||
props.media === "print" ? ViewComponents[viewType].print : ViewComponents[viewType].normal
|
||||
props.media === "print"
|
||||
? ViewComponents[viewType].print ?? ViewComponents[viewType].normal
|
||||
: ViewComponents[viewType].normal
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 0 5px 0 10px;
|
||||
|
||||
.tabulator-tableholder {
|
||||
height: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table-view-container {
|
||||
@@ -68,4 +72,4 @@
|
||||
inset-inline-start: 0;
|
||||
font-size: 1.5em;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
||||
import { EventCallBackMethods, Module, Options, Tabulator as VanillaTabulator } from "tabulator-tables";
|
||||
import "tabulator-tables/dist/css/tabulator.css";
|
||||
import "../../../../src/stylesheets/table.css";
|
||||
import { ParentComponent, renderReactWidget } from "../../react/react_utils";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
|
||||
import { isValidElement, RefObject } from "preact";
|
||||
import { useContext, useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { EventCallBackMethods, Module, Options, Tabulator as VanillaTabulator } from "tabulator-tables";
|
||||
|
||||
import { ParentComponent, renderReactWidget } from "../../react/react_utils";
|
||||
|
||||
interface TableProps<T> extends Omit<Options, "data" | "footerElement" | "index"> {
|
||||
tabulatorRef: RefObject<VanillaTabulator>;
|
||||
tabulatorRef?: RefObject<VanillaTabulator>;
|
||||
className?: string;
|
||||
data?: T[];
|
||||
modules?: (new (table: VanillaTabulator) => Module)[];
|
||||
events?: Partial<EventCallBackMethods>;
|
||||
index: keyof T;
|
||||
index?: keyof T;
|
||||
footerElement?: string | HTMLElement | JSX.Element;
|
||||
onReady?: () => void;
|
||||
}
|
||||
@@ -43,7 +45,9 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
|
||||
|
||||
tabulator.on("tableBuilt", () => {
|
||||
tabulatorRef.current = tabulator;
|
||||
externalTabulatorRef.current = tabulator;
|
||||
if (externalTabulatorRef) {
|
||||
externalTabulatorRef.current = tabulator;
|
||||
}
|
||||
onReady?.();
|
||||
});
|
||||
|
||||
@@ -62,12 +66,15 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
|
||||
for (const [ eventName, handler ] of Object.entries(events)) {
|
||||
tabulator.off(eventName as keyof EventCallBackMethods, handler);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, Object.values(events ?? {}));
|
||||
|
||||
// Change in data.
|
||||
useEffect(() => { tabulatorRef.current?.setData(data) }, [ data ]);
|
||||
useEffect(() => { columns && tabulatorRef.current?.setColumns(columns)}, [ data]);
|
||||
useEffect(() => { tabulatorRef.current?.setData(data); }, [ data ]);
|
||||
useEffect(() => {
|
||||
if (!columns) return;
|
||||
tabulatorRef.current?.setColumns(columns);
|
||||
}, [ columns ]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={className} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ComponentChild } from "preact";
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { ViewScope } from "../../services/link";
|
||||
import { formatDateTime } from "../../utils/formatters";
|
||||
import NoteIcon from "../note_icon";
|
||||
@@ -22,12 +23,12 @@ const supportedNoteTypes = new Set<NoteType>([
|
||||
export default function InlineTitle() {
|
||||
const { note, parentComponent, viewScope } = useNoteContext();
|
||||
const type = useNoteProperty(note, "type");
|
||||
const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope));
|
||||
const [ shown, setShown ] = useState(shouldShow(note, type, viewScope));
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ titleHidden, setTitleHidden ] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setShown(shouldShow(note?.noteId, type, viewScope));
|
||||
setShown(shouldShow(note, type, viewScope));
|
||||
}, [ note, type, viewScope ]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -69,9 +70,10 @@ export default function InlineTitle() {
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
|
||||
function shouldShow(note: FNote | null | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) {
|
||||
if (viewScope?.viewMode !== "default") return false;
|
||||
if (noteId?.startsWith("_options")) return true;
|
||||
if (note?.noteId?.startsWith("_options")) return true;
|
||||
if (note?.isTriliumSqlite()) return false;
|
||||
return type && supportedNoteTypes.has(type);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function NoteTypeSwitcher() {
|
||||
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
|
||||
const { builtinTemplates, collectionTemplates } = useBuiltinTemplates();
|
||||
|
||||
return (currentNoteType && supportedNoteTypes.has(currentNoteType) &&
|
||||
return (currentNoteType && supportedNoteTypes.has(currentNoteType) && !note?.isTriliumSqlite() &&
|
||||
<div
|
||||
className="note-type-switcher"
|
||||
onWheel={onWheelHorizontalScroll}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./StatusBar.css";
|
||||
|
||||
import { Locale, NoteType } from "@triliumnext/commons";
|
||||
import { Locale, NOTE_TYPE_ICONS, NoteType } from "@triliumnext/commons";
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { type ComponentChildren, RefObject } from "preact";
|
||||
@@ -9,7 +9,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "p
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote, { NOTE_TYPE_ICONS } from "../../entities/fnote";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ViewScope } from "../../services/link";
|
||||
|
||||
@@ -1232,7 +1232,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
refreshCtx.noteIdsToUpdate.add(noteId);
|
||||
}
|
||||
|
||||
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
|
||||
const hasNotesToUpdateOrReload = refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0;
|
||||
const hasNoteReorderingChange = loadResults.getNoteReorderings().length > 0;
|
||||
if (hasNotesToUpdateOrReload || hasNoteReorderingChange) {
|
||||
await this.#executeTreeUpdates(refreshCtx, loadResults);
|
||||
}
|
||||
|
||||
@@ -1393,6 +1395,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
for (const parentNoteId of loadResults.getNoteReorderings()) {
|
||||
for (const node of this.getNodesByNoteId(parentNoteId)) {
|
||||
console.log("Reordering ", node);
|
||||
if (node.isLoaded()) {
|
||||
this.sortChildren(node);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
|
||||
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
|
||||
* for protected session or attachment information.
|
||||
*/
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
|
||||
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat" | "sqlConsole";
|
||||
|
||||
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
|
||||
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
|
||||
@@ -140,5 +140,10 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
|
||||
view: () => import("./type_widgets/AiChat"),
|
||||
className: "ai-chat-widget-container",
|
||||
isFullHeight: true
|
||||
},
|
||||
sqlConsole: {
|
||||
view: () => import("./type_widgets/SqlConsole"),
|
||||
className: "sql-console-widget-container",
|
||||
isFullHeight: true
|
||||
}
|
||||
};
|
||||
|
||||
18
apps/client/src/widgets/react/NoItems.css
Normal file
18
apps/client/src/widgets/react/NoItems.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.no-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
padding: 0.75em;
|
||||
color: var(--muted-text-color);
|
||||
height: 100%;
|
||||
|
||||
.tn-icon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
21
apps/client/src/widgets/react/NoItems.tsx
Normal file
21
apps/client/src/widgets/react/NoItems.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import "./NoItems.css";
|
||||
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface NoItemsProps {
|
||||
icon: string;
|
||||
text: string;
|
||||
children?: ComponentChildren;
|
||||
}
|
||||
|
||||
export default function NoItems({ icon, text, children }: NoItemsProps) {
|
||||
return (
|
||||
<div className="no-items">
|
||||
<Icon icon={icon} />
|
||||
{text}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -184,7 +184,8 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: NoteActionsCustomInnerProps) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
|
||||
const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely();
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <ActionButton
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { useNoteContext } from "./react/hooks";
|
||||
|
||||
export default function ScrollPadding() {
|
||||
const { note, parentComponent, ntxId, viewScope } = useNoteContext();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState<number>(10);
|
||||
const isEnabled = ["text", "code"].includes(note?.type ?? "") && viewScope?.viewMode === "default";
|
||||
const isEnabled = ["text", "code"].includes(note?.type ?? "")
|
||||
&& viewScope?.viewMode === "default"
|
||||
&& !note?.isTriliumSqlite();
|
||||
|
||||
const refreshHeight = () => {
|
||||
if (!ref.current) return;
|
||||
@@ -37,6 +40,6 @@ export default function ScrollPadding() {
|
||||
style={{ height }}
|
||||
onClick={() => parentComponent.triggerCommand("scrollToEnd", { ntxId })}
|
||||
/>
|
||||
: <div></div>
|
||||
)
|
||||
: <div />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,22 +40,4 @@ body.experimental-feature-new-layout #right-pane {
|
||||
.gutter-vertical + .card .card-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.no-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
padding: 0.75em;
|
||||
color: var(--muted-text-color);
|
||||
|
||||
.tn-icon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import "./RightPanelContainer.css";
|
||||
|
||||
import Split from "@triliumnext/split.js";
|
||||
import { VNode } from "preact";
|
||||
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import { WidgetsByParent } from "../../services/bundle";
|
||||
@@ -12,7 +12,7 @@ import options from "../../services/options";
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
|
||||
import Button from "../react/Button";
|
||||
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionJson } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import NoItems from "../react/NoItems";
|
||||
import LegacyRightPanelWidget from "../right_panel_widget";
|
||||
import HighlightsList from "./HighlightsList";
|
||||
import PdfAttachments from "./pdf/PdfAttachments";
|
||||
@@ -47,14 +47,15 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
|
||||
items.length > 0 ? (
|
||||
items
|
||||
) : (
|
||||
<div className="no-items">
|
||||
<Icon icon="bx bx-sidebar" />
|
||||
{t("right_pane.empty_message")}
|
||||
<NoItems
|
||||
icon="bx bx-sidebar"
|
||||
text={t("right_pane.empty_message")}
|
||||
>
|
||||
<Button
|
||||
text={t("right_pane.empty_button")}
|
||||
triggerCommand="toggleRightPane"
|
||||
/>
|
||||
</div>
|
||||
</NoItems>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.sql-result-widget {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sql-console-result-container td {
|
||||
white-space: preserve;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import "./sql_result.css";
|
||||
import { useState } from "preact/hooks";
|
||||
import Alert from "./react/Alert";
|
||||
import { t } from "../services/i18n";
|
||||
|
||||
export default function SqlResults() {
|
||||
const { note, ntxId } = useNoteContext();
|
||||
const [ results, setResults ] = useState<SqlExecuteResults>();
|
||||
|
||||
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, results }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
setResults(results);
|
||||
})
|
||||
|
||||
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium";
|
||||
return (
|
||||
<div className={`sql-result-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
results?.length === 1 && Array.isArray(results[0]) && results[0].length === 0 ? (
|
||||
<Alert type="info">
|
||||
{t("sql_result.no_rows")}
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
{results?.map(rows => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
return <pre>{JSON.stringify(rows, null, "\t")}</pre>
|
||||
}
|
||||
|
||||
// selects
|
||||
return <SqlResultTable rows={rows} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SqlResultTable({ rows }: { rows: object[] }) {
|
||||
if (!rows.length) return;
|
||||
|
||||
return (
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
{Object.keys(rows[0]).map(key => <th>{key}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr>
|
||||
{Object.values(row).map(cell => <td>{cell}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
.sql-table-schemas-widget {
|
||||
padding: 12px;
|
||||
padding-inline-end: 10%;
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
.sql-table-schemas > .dropdown {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.sql-table-schemas button.btn {
|
||||
padding: 0.25rem 0.4rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 0.5;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sql-console-result-container {
|
||||
width: 100%;
|
||||
font-size: smaller;
|
||||
margin-top: 10px;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.table-schema td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown .table-schema {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
/* Data type */
|
||||
.dropdown .table-schema td:nth-child(2) {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { t } from "../services/i18n";
|
||||
import { useNoteContext } from "./react/hooks";
|
||||
import "./sql_table_schemas.css";
|
||||
import { SchemaResponse } from "@triliumnext/commons";
|
||||
import server from "../services/server";
|
||||
import Dropdown from "./react/Dropdown";
|
||||
|
||||
export default function SqlTableSchemas() {
|
||||
const { note } = useNoteContext();
|
||||
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
|
||||
|
||||
useEffect(() => {
|
||||
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
|
||||
}, []);
|
||||
|
||||
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium" && schemas;
|
||||
return (
|
||||
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
<>
|
||||
{t("sql_table_schemas.tables")}{": "}
|
||||
|
||||
<span class="sql-table-schemas">
|
||||
{schemas.map(({ name, columns }) => (
|
||||
<>
|
||||
<Dropdown text={name} noSelectButtonStyle hideToggleArrow
|
||||
>
|
||||
<table className="table-schema">
|
||||
{columns.map(column => (
|
||||
<tr>
|
||||
<td>{column.name}</td>
|
||||
<td>{column.type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</Dropdown>
|
||||
{" "}
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
apps/client/src/widgets/type_widgets/SqlConsole.css
Normal file
81
apps/client/src/widgets/type_widgets/SqlConsole.css
Normal file
@@ -0,0 +1,81 @@
|
||||
.sql-console-widget-container {
|
||||
.note-detail-split.split-vertical {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.note-detail-split-preview {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
background-color: var(--accented-background-color) !important;
|
||||
}
|
||||
|
||||
.sql-result-widget {
|
||||
height: 100%;
|
||||
|
||||
> .sql-console-result-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: smaller;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
|
||||
> .tabulator {
|
||||
--cell-vert-padding-size: 4px;
|
||||
|
||||
> .tabulator-tableholder {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> .tabulator-footer,
|
||||
> .tabulator-footer .tabulator-footer-contents {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sql-table-schemas-widget {
|
||||
padding: 12px;
|
||||
padding-inline-end: 10%;
|
||||
contain: none !important;
|
||||
|
||||
.sql-table-schemas {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
> .dropdown {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
button.btn {
|
||||
padding: 0.25rem 0.4rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 0.5;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-schema td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown .table-schema {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
/* Data type */
|
||||
.dropdown .table-schema td:nth-child(2) {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
apps/client/src/widgets/type_widgets/SqlConsole.tsx
Normal file
176
apps/client/src/widgets/type_widgets/SqlConsole.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import "./SqlConsole.css";
|
||||
|
||||
import { SchemaResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { ClipboardModule, EditModule, ExportModule, FilterModule, FormatModule, FrozenColumnsModule, KeybindingsModule, PageModule, ResizeColumnsModule, SelectRangeModule, SelectRowModule, SortModule } from "tabulator-tables";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import server from "../../services/server";
|
||||
import Tabulator from "../collections/table/tabulator";
|
||||
import Button from "../react/Button";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import NoItems from "../react/NoItems";
|
||||
import SplitEditor from "./helpers/SplitEditor";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
export default function SqlConsole(props: TypeWidgetProps) {
|
||||
return (
|
||||
<SplitEditor
|
||||
noteType="code"
|
||||
{...props}
|
||||
editorBefore={<SqlTableSchemas {...props} />}
|
||||
previewContent={<SqlResults key={props.note.noteId} {...props} />}
|
||||
forceOrientation="vertical"
|
||||
splitOptions={{
|
||||
sizes: [ 70, 30 ]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SqlResults({ ntxId }: TypeWidgetProps) {
|
||||
const [ response, setResponse ] = useState<SqlExecuteResponse>();
|
||||
|
||||
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, response }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
setResponse(response);
|
||||
});
|
||||
|
||||
// Not yet executed.
|
||||
if (response === undefined) {
|
||||
return (
|
||||
<NoItems
|
||||
icon="bx bx-data"
|
||||
text={t("sql_result.not_executed")}
|
||||
>
|
||||
<Button
|
||||
text={t("sql_result.execute_now")}
|
||||
triggerCommand="runActiveNote"
|
||||
/>
|
||||
</NoItems>
|
||||
);
|
||||
}
|
||||
|
||||
// Executed but failed.
|
||||
if (response && !response.success) {
|
||||
return (
|
||||
<NoItems
|
||||
icon="bx bx-error"
|
||||
text={t("sql_result.failed")}
|
||||
>
|
||||
<pre className="sql-error-message selectable-text">{response.error}</pre>
|
||||
</NoItems>
|
||||
);
|
||||
}
|
||||
|
||||
// Zero results.
|
||||
if (response?.results.length === 1 && Array.isArray(response.results[0]) && response.results[0].length === 0) {
|
||||
return (
|
||||
<NoItems
|
||||
icon="bx bx-rectangle"
|
||||
text={t("sql_result.no_rows")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sql-result-widget">
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
{response?.results.map((rows, index) => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
return (
|
||||
<NoItems
|
||||
key={index}
|
||||
icon="bx bx-play"
|
||||
text={t("sql_result.statement_result")}
|
||||
>
|
||||
<pre key={index}>{JSON.stringify(rows, null, "\t")}</pre>
|
||||
</NoItems>
|
||||
);
|
||||
}
|
||||
|
||||
// selects
|
||||
return <SqlResultTable key={index} rows={rows} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SqlResultTable({ rows }: { rows: object[] }) {
|
||||
if (!rows.length) return;
|
||||
|
||||
return (
|
||||
<Tabulator
|
||||
layout="fitDataFill"
|
||||
modules={[ ResizeColumnsModule, SortModule, SelectRangeModule, ClipboardModule, KeybindingsModule, EditModule, ExportModule, SelectRowModule, FormatModule, FrozenColumnsModule, FilterModule, PageModule ]}
|
||||
selectableRange
|
||||
clipboard="copy"
|
||||
clipboardCopyRowRange="range"
|
||||
clipboardCopyConfig={{
|
||||
rowHeaders: false,
|
||||
columnHeaders: false
|
||||
}}
|
||||
pagination
|
||||
paginationSize={15}
|
||||
paginationSizeSelector
|
||||
paginationCounter="rows"
|
||||
height="100%"
|
||||
columns={[
|
||||
{
|
||||
title: "#",
|
||||
formatter: "rownum",
|
||||
width: 60,
|
||||
hozAlign: "right",
|
||||
frozen: true
|
||||
},
|
||||
...Object.keys(rows[0]).map(key => ({
|
||||
title: key,
|
||||
field: key,
|
||||
width: 250,
|
||||
minWidth: 100,
|
||||
widthGrow: 1,
|
||||
resizable: true,
|
||||
headerFilter: true as const
|
||||
}))
|
||||
]}
|
||||
data={rows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SqlTableSchemas({ note }: TypeWidgetProps) {
|
||||
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
|
||||
|
||||
useEffect(() => {
|
||||
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
|
||||
}, []);
|
||||
|
||||
const isEnabled = note.isTriliumSqlite() && schemas;
|
||||
return (
|
||||
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
<>
|
||||
{t("sql_table_schemas.tables")}{": "}
|
||||
|
||||
<span class="sql-table-schemas">
|
||||
{schemas.map(({ name, columns }) => (
|
||||
<Dropdown key={name} text={name} noSelectButtonStyle hideToggleArrow>
|
||||
<table className="table-schema">
|
||||
{columns.map(column => (
|
||||
<tr key={column.name}>
|
||||
<td>{column.name}</td>
|
||||
<td>{column.type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</Dropdown>
|
||||
))}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import "./SplitEditor.css";
|
||||
|
||||
import Split from "@triliumnext/split.js";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
|
||||
import utils, { isMobile } from "../../../services/utils";
|
||||
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||
import "./SplitEditor.css";
|
||||
import Split from "@triliumnext/split.js";
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
|
||||
import { EditableCode, EditableCodeProps } from "../code/Code";
|
||||
import { ComponentChildren } from "preact";
|
||||
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
|
||||
|
||||
export interface SplitEditorProps extends EditableCodeProps {
|
||||
className?: string;
|
||||
@@ -15,6 +17,8 @@ export interface SplitEditorProps extends EditableCodeProps {
|
||||
splitOptions?: Split.Options;
|
||||
previewContent: ComponentChildren;
|
||||
previewButtons?: ComponentChildren;
|
||||
editorBefore?: ComponentChildren;
|
||||
forceOrientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,13 +30,14 @@ export interface SplitEditorProps extends EditableCodeProps {
|
||||
* - Can display errors to the user via {@link setError}.
|
||||
* - Horizontal or vertical orientation for the editor/preview split, adjustable via the switch split orientation button floating button.
|
||||
*/
|
||||
export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, className, ...editorProps }: SplitEditorProps) {
|
||||
const splitEditorOrientation = useSplitOrientation();
|
||||
export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, className, editorBefore, forceOrientation, ...editorProps }: SplitEditorProps) {
|
||||
const splitEditorOrientation = useSplitOrientation(forceOrientation);
|
||||
const [ readOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const editor = (!readOnly &&
|
||||
<div className="note-detail-split-editor-col">
|
||||
{editorBefore}
|
||||
<div className="note-detail-split-editor">
|
||||
<EditableCode
|
||||
note={note}
|
||||
@@ -74,12 +79,12 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
}, [ readOnly, splitEditorOrientation ]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${"split-" + splitEditorOrientation} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${splitEditorOrientation}`} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
|
||||
{splitEditorOrientation === "horizontal"
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
|
||||
@@ -88,11 +93,12 @@ export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
|
||||
className="tn-tool-button"
|
||||
noIconActionClass
|
||||
titlePosition="top"
|
||||
/>
|
||||
/>;
|
||||
}
|
||||
|
||||
function useSplitOrientation() {
|
||||
function useSplitOrientation(forceOrientation?: "horizontal" | "vertical") {
|
||||
const [ splitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
|
||||
if (forceOrientation) return forceOrientation;
|
||||
if (isMobile()) return "vertical";
|
||||
if (!splitEditorOrientation) return "horizontal";
|
||||
return splitEditorOrientation as "horizontal" | "vertical";
|
||||
|
||||
@@ -93,7 +93,15 @@ export default defineConfig(() => ({
|
||||
print: join(__dirname, "src", "print.tsx")
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "src/[name].js",
|
||||
entryFileNames: (chunk) => {
|
||||
// We enforce a hash in the main index file to avoid caching issues, this only works because we have the HTML entry point.
|
||||
if (chunk.name === "index") {
|
||||
return "src/[name]-[hash].js";
|
||||
}
|
||||
|
||||
// For EJS-rendered pages (e.g. login) we need to have a stable name.
|
||||
return "src/[name].js";
|
||||
},
|
||||
chunkFileNames: "src/[name]-[hash].js",
|
||||
assetFileNames: "src/[name]-[hash].[ext]",
|
||||
manualChunks: {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "Tool to compare content of Trilium databases. Useful for debugging sync problems.",
|
||||
"dependencies": {
|
||||
"colors": "1.4.0",
|
||||
"diff": "8.0.2",
|
||||
"diff": "8.0.3",
|
||||
"sqlite": "5.1.1",
|
||||
"sqlite3": "5.1.7"
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.3",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"better-sqlite3": "12.6.2",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-dl": "4.0.0",
|
||||
"electron-squirrel-startup": "1.0.1",
|
||||
@@ -35,15 +35,15 @@
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "39.2.7",
|
||||
"@electron-forge/cli": "7.10.2",
|
||||
"@electron-forge/maker-deb": "7.10.2",
|
||||
"@electron-forge/maker-dmg": "7.10.2",
|
||||
"@electron-forge/maker-flatpak": "7.10.2",
|
||||
"@electron-forge/maker-rpm": "7.10.2",
|
||||
"@electron-forge/maker-squirrel": "7.10.2",
|
||||
"@electron-forge/maker-zip": "7.10.2",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.10.2",
|
||||
"electron": "40.0.0",
|
||||
"@electron-forge/cli": "7.11.1",
|
||||
"@electron-forge/maker-deb": "7.11.1",
|
||||
"@electron-forge/maker-dmg": "7.11.1",
|
||||
"@electron-forge/maker-flatpak": "7.11.1",
|
||||
"@electron-forge/maker-rpm": "7.11.1",
|
||||
"@electron-forge/maker-squirrel": "7.11.1",
|
||||
"@electron-forge/maker-zip": "7.11.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.11.1",
|
||||
"prebuild-install": "7.1.3"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.6.0",
|
||||
"better-sqlite3": "12.6.2",
|
||||
"mime-types": "3.0.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"tsx": "4.21.0",
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
{
|
||||
"name": "@triliumnext/edit-docs",
|
||||
"version": "0.0.1",
|
||||
"version": "0.101.3",
|
||||
"private": true,
|
||||
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
|
||||
"dependencies": {
|
||||
"archiver": "7.0.1",
|
||||
"better-sqlite3": "12.6.0"
|
||||
"better-sqlite3": "12.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@triliumnext/client": "workspace:*",
|
||||
"@triliumnext/desktop": "workspace:*",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "39.2.7",
|
||||
"electron": "40.0.0",
|
||||
"fs-extra": "11.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-docs.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"test-build": "vitest --config vitest.build.config.mts",
|
||||
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store tsx ../../scripts/electron-start.mts src/edit-docs.ts",
|
||||
"edit-demo": "cross-env TRILIUM_PORT=37744 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
|
||||
}
|
||||
}
|
||||
40
apps/edit-docs/scripts/build.ts
Normal file
40
apps/edit-docs/scripts/build.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
import BuildHelper from "../../../scripts/build-utils";
|
||||
import originalPackageJson from "../package.json" with { type: "json" };
|
||||
|
||||
const build = new BuildHelper("apps/edit-docs");
|
||||
|
||||
async function main() {
|
||||
await build.buildBackend(["src/edit-docs.ts", "src/utils.ts"]);
|
||||
|
||||
// Copy assets from server (needed for DB initialization)
|
||||
build.copy("/apps/server/src/assets", "assets/");
|
||||
build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/");
|
||||
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
|
||||
build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css");
|
||||
build.buildFrontend();
|
||||
|
||||
// Copy node modules dependencies
|
||||
build.copyNodeModules(["better-sqlite3", "bindings", "file-uri-to-path", "@electron/remote"]);
|
||||
|
||||
generatePackageJson();
|
||||
}
|
||||
|
||||
function generatePackageJson() {
|
||||
const { version, author, license, description, dependencies, devDependencies } = originalPackageJson;
|
||||
const packageJson = {
|
||||
name: "trilium-edit-docs",
|
||||
main: "edit-docs.cjs",
|
||||
version,
|
||||
author,
|
||||
license,
|
||||
description,
|
||||
dependencies: {"better-sqlite3": dependencies["better-sqlite3"]},
|
||||
devDependencies: {electron: devDependencies.electron},
|
||||
};
|
||||
writeFileSync(join(build.outDir, "package.json"), JSON.stringify(packageJson, null, "\t"), "utf-8");
|
||||
}
|
||||
|
||||
main();
|
||||
48
apps/edit-docs/spec/build-checks/artifacts.spec.ts
Normal file
48
apps/edit-docs/spec/build-checks/artifacts.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { globSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { it, describe, expect } from "vitest";
|
||||
|
||||
describe("Check artifacts are present", () => {
|
||||
const distPath = join(__dirname, "../../dist");
|
||||
|
||||
it("has the necessary node modules", async () => {
|
||||
const paths = [
|
||||
"node_modules/better-sqlite3",
|
||||
"node_modules/bindings",
|
||||
"node_modules/file-uri-to-path",
|
||||
"node_modules/@electron/remote"
|
||||
];
|
||||
|
||||
ensurePathsExist(paths);
|
||||
});
|
||||
|
||||
it("includes the client", async () => {
|
||||
const paths = [
|
||||
"public/assets",
|
||||
"public/fonts",
|
||||
"public/node_modules",
|
||||
"public/src",
|
||||
"public/stylesheets",
|
||||
"public/translations"
|
||||
];
|
||||
|
||||
ensurePathsExist(paths);
|
||||
});
|
||||
|
||||
it("includes necessary assets", async () => {
|
||||
const paths = [
|
||||
"assets",
|
||||
"share-theme",
|
||||
"ckeditor5-content.css"
|
||||
];
|
||||
|
||||
ensurePathsExist(paths);
|
||||
});
|
||||
|
||||
function ensurePathsExist(paths: string[]) {
|
||||
for (const path of paths) {
|
||||
const result = globSync(join(distPath, path, "**"));
|
||||
expect(result, path).not.toHaveLength(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,14 +1,17 @@
|
||||
import fs from "fs/promises";
|
||||
import fsExtra from "fs-extra";
|
||||
import path from "path";
|
||||
import type { NoteMetaFile } from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||
import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js";
|
||||
import debounce from "@triliumnext/client/src/services/debounce.js";
|
||||
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
|
||||
import cls from "@triliumnext/server/src/services/cls.js";
|
||||
import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/server/src/services/export/zip/abstract_provider.js";
|
||||
import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js";
|
||||
import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js";
|
||||
import type { NoteMetaFile } from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||
import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||
import fs from "fs/promises";
|
||||
import fsExtra from "fs-extra";
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
|
||||
|
||||
interface NoteMapping {
|
||||
rootNoteId: string;
|
||||
@@ -18,47 +21,113 @@ interface NoteMapping {
|
||||
exportOnly?: boolean;
|
||||
}
|
||||
|
||||
const { DOCS_ROOT, USER_GUIDE_ROOT } = process.env;
|
||||
if (!DOCS_ROOT || !USER_GUIDE_ROOT) {
|
||||
throw new Error("Missing DOCS_ROOT or USER_GUIDE_ROOT environment variable.");
|
||||
interface Config {
|
||||
baseUrl: string;
|
||||
noteMappings: NoteMapping[];
|
||||
}
|
||||
|
||||
const BASE_URL = "https://docs.triliumnotes.org";
|
||||
// Parse command-line arguments
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let configPath: string | undefined;
|
||||
let showHelp = false;
|
||||
let showVersion = false;
|
||||
|
||||
const NOTE_MAPPINGS: NoteMapping[] = [
|
||||
{
|
||||
rootNoteId: "pOsGYCXsbNQG",
|
||||
path: path.join(__dirname, DOCS_ROOT, "User Guide"),
|
||||
format: "markdown"
|
||||
},
|
||||
{
|
||||
rootNoteId: "pOsGYCXsbNQG",
|
||||
path: path.join(__dirname, USER_GUIDE_ROOT),
|
||||
format: "html",
|
||||
ignoredFiles: ["index.html", "navigation.html", "style.css", "User Guide.html"],
|
||||
exportOnly: true
|
||||
},
|
||||
{
|
||||
rootNoteId: "jdjRLhLV3TtI",
|
||||
path: path.join(__dirname, DOCS_ROOT, "Developer Guide"),
|
||||
format: "markdown"
|
||||
},
|
||||
{
|
||||
rootNoteId: "hD3V4hiu2VW4",
|
||||
path: path.join(__dirname, DOCS_ROOT, "Release Notes"),
|
||||
format: "markdown"
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--config' || args[i] === '-c') {
|
||||
configPath = args[i + 1];
|
||||
if (!configPath) {
|
||||
console.error("Error: --config/-c requires a path argument");
|
||||
process.exit(1);
|
||||
}
|
||||
i++; // Skip the next argument as it's the value
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
showHelp = true;
|
||||
} else if (args[i] === '--version' || args[i] === '-v') {
|
||||
showVersion = true;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return { configPath, showHelp, showVersion };
|
||||
}
|
||||
|
||||
function getVersion(): string {
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
const version = getVersion();
|
||||
console.log(`
|
||||
Usage: trilium-edit-docs [options]
|
||||
|
||||
Options:
|
||||
-c, --config <path> Path to the configuration file (default: edit-docs-config.yaml in the root)
|
||||
-h, --help Display this help message
|
||||
-v, --version Display version information
|
||||
|
||||
Version: ${version}
|
||||
`);
|
||||
}
|
||||
|
||||
function printVersion() {
|
||||
const version = getVersion();
|
||||
console.log(version);
|
||||
}
|
||||
|
||||
const { configPath, showHelp, showVersion } = parseArgs();
|
||||
|
||||
if (showHelp) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
} else if (showVersion) {
|
||||
printVersion();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Configuration variables to be initialized
|
||||
let BASE_URL: string;
|
||||
let NOTE_MAPPINGS: NoteMapping[];
|
||||
|
||||
// Load configuration from edit-docs-config.yaml
|
||||
async function loadConfig() {
|
||||
let CONFIG_PATH = configPath
|
||||
? path.resolve(configPath)
|
||||
: path.join(process.cwd(), "edit-docs-config.yaml");
|
||||
|
||||
const exists = await fs.access(CONFIG_PATH).then(() => true).catch(() => false);
|
||||
if (!exists && !configPath) {
|
||||
// Fallback to project root if running from within a subproject
|
||||
CONFIG_PATH = path.join(__dirname, "../../../edit-docs-config.yaml");
|
||||
}
|
||||
|
||||
const configContent = await fs.readFile(CONFIG_PATH, "utf-8");
|
||||
const config = yaml.load(configContent) as Config;
|
||||
|
||||
BASE_URL = config.baseUrl;
|
||||
// Resolve all paths relative to the config file's directory (for flexibility with external configs)
|
||||
const CONFIG_DIR = path.dirname(CONFIG_PATH);
|
||||
NOTE_MAPPINGS = config.noteMappings.map((mapping) => ({
|
||||
...mapping,
|
||||
path: path.resolve(CONFIG_DIR, mapping.path)
|
||||
}));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await loadConfig();
|
||||
const initializedPromise = startElectron(() => {
|
||||
// Wait for the import to be finished and the application to be loaded before we listen to changes.
|
||||
setTimeout(() => registerHandlers(), 10_000);
|
||||
setTimeout(() => {
|
||||
registerHandlers();
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
await initializeTranslations();
|
||||
await initializeDatabase(true);
|
||||
|
||||
// Wait for becca to be loaded before importing data
|
||||
const beccaLoader = await import("@triliumnext/server/src/becca/becca_loader.js");
|
||||
await beccaLoader.beccaLoaded;
|
||||
|
||||
cls.init(async () => {
|
||||
for (const mapping of NOTE_MAPPINGS) {
|
||||
if (!mapping.exportOnly) {
|
||||
@@ -116,6 +185,9 @@ async function exportData(noteId: string, format: ExportFormat, outputPath: stri
|
||||
return components.join("/");
|
||||
});
|
||||
|
||||
// Remove data-list-item-id created by CKEditor for lists
|
||||
content = content.replace(/ data-list-item-id="[^"]*"/g, "");
|
||||
|
||||
return content;
|
||||
|
||||
function findAttachment(targetAttachmentId: string) {
|
||||
@@ -142,7 +214,7 @@ async function exportData(noteId: string, format: ExportFormat, outputPath: stri
|
||||
}
|
||||
}
|
||||
|
||||
const minifyMeta = (format === "html");
|
||||
const minifyMeta = (format === "html" || format === "share");
|
||||
await cleanUpMeta(outputPath, minifyMeta);
|
||||
}
|
||||
|
||||
@@ -195,7 +267,6 @@ async function registerHandlers() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Got entity changed", e.entityName, e.entity.title);
|
||||
debouncer();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import cls from "@triliumnext/server/src/services/cls.js";
|
||||
import TaskContext from "@triliumnext/server/src/services/task_context.js";
|
||||
import windowService from "@triliumnext/server/src/services/window.js";
|
||||
import archiver, { type Archiver } from "archiver";
|
||||
import electron from "electron";
|
||||
import type { WriteStream } from "fs";
|
||||
import fs from "fs/promises";
|
||||
import fsExtra from "fs-extra";
|
||||
import path from "path";
|
||||
import electron from "electron";
|
||||
import windowService from "@triliumnext/server/src/services/window.js";
|
||||
import archiver, { type Archiver } from "archiver";
|
||||
import type { WriteStream } from "fs";
|
||||
import TaskContext from "@triliumnext/server/src/services/task_context.js";
|
||||
import { resolve } from "path";
|
||||
import { deferred, DeferredPromise } from "../../../packages/commons/src";
|
||||
|
||||
export function initializeDatabase(skipDemoDb: boolean) {
|
||||
return new Promise<void>(async (resolve) => {
|
||||
const sqlInit = (await import("@triliumnext/server/src/services/sql_init.js")).default;
|
||||
cls.init(async () => {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
await sqlInit.createInitialDatabase(skipDemoDb);
|
||||
}
|
||||
resolve();
|
||||
import { deferred, type DeferredPromise } from "../../../packages/commons/src/index.js";
|
||||
|
||||
export function initializeDatabase(skipDemoDb: boolean): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
import("@triliumnext/server/src/services/sql_init.js").then((m) => {
|
||||
const sqlInit = m.default;
|
||||
cls.init(async () => {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
sqlInit.createInitialDatabase(skipDemoDb).then(() => resolve());
|
||||
} else {
|
||||
sqlInit.dbReady.resolve();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -32,10 +36,9 @@ export function initializeDatabase(skipDemoDb: boolean) {
|
||||
*/
|
||||
export function startElectron(callback: () => void): DeferredPromise<void> {
|
||||
const initializedPromise = deferred<void>();
|
||||
electron.app.on("ready", async () => {
|
||||
await initializedPromise;
|
||||
|
||||
console.log("Electron is ready!");
|
||||
const readyHandler = async () => {
|
||||
await initializedPromise;
|
||||
|
||||
// Start the server.
|
||||
const startTriliumServer = (await import("@triliumnext/server/src/www.js")).default;
|
||||
@@ -45,7 +48,15 @@ export function startElectron(callback: () => void): DeferredPromise<void> {
|
||||
await windowService.createMainWindow(electron.app);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
// Handle race condition: Electron ready event may have already fired
|
||||
if (electron.app.isReady()) {
|
||||
readyHandler();
|
||||
} else {
|
||||
electron.app.on("ready", readyHandler);
|
||||
}
|
||||
|
||||
return initializedPromise;
|
||||
}
|
||||
|
||||
@@ -70,7 +81,6 @@ async function createImportZip(path: string) {
|
||||
zlib: { level: 0 }
|
||||
});
|
||||
|
||||
console.log("Archive path is ", resolve(path))
|
||||
archive.directory(path, "/");
|
||||
|
||||
const outputStream = fsExtra.createWriteStream(inputFile);
|
||||
@@ -85,14 +95,16 @@ async function createImportZip(path: string) {
|
||||
}
|
||||
|
||||
function waitForEnd(archive: Archiver, stream: WriteStream) {
|
||||
return new Promise<void>(async (res, rej) => {
|
||||
stream.on("finish", () => res());
|
||||
await archive.finalize();
|
||||
return new Promise<void>((res, rej) => {
|
||||
stream.on("finish", res);
|
||||
stream.on("error", rej);
|
||||
archive.on("error", rej);
|
||||
archive.finalize().catch(rej);
|
||||
});
|
||||
}
|
||||
|
||||
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
|
||||
const promise = deferred<void>()
|
||||
const promise = deferred<void>();
|
||||
setTimeout(async () => {
|
||||
// Then extract the zip.
|
||||
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
|
||||
|
||||
17
apps/edit-docs/vitest.build.config.mts
Normal file
17
apps/edit-docs/vitest.build.config.mts
Normal file
@@ -0,0 +1,17 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/edit-docs',
|
||||
plugins: [],
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ['spec/build-checks/**'],
|
||||
reporters: [
|
||||
"verbose"
|
||||
]
|
||||
},
|
||||
}));
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-bullseye-slim AS builder
|
||||
FROM node:24.13.0-bullseye-slim AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.12.0-bullseye-slim
|
||||
FROM node:24.13.0-bullseye-slim
|
||||
# Install only runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-alpine AS builder
|
||||
FROM node:24.13.0-alpine AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.12.0-alpine
|
||||
FROM node:24.13.0-alpine
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache su-exec shadow
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-alpine AS builder
|
||||
FROM node:24.13.0-alpine AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.12.0-alpine
|
||||
FROM node:24.13.0-alpine
|
||||
# Create a non-root user with configurable UID/GID
|
||||
ARG USER=trilium
|
||||
ARG UID=1001
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-bullseye-slim AS builder
|
||||
FROM node:24.13.0-bullseye-slim AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.12.0-bullseye-slim
|
||||
FROM node:24.13.0-bullseye-slim
|
||||
# Create a non-root user with configurable UID/GID
|
||||
ARG USER=trilium
|
||||
ARG UID=1001
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.6.0"
|
||||
"better-sqlite3": "12.6.2"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.6.0",
|
||||
"better-sqlite3": "12.6.2",
|
||||
"html-to-text": "9.0.5",
|
||||
"node-html-parser": "7.0.2",
|
||||
"sucrase": "3.35.1"
|
||||
@@ -82,8 +82,8 @@
|
||||
"csrf-csrf": "3.2.2",
|
||||
"debounce": "3.0.0",
|
||||
"debug": "4.4.3",
|
||||
"ejs": "3.1.10",
|
||||
"electron": "39.2.7",
|
||||
"ejs": "4.0.1",
|
||||
"electron": "40.0.0",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
@@ -99,7 +99,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.7.3",
|
||||
"i18next": "25.7.4",
|
||||
"i18next-fs-backend": "2.6.1",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "6.0.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"time2fa": "1.4.2",
|
||||
"tmp": "0.2.5",
|
||||
"turndown": "7.2.2",
|
||||
"turnish": "1.8.0",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "7.3.1",
|
||||
"ws": "8.19.0",
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import favicon from "serve-favicon";
|
||||
import cookieParser from "cookie-parser";
|
||||
import helmet from "helmet";
|
||||
import compression from "compression";
|
||||
import config from "./services/config.js";
|
||||
import utils, { getResourceDir, isDev } from "./services/utils.js";
|
||||
import assets from "./routes/assets.js";
|
||||
import routes from "./routes/routes.js";
|
||||
import custom from "./routes/custom.js";
|
||||
import error_handlers from "./routes/error_handlers.js";
|
||||
import { startScheduledCleanup } from "./services/erase.js";
|
||||
import sql_init from "./services/sql_init.js";
|
||||
import { auth } from "express-openid-connect";
|
||||
import openID from "./services/open_id.js";
|
||||
import { t } from "i18next";
|
||||
import eventService from "./services/events.js";
|
||||
import log from "./services/log.js";
|
||||
import "./services/handlers.js";
|
||||
import "./becca/becca_loader.js";
|
||||
|
||||
import compression from "compression";
|
||||
import cookieParser from "cookie-parser";
|
||||
import ejs from "ejs";
|
||||
import express from "express";
|
||||
import { auth } from "express-openid-connect";
|
||||
import helmet from "helmet";
|
||||
import { t } from "i18next";
|
||||
import path from "path";
|
||||
import favicon from "serve-favicon";
|
||||
|
||||
import assets from "./routes/assets.js";
|
||||
import custom from "./routes/custom.js";
|
||||
import error_handlers from "./routes/error_handlers.js";
|
||||
import routes from "./routes/routes.js";
|
||||
import config from "./services/config.js";
|
||||
import { startScheduledCleanup } from "./services/erase.js";
|
||||
import log from "./services/log.js";
|
||||
import openID from "./services/open_id.js";
|
||||
import { RESOURCE_DIR } from "./services/resource_dir.js";
|
||||
import sql_init from "./services/sql_init.js";
|
||||
import utils, { getResourceDir, isDev } from "./services/utils.js";
|
||||
|
||||
export default async function buildApp() {
|
||||
const app = express();
|
||||
@@ -33,7 +35,7 @@ export default async function buildApp() {
|
||||
|
||||
// view engine setup
|
||||
app.set("views", path.join(assetsDir, "views"));
|
||||
app.engine("ejs", (await import("ejs")).renderFile);
|
||||
app.engine("ejs", (filePath, options, callback) => ejs.renderFile(filePath, options, callback));
|
||||
app.set("view engine", "ejs");
|
||||
|
||||
app.use((req, res, next) => {
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 230 B |
Binary file not shown.
|
Before Width: | Height: | Size: 230 B After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 265 B |
@@ -1,36 +1,43 @@
|
||||
<p>The SQL Console is Trilium's built-in database editor.</p>
|
||||
<p>It can be accessed by going to the <a href="#root/_help_Vc8PjrjAGuOp">global menu</a> →
|
||||
<p>It can be accessed by going to the <a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a> →
|
||||
Advanced → Open SQL Console.</p>
|
||||
<p>
|
||||
<img src="SQL Console_image.png">
|
||||
</p>
|
||||
<h3>Interaction</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Hovering the mouse over one of the tables listed at the top of the document
|
||||
will show the columns and their data type.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Only one SQL statement can be run at once.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>To run the statement, press the
|
||||
<img src="3_SQL Console_image.png">icon.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For queries that return a result, the data will displayed in a table.</p>
|
||||
<p>
|
||||
<img src="1_SQL Console_image.png">
|
||||
</p>
|
||||
</li>
|
||||
<li>Hovering the mouse over one of the tables listed at the top of the document
|
||||
will show the columns and their data type.</li>
|
||||
<li>Only one SQL statement can be run at once.</li>
|
||||
<li>To run the statement, press the <em>Execute</em> icon.</li>
|
||||
<li>For queries that return a result, the data will displayed in a table.</li>
|
||||
<li>For statements (e.g. <code spellcheck="false">INSERT</code>, <code spellcheck="false">UPDATE</code>),
|
||||
the number of affected rows is displayed.</li>
|
||||
</ul>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:1124/571;" src="2_SQL Console_image.png"
|
||||
width="1124" height="571">
|
||||
</figure>
|
||||
|
||||
<h3>Interacting with the table</h3>
|
||||
<p>After executing a query, a table with the results will be displayed:</p>
|
||||
<ul>
|
||||
<li>Clicking on a column allows sorting ascending or descending.</li>
|
||||
<li>Underneath each column there is an input field which allows filtering
|
||||
by text.</li>
|
||||
<li>Press <kbd>Ctrl</kbd>+<kbd>C</kbd> to copy the current cell to clipboard.</li>
|
||||
<li>Multiple cells can be selected by dragging or by holding <kbd>Shift</kbd> +
|
||||
arrow keys</li>
|
||||
<li>Results are paginated for performance reasons. The controls at the bottom
|
||||
of the table can be used to navigate through pages.</li>
|
||||
</ul>
|
||||
<h3>Saved SQL console</h3>
|
||||
<p>SQL queries or commands can be saved into a dedicated note.</p>
|
||||
<p>To do so, simply write the query and press the
|
||||
<img src="2_SQL Console_image.png">button. Once saved, the note will appear in <a href="#root/_help_l0tKav7yLHGF">Day Notes</a>.</p>
|
||||
<ul>
|
||||
<li>The SQL expression will not be displayed by default, but it can still
|
||||
be viewed by going to the note context menu and selecting <em>Note source</em>.</li>
|
||||
<li>The expression cannot be modified. If needed, recreate it by copying the
|
||||
statement back into the SQL console and then saving it again.</li>
|
||||
</ul>
|
||||
<img src="1_SQL Console_image.png">button. Once saved, the note will appear in <a class="reference-link"
|
||||
href="#root/_help_l0tKav7yLHGF">Day Notes</a>.</p>
|
||||
<p>The note can be locked for editing by pressing the <em>Lock</em> button
|
||||
in the note actions section near the title bar (on the <a class="reference-link"
|
||||
href="#root/_help_IjZS7iK5EXtb">New Layout</a>, or in the <a class="reference-link"
|
||||
href="#root/_help_XpOYSgsLkTJy">Floating buttons</a> area if using the old
|
||||
layout). When editing is locked, the SQL statement is hidden from view.</p>
|
||||
34
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html
generated
vendored
34
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Sharing.html
generated
vendored
@@ -38,17 +38,17 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e26b4ce9ba4e9dfe224d04e0f341925ed">Table of contents.</li>
|
||||
<li data-list-item-id="e9707fdfa2c92d66690cf932f7e647253">Syntax highlight of code blocks, provided a language is selected (does
|
||||
<li>Table of contents.</li>
|
||||
<li>Syntax highlight of code blocks, provided a language is selected (does
|
||||
not work if “Auto-detected” is enabled).</li>
|
||||
<li data-list-item-id="e84420a10c6d64bd107edb6e867c91d4b">Rendering for math equations.</li>
|
||||
<li data-list-item-id="e10834dcd0619d77ae2e94d3695bedf58"><a href="#root/_help_nBAXQFj20hS1">Including notes</a> (only if the included
|
||||
<li>Rendering for math equations.</li>
|
||||
<li><a href="#root/_help_nBAXQFj20hS1">Including notes</a> (only if the included
|
||||
notes are also shared).</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e41cc4139377f9f88d653d1eb8ca47bb4">Inline Mermaid diagrams are not rendered.</li>
|
||||
<li>Inline Mermaid diagrams are not rendered.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -57,12 +57,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e291ae6d5130677b4c99f7c3bdbe974b4">Basic support (displaying the contents of the note in a monospace font).</li>
|
||||
<li>Basic support (displaying the contents of the note in a monospace font).</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e0270680bbdd7a129306e61e11691e36d">No syntax highlight.</li>
|
||||
<li>No syntax highlight.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -95,12 +95,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ea031e1d4149eb443ace756234490c5a4">The child notes are displayed in a fixed format. </li>
|
||||
<li>The child notes are displayed in a fixed format. </li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ea4a9d424aec2afbaecc07bbf64b7bebd">More advanced view types such as the calendar view are not supported.</li>
|
||||
<li>More advanced view types such as the calendar view are not supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -109,12 +109,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e582d283f2b1b30cbe5ae35d8e01b2bf2">The diagram is displayed as a vector image.</li>
|
||||
<li>The diagram is displayed as a vector image.</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e33268686446e3c217077201bb5964364">No further interaction supported.</li>
|
||||
<li>No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -123,12 +123,12 @@ class="image">
|
||||
</th>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e443dd0e97c30cb12c77e8906a71569ea">The diagram is displayed as a vector image.</li>
|
||||
<li>The diagram is displayed as a vector image.</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="efe151ef3f3826c825416417525fb5fb2">No further interaction supported.</li>
|
||||
<li>No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -144,7 +144,7 @@ class="image">
|
||||
<td>The diagram is displayed as a vector image.</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ed3b4fb473042f6e32b4502d4fa11a767">No further interaction supported.</li>
|
||||
<li>No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -160,7 +160,7 @@ class="image">
|
||||
<td>Basic interaction (downloading the file).</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ed87e836a39d127ebcbb33e9e59045afb">No further interaction supported.</li>
|
||||
<li>No further interaction supported.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -392,8 +392,8 @@ for (const attr of parentNote.attributes) {
|
||||
<p>Indicates to web crawlers that the page should not be indexed of this
|
||||
note by:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li>
|
||||
<li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li>
|
||||
<li>Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li>
|
||||
<li>Setting the <code>noindex, follow</code> meta tag.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,16 +1,79 @@
|
||||
<p>Trilium can import ENEX files which are used by Evernote for backup/export.
|
||||
One ENEX file represents content (notes and resources) of one notebook.</p>
|
||||
<p>Trilium can import ENEX files, which are used by Evernote for backup/export.
|
||||
One ENEX file represents the content (notes and resources) of one notebook.</p>
|
||||
<h2>Export ENEX from Evernote</h2>
|
||||
<p>To export ENEX file, you need to have a <em>legacy</em> desktop version
|
||||
of Evernote (i.e. not web/mobile). Right click on notebook and select export
|
||||
and follow the wizard.</p>
|
||||
<p>To export ENEX files from Evernote, you can use:</p>
|
||||
<ul>
|
||||
<li>Evernote desktop application. See Evernote <a href="https://help.evernote.com/hc/en-us/articles/209005557-Export-Notes-and-Notebooks-as-ENEX-or-HTML">documentation</a>.
|
||||
Note that the limitation of this method is that you can only export 100
|
||||
notes at a time or one notebook at a time.</li>
|
||||
<li>A third-party <a href="https://github.com/vzhd1701/evernote-backup">evernote-backup</a> CLI
|
||||
tool. This tool can export all of your notebooks in bulk.</li>
|
||||
</ul>
|
||||
<h2>Import ENEX in Trilium</h2>
|
||||
<p>Once you have ENEX file, you can import it to Trilium. Right click on
|
||||
some note (to which you want to import the file), click on "Import" and
|
||||
select the ENEX file.</p>
|
||||
<p>After importing the ENEX file, go over the imported notes and resources
|
||||
to be sure the import went well, and you didn't lose any data.</p>
|
||||
<p>Once you have your ENEX files, do the following to import them in Trilium:</p>
|
||||
<ol>
|
||||
<li>In the Trilium note tree, right-click the note under which you want to
|
||||
import one or more of your ENEX files. The notes in the files will be imported
|
||||
as child notes of the selected note.</li>
|
||||
<li>Click Import into note.</li>
|
||||
<li>Choose your ENEX file or files and click Import.</li>
|
||||
<li>During the import, you will see "Import in progress" message. If the import
|
||||
is successful, the message will change to “Import finished successfully”
|
||||
and then disappear.</li>
|
||||
<li>We recommend you to check the imported notes and their attachments to
|
||||
verify that you haven’t lost any data.</li>
|
||||
</ol>
|
||||
<p>A non-exhaustive list of what the importer preserves:</p>
|
||||
<ul>
|
||||
<li>Attachments</li>
|
||||
<li>The hierarchy of headings (these are shifted to start with H2 because
|
||||
H1 is reserved for note title, see <a href="#root/_help_Gr6xFaF6ioJ5">Headings</a>)</li>
|
||||
<li>Tables</li>
|
||||
<li>Bulleted lists</li>
|
||||
<li>Numbered lists</li>
|
||||
<li>Bold</li>
|
||||
<li>Italics</li>
|
||||
<li>Strikethrough</li>
|
||||
<li>Highlights</li>
|
||||
<li>Font colors</li>
|
||||
<li>Soft line breaks</li>
|
||||
<li>External links</li>
|
||||
</ul>
|
||||
<p>However, we do not guarantee that all of your formatting will be imported
|
||||
100% correctly.</p>
|
||||
<h2>Limitations</h2>
|
||||
<p>All resources (except for images) are created as note's attachments.</p>
|
||||
<p>HTML inside ENEX files is not exactly valid so some formatting maybe broken
|
||||
or lost. You can report major problems into <a href="https://github.com/TriliumNext/Trilium/issues">Trilium issue tracker</a>.</p>
|
||||
<ul>
|
||||
<li>The size limit of one import is 250Mb. If the total size of your files
|
||||
is larger, you can increase the <a href="#root/_help_WOcw2SLH6tbX">upload limit</a>,
|
||||
or divide your files, and run the import as many times as necessary.</li>
|
||||
<li>All resources (except for images) are created as notes’ attachments.</li>
|
||||
<li>If you have HTML inside ENEX files, the HTML formatting may be broken
|
||||
or lost after import in Trilium. See <a class="reference-link" href="#root/_help_wy8So3yZZlH9">Reporting issues</a>.</li>
|
||||
</ul>
|
||||
<h3>Internal links</h3>
|
||||
<p>The importer cannot transform Evernote internal links into Trilium internal
|
||||
links because Evernote internal note IDs are not preserved in ENEX files.</p>
|
||||
<p>If you want to restore the internal links in Trilium after you import
|
||||
all of your ENEX files, you can use or adapt this custom script:
|
||||
<a
|
||||
class="reference-link" href="#root/_help_dj3j8dG4th4l">Process internal links by title</a>
|
||||
</p>
|
||||
<p>The script does the following:</p>
|
||||
<ol>
|
||||
<li>It finds all Evernote internal links.</li>
|
||||
<li>For each one, it checks if its link text matches a note title, and if
|
||||
yes, it replaces the Evernote link with an internal Trilium link. If not,
|
||||
it leaves the Evernote link in place.</li>
|
||||
<li>If it finds more than one note with a matching note title, it leaves the
|
||||
Evernote link in place.</li>
|
||||
<li>It outputs the results in a log that you can see in the respective code
|
||||
note in Trilium.</li>
|
||||
</ol>
|
||||
<p>The script has the following limitations:</p>
|
||||
<ul>
|
||||
<li>It will not fix links to anchors and links to notes that you renamed in
|
||||
Evernote after you created the links.</li>
|
||||
<li>Some note titles might not be well identified, even if they exist. This
|
||||
is especially the case if the note title contains some special characters.
|
||||
Should this be problematic, consider <a class="reference-link" href="#root/_help_wy8So3yZZlH9">Reporting issues</a>.</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,35 @@
|
||||
const query = `note.type = "text" and note.content *=* "evernote:///view/"`;
|
||||
const notes = api.searchForNotes(query);
|
||||
|
||||
for (const note of notes) {
|
||||
api.log(`Processing note ${note.title}...`);
|
||||
|
||||
const content = note.getContent();
|
||||
const $ = api.cheerio.load(content);
|
||||
|
||||
$("a").each((i, el) => {
|
||||
const $el = $(el);
|
||||
|
||||
const url = $el.attr("href");
|
||||
if (!url.startsWith("evernote:///")) return;
|
||||
|
||||
const text = $el.text();
|
||||
const matchingNotes = api.searchForNotes(`note.title = "${text}"`);
|
||||
if (matchingNotes.length === 0) {
|
||||
api.log(`No matching notes for "${text}..."`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchingNotes.length > 1) {
|
||||
api.log(`Found multiple matching notes for "${text}". Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingNote = matchingNotes[0];
|
||||
|
||||
api.log(`Found matching note: ${matchingNote.title} ${matchingNote.noteId}`);
|
||||
$el.attr("href", `#root/${matchingNote.noteId}`);
|
||||
$el.addClass("reference-link");
|
||||
});
|
||||
note.setContent($("body").html());
|
||||
}
|
||||
@@ -8,39 +8,37 @@
|
||||
the number of items stays small. When a note has a large number of notes
|
||||
(in the order of thousands or tens of thousands), two problems arise:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e536c86d371061c12f76f7de2a0af67be">Navigating between notes becomes cumbersome and the tree itself gets cluttered
|
||||
<li>Navigating between notes becomes cumbersome and the tree itself gets cluttered
|
||||
with a large amount of notes.</li>
|
||||
<li data-list-item-id="ecc37d6c4d0430254e98615842b94429d">The large amount of notes can slow down the application considerably.</li>
|
||||
<li>The large amount of notes can slow down the application considerably.</li>
|
||||
</ul>
|
||||
<p>Since v0.102.0, Trilium allows the tree to hide the child notes of particular
|
||||
notes. This works for both <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a> and
|
||||
notes. This works for both <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a> and
|
||||
normal notes.</p>
|
||||
<h2>Interaction</h2>
|
||||
<p>When the subtree of a note is hidden, there are a few subtle changes:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="ec1ce3d2030f36e4847f3bbd9468d28e3">To indicate that the subtree is hidden, the note will not have an expand
|
||||
<li>To indicate that the subtree is hidden, the note will not have an expand
|
||||
button and it will display the number of children to the right.</li>
|
||||
<li
|
||||
data-list-item-id="ea99d38ea6c8a816cf2ab7a7e73cfcac5">It's not possible to add a new note directly from the tree.
|
||||
<li>It's not possible to add a new note directly from the tree.
|
||||
<ul>
|
||||
<li data-list-item-id="ef0132a903a11e9f667b2b2f4c4fff17a">For <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>,
|
||||
<li>For <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>,
|
||||
it's best to use the built-in mechanism to create notes (for example by
|
||||
creating a new point on a geo-map, or by adding a new row in a table).</li>
|
||||
<li
|
||||
data-list-item-id="e7db44100046c8c79bf79841285aacd1f">For normal notes, it's still possible to create children via other means
|
||||
such as using the <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/QEAPj01N5f7w/_help_hrZ1D00cLbal">Internal (reference) links</a> system.</li>
|
||||
<li>For normal notes, it's still possible to create children via other means
|
||||
such as using the <a class="reference-link" href="#root/_help_hrZ1D00cLbal">Internal (reference) links</a> system.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="eb049f46cf91db6de113af1099a14944e">Notes can be dragged from outside the note, case in which they will be
|
||||
cloned into it.
|
||||
<ul>
|
||||
<li data-list-item-id="e96d9b7a0755e9c054bab5db4fc1aa25e">Instead of switching to the child notes that were copied, the parent note
|
||||
is highlighted instead.</li>
|
||||
<li data-list-item-id="ec667e3f94a0cfa3fa41ce38d3ed6ee95">A notification will indicate this behavior.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="eb64670dd7ace6764c18602b440f88049">Similarly, features such as cut/copy and then paste into the note will
|
||||
also work.</li>
|
||||
</li>
|
||||
<li>Notes can be dragged from outside the note, case in which they will be
|
||||
cloned into it.
|
||||
<ul>
|
||||
<li>Instead of switching to the child notes that were copied, the parent note
|
||||
is highlighted instead.</li>
|
||||
<li>A notification will indicate this behavior.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Similarly, features such as cut/copy and then paste into the note will
|
||||
also work.</li>
|
||||
</ul>
|
||||
<h2>Spotlighting</h2>
|
||||
<figure class="image image-style-align-right">
|
||||
@@ -52,12 +50,11 @@
|
||||
<p>During this state, the note remains under its normal hierarchy, so that
|
||||
its easy to tell its location. In addition, this means that:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e2490369eb3d99ca694dba23a3410abef">The note position is clearly visible when using the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a>.</li>
|
||||
<li
|
||||
data-list-item-id="e041d3807f80dc77b022540b0551b8376">The note can still be operated on from the tree, such as adding a
|
||||
<li>The note position is clearly visible when using the <a class="reference-link"
|
||||
href="#root/_help_eIg8jdvaoNNd">Search</a>.</li>
|
||||
<li>The note can still be operated on from the tree, such as adding a
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/IakOLONlIfGI/_help_TBwsyfadTA18">Branch prefix</a> or moving it outside the collection.</li>
|
||||
class="reference-link" href="#root/_help_TBwsyfadTA18">Branch prefix</a> or moving it outside the collection.</li>
|
||||
</ul>
|
||||
<p>The note appears in italics to indicate its temporary display. When switching
|
||||
to another note, the spotlighted note will disappear.</p>
|
||||
@@ -67,29 +64,27 @@
|
||||
This is intentional to avoid displaying a partial state of the subtree.</p>
|
||||
</aside>
|
||||
<h2>Working with collections</h2>
|
||||
<p>By default, some of the <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a> will
|
||||
<p>By default, some of the <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a> will
|
||||
automatically hide their child notes, for example the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_CtBQqbwXDx1w">Kanban Board</a> or
|
||||
the <a class="reference-link" href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_2FvYrpmOXm29">Table</a>.</p>
|
||||
href="#root/_help_CtBQqbwXDx1w">Kanban Board</a> or the <a class="reference-link"
|
||||
href="#root/_help_2FvYrpmOXm29">Table</a>.</p>
|
||||
<p>The reasoning behind this is that collections are generally opaque to
|
||||
the rest of the notes and they can generate a large amount of sub-notes
|
||||
since they intentionally lack structure (in order to allow easy swapping
|
||||
between views).</p>
|
||||
<p>Some types of collections have the child notes intentionally shown, for
|
||||
example the legacy ones (Grid and List), but also the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation</a> which
|
||||
requires the tree structure in order to organize and edit the slides.</p>
|
||||
href="#root/_help_zP3PMqaG71Ct">Presentation</a> which requires the tree
|
||||
structure in order to organize and edit the slides.</p>
|
||||
<p>To toggle this behavior:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e6d8c8c98802d70f13df626ea1f062122">In the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
|
||||
<li>In the <a class="reference-link" href="#root/_help_IjZS7iK5EXtb">New Layout</a>,
|
||||
press the Options button underneath the title and uncheck <em>Hide child notes in tree</em>.</li>
|
||||
<li
|
||||
data-list-item-id="e2398432e127c54239d679a6b13d8390b">Right click the collection note in the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
select <em>Advanced</em> → <em>Show subtree</em>.</li>
|
||||
<li>Right click the collection note in the <a class="reference-link"
|
||||
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and select <em>Advanced</em> → <em>Show subtree</em>.</li>
|
||||
</ul>
|
||||
<h2>Working with normal notes</h2>
|
||||
<p>It's possible to hide the subtree for normal notes as well, not just collections.
|
||||
To do so, right click the note in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
To do so, right click the note in the <a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
select <em>Advanced</em> → <em>Hide subtree.</em>
|
||||
</p>
|
||||
8
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Calendar.html
generated
vendored
8
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Calendar.html
generated
vendored
@@ -148,10 +148,10 @@
|
||||
<td>
|
||||
<p>Which view to display in the calendar:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e2cd230dc41f41fe91ee74d7d1fa87372"><code>timeGridWeek</code> for the <em>week</em> view;</li>
|
||||
<li data-list-item-id="eee1dba4c6cc51ebd53d0a0dd52044cd6"><code>dayGridMonth</code> for the <em>month</em> view;</li>
|
||||
<li data-list-item-id="ed8721a76a1865dac882415f662ed45b9"><code>multiMonthYear</code> for the <em>year</em> view;</li>
|
||||
<li data-list-item-id="edf09a13759102d98dac34c33eb690c05"><code>listMonth</code> for the <em>list</em> view.</li>
|
||||
<li><code>timeGridWeek</code> for the <em>week</em> view;</li>
|
||||
<li><code>dayGridMonth</code> for the <em>month</em> view;</li>
|
||||
<li><code>multiMonthYear</code> for the <em>year</em> view;</li>
|
||||
<li><code>listMonth</code> for the <em>list</em> view.</li>
|
||||
</ul>
|
||||
<p>Any other value will be dismissed and the default view (month) will be
|
||||
used instead.</p>
|
||||
|
||||
68
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html
generated
vendored
68
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html
generated
vendored
@@ -33,12 +33,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e04c84d59d44645ee89b2a8541ed99f90">Headings (section titles, paragraph)</li>
|
||||
<li data-list-item-id="e39d25bd3d8bd06185b9d259e5827d451">Font size</li>
|
||||
<li data-list-item-id="e1f7e2a2f4b03449d82bdf5b5c6ea8d44">Bold, italic, underline, strike-through</li>
|
||||
<li data-list-item-id="e3decae72884f65b4d538151b6a297072">Superscript, subscript</li>
|
||||
<li data-list-item-id="e59adf00fef65304c163ae190fac5e92a">Font color & background color</li>
|
||||
<li data-list-item-id="ed3f09156147a2769e91db111c76376e2">Remove formatting</li>
|
||||
<li>Headings (section titles, paragraph)</li>
|
||||
<li>Font size</li>
|
||||
<li>Bold, italic, underline, strike-through</li>
|
||||
<li>Superscript, subscript</li>
|
||||
<li>Font color & background color</li>
|
||||
<li>Remove formatting</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -47,9 +47,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ee87806a913900d85d8f018af81f41df8">Bulleted lists</li>
|
||||
<li data-list-item-id="e3ae314e365fa418ca6e0f061d63834c5">Numbered lists</li>
|
||||
<li data-list-item-id="ee84e08694165f95430046cb34f4cd123">To-do lists</li>
|
||||
<li>Bulleted lists</li>
|
||||
<li>Numbered lists</li>
|
||||
<li>To-do lists</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -58,8 +58,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e2892dc35a0d4b7ad65daffb8f9404daa">Block quotes</li>
|
||||
<li data-list-item-id="e7297e3ad1002f8de15aa0bd66c6f3f22">Admonitions</li>
|
||||
<li>Block quotes</li>
|
||||
<li>Admonitions</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -68,10 +68,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="eb358a4567d93f66004f4195df2dda05a">Basic tables</li>
|
||||
<li data-list-item-id="e6135a555d6c63c30e4b84806a4870830">Merging cells</li>
|
||||
<li data-list-item-id="e29ac76563d0998b28fb1baf94dbdac8c">Styling tables and cells.</li>
|
||||
<li data-list-item-id="e372446e81fdedada64b8bed89ca93d1a">Table captions</li>
|
||||
<li>Basic tables</li>
|
||||
<li>Merging cells</li>
|
||||
<li>Styling tables and cells.</li>
|
||||
<li>Table captions</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -80,9 +80,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="eb260b76afcbc07bd9d4ceec4e000e8a0">Inline code</li>
|
||||
<li data-list-item-id="e9864352286369ebe7b41c1599f498de8">Code blocks</li>
|
||||
<li data-list-item-id="ee62fb9ed7f349178e8f2a2bd9ec8cd74">Keyboard shortcuts</li>
|
||||
<li>Inline code</li>
|
||||
<li>Code blocks</li>
|
||||
<li>Keyboard shortcuts</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -91,7 +91,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="edf62ec004eff35cfcb7e361deef19aaf">Footnotes</li>
|
||||
<li>Footnotes</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -100,7 +100,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ebe6277e643041403489c3ceb30c36f7f">Images</li>
|
||||
<li>Images</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -109,8 +109,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e3f988be2f259bb40607cb61541955395">External links</li>
|
||||
<li data-list-item-id="e3f91cc4f0cccd2c077cc306bacd68ef2">Internal Trilium links</li>
|
||||
<li>External links</li>
|
||||
<li>Internal Trilium links</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -119,7 +119,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="eac8015a64bce7b749cc67d1599062007">Include note</li>
|
||||
<li>Include note</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -128,12 +128,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e5cdf5d3885ec0ea67f924b4b8fe5c483">Symbols</li>
|
||||
<li data-list-item-id="e95082e6642ed5b1eec6e4e116b899a40"><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
|
||||
<li>Symbols</li>
|
||||
<li><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
|
||||
</li>
|
||||
<li data-list-item-id="ecbef6a358a5b8d27f0d3e08bbc750aa9">Mermaid diagrams</li>
|
||||
<li data-list-item-id="e6e97ee14dd29b7ccf53227107e5dc72d">Horizontal ruler</li>
|
||||
<li data-list-item-id="e6198c7c535c249faec2e8906775f11de">Page break</li>
|
||||
<li>Mermaid diagrams</li>
|
||||
<li>Horizontal ruler</li>
|
||||
<li>Page break</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -142,12 +142,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e0c14456cb83d483b07ea432ef9d4728e">Indentation
|
||||
<li>Indentation
|
||||
<ul>
|
||||
<li data-list-item-id="e2029812c5e105c595590f70ee227631e">Markdown import</li>
|
||||
<li>Markdown import</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="ea1ee012286e05190c89c9f4e64cf2036"><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
|
||||
<li><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
@@ -157,11 +157,11 @@
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="e1ab173193a533ccf33dccfd0cb916f1f"><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
|
||||
<li><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
|
||||
</li>
|
||||
<li data-list-item-id="e564b978c09fe5adf476b331b1e0640e3"><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
|
||||
<li><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
|
||||
</li>
|
||||
<li data-list-item-id="e756306c31d9beffbba3820b6d1b9bc61"><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
|
||||
<li><a class="reference-link" href="#root/_help_5wZallV2Qo1t">Format Painter</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
<td>
|
||||
<p>Defines on which events script should run. Possible values are:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e244b14e102cf1b0d4954e8fd455ea77b"><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
<li><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
but not on mobile.</li>
|
||||
<li data-list-item-id="ea8f8ca86e7b351dd86108848ccb9103a"><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
<li><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
on mobile.</li>
|
||||
<li data-list-item-id="e658488cf1a0862603088ef384e41b8b6"><code>backendStartup</code> - when Trilium backend starts up</li>
|
||||
<li data-list-item-id="ef40ba992fc450d33a18ca4cb031eca66"><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to
|
||||
<li><code>backendStartup</code> - when Trilium backend starts up</li>
|
||||
<li><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to
|
||||
specify at which hour, on the back-end.</li>
|
||||
<li data-list-item-id="e07458d4f55b6eb42468a5535b8425c5f"><code>daily</code> - run once a day, on the back-end</li>
|
||||
<li><code>daily</code> - run once a day, on the back-end</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
27
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Breaking changes.html
generated
vendored
Normal file
27
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Breaking changes.html
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<h2>v0.102.0: Upgrade to jQuery 4.0.0</h2>
|
||||
<p>jQuery 4 removes legacy browser support (such as IE11 support), but it
|
||||
also removes some APIs that are considered deprecated such as:</p>
|
||||
<blockquote>
|
||||
<p><code spellcheck="false">jQuery.isArray</code>, <code spellcheck="false">jQuery.parseJSON</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.trim</code>, <code spellcheck="false">jQuery.type</code>, <code spellcheck="false">jQuery.now</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.isNumeric</code>, <code spellcheck="false">jQuery.isFunction</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.isWindow</code>, <code spellcheck="false">jQuery.camelCase</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.nodeName</code>, <code spellcheck="false">jQuery.cssNumber</code>,
|
||||
<code
|
||||
spellcheck="false">jQuery.cssProps</code>, and <code spellcheck="false">jQuery.fx.interval</code>.</p>
|
||||
<p>Use native equivalents like <code spellcheck="false">Array.isArray()</code>,
|
||||
<code
|
||||
spellcheck="false">JSON.parse()</code>, <code spellcheck="false">String.prototype.trim()</code>,
|
||||
and <code spellcheck="false">Date.now()</code> instead.</p>
|
||||
</blockquote>
|
||||
<p>This may affect custom scripts if they (or the custom jQuery libraries
|
||||
used) depend on the deprecated APIs.</p>
|
||||
<p>Note that Trilium polyfills <code spellcheck="false">jQuery.isArray</code>,
|
||||
<code
|
||||
spellcheck="false">isFunction</code>and <code spellcheck="false">isPlainObject</code> because
|
||||
they were required by one of our dependencies (the autocomplete).</p>
|
||||
<p>For more information, consult <a href="https://blog.jquery.com/2026/01/17/jquery-4-0-0/">the official blog post</a>.</p>
|
||||
@@ -107,10 +107,10 @@ class="ck-table-resized">
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="ec06332efcc3039721606c052f0d913fa">The widget must export a <code>class</code> and not an instance of the class
|
||||
<li>The widget must export a <code>class</code> and not an instance of the class
|
||||
(e.g. <code>no new</code>) because it needs to be multiplied for each note,
|
||||
so that splits work correctly.</li>
|
||||
<li data-list-item-id="e8da690a2a8df148f6b5fc04ba1611688">Since the <code>class</code> is exported instead of an instance, the <code>parentWidget</code> getter
|
||||
<li>Since the <code>class</code> is exported instead of an instance, the <code>parentWidget</code> getter
|
||||
must be <code>static</code>, otherwise the widget is ignored.</li>
|
||||
</ul>
|
||||
</td>
|
||||
@@ -124,7 +124,7 @@ class="ck-table-resized">
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li data-list-item-id="efe008d361e224f422582552648e1afe7">Although not mandatory, it's best to use a <code>RightPanelWidget</code> instead
|
||||
<li>Although not mandatory, it's best to use a <code>RightPanelWidget</code> instead
|
||||
of a <code>BasicWidget</code> or a <code>NoteContextAwareWidget</code>.</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"quick-search": "क्विक सर्च बार को एक्टिवेट करें",
|
||||
"search-in-subtree": "एक्टिव नोट के सब-ट्री में नोट्स खोजें",
|
||||
"expand-subtree": "मौजूदा नोट के सब-ट्री को (subtree) एक्सपैंड करें",
|
||||
"delete-note": "नोट डिलीट करें"
|
||||
"delete-note": "नोट डिलीट करें",
|
||||
"move-note-up-in-hierarchy": "नोट एक लेवल ऊपर मूव करें",
|
||||
"move-note-down-in-hierarchy": "नोट एक लेवल नीचे ले जाएँ",
|
||||
"dialogs": "डायलॉग्स"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,36 @@
|
||||
"search-in-subtree": "Søk etter notater i det aktive notatets understruktur",
|
||||
"creating-and-moving-notes": "Lage og flytte notater",
|
||||
"dialogs": "Dialogbokser",
|
||||
"other": "Andre"
|
||||
"other": "Andre",
|
||||
"expand-subtree": "Utvid undertre for gjeldende notat"
|
||||
},
|
||||
"setup_sync-from-desktop": {
|
||||
"step6-here": "her"
|
||||
},
|
||||
"set_password": {
|
||||
"password": "Passord"
|
||||
},
|
||||
"setup": {
|
||||
"next": "Neste",
|
||||
"title": "Konfigurasjon"
|
||||
},
|
||||
"login": {
|
||||
"title": "Logg inn",
|
||||
"password": "Passord",
|
||||
"button": "Logg inn"
|
||||
},
|
||||
"setup_sync-from-server": {
|
||||
"server-host-placeholder": "https://<hostnavn>:<port>",
|
||||
"proxy-server-placeholder": "https://<hostnavn>:<port>",
|
||||
"note": "Obs:",
|
||||
"password": "Passord",
|
||||
"password-placeholder": "Passord",
|
||||
"back": "Tilbake"
|
||||
},
|
||||
"setup_sync-in-progress": {
|
||||
"outstanding-items-default": "N/A"
|
||||
},
|
||||
"share_page": {
|
||||
"parent": "overordnet notat:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
"move-note-up": "Notu bir üste taşı",
|
||||
"collapse-tree": "Tüm not ağacını daraltır",
|
||||
"collapse-subtree": "Geçerli notun alt ağacını daraltır",
|
||||
"sort-child-notes": "Alt notları sırala"
|
||||
"sort-child-notes": "Alt notları sırala",
|
||||
"creating-and-moving-notes": "Notları oluşturma ve yerlerini değiştirme",
|
||||
"create-note-into": "Aktif nota bağlı alt not oluştur",
|
||||
"create-note-after": "Aktif nottan sonra yeni bir not oluştur",
|
||||
"delete-note": "Notu sil",
|
||||
"move-note-down": "Notu aşağıya kaydır"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import { dayjs, getNoteIcon } from "@triliumnext/commons";
|
||||
|
||||
import cloningService from "../../services/cloning.js";
|
||||
import dateUtils from "../../services/date_utils.js";
|
||||
@@ -24,26 +24,6 @@ import BRevision from "./brevision.js";
|
||||
const LABEL = "label";
|
||||
const RELATION = "relation";
|
||||
|
||||
// TODO: Deduplicate with fnote
|
||||
export const NOTE_TYPE_ICONS = {
|
||||
file: "bx bx-file",
|
||||
image: "bx bx-image",
|
||||
code: "bx bx-code",
|
||||
render: "bx bx-extension",
|
||||
search: "bx bx-file-find",
|
||||
relationMap: "bx bxs-network-chart",
|
||||
book: "bx bx-book",
|
||||
noteMap: "bx bxs-network-chart",
|
||||
mermaid: "bx bx-selection",
|
||||
canvas: "bx bx-pen",
|
||||
webView: "bx bx-globe-alt",
|
||||
launcher: "bx bx-link",
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap",
|
||||
geoMap: "bx bx-map-alt"
|
||||
};
|
||||
|
||||
interface NotePathRecord {
|
||||
isArchived: boolean;
|
||||
isInHoistedSubTree: boolean;
|
||||
@@ -1698,30 +1678,17 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return `tn-icon ${this.#getIconInternal()}`;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with fnote
|
||||
#getIconInternal() {
|
||||
const iconClassLabels = this.getLabels("iconClass");
|
||||
const icon = getNoteIcon({
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
|
||||
workspaceIconClass: undefined,
|
||||
isFolder: this.isFolder.bind(this)
|
||||
});
|
||||
|
||||
if (iconClassLabels && iconClassLabels.length > 0) {
|
||||
return iconClassLabels[0].value;
|
||||
} else if (this.noteId === "root") {
|
||||
return "bx bx-home-alt-2";
|
||||
}
|
||||
if (this.noteId === "_share") {
|
||||
return "bx bx-share-alt";
|
||||
} else if (this.type === "text") {
|
||||
if (this.isFolder()) {
|
||||
return "bx bx-folder";
|
||||
}
|
||||
return "bx bx-note";
|
||||
|
||||
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
|
||||
return "bx bx-data";
|
||||
}
|
||||
return NOTE_TYPE_ICONS[this.type];
|
||||
return `tn-icon ${icon}`;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with fnote
|
||||
|
||||
@@ -10,6 +10,7 @@ describe("data_dir.ts unit tests", async () => {
|
||||
const mockFn = {
|
||||
existsSyncMock: vi.fn(),
|
||||
mkdirSyncMock: vi.fn(),
|
||||
statSyncMock: vi.fn(),
|
||||
osHomedirMock: vi.fn(),
|
||||
osPlatformMock: vi.fn(),
|
||||
pathJoinMock: vi.fn()
|
||||
@@ -21,7 +22,8 @@ describe("data_dir.ts unit tests", async () => {
|
||||
return {
|
||||
default: {
|
||||
existsSync: mockFn.existsSyncMock,
|
||||
mkdirSync: mockFn.mkdirSyncMock
|
||||
mkdirSync: mockFn.mkdirSyncMock,
|
||||
statSync: mockFn.statSyncMock
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -109,34 +111,36 @@ describe("data_dir.ts unit tests", async () => {
|
||||
*/
|
||||
|
||||
describe("case A", () => {
|
||||
it("when folder exists – it should return the path, without attempting to create the folder", async () => {
|
||||
it("when folder exists – it should return the path, handling EEXIST gracefully", async () => {
|
||||
const mockTriliumDataPath = "/home/mock/trilium-data-ENV-A1";
|
||||
process.env.TRILIUM_DATA_DIR = mockTriliumDataPath;
|
||||
|
||||
// set fs.existsSync to true, i.e. the folder does exist
|
||||
mockFn.existsSyncMock.mockImplementation(() => true);
|
||||
// mkdirSync throws EEXIST when folder already exists (EAFP pattern)
|
||||
const eexistError = new Error("EEXIST: file already exists") as NodeJS.ErrnoException;
|
||||
eexistError.code = "EEXIST";
|
||||
mockFn.mkdirSyncMock.mockImplementation(() => { throw eexistError; });
|
||||
|
||||
// statSync confirms it's a directory
|
||||
mockFn.statSyncMock.mockImplementation(() => ({ isDirectory: () => true }));
|
||||
|
||||
const result = getTriliumDataDir("trilium-data");
|
||||
|
||||
// createDirIfNotExisting should call existsync 1 time and mkdirSync 0 times -> as it does not need to create the folder
|
||||
// and return value should be TRILIUM_DATA_DIR value from process.env
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
|
||||
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(0);
|
||||
// createDirIfNotExisting tries mkdirSync first (EAFP), then statSync to verify it's a directory
|
||||
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||
expect(mockFn.statSyncMock).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(process.env.TRILIUM_DATA_DIR);
|
||||
});
|
||||
|
||||
it("when folder does not exist – it should attempt to create the folder and return the path", async () => {
|
||||
it("when folder does not exist – it should create the folder and return the path", async () => {
|
||||
const mockTriliumDataPath = "/home/mock/trilium-data-ENV-A2";
|
||||
process.env.TRILIUM_DATA_DIR = mockTriliumDataPath;
|
||||
|
||||
// set fs.existsSync mock to return false, i.e. the folder does not exist
|
||||
mockFn.existsSyncMock.mockImplementation(() => false);
|
||||
// mkdirSync succeeds when folder doesn't exist
|
||||
mockFn.mkdirSyncMock.mockImplementation(() => undefined);
|
||||
|
||||
const result = getTriliumDataDir("trilium-data");
|
||||
|
||||
// createDirIfNotExisting should call existsync 1 time and mkdirSync 1 times -> as it has to create the folder
|
||||
// and return value should be TRILIUM_DATA_DIR value from process.env
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
|
||||
// createDirIfNotExisting calls mkdirSync which succeeds
|
||||
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(process.env.TRILIUM_DATA_DIR);
|
||||
});
|
||||
@@ -171,19 +175,19 @@ describe("data_dir.ts unit tests", async () => {
|
||||
|
||||
// use Generator to precisely control order of fs.existSync return values
|
||||
const existsSyncMockGen = (function* () {
|
||||
// 1) fs.existSync -> case B
|
||||
// 1) fs.existSync -> case B -> checking if folder exists in home dir
|
||||
yield false;
|
||||
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
|
||||
yield true;
|
||||
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
|
||||
yield false;
|
||||
})();
|
||||
|
||||
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
|
||||
// mkdirSync succeeds (folder doesn't exist)
|
||||
mockFn.mkdirSyncMock.mockImplementation(() => undefined);
|
||||
|
||||
const result = getTriliumDataDir(dataDirName);
|
||||
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
|
||||
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(mockPlatformDataPath);
|
||||
});
|
||||
@@ -198,21 +202,26 @@ describe("data_dir.ts unit tests", async () => {
|
||||
|
||||
// use Generator to precisely control order of fs.existSync return values
|
||||
const existsSyncMockGen = (function* () {
|
||||
// 1) fs.existSync -> case B
|
||||
// 1) fs.existSync -> case B -> checking if folder exists in home dir
|
||||
yield false;
|
||||
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
|
||||
yield true;
|
||||
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
|
||||
yield true;
|
||||
})();
|
||||
|
||||
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
|
||||
|
||||
// mkdirSync throws EEXIST (folder already exists), statSync confirms it's a directory
|
||||
const eexistError = new Error("EEXIST: file already exists") as NodeJS.ErrnoException;
|
||||
eexistError.code = "EEXIST";
|
||||
mockFn.mkdirSyncMock.mockImplementation(() => { throw eexistError; });
|
||||
mockFn.statSyncMock.mockImplementation(() => ({ isDirectory: () => true }));
|
||||
|
||||
const result = getTriliumDataDir(dataDirName);
|
||||
|
||||
expect(result).toEqual(mockPlatformDataPath);
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
|
||||
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(0);
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
|
||||
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||
expect(mockFn.statSyncMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("w/ Platform 'win32' and set process.env.APPDATA behaviour", async () => {
|
||||
@@ -227,20 +236,20 @@ describe("data_dir.ts unit tests", async () => {
|
||||
|
||||
// use Generator to precisely control order of fs.existSync return values
|
||||
const existsSyncMockGen = (function* () {
|
||||
// 1) fs.existSync -> case B
|
||||
// 1) fs.existSync -> case B -> checking if folder exists in home dir
|
||||
yield false;
|
||||
// 2) fs.existSync -> case C -> checking if default OS PlatformAppDataDir exists
|
||||
yield true;
|
||||
// 3) fs.existSync -> case C -> checking if Trilium Data folder exists
|
||||
yield false;
|
||||
})();
|
||||
|
||||
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
|
||||
// mkdirSync succeeds (folder doesn't exist)
|
||||
mockFn.mkdirSyncMock.mockImplementation(() => undefined);
|
||||
|
||||
const result = getTriliumDataDir(dataDirName);
|
||||
|
||||
expect(result).toEqual(mockPlatformDataPath);
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(3);
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
|
||||
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -253,19 +262,15 @@ describe("data_dir.ts unit tests", async () => {
|
||||
|
||||
setMockPlatform("aix", homedir, mockPlatformDataPath);
|
||||
|
||||
const existsSyncMockGen = (function* () {
|
||||
// first fs.existSync -> case B -> checking if folder exists in home folder
|
||||
yield false;
|
||||
// second fs.existSync -> case D -> triggered by createDirIfNotExisting
|
||||
yield false;
|
||||
})();
|
||||
|
||||
mockFn.existsSyncMock.mockImplementation(() => existsSyncMockGen.next().value);
|
||||
// fs.existSync -> case B -> checking if folder exists in home folder
|
||||
mockFn.existsSyncMock.mockImplementation(() => false);
|
||||
// mkdirSync succeeds (folder doesn't exist)
|
||||
mockFn.mkdirSyncMock.mockImplementation(() => undefined);
|
||||
|
||||
const result = getTriliumDataDir(dataDirName);
|
||||
|
||||
expect(result).toEqual(mockPlatformDataPath);
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(2);
|
||||
expect(mockFn.existsSyncMock).toHaveBeenCalledTimes(1);
|
||||
expect(mockFn.mkdirSyncMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,9 +75,56 @@ export function getPlatformAppDataDir(platform: ReturnType<typeof os.platform>,
|
||||
}
|
||||
}
|
||||
|
||||
function outputPermissionDiagnostics(targetPath: fs.PathLike) {
|
||||
const pathStr = targetPath.toString();
|
||||
const parentDir = pathJoin(pathStr, "..");
|
||||
|
||||
console.error("\n========== PERMISSION ERROR DIAGNOSTICS ==========");
|
||||
console.error(`Failed to create directory: ${pathStr}`);
|
||||
|
||||
// Output current process UID:GID (Unix only)
|
||||
if (typeof process.getuid === "function" && typeof process.getgid === "function") {
|
||||
console.error(`Process running as UID:GID = ${process.getuid()}:${process.getgid()}`);
|
||||
}
|
||||
|
||||
// Try to get parent directory stats
|
||||
try {
|
||||
const stats = fs.statSync(parentDir);
|
||||
console.error(`Parent directory: ${parentDir}`);
|
||||
console.error(` Owner UID:GID = ${stats.uid}:${stats.gid}`);
|
||||
console.error(` Permissions = ${(stats.mode & 0o777).toString(8)} (octal)`);
|
||||
} catch {
|
||||
console.error(`Parent directory ${parentDir} is not accessible`);
|
||||
}
|
||||
|
||||
console.error("\nTo fix this issue:");
|
||||
console.error(" - Ensure the data directory is owned by the user running Trilium");
|
||||
console.error(" - Or set USER_UID and USER_GID environment variables to match the directory owner");
|
||||
console.error(" - Example: docker run -e USER_UID=$(id -u) -e USER_GID=$(id -g) ...");
|
||||
console.error("====================================================\n");
|
||||
}
|
||||
|
||||
function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOLDER_PERMISSIONS) {
|
||||
if (!fs.existsSync(path)) {
|
||||
try {
|
||||
fs.mkdirSync(path, permissionMode);
|
||||
} catch (err: unknown) {
|
||||
if (err && typeof err === "object" && "code" in err) {
|
||||
const code = (err as { code: string }).code;
|
||||
|
||||
if (code === "EACCES") {
|
||||
outputPermissionDiagnostics(path);
|
||||
} else if (code === "EEXIST") {
|
||||
// Directory already exists - verify it's actually a directory
|
||||
try {
|
||||
if (fs.statSync(path).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// If we can't stat it, fall through to re-throw original error
|
||||
}
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -387,4 +387,58 @@ describe("Markdown export", () => {
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("maintains escaped HTML tags", () => {
|
||||
const html = /*html*/`<p><div>Hello World</div></p>`;
|
||||
const expected = `\\<div\\>Hello World\\</div\\>`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("escapes HTML tags inside list", () => {
|
||||
const html = trimIndentation/*html*/`\
|
||||
<ul>
|
||||
<li data-list-item-id="e07fda078f7dd7103a3b9017f49eb1589">
|
||||
<note> is note.
|
||||
</li>
|
||||
</ul>
|
||||
`;
|
||||
const expected = trimIndentation`\
|
||||
* \\<note\\> is note.`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("exports jQuery code in table properly", () => {
|
||||
const html = trimIndentation`\
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Code
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<pre>
|
||||
<code class="language-text-x-trilium-auto">this.$widget = $("<div>");</code>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
`;
|
||||
const expected = trimIndentation`\
|
||||
<table><thead><tr><th>Code</th></tr></thead><tbody><tr><td><pre><code class="language-text-x-trilium-auto">this.$widget = $("<div>");</code>
|
||||
</pre></td></tr></tbody></table>`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("renders emphasis with underscore", () => {
|
||||
const html = /*html*/`<p>This is <em>underlined</em> text.</p>`;
|
||||
const expected = `This is _underlined_ text.`;
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import TurndownService, { type Rule } from "turndown";
|
||||
import { gfm } from "@triliumnext/turndown-plugin-gfm";
|
||||
import Turnish, { type Rule } from "turnish";
|
||||
|
||||
let instance: TurndownService | null = null;
|
||||
let instance: Turnish | null = null;
|
||||
|
||||
// TODO: Move this to a dedicated file someday.
|
||||
export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
|
||||
@@ -16,12 +14,12 @@ export const ADMONITION_TYPE_MAPPINGS: Record<string, string> = {
|
||||
|
||||
export const DEFAULT_ADMONITION_TYPE = ADMONITION_TYPE_MAPPINGS.note;
|
||||
|
||||
const fencedCodeBlockFilter: TurndownService.Rule = {
|
||||
filter: function (node, options) {
|
||||
const fencedCodeBlockFilter: Rule = {
|
||||
filter (node, options) {
|
||||
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild !== null && node.firstChild.nodeName === "CODE";
|
||||
},
|
||||
|
||||
replacement: function (content, node, options) {
|
||||
replacement (content, node, options) {
|
||||
if (!node.firstChild || !("getAttribute" in node.firstChild) || typeof node.firstChild.getAttribute !== "function") {
|
||||
return content;
|
||||
}
|
||||
@@ -29,23 +27,25 @@ const fencedCodeBlockFilter: TurndownService.Rule = {
|
||||
const className = node.firstChild.getAttribute("class") || "";
|
||||
const language = rewriteLanguageTag((className.match(/language-(\S+)/) || [null, ""])[1]);
|
||||
|
||||
return "\n\n" + options.fence + language + "\n" + node.firstChild.textContent + "\n" + options.fence + "\n\n";
|
||||
return `\n\n${options.fence}${language}\n${node.firstChild.textContent}\n${options.fence}\n\n`;
|
||||
}
|
||||
};
|
||||
|
||||
function toMarkdown(content: string) {
|
||||
if (instance === null) {
|
||||
instance = new TurndownService({
|
||||
instance = new Turnish({
|
||||
headingStyle: "atx",
|
||||
bulletListMarker: "*",
|
||||
emDelimiter: "_",
|
||||
codeBlockStyle: "fenced",
|
||||
blankReplacement(content, node, options) {
|
||||
if (node.nodeName === "SECTION" && (node as HTMLElement).classList.contains("include-note")) {
|
||||
return (node as HTMLElement).outerHTML;
|
||||
blankReplacement(_content, node) {
|
||||
if (node.nodeName === "SECTION" && node.classList.contains("include-note")) {
|
||||
return node.outerHTML;
|
||||
}
|
||||
|
||||
// Original implementation as per https://github.com/mixmark-io/turndown/blob/master/src/turndown.js.
|
||||
return ("isBlock" in node && node.isBlock) ? '\n\n' : ''
|
||||
}
|
||||
return ("isBlock" in node && node.isBlock) ? '\n\n' : '';
|
||||
},
|
||||
});
|
||||
// Filter is heavily based on: https://github.com/mixmark-io/turndown/issues/274#issuecomment-458730974
|
||||
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
|
||||
@@ -59,7 +59,7 @@ function toMarkdown(content: string) {
|
||||
instance.keep([ "kbd", "sup", "sub" ]);
|
||||
}
|
||||
|
||||
return instance.turndown(content);
|
||||
return instance.render(content);
|
||||
}
|
||||
|
||||
function rewriteLanguageTag(source: string) {
|
||||
@@ -85,14 +85,14 @@ function buildImageFilter() {
|
||||
const ESCAPE_PATTERNS = {
|
||||
before: /([\\*`[\]_]|(?:^[-+>])|(?:^~~~)|(?:^#{1-6}))/g,
|
||||
after: /((?:^\d+(?=\.)))/
|
||||
}
|
||||
};
|
||||
|
||||
const escapePattern = new RegExp('(?:' + ESCAPE_PATTERNS.before.source + '|' + ESCAPE_PATTERNS.after.source + ')', 'g');
|
||||
const escapePattern = new RegExp(`(?:${ESCAPE_PATTERNS.before.source}|${ESCAPE_PATTERNS.after.source})`, 'g');
|
||||
|
||||
function escapeMarkdown (content: string) {
|
||||
return content.replace(escapePattern, function (match, before, after) {
|
||||
return before ? '\\' + before : after + '\\'
|
||||
})
|
||||
return content.replace(escapePattern, (match, before, after) => {
|
||||
return before ? `\\${before}` : `${after}\\`;
|
||||
});
|
||||
}
|
||||
|
||||
function escapeLinkDestination(destination: string) {
|
||||
@@ -102,10 +102,10 @@ function buildImageFilter() {
|
||||
}
|
||||
|
||||
function escapeLinkTitle (title: string) {
|
||||
return title.replace(/"/g, '\\"')
|
||||
return title.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
const imageFilter: TurndownService.Rule = {
|
||||
const imageFilter: Rule = {
|
||||
filter: "img",
|
||||
replacement(content, _node) {
|
||||
const node = _node as HTMLElement;
|
||||
@@ -117,12 +117,12 @@ function buildImageFilter() {
|
||||
|
||||
// TODO: Deduplicate with upstream.
|
||||
const untypedNode = (node as any);
|
||||
const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')))
|
||||
const src = escapeLinkDestination(untypedNode.getAttribute('src') || '')
|
||||
const title = cleanAttribute(untypedNode.getAttribute('title'))
|
||||
const titlePart = title ? ' "' + escapeLinkTitle(title) + '"' : ''
|
||||
const alt = escapeMarkdown(cleanAttribute(untypedNode.getAttribute('alt')));
|
||||
const src = escapeLinkDestination(untypedNode.getAttribute('src') || '');
|
||||
const title = cleanAttribute(untypedNode.getAttribute('title'));
|
||||
const titlePart = title ? ` "${escapeLinkTitle(title)}"` : '';
|
||||
|
||||
return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''
|
||||
return src ? `` : '';
|
||||
}
|
||||
};
|
||||
return imageFilter;
|
||||
@@ -151,7 +151,7 @@ function buildAdmonitionFilter() {
|
||||
return DEFAULT_ADMONITION_TYPE;
|
||||
}
|
||||
|
||||
const admonitionFilter: TurndownService.Rule = {
|
||||
const admonitionFilter: Rule = {
|
||||
filter(node, options) {
|
||||
return node.nodeName === "ASIDE" && node.classList.contains("admonition");
|
||||
},
|
||||
@@ -161,11 +161,11 @@ function buildAdmonitionFilter() {
|
||||
|
||||
content = content.replace(/^\n+|\n+$/g, '');
|
||||
content = content.replace(/^/gm, '> ');
|
||||
content = `> [!${admonitionType}]\n` + content;
|
||||
content = `> [!${admonitionType}]\n${content}`;
|
||||
|
||||
return "\n\n" + content + "\n\n";
|
||||
return `\n\n${content}\n\n`;
|
||||
}
|
||||
}
|
||||
};
|
||||
return admonitionFilter;
|
||||
}
|
||||
|
||||
@@ -178,15 +178,15 @@ function buildAdmonitionFilter() {
|
||||
*/
|
||||
function buildInlineLinkFilter(): Rule {
|
||||
return {
|
||||
filter: function (node, options) {
|
||||
filter (node, options) {
|
||||
return (
|
||||
options.linkStyle === 'inlined' &&
|
||||
node.nodeName === 'A' &&
|
||||
!!node.getAttribute('href')
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
replacement: function (content, _node) {
|
||||
replacement (content, _node) {
|
||||
const node = _node as HTMLElement;
|
||||
|
||||
// Return reference links verbatim.
|
||||
@@ -196,13 +196,13 @@ function buildInlineLinkFilter(): Rule {
|
||||
|
||||
// Otherwise treat as normal.
|
||||
// TODO: Call super() somehow instead of duplicating the implementation.
|
||||
let href = node.getAttribute('href')
|
||||
if (href) href = href.replace(/([()])/g, '\\$1')
|
||||
let title = cleanAttribute(node.getAttribute('title'))
|
||||
if (title) title = ' "' + title.replace(/"/g, '\\"') + '"'
|
||||
return '[' + content + '](' + href + title + ')'
|
||||
let href = node.getAttribute('href');
|
||||
if (href) href = href.replace(/([()])/g, '\\$1');
|
||||
let title = cleanAttribute(node.getAttribute('title'));
|
||||
if (title) title = ` "${title.replace(/"/g, '\\"')}"`;
|
||||
return `[${content}](${href}${title})`;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildFigureFilter(): Rule {
|
||||
@@ -214,7 +214,7 @@ function buildFigureFilter(): Rule {
|
||||
replacement(content, node) {
|
||||
return (node as HTMLElement).outerHTML;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Keep in line with https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js.
|
||||
@@ -224,13 +224,13 @@ function buildListItemFilter(): Rule {
|
||||
replacement(content, node, options) {
|
||||
content = content
|
||||
.trim()
|
||||
.replace(/\n/gm, '\n ') // indent
|
||||
let prefix = options.bulletListMarker + ' '
|
||||
.replace(/\n/gm, '\n '); // indent
|
||||
let prefix = `${options.bulletListMarker} `;
|
||||
const parent = node.parentNode as HTMLElement;
|
||||
if (parent.nodeName === 'OL') {
|
||||
var start = parent.getAttribute('start')
|
||||
var index = Array.prototype.indexOf.call(parent.children, node)
|
||||
prefix = (start ? Number(start) + index : index + 1) + '. '
|
||||
const start = parent.getAttribute('start');
|
||||
const index = Array.prototype.indexOf.call(parent.children, node);
|
||||
prefix = `${start ? Number(start) + index : index + 1}. `;
|
||||
} else if (parent.classList.contains("todo-list")) {
|
||||
const isChecked = node.querySelector("input[type=checkbox]:checked");
|
||||
prefix = (isChecked ? "- [x] " : "- [ ] ");
|
||||
@@ -239,7 +239,7 @@ function buildListItemFilter(): Rule {
|
||||
const result = prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildMathFilter(): Rule {
|
||||
@@ -270,13 +270,13 @@ function buildMathFilter(): Rule {
|
||||
// Unknown.
|
||||
return content;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Taken from upstream since it's not exposed.
|
||||
// https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js
|
||||
function cleanAttribute(attribute: string | null | undefined) {
|
||||
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : ''
|
||||
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -314,4 +314,9 @@ $$`;
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("preserves HTML entities in list", () => {
|
||||
const input = `* <note> is note.`;
|
||||
const expected = /*html*/`<ul><li><note> is note.</li></ul>`;
|
||||
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
"use strict";
|
||||
import type { NoteType } from "@triliumnext/commons";
|
||||
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
|
||||
import noteService from "../../services/notes.js";
|
||||
import imageService from "../../services/image.js";
|
||||
import noteService from "../../services/notes.js";
|
||||
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import type { File } from "./common.js";
|
||||
import markdownService from "./markdown.js";
|
||||
import mimeService from "./mime.js";
|
||||
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
|
||||
import importUtils from "./utils.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import type { File } from "./common.js";
|
||||
import type { NoteType } from "@triliumnext/commons";
|
||||
|
||||
function importSingleFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
|
||||
const mime = mimeService.getMime(file.originalname) || file.mimetype;
|
||||
@@ -56,13 +54,14 @@ function importImage(file: File, parentNote: BNote, taskContext: TaskContext<"im
|
||||
function importFile(taskContext: TaskContext<"importNotes">, file: File, parentNote: BNote) {
|
||||
const originalName = file.originalname;
|
||||
|
||||
const mime = mimeService.getMime(originalName) || file.mimetype;
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: parentNote.noteId,
|
||||
title: originalName,
|
||||
title: getNoteTitle(originalName, mime === "application/pdf"),
|
||||
content: file.buffer,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
|
||||
type: "file",
|
||||
mime: mimeService.getMime(originalName) || file.mimetype
|
||||
mime
|
||||
});
|
||||
|
||||
note.addLabel("originalFileName", originalName);
|
||||
@@ -88,7 +87,7 @@ function importCodeNote(taskContext: TaskContext<"importNotes">, file: File, par
|
||||
title,
|
||||
content,
|
||||
type,
|
||||
mime: mime,
|
||||
mime,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
@@ -106,7 +105,7 @@ function importCustomType(taskContext: TaskContext<"importNotes">, file: File, p
|
||||
title,
|
||||
content,
|
||||
type,
|
||||
mime: mime,
|
||||
mime,
|
||||
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||
});
|
||||
|
||||
@@ -214,7 +213,7 @@ function importAttachment(taskContext: TaskContext<"importNotes">, file: File, p
|
||||
title: file.originalname,
|
||||
content: file.buffer,
|
||||
role: "file",
|
||||
mime: mime
|
||||
mime
|
||||
});
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
"use strict";
|
||||
|
||||
import BAttribute from "../../becca/entities/battribute.js";
|
||||
import { removeTextFileExtension, newEntityId, getNoteTitle, processStringOrBuffer, unescapeHtml } from "../../services/utils.js";
|
||||
import log from "../../services/log.js";
|
||||
import noteService from "../../services/notes.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import BBranch from "../../becca/entities/bbranch.js";
|
||||
|
||||
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
|
||||
import path from "path";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import mimeService from "./mime.js";
|
||||
import treeService from "../tree.js";
|
||||
import type { Stream } from "stream";
|
||||
import yauzl from "yauzl";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import BAttachment from "../../becca/entities/battachment.js";
|
||||
import markdownService from "./markdown.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import BAttribute from "../../becca/entities/battribute.js";
|
||||
import BBranch from "../../becca/entities/bbranch.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import type NoteMeta from "../meta/note_meta.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import log from "../../services/log.js";
|
||||
import noteService from "../../services/notes.js";
|
||||
import { getNoteTitle, newEntityId, processStringOrBuffer, removeFileExtension, unescapeHtml } from "../../services/utils.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import type AttributeMeta from "../meta/attribute_meta.js";
|
||||
import type { Stream } from "stream";
|
||||
import { ALLOWED_NOTE_TYPES, type NoteType } from "@triliumnext/commons";
|
||||
import type NoteMeta from "../meta/note_meta.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import type TaskContext from "../task_context.js";
|
||||
import treeService from "../tree.js";
|
||||
import markdownService from "./markdown.js";
|
||||
import mimeService from "./mime.js";
|
||||
|
||||
interface MetaFile {
|
||||
files: NoteMeta[];
|
||||
@@ -108,7 +109,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
dataFileName: ""
|
||||
};
|
||||
|
||||
let parent: NoteMeta | undefined = undefined;
|
||||
let parent: NoteMeta | undefined;
|
||||
|
||||
for (let segment of pathSegments) {
|
||||
if (!cursor?.children?.length) {
|
||||
@@ -161,7 +162,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
|
||||
// in case we lack metadata, we treat e.g. "Programming.html" and "Programming" as the same note
|
||||
// (one data file, the other directory for children)
|
||||
const filePathNoExt = removeTextFileExtension(filePath);
|
||||
const filePathNoExt = removeFileExtension(filePath);
|
||||
|
||||
if (filePathNoExt in createdPaths) {
|
||||
return createdPaths[filePathNoExt];
|
||||
@@ -241,10 +242,10 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
}
|
||||
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: parentNoteId,
|
||||
parentNoteId,
|
||||
title: noteTitle || "",
|
||||
content: "",
|
||||
noteId: noteId,
|
||||
noteId,
|
||||
type: resolveNoteType(noteMeta?.type),
|
||||
mime: noteMeta ? noteMeta.mime : "text/html",
|
||||
prefix: noteMeta?.prefix || "",
|
||||
@@ -294,12 +295,12 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
|
||||
noteId: getNewNoteId(noteMeta.noteId)
|
||||
};
|
||||
} else {
|
||||
// don't check for noteMeta since it's not mandatory for notes
|
||||
return {
|
||||
noteId: getNoteId(noteMeta, absUrl)
|
||||
};
|
||||
}
|
||||
}
|
||||
// don't check for noteMeta since it's not mandatory for notes
|
||||
return {
|
||||
noteId: getNoteId(noteMeta, absUrl)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function processTextNoteContent(content: string, noteTitle: string, filePath: string, noteMeta?: NoteMeta) {
|
||||
@@ -312,9 +313,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
|
||||
if (noteTitle.trim() === text.trim()) {
|
||||
return ""; // remove whole H1 tag
|
||||
} else {
|
||||
return `<h2>${text}</h2>`;
|
||||
}
|
||||
}
|
||||
return `<h2>${text}</h2>`;
|
||||
|
||||
});
|
||||
|
||||
if (taskContext.data?.safeImport) {
|
||||
@@ -347,9 +348,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
return `src="api/attachments/${target.attachmentId}/image/${path.basename(url)}"`;
|
||||
} else if (target.noteId) {
|
||||
return `src="api/images/${target.noteId}/${path.basename(url)}"`;
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
|
||||
});
|
||||
|
||||
content = content.replace(/href="([^"]*)"/g, (match, url) => {
|
||||
@@ -373,9 +374,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
return `href="#root/${target.noteId}?viewMode=attachments&attachmentId=${target.attachmentId}"`;
|
||||
} else if (target.noteId) {
|
||||
return `href="#root/${target.noteId}"`;
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
|
||||
});
|
||||
|
||||
if (noteMeta) {
|
||||
@@ -525,9 +526,9 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
}
|
||||
|
||||
({ note } = noteService.createNewNote({
|
||||
parentNoteId: parentNoteId,
|
||||
parentNoteId,
|
||||
title: noteTitle || "",
|
||||
content: content,
|
||||
content,
|
||||
noteId,
|
||||
type,
|
||||
mime,
|
||||
@@ -536,7 +537,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
// root notePosition should be ignored since it relates to the original document
|
||||
// now import root should be placed after existing notes into new parent
|
||||
notePosition: noteMeta && firstNote ? noteMeta.notePosition : undefined,
|
||||
isProtected: isProtected
|
||||
isProtected
|
||||
}));
|
||||
|
||||
createdNoteIds.add(note.noteId);
|
||||
@@ -648,7 +649,7 @@ function streamToBuffer(stream: Stream): Promise<Buffer> {
|
||||
|
||||
export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer> {
|
||||
return new Promise((res, rej) => {
|
||||
zipfile.openReadStream(entry, function (err, readStream) {
|
||||
zipfile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) rej(err);
|
||||
if (!readStream) throw new Error("Unable to read content.");
|
||||
|
||||
@@ -659,7 +660,7 @@ export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise
|
||||
|
||||
export function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => Promise<void>) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, function (err, zipfile) {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, (err, zipfile) => {
|
||||
if (err) rej(err);
|
||||
if (!zipfile) throw new Error("Unable to read zip file.");
|
||||
|
||||
@@ -691,9 +692,9 @@ function resolveNoteType(type: string | undefined): NoteType {
|
||||
|
||||
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {
|
||||
return type as NoteType;
|
||||
} else {
|
||||
return "text";
|
||||
}
|
||||
}
|
||||
return "text";
|
||||
|
||||
}
|
||||
|
||||
export function removeTriliumTags(content: string) {
|
||||
@@ -702,7 +703,7 @@ export function removeTriliumTags(content: string) {
|
||||
"<title data-trilium-title>([^<]*)<\/title>"
|
||||
];
|
||||
for (const tag of tagsToRemove) {
|
||||
let re = new RegExp(tag, "gi");
|
||||
const re = new RegExp(tag, "gi");
|
||||
content = content.replace(re, "");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { note, NoteBuilder } from "../test/becca_mocking.js";
|
||||
import {beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import {note, NoteBuilder} from "../test/becca_mocking.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import BBranch from "../becca/entities/bbranch.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
import tree from "./tree.js";
|
||||
import cls from "./cls.js";
|
||||
import { buildNote } from "../test/becca_easy_mocking.js";
|
||||
import {buildNote} from "../test/becca_easy_mocking.js";
|
||||
|
||||
describe("Tree", () => {
|
||||
let rootNote!: NoteBuilder;
|
||||
@@ -48,6 +48,23 @@ describe("Tree", () => {
|
||||
};
|
||||
});
|
||||
});
|
||||
it("sorts notes by title (base case)", () => {
|
||||
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{title: "1"},
|
||||
{title: "2"},
|
||||
{title: "3"},
|
||||
],
|
||||
"#sorted": "",
|
||||
});
|
||||
cls.init(() => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual(["1", "2", "3"]);
|
||||
}
|
||||
)
|
||||
|
||||
it("custom sort order is idempotent", () => {
|
||||
rootNote.label("sorted", "order");
|
||||
@@ -56,13 +73,15 @@ describe("Tree", () => {
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
rootNote.child(note(String(i)).label("order", String(i)));
|
||||
}
|
||||
rootNote.child(note("top").label("top"));
|
||||
rootNote.child(note("bottom").label("bottom"));
|
||||
|
||||
// Add a few values which have no defined order.
|
||||
for (let i = 6; i < 10; i++) {
|
||||
rootNote.child(note(String(i)));
|
||||
}
|
||||
|
||||
const expectedOrder = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ];
|
||||
const expectedOrder = ["top", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "bottom"];
|
||||
|
||||
// Sort a few times to ensure that the resulting order is the same.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
@@ -78,12 +97,12 @@ describe("Tree", () => {
|
||||
it("pins to the top and bottom", () => {
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{ title: "bottom", "#bottom": "" },
|
||||
{ title: "5" },
|
||||
{ title: "3" },
|
||||
{ title: "2" },
|
||||
{ title: "1" },
|
||||
{ title: "top", "#top": "" }
|
||||
{title: "bottom", "#bottom": ""},
|
||||
{title: "5"},
|
||||
{title: "3"},
|
||||
{title: "2"},
|
||||
{title: "1"},
|
||||
{title: "top", "#top": ""}
|
||||
],
|
||||
"#sorted": ""
|
||||
});
|
||||
@@ -91,18 +110,18 @@ describe("Tree", () => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual([ "top", "1", "2", "3", "5", "bottom" ]);
|
||||
expect(orderedTitles).toStrictEqual(["top", "1", "2", "3", "5", "bottom"]);
|
||||
});
|
||||
|
||||
it("pins to the top and bottom in reverse order", () => {
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{ title: "bottom", "#bottom": "" },
|
||||
{ title: "1" },
|
||||
{ title: "2" },
|
||||
{ title: "3" },
|
||||
{ title: "5" },
|
||||
{ title: "top", "#top": "" }
|
||||
{title: "bottom", "#bottom": ""},
|
||||
{title: "1"},
|
||||
{title: "2"},
|
||||
{title: "3"},
|
||||
{title: "5"},
|
||||
{title: "top", "#top": ""}
|
||||
],
|
||||
"#sorted": "",
|
||||
"#sortDirection": "desc"
|
||||
@@ -111,6 +130,50 @@ describe("Tree", () => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual([ "top", "5", "3", "2", "1", "bottom" ]);
|
||||
expect(orderedTitles).toStrictEqual(["top", "5", "3", "2", "1", "bottom"]);
|
||||
});
|
||||
|
||||
it("keeps folder notes on top when #sortFolderFirst is set, but not above #top", () => {
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{title: "bottom", "#bottom": ""},
|
||||
{title: "1"},
|
||||
{title: "2"},
|
||||
{title: "p1", children: [{title: "1.1"}, {title: "1.2"}]},
|
||||
{title: "p2", children: [{title: "2.1"}, {title: "2.2"}]},
|
||||
{title: "3"},
|
||||
{title: "5"},
|
||||
{title: "top", "#top": ""}
|
||||
],
|
||||
"#sorted": "",
|
||||
"#sortFoldersFirst": ""
|
||||
});
|
||||
cls.init(() => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual(["top", "p1", "p2", "1", "2", "3", "5", "bottom"]);
|
||||
});
|
||||
|
||||
it("sorts notes accordingly when #sortNatural is set", () => {
|
||||
const note = buildNote({
|
||||
children: [
|
||||
{title: "bottom", "#bottom": ""},
|
||||
{title: "1"},
|
||||
{title: "2"},
|
||||
{title: "10"},
|
||||
{title: "20"},
|
||||
{title: "3"},
|
||||
{title: "top", "#top": ""}
|
||||
],
|
||||
"#sorted": "",
|
||||
"#sortNatural": ""
|
||||
});
|
||||
cls.init(() => {
|
||||
tree.sortNotesIfNeeded(note.noteId);
|
||||
});
|
||||
const orderedTitles = note.children.map((child) => child.title);
|
||||
expect(orderedTitles).toStrictEqual(["top", "1", "2", "3", "10", "20", "bottom"]);
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
@@ -98,15 +98,6 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
|
||||
}
|
||||
|
||||
notes.sort((a, b) => {
|
||||
if (foldersFirst) {
|
||||
const aHasChildren = a.hasChildren();
|
||||
const bHasChildren = b.hasChildren();
|
||||
|
||||
if ((aHasChildren && !bHasChildren) || (!aHasChildren && bHasChildren)) {
|
||||
// exactly one note of the two is a directory, so the sorting will be done based on this status
|
||||
return aHasChildren ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchValue(note: BNote, key: string) {
|
||||
let rawValue: string | null;
|
||||
@@ -154,6 +145,16 @@ function sortNotes(parentNoteId: string, customSortBy: string = "title", reverse
|
||||
return compare(bottomBEl, bottomAEl) * (reverse ? -1 : 1);
|
||||
}
|
||||
|
||||
if (foldersFirst) {
|
||||
const aHasChildren = a.hasChildren();
|
||||
const bHasChildren = b.hasChildren();
|
||||
|
||||
if ((aHasChildren && !bHasChildren) || (!aHasChildren && bHasChildren)) {
|
||||
// exactly one note of the two is a directory, so the sorting will be done based on this status
|
||||
return aHasChildren ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
const customAEl = fetchValue(a, customSortBy) ?? fetchValue(a, "title") as string;
|
||||
const customBEl = fetchValue(b, customSortBy) ?? fetchValue(b, "title") as string;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, expect,it } from "vitest";
|
||||
|
||||
import utils from "./utils.js";
|
||||
|
||||
type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
|
||||
@@ -120,7 +121,7 @@ describe("#toObject", () => {
|
||||
{ testPropA: "keyA", testPropB: "valueA" },
|
||||
{ testPropA: "keyB", testPropB: "valueB" }
|
||||
];
|
||||
const fn: TestListFn = (testListEntry: TestListEntry) => [ testListEntry.testPropA + "_fn", testListEntry.testPropB + "_fn" ];
|
||||
const fn: TestListFn = (testListEntry: TestListEntry) => [ `${testListEntry.testPropA }_fn`, `${testListEntry.testPropB }_fn` ];
|
||||
|
||||
const result = utils.toObject(testList, fn);
|
||||
expect(result).toStrictEqual({
|
||||
@@ -240,8 +241,8 @@ describe.todo("#quoteRegex", () => {});
|
||||
|
||||
describe.todo("#replaceAll", () => {});
|
||||
|
||||
describe("#removeTextFileExtension", () => {
|
||||
const testCases: TestCase<typeof utils.removeTextFileExtension>[] = [
|
||||
describe("#removeFileExtension", () => {
|
||||
const testCases: TestCase<typeof utils.removeFileExtension>[] = [
|
||||
[ "w/ 'test.md' it should strip '.md'", [ "test.md" ], "test" ],
|
||||
[ "w/ 'test.markdown' it should strip '.markdown'", [ "test.markdown" ], "test" ],
|
||||
[ "w/ 'test.html' it should strip '.html'", [ "test.html" ], "test" ],
|
||||
@@ -252,7 +253,7 @@ describe("#removeTextFileExtension", () => {
|
||||
testCases.forEach((testCase) => {
|
||||
const [ desc, fnParams, expected ] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.removeTextFileExtension(...fnParams);
|
||||
const result = utils.removeFileExtension(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import chardet from "chardet";
|
||||
import stripBom from "strip-bom";
|
||||
import crypto from "crypto";
|
||||
import { generator } from "rand-token";
|
||||
import unescape from "unescape";
|
||||
import escape from "escape-html";
|
||||
import sanitize from "sanitize-filename";
|
||||
import mimeTypes from "mime-types";
|
||||
import path from "path";
|
||||
import type NoteMeta from "./meta/note_meta.js";
|
||||
import log from "./log.js";
|
||||
import { t } from "i18next";
|
||||
import mimeTypes from "mime-types";
|
||||
import { release as osRelease } from "os";
|
||||
import path from "path";
|
||||
import { generator } from "rand-token";
|
||||
import sanitize from "sanitize-filename";
|
||||
import stripBom from "strip-bom";
|
||||
import unescape from "unescape";
|
||||
|
||||
import log from "./log.js";
|
||||
import type NoteMeta from "./meta/note_meta.js";
|
||||
|
||||
const osVersion = osRelease().split('.').map(Number);
|
||||
|
||||
@@ -204,7 +205,7 @@ export function formatDownloadTitle(fileName: string, type: string | null, mime:
|
||||
return `${fileNameBase}${getExtension()}`;
|
||||
}
|
||||
|
||||
export function removeTextFileExtension(filePath: string) {
|
||||
export function removeFileExtension(filePath: string) {
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
|
||||
switch (extension) {
|
||||
@@ -216,6 +217,7 @@ export function removeTextFileExtension(filePath: string) {
|
||||
case ".excalidraw":
|
||||
case ".mermaid":
|
||||
case ".mmd":
|
||||
case ".pdf":
|
||||
return filePath.substring(0, filePath.length - extension.length);
|
||||
default:
|
||||
return filePath;
|
||||
@@ -226,7 +228,7 @@ export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boo
|
||||
const trimmedNoteMeta = noteMeta?.title?.trim();
|
||||
if (trimmedNoteMeta) return trimmedNoteMeta;
|
||||
|
||||
const basename = path.basename(removeTextFileExtension(filePath));
|
||||
const basename = path.basename(removeFileExtension(filePath));
|
||||
return replaceUnderscoresWithSpaces ? basename.replace(/_/g, " ").trim() : basename;
|
||||
}
|
||||
|
||||
@@ -467,28 +469,28 @@ export function normalizeCustomHandlerPattern(pattern: string | null | undefined
|
||||
|
||||
// If already ends with slash, create both versions
|
||||
if (basePattern.endsWith('/')) {
|
||||
const withoutSlash = basePattern.slice(0, -1) + '$';
|
||||
const withoutSlash = `${basePattern.slice(0, -1) }$`;
|
||||
const withSlash = pattern;
|
||||
return [withoutSlash, withSlash];
|
||||
} else {
|
||||
// Add optional trailing slash
|
||||
const withSlash = basePattern + '/?$';
|
||||
return [withSlash];
|
||||
}
|
||||
// Add optional trailing slash
|
||||
const withSlash = `${basePattern }/?$`;
|
||||
return [withSlash];
|
||||
|
||||
}
|
||||
|
||||
// For patterns without $, add both versions
|
||||
if (pattern.endsWith('/')) {
|
||||
const withoutSlash = pattern.slice(0, -1);
|
||||
return [withoutSlash, pattern];
|
||||
} else {
|
||||
const withSlash = pattern + '/';
|
||||
return [pattern, withSlash];
|
||||
}
|
||||
const withSlash = `${pattern }/`;
|
||||
return [pattern, withSlash];
|
||||
|
||||
}
|
||||
|
||||
export function formatUtcTime(time: string) {
|
||||
return time.replace("T", " ").substring(0, 19)
|
||||
return time.replace("T", " ").substring(0, 19);
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with client utils
|
||||
@@ -501,9 +503,9 @@ export function formatSize(size: number | null | undefined) {
|
||||
|
||||
if (size < 1024) {
|
||||
return `${size} KiB`;
|
||||
} else {
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
}
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
|
||||
}
|
||||
|
||||
function slugify(text: string) {
|
||||
@@ -544,7 +546,7 @@ export default {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
removeDiacritic,
|
||||
removeTextFileExtension,
|
||||
removeFileExtension,
|
||||
replaceAll,
|
||||
safeExtractMessageAndStackFromError,
|
||||
sanitizeSqlIdentifier,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getNoteIcon, NoteType } from "@triliumnext/commons";
|
||||
import escape from "escape-html";
|
||||
|
||||
import { NOTE_TYPE_ICONS } from "../../../becca/entities/bnote.js";
|
||||
import type { Blob } from "../../../services/blob-interface.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import sql from "../../sql.js";
|
||||
@@ -19,7 +19,7 @@ const isCredentials = (attr: SAttribute) => attr.type === "label" && attr.name =
|
||||
class SNote extends AbstractShacaEntity {
|
||||
noteId: string;
|
||||
title: string;
|
||||
type: string;
|
||||
type: NoteType;
|
||||
mime: string;
|
||||
private blobId: string;
|
||||
utcDateModified: string;
|
||||
@@ -38,7 +38,7 @@ class SNote extends AbstractShacaEntity {
|
||||
|
||||
this.noteId = noteId;
|
||||
this.title = isProtected ? "[protected]" : title;
|
||||
this.type = type;
|
||||
this.type = type as NoteType;
|
||||
this.mime = mime;
|
||||
this.blobId = blobId;
|
||||
this.utcDateModified = utcDateModified; // used for caching of images
|
||||
@@ -528,33 +528,22 @@ class SNote extends AbstractShacaEntity {
|
||||
}
|
||||
|
||||
getIcon(filterByPrefix: string[] = []) {
|
||||
return `tn-icon ${this.#getIconInternal(filterByPrefix)}`;
|
||||
}
|
||||
|
||||
#getIconInternal(filterByPrefix: string[] = []) {
|
||||
const iconClassLabels = this.getLabels("iconClass").filter(label => {
|
||||
if (filterByPrefix.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return filterByPrefix.some(prefix => label.value.startsWith(prefix));
|
||||
});
|
||||
const icon = getNoteIcon({
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
workspaceIconClass: undefined,
|
||||
iconClass: iconClassLabels.length > 0 ? iconClassLabels[0].value : undefined,
|
||||
isFolder: this.isFolder.bind(this)
|
||||
});
|
||||
|
||||
if (iconClassLabels && iconClassLabels.length > 0) {
|
||||
return iconClassLabels[0].value;
|
||||
} else if (this.noteId === "root") {
|
||||
return "bx bx-home-alt-2";
|
||||
}
|
||||
if (this.noteId === "_share") {
|
||||
return "bx bx-share-alt";
|
||||
} else if (this.type === "text") {
|
||||
if (this.isFolder()) {
|
||||
return "bx bx-folder";
|
||||
}
|
||||
return "bx bx-note";
|
||||
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
|
||||
return "bx bx-data";
|
||||
}
|
||||
return NOTE_TYPE_ICONS[this.type];
|
||||
return `tn-icon ${icon}`;
|
||||
}
|
||||
|
||||
isFolder() {
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
"preview": "pnpm build && vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "25.7.3",
|
||||
"i18next": "25.7.4",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"preact": "10.28.2",
|
||||
"preact-iso": "2.11.1",
|
||||
"preact-render-to-string": "6.6.5",
|
||||
"react-i18next": "16.5.1"
|
||||
"react-i18next": "16.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
@@ -23,7 +23,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"user-agent-data-types": "0.4.2",
|
||||
"vite": "7.3.1",
|
||||
"vitest": "4.0.16"
|
||||
"vitest": "4.0.17"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "preact"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"note_structure_description": "Notizen lassen sich hierarchisch anordnen. Ordner sind nicht nötig, da jede Notiz Unternotizen enthalten kann. Eine einzelne Notiz kann an mehreren Stellen in der Hierarchie hinzugefügt werden.",
|
||||
"hoisting_description": "Trennen Sie Ihre persönlichen und beruflichen Notizen ganz einfach, indem Sie sie in einem Arbeitsbereich gruppieren. Dadurch wird Ihre Notizstruktur so fokussiert, dass nur ein bestimmter Satz von Notizen angezeigt wird.",
|
||||
"hoisting_title": "Arbeitsbereiche und Fokusansicht",
|
||||
"attributes_description": "Verwenden Sie Beziehungen zwischen Notizen oder fügen Sie Beschriftungen hinzu, um die Kategorisierung zu vereinfachen. Verwenden Sie hervorgehobene Attribute, um strukturierte Informationen einzugeben, die in Tabellen und Boards verwendet werden können."
|
||||
"attributes_description": "Für leichtes kategorsieren, nutze Verbindungen zwischen Notizen oder füge Label hinzu. Verwende hervorgehobene Attribute, um sie als strukturierte Informationen in Tabellen oder Anschlagbretter zu verwenden."
|
||||
},
|
||||
"productivity_benefits": {
|
||||
"revisions_title": "Notizrevisionen",
|
||||
@@ -30,7 +30,7 @@
|
||||
"protected_notes_title": "Geschützte Notizen",
|
||||
"jump_to_title": "Schnellsuche und Kommandos",
|
||||
"search_title": "Leistungsstarke Suche",
|
||||
"web_clipper_title": "Web clipper",
|
||||
"web_clipper_title": "Webschnipsel",
|
||||
"revisions_content": "Notizen werden regelmäßig im Hintergrund gespeichert und Revisionen können zur Überprüfung oder zum Rückgängigmachen versehentlicher Änderungen verwendet werden. Revisionen können auch bei Bedarf erstellt werden.",
|
||||
"sync_content": "Verwenden Sie eine selbst gehostete oder Cloud-Instanz, um Ihre Notizen ganz einfach auf mehreren Geräten zu synchronisieren und über eine WebApp von Ihrem mobilen Gerät aus darauf zuzugreifen.",
|
||||
"protected_notes_content": "Halten Sie vertrauliche Informationen sicher, indem Sie Notizen verschlüsseln und mit einem Passwort schützen.",
|
||||
@@ -41,7 +41,7 @@
|
||||
"note_types": {
|
||||
"text_title": "Text Notizen",
|
||||
"code_title": "Code Notizen",
|
||||
"canvas_title": "Canvas",
|
||||
"canvas_title": "Leinwand",
|
||||
"mermaid_title": "Mermaid Diagramm",
|
||||
"mindmap_title": "Mind Map",
|
||||
"text_description": "Die Notizen werden mit einem visuellen Editor (WYSIWYG) bearbeitet, der Tabellen, Bilder, mathematische Ausdrücke und Code-Blöcke mit Syntaxhervorhebung unterstützt. Formatieren Sie den Text schnell mit einer Markdown-ähnlichen Syntax oder mit Slash-Befehlen.",
|
||||
@@ -162,7 +162,7 @@
|
||||
},
|
||||
"social_buttons": {
|
||||
"github": "GitHub",
|
||||
"github_discussions": "GitHub Discussions",
|
||||
"github_discussions": "GitHub Diskussionen",
|
||||
"matrix": "Matrix",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
@@ -184,7 +184,7 @@
|
||||
"way_market": "Weitersagen: Teilen Sie Trilium Notes mit Freunden, in Blogs und sozialen Medien."
|
||||
},
|
||||
"404": {
|
||||
"title": "404: Not Found",
|
||||
"title": "404: Nicht gefunden",
|
||||
"description": "Die gesuchte Seite konnte nicht gefunden werden. Möglicherweise wurde sie gelöscht oder die URL ist falsch."
|
||||
},
|
||||
"download_helper_desktop_windows": {
|
||||
@@ -193,7 +193,7 @@
|
||||
"description_x64": "Kompatibel mit Intel- oder AMD-Geräten unter Windows 10 und 11.",
|
||||
"description_arm64": "Kompatibel mit ARM-Geräten (z. B. mit Qualcomm Snapdragon).",
|
||||
"quick_start": "Installation über Winget:",
|
||||
"download_exe": "Download Installer (.exe)",
|
||||
"download_exe": "Installationsdatei herunterladen (.exe)",
|
||||
"download_zip": "Portable (.zip)",
|
||||
"download_scoop": "Scoop"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"mindmap_title": "माइंडमैप"
|
||||
},
|
||||
"extensibility_benefits": {
|
||||
"share_title": "वेब पर नोट्स शेयर करें"
|
||||
"share_title": "वेब पर नोट्स शेयर करें",
|
||||
"share_description": "अगर आपके पास सर्वर है, तो इसका उपयोग अपने नोट्स के एक हिस्से को अन्य लोगों के साथ शेयर करने के लिए किया जा सकता है।"
|
||||
},
|
||||
"collections": {
|
||||
"calendar_title": "कैलेंडर",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"productivity_benefits": {
|
||||
"sync_title": "Synkronisering",
|
||||
"search_title": "Kraftig søk",
|
||||
"web_clipper_title": "Web clipper",
|
||||
"web_clipper_title": "Webklipper",
|
||||
"revisions_title": "Notatrevisjon",
|
||||
"protected_notes_title": "Beskyttede notater",
|
||||
"title": "Produktivitet og sikkerhet",
|
||||
@@ -87,7 +87,7 @@
|
||||
"github": "GitHub",
|
||||
"matrix": "Matrix",
|
||||
"reddit": "Reddit",
|
||||
"github_discussions": "GitHub Discussions"
|
||||
"github_discussions": "GitHub-diskusjoner"
|
||||
},
|
||||
"support_us": {
|
||||
"paypal": "PayPal",
|
||||
|
||||
@@ -13,5 +13,12 @@
|
||||
"github": "GitHub",
|
||||
"dockerhub": "Docker Hub",
|
||||
"screenshot_alt": "Trilium Notes masaüstü uygulamasının ekran görüntüsü"
|
||||
},
|
||||
"organization_benefits": {
|
||||
"title": "Organizasyon",
|
||||
"note_structure_title": "Not yapısı",
|
||||
"note_structure_description": "Notlar hiyerarşik olarak düzenlenebilir. Her not 'alt notlar' içerebildiği için klasörlere ihtiyaç duyulmaz. Tek bir not, hiyerarşinin birden fazla noktasına eklenebilir.",
|
||||
"attributes_description": "Notlar arasında ilişkiler kurun veya kolay kategorizasyon için etiketler ekleyin. Tablolarda ve panolarda kullanılabilen yapılandırılmış bilgileri eklemek için öne çıkan öznitelikleri kullanın.",
|
||||
"hoisting_description": "Kişisel ve iş notlarınızı bir çalışma alanı altında gruplandırarak kolayca ayırın; bu sayede not ağacınız yalnızca belirli bir not kümesini gösterecek şekilde odaklanacaktır."
|
||||
}
|
||||
}
|
||||
|
||||
2
docs/Developer Guide/!!!meta.json
vendored
2
docs/Developer Guide/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.101.1",
|
||||
"appVersion": "0.101.3",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Documentation
|
||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/ywZE36YO8YGQ/Documentation_image.png" width="205" height="162">
|
||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/TzNwfb67k5Dh/Documentation_image.png" width="205" height="162">
|
||||
|
||||
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
|
||||
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.
|
||||
|
||||
4
docs/README-de.md
vendored
4
docs/README-de.md
vendored
@@ -132,8 +132,8 @@ Unsere Dokumentation ist verfügbar in mehreren Formaten:
|
||||
einfachen Speichern von Webinhalten
|
||||
* Anpassbare Benutzeroberfläche (Seitenleisten-Schaltflächen, benutzerdefinierte
|
||||
Widgets, ...)
|
||||
* [Metriken](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics)
|
||||
zusammen mit einem Grafana-Dashboard.
|
||||
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics),
|
||||
zusätzlich mit dem Grafana Dashboard.
|
||||
|
||||
✨ Weitere Informationen zu TriliumNext findet man in den folgenden
|
||||
Ressourcen/Communities von Drittanbietern:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user