mirror of
https://github.com/zadam/trilium.git
synced 2025-12-06 15:34:26 +01:00
Compare commits
19 Commits
b36b3523e0
...
8a3cbb85a6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3cbb85a6 | ||
|
|
2fb78275f7 | ||
|
|
98f421c697 | ||
|
|
a5572b7d45 | ||
|
|
fdecbaaa6a | ||
|
|
c6afd7fa24 | ||
|
|
5cad522a60 | ||
|
|
82f64677cb | ||
|
|
3ee086a063 | ||
|
|
13da444a69 | ||
|
|
b51ceaaadc | ||
|
|
2024c72209 | ||
|
|
b5959c55e1 | ||
|
|
16f0ac97f4 | ||
|
|
073c02ee0c | ||
|
|
786f0db4bb | ||
|
|
6958e4b74f | ||
|
|
1f21c65a99 | ||
|
|
5d5fd2079a |
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,6 +8,7 @@ out-tsc
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
@ -18,6 +19,7 @@ node_modules
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
.devcontainer
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
|
||||
19
README.md
19
README.md
@ -146,6 +146,21 @@ Here's the language coverage we have so far:
|
||||
|
||||
### Code
|
||||
|
||||
General (OS / docker / podman, etc.) dependencies:
|
||||
|
||||
Debian
|
||||
```
|
||||
apt update
|
||||
apt install -y build-essential python3 make g++ libsqlite3-dev
|
||||
corepack enable
|
||||
```
|
||||
|
||||
Alpine
|
||||
```
|
||||
apk add --no-cache build-base python3 python3-dev sqlite-dev
|
||||
corepack enable
|
||||
```
|
||||
|
||||
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
@ -154,6 +169,10 @@ pnpm install
|
||||
pnpm run server:start
|
||||
```
|
||||
|
||||
> If you faced with some problems, try to delete all `node_modules` and `.pnpm-store` folders, not only from the root, from every directory, like `apps/{app_name}/node_modules`and `/packages/{package_name}/node_modules` and then reinstall it by the `pnpm install`.
|
||||
|
||||
Share styles not compiling by default, if you see share page without styles, make `pnpm run server:build` and then run development server.
|
||||
|
||||
### Documentation
|
||||
|
||||
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
||||
|
||||
@ -58,6 +58,7 @@ function initOnElectron() {
|
||||
|
||||
initDarkOrLightMode(style);
|
||||
initTransparencyEffects(style, currentWindow);
|
||||
initFullScreenDetection(currentWindow);
|
||||
|
||||
if (options.get("nativeTitleBarVisible") !== "true") {
|
||||
initTitleBarButtons(style, currentWindow);
|
||||
@ -87,6 +88,11 @@ function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron
|
||||
}
|
||||
}
|
||||
|
||||
function initFullScreenDetection(currentWindow: Electron.BrowserWindow) {
|
||||
currentWindow.on("enter-full-screen", () => document.body.classList.add("full-screen"));
|
||||
currentWindow.on("leave-full-screen", () => document.body.classList.remove("full-screen"));
|
||||
}
|
||||
|
||||
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
|
||||
if (window.glob.platform === "win32") {
|
||||
const material = style.getPropertyValue("--background-material");
|
||||
|
||||
@ -75,10 +75,7 @@ class ContextMenu {
|
||||
if (this.isMobile) {
|
||||
this.$cover.on("click", () => this.hide());
|
||||
} else {
|
||||
$(document).on("click", (e) => {
|
||||
console.log("Hide due to clickus")
|
||||
this.hide()
|
||||
});
|
||||
$(document).on("click", (e) => this.hide());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2009,7 +2009,7 @@ body.electron.platform-darwin:not(.native-titlebar) .tab-row-container {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
body.electron.platform-darwin:not(.native-titlebar) #tab-row-left-spacer {
|
||||
body.electron.platform-darwin:not(.native-titlebar):not(.full-screen) #tab-row-left-spacer {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
|
||||
@ -1702,7 +1702,7 @@
|
||||
"paste": "粘贴",
|
||||
"paste-as-plain-text": "以纯文本粘贴",
|
||||
"search_online": "用 {{searchEngine}} 搜索 \"{{term}}\"",
|
||||
"search_in_trilium": "在 Trilium 中查找搜索 \"{{term}}\""
|
||||
"search_in_trilium": "在 Trilium 中搜索「{{term}}」"
|
||||
},
|
||||
"image_context_menu": {
|
||||
"copy_reference_to_clipboard": "复制引用到剪贴板",
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"link_title_arbitrary": "titulek odkazu může být změněn libovolně"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"prefix": "Prefix: ",
|
||||
"prefix": "Předpona: ",
|
||||
"save": "Uložit",
|
||||
"edit_branch_prefix": "Upravit prefix větve",
|
||||
"edit_branch_prefix_multiple": "Upravit prefix větve pro {{count}} větví",
|
||||
@ -68,20 +68,54 @@
|
||||
},
|
||||
"confirm": {
|
||||
"cancel": "Zrušit",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"confirmation": "Potvrzení",
|
||||
"are_you_sure_remove_note": "Opravdu chcete odstranit poznámku „{{title}}“ z mapy vztahů?",
|
||||
"if_you_dont_check": "Pokud tuto možnost nezaškrtnete, poznámka bude odstraněna pouze z mapy vztahů.",
|
||||
"also_delete_note": "Odstraňte také poznámku"
|
||||
},
|
||||
"delete_notes": {
|
||||
"cancel": "Zrušit",
|
||||
"ok": "OK",
|
||||
"close": "Zavřít"
|
||||
"close": "Zavřít",
|
||||
"delete_notes_preview": "Odstranit náhled poznámek",
|
||||
"delete_all_clones_description": "Odstraňte také všechny klony (lze vrátit zpět v nedávných změnách)",
|
||||
"erase_notes_description": "Normální (měkké) smazání pouze označí poznámky jako smazané a lze je během určité doby obnovit (v dialogovém okně posledních změn). Zaškrtnutím této možnosti se poznámky okamžitě vymažou a nebude možné je obnovit.",
|
||||
"erase_notes_warning": "Trvale smažte poznámky (nelze vrátit zpět), včetně všech klonů. Tím se vynutí opětovné načtení aplikace.",
|
||||
"notes_to_be_deleted": "Následující poznámky budou smazány ({{notesCount}})",
|
||||
"no_note_to_delete": "Žádná poznámka nebude smazána (pouze klony).",
|
||||
"broken_relations_to_be_deleted": "Následující vazby budou přerušeny a smazány ({{relationCount}})",
|
||||
"deleted_relation_text": "Poznámka {{- note}} (bude smazána) je odkazována vazbou {{- relation}} pocházející z {{- source}}."
|
||||
},
|
||||
"export": {
|
||||
"close": "Zavřít"
|
||||
"close": "Zavřít",
|
||||
"export_note_title": "Exportovat poznámku",
|
||||
"export_type_subtree": "Tato poznámka a všechny její odvozené poznámky",
|
||||
"format_html": "HTML – doporučeno, protože zachovává veškeré formátování",
|
||||
"format_html_zip": "HTML v archivu ZIP – toto se doporučuje, protože se tak zachová veškeré formátování.",
|
||||
"format_markdown": "Markdown – zachovává většinu formátování.",
|
||||
"format_opml": "OPML – formát pro výměnu osnov pouze pro text. Formátování, obrázky a soubory nejsou zahrnuty.",
|
||||
"opml_version_1": "OPML v1.0 – pouze prostý text",
|
||||
"opml_version_2": "OPML v2.0 – umožňuje také HTML",
|
||||
"export_type_single": "Pouze tato poznámka bez jejích potomků",
|
||||
"export": "Exportovat",
|
||||
"choose_export_type": "Nejprve vyberte typ exportu",
|
||||
"export_status": "Stav exportu",
|
||||
"export_in_progress": "Export probíhá: {{progressCount}}",
|
||||
"export_finished_successfully": "Export byl úspěšně dokončen.",
|
||||
"format_pdf": "PDF – pro tisk nebo sdílení.",
|
||||
"share-format": "HTML pro publikování na webu – používá stejný motiv jako sdílené poznámky, ale lze jej publikovat jako statický web."
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Klonovat poznámky do...",
|
||||
"help_on_links": "Nápověda k odkazům",
|
||||
"notes_to_clone": "Poznámky na klonování",
|
||||
"search_for_note_by_its_name": "hledat poznámku dle jejího názvu"
|
||||
"search_for_note_by_its_name": "hledat poznámku dle jejího názvu",
|
||||
"prefix_optional": "Předpona (volitelná)",
|
||||
"target_parent_note": "Zaměřit rodičovskou poznámku",
|
||||
"cloned_note_prefix_title": "Klonovaná poznámka se zobrazí ve stromu poznámek s danou předponou",
|
||||
"clone_to_selected_note": "Klonovat vybranou poznámku",
|
||||
"no_path_to_clone_to": "Žádná cest pro klonování.",
|
||||
"note_cloned": "Poznámka: „{{clonedTitle}}“ bylo naklonováno do „{{targetTitle}}“"
|
||||
}
|
||||
}
|
||||
|
||||
@ -769,7 +769,8 @@
|
||||
"geo-map": "Weltkarte",
|
||||
"board": "Tafel",
|
||||
"include_archived_notes": "Zeige archivierte Notizen",
|
||||
"presentation": "Präsentation"
|
||||
"presentation": "Präsentation",
|
||||
"expand_all_levels": "Alle Ebenen erweitern"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
|
||||
|
||||
@ -1727,7 +1727,8 @@
|
||||
"refresh-saved-search-results": "Refresh saved search results",
|
||||
"create-child-note": "Create child note",
|
||||
"unhoist": "Unhoist",
|
||||
"toggle-sidebar": "Toggle sidebar"
|
||||
"toggle-sidebar": "Toggle sidebar",
|
||||
"dropping-not-allowed": "Dropping notes into this location is not allowed."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Keep Window on Top"
|
||||
@ -1830,7 +1831,8 @@
|
||||
"duplicate-launcher": "Duplicate launcher <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Auto-detected"
|
||||
"auto-detect-language": "Auto-detected",
|
||||
"keeps-crashing": "Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report."
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Code Blocks",
|
||||
|
||||
@ -541,7 +541,11 @@
|
||||
"geo-map": "ジオマップ",
|
||||
"board": "ボード",
|
||||
"include_archived_notes": "アーカイブされたノートを表示",
|
||||
"presentation": "プレゼンテーション"
|
||||
"presentation": "プレゼンテーション",
|
||||
"expand_tooltip": "このコレクションの直下の子(1階層下)を展開します。その他のオプションについては、右側の矢印を押してください。",
|
||||
"expand_first_level": "直下の子を展開",
|
||||
"expand_nth_level": "{{depth}} 階層下まで展開",
|
||||
"expand_all_levels": "すべての階層を展開"
|
||||
},
|
||||
"note_types": {
|
||||
"geo-map": "ジオマップ",
|
||||
|
||||
@ -1661,7 +1661,7 @@
|
||||
"paste": "貼上",
|
||||
"paste-as-plain-text": "以純文字貼上",
|
||||
"search_online": "用 {{searchEngine}} 搜尋 \"{{term}}\"",
|
||||
"search_in_trilium": "在 Trilium 中搜尋 \"{{term}}\""
|
||||
"search_in_trilium": "在 Trilium 中搜尋「{{term}}」"
|
||||
},
|
||||
"image_context_menu": {
|
||||
"copy_reference_to_clipboard": "複製引用到剪貼簿",
|
||||
|
||||
@ -508,7 +508,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
(data.hitMode === "over" && node.data.noteType === "search") ||
|
||||
(["after", "before"].includes(data.hitMode) && (node.data.noteId === hoistedNoteService.getHoistedNoteId() || node.getParent().data.noteType === "search"))
|
||||
) {
|
||||
await dialogService.info("Dropping notes into this location is not allowed.");
|
||||
await dialogService.info(t("note_tree.dropping-not-allowed"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } fro
|
||||
import { RefObject } from "preact";
|
||||
import { buildSelectedBackgroundColor } from "../../../components/touch_bar";
|
||||
import { deferred } from "@triliumnext/commons";
|
||||
import { t } from "../../../services/i18n";
|
||||
|
||||
/**
|
||||
* The editor can operate into two distinct modes:
|
||||
@ -279,7 +280,7 @@ function onWatchdogStateChange(watchdog: EditorWatchdog) {
|
||||
logError(`CKEditor crash logs: ${JSON.stringify(watchdog.crashes, null, 4)}`);
|
||||
|
||||
if (currentState === "crashedPermanently") {
|
||||
dialog.info(`Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report.`);
|
||||
dialog.info(t("editable-text.keeps-crashing"));
|
||||
watchdog.editor?.enableReadOnlyMode("crashed-editor");
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,7 +61,32 @@
|
||||
"run-active-note": "Spustit aktivní kód JavaScript (frontend/backend) poznámky",
|
||||
"open-dev-tools": "Otevřít vývojářské nástroje",
|
||||
"zoom-out": "Oddálit",
|
||||
"zoom-in": "Přiblížit"
|
||||
"zoom-in": "Přiblížit",
|
||||
"text-note-operations": "Operace textových poznámek",
|
||||
"show-cheatsheet": "Zobrazit modal s běžnými operacemi klávesnice",
|
||||
"add-new-label": "Vytvořít nový štítek",
|
||||
"insert-date-and-time-to-text": "Vložit do textu současné datum a čas",
|
||||
"dialogs": "Dialogy",
|
||||
"show-revisions": "Ukázat dialog „Revize poznámky“",
|
||||
"add-link-to-text": "Otevřít dialog pro přidání odkazu do textu",
|
||||
"follow-link-under-cursor": "Následujte odkaz, na kterém je umístěna kurzorová šipka",
|
||||
"paste-markdown-into-text": "Vložit Markdown ze schránky do poznámky",
|
||||
"cut-into-note": "Vyjmout výběr z aktuální poznámky a vytvořit podpoznámku s vybraným textem",
|
||||
"add-include-note-to-text": "Otevřít dialog pro vložení poznámky",
|
||||
"attributes-labels-and-relations": "Atributy (štítky a vazby)",
|
||||
"create-new-relation": "Vytvořit novou vazbu",
|
||||
"ribbon-tabs": "Karty pásu záložek",
|
||||
"print-active-note": "Vytiskonout aktiivní poznámku",
|
||||
"render-active-note": "Vykreslit (znovu vykreslit) aktivní poznámku",
|
||||
"open-note-externally": "Otevřít poznámku jako soubor ve výchozí aplikaci",
|
||||
"reload-frontend-app": "Znovu načíst frontend",
|
||||
"unhoist": "Odpojit všude",
|
||||
"note-navigation": "Navigace v poznámce",
|
||||
"reset-zoom-level": "Resetovat úroveň přiblížení",
|
||||
"copy-without-formatting": "Kopírovat vybraný text bez formátování",
|
||||
"force-save-revision": "Vynutit vytvoření / uložení nové revize aktivní poznámky",
|
||||
"export-as-pdf": "Exportovat současnou poznámku jako PDF",
|
||||
"toggle-zen-mode": "Zapnout/vypnout režim zen (minimalistické uživatelské rozhraní pro soustředěnější úpravy)"
|
||||
},
|
||||
"keyboard_action_names": {
|
||||
"jump-to-note": "Přejít na...",
|
||||
@ -73,6 +98,15 @@
|
||||
"edit-note-title": "Upravit nadpis poznámky",
|
||||
"clone-notes-to": "Klonovat poznámku do",
|
||||
"move-notes-to": "Přemístit poznámku do",
|
||||
"copy-notes-to-clipboard": "Kopírovat poznámky do schránky"
|
||||
"copy-notes-to-clipboard": "Kopírovat poznámky do schránky",
|
||||
"back-in-note-history": "Zpět v historii poznámky",
|
||||
"forward-in-note-history": "Vpřed v historii poznámky",
|
||||
"command-palette": "Paleta příkazů",
|
||||
"scroll-to-active-note": "Posunout na aktivní poznámku",
|
||||
"search-in-subtree": "Hledat v podstromu",
|
||||
"expand-subtree": "Otevřít podstrom",
|
||||
"collapse-tree": "Zavřít strom",
|
||||
"collapse-subtree": "Zavřít podstrom",
|
||||
"sort-child-notes": "Seřadit dceřiné poznámky"
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,7 +257,8 @@
|
||||
"ai-llm-title": "AI/LLM",
|
||||
"localization": "Sprache & Region",
|
||||
"inbox-title": "Posteingang",
|
||||
"zen-mode": "Zen-Modus"
|
||||
"zen-mode": "Zen-Modus",
|
||||
"command-palette": "Befehlspalette öffnen"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "Neue Notiz",
|
||||
|
||||
@ -40,15 +40,21 @@ interface Subroot {
|
||||
|
||||
type GetNoteFunction = (id: string) => SNote | BNote | null;
|
||||
|
||||
function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot {
|
||||
function addContentAccessQuery(note: SNote | BNote, secondEl?:boolean) {
|
||||
if (!(note instanceof BNote) && note.contentAccessor && note.contentAccessor?.type === "query") {
|
||||
return secondEl ? `&cat=${note.contentAccessor.getToken()}` : `?cat=${note.contentAccessor.getToken()}`;
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
export function getSharedSubTreeRoot(note: SNote | BNote | undefined, parentId: string | undefined = undefined): Subroot {
|
||||
if (!note || note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
// share root itself is not shared
|
||||
return {};
|
||||
}
|
||||
|
||||
// every path leads to share root, but which one to choose?
|
||||
// for the sake of simplicity, URLs are not note paths
|
||||
const parentBranch = note.getParentBranches()[0];
|
||||
const parentBranches = note.getParentBranches()
|
||||
const parentBranch = (parentId ? parentBranches.find((pb: SBranch | BBranch) => pb.parentNoteId === parentId) : undefined) || parentBranches[0];
|
||||
|
||||
if (note instanceof BNote) {
|
||||
return {
|
||||
@ -64,7 +70,7 @@ function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot {
|
||||
};
|
||||
}
|
||||
|
||||
return getSharedSubTreeRoot(parentBranch.getParentNote());
|
||||
return getSharedSubTreeRoot(parentBranch.getParentNote(), parentId);
|
||||
}
|
||||
|
||||
export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath: string, ancestors: string[]) {
|
||||
@ -91,7 +97,7 @@ export function renderNoteForExport(note: BNote, parentBranch: BBranch, basePath
|
||||
}
|
||||
|
||||
export function renderNoteContent(note: SNote) {
|
||||
const subRoot = getSharedSubTreeRoot(note);
|
||||
const subRoot = getSharedSubTreeRoot(note, note.parentId);
|
||||
|
||||
const ancestors: string[] = [];
|
||||
let notePointer = note;
|
||||
@ -107,23 +113,23 @@ export function renderNoteContent(note: SNote) {
|
||||
// Determine CSS to load.
|
||||
const cssToLoad: string[] = [];
|
||||
if (!note.isLabelTruthy("shareOmitDefaultCss")) {
|
||||
cssToLoad.push(`assets/styles.css`);
|
||||
cssToLoad.push(`assets/scripts.css`);
|
||||
cssToLoad.push(`../assets/styles.css`);
|
||||
cssToLoad.push(`../assets/scripts.css`);
|
||||
}
|
||||
for (const cssRelation of note.getRelations("shareCss")) {
|
||||
cssToLoad.push(`api/notes/${cssRelation.value}/download`);
|
||||
cssToLoad.push(`../api/notes/${cssRelation.value}/download${addContentAccessQuery(note)}`);
|
||||
}
|
||||
|
||||
// Determine JS to load.
|
||||
const jsToLoad: string[] = [
|
||||
"assets/scripts.js"
|
||||
"../assets/scripts.js"
|
||||
];
|
||||
for (const jsRelation of note.getRelations("shareJs")) {
|
||||
jsToLoad.push(`api/notes/${jsRelation.value}/download`);
|
||||
jsToLoad.push(`../api/notes/${jsRelation.value}/download${addContentAccessQuery(note)}`);
|
||||
}
|
||||
|
||||
const customLogoId = note.getRelation("shareLogo")?.value;
|
||||
const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`;
|
||||
const logoUrl = customLogoId ? `../api/images/${customLogoId}/image.png${addContentAccessQuery(note)}` : `../../${assetUrlFragment}/images/icon-color.svg`;
|
||||
|
||||
return renderNoteContentInternal(note, {
|
||||
subRoot,
|
||||
@ -133,7 +139,7 @@ export function renderNoteContent(note: SNote) {
|
||||
logoUrl,
|
||||
ancestors,
|
||||
isStatic: false,
|
||||
faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download` : `../favicon.ico`
|
||||
faviconUrl: note.hasRelation("shareFavicon") ? `../api/notes/${note.getRelationValue("shareFavicon")}/download${addContentAccessQuery(note)}` : `../../favicon.ico`
|
||||
});
|
||||
}
|
||||
|
||||
@ -158,6 +164,7 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs)
|
||||
isEmpty,
|
||||
assetPath: shareAdjustedAssetPath,
|
||||
assetUrlFragment,
|
||||
addContentAccessQuery: (second: boolean | undefined) => addContentAccessQuery(note, second),
|
||||
showLoginInShareTheme,
|
||||
t,
|
||||
isDev,
|
||||
@ -325,7 +332,7 @@ function renderText(result: Result, note: SNote | BNote) {
|
||||
}
|
||||
|
||||
if (href?.startsWith("#")) {
|
||||
handleAttachmentLink(linkEl, href, getNote, getAttachment);
|
||||
handleAttachmentLink(linkEl, href, getNote, getAttachment, note);
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,7 +356,7 @@ function renderText(result: Result, note: SNote | BNote) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNoteFunction, getAttachment: (id: string) => BAttachment | SAttachment | null) {
|
||||
function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNoteFunction, getAttachment: (id: string) => BAttachment | SAttachment | null, note: SNote | BNote) {
|
||||
const linkRegExp = /attachmentId=([a-zA-Z0-9_]+)/g;
|
||||
let attachmentMatch;
|
||||
if ((attachmentMatch = linkRegExp.exec(href))) {
|
||||
@ -357,7 +364,7 @@ function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNot
|
||||
const attachment = getAttachment(attachmentId);
|
||||
|
||||
if (attachment) {
|
||||
linkEl.setAttribute("href", `api/attachments/${attachmentId}/download`);
|
||||
linkEl.setAttribute("href", `../api/attachments/${attachmentId}/download${addContentAccessQuery(note)}`);
|
||||
linkEl.classList.add(`attachment-link`);
|
||||
linkEl.classList.add(`role-${attachment.role}`);
|
||||
linkEl.childNodes.length = 0;
|
||||
@ -373,7 +380,7 @@ function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNot
|
||||
const linkedNote = getNote(noteId);
|
||||
if (linkedNote) {
|
||||
const isExternalLink = linkedNote.hasLabel("shareExternalLink");
|
||||
const href = isExternalLink ? linkedNote.getLabelValue("shareExternalLink") : `./${linkedNote.shareId}`;
|
||||
const href = isExternalLink ? linkedNote.getLabelValue("shareExternalLink") : `../${linkedNote.shareId}`;
|
||||
if (href) {
|
||||
linkEl.setAttribute("href", href);
|
||||
}
|
||||
@ -430,7 +437,7 @@ function renderMermaid(result: Result, note: SNote | BNote) {
|
||||
}
|
||||
|
||||
result.content = `
|
||||
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">
|
||||
<img src="../api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}${addContentAccessQuery(note, true)}">
|
||||
<hr>
|
||||
<details>
|
||||
<summary>Chart source</summary>
|
||||
@ -439,14 +446,14 @@ function renderMermaid(result: Result, note: SNote | BNote) {
|
||||
}
|
||||
|
||||
function renderImage(result: Result, note: SNote | BNote) {
|
||||
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`;
|
||||
result.content = `<img src="../api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}${addContentAccessQuery(note, true)}">`;
|
||||
}
|
||||
|
||||
function renderFile(note: SNote | BNote, result: Result) {
|
||||
if (note.mime === "application/pdf") {
|
||||
result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`;
|
||||
result.content = `<iframe class="pdf-view" src="../api/notes/${note.noteId}/view${addContentAccessQuery(note)}"></iframe>`;
|
||||
} else {
|
||||
result.content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
|
||||
result.content = `<button type="button" onclick="location.href='../api/notes/${note.noteId}/download${addContentAccessQuery(note)}'">Download file</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import searchService from "../services/search/services/search.js";
|
||||
import SearchContext from "../services/search/search_context.js";
|
||||
import type SNote from "./shaca/entities/snote.js";
|
||||
import type SAttachment from "./shaca/entities/sattachment.js";
|
||||
import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js";
|
||||
import { getDefaultTemplatePath, getSharedSubTreeRoot, renderNoteContent } from "./content_renderer.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
function addNoIndexHeader(note: SNote, res: Response) {
|
||||
@ -60,6 +60,20 @@ function checkNoteAccess(noteId: string, req: Request, res: Response) {
|
||||
const header = req.header("Authorization");
|
||||
|
||||
if (!header?.startsWith("Basic ")) {
|
||||
if (req.path.startsWith("/share/api") && note.contentAccessor) {
|
||||
let contentAccessToken = ""
|
||||
if (note.contentAccessor.type === "cookie") contentAccessToken += req.cookies["trilium.cat"] || ""
|
||||
else if (note.contentAccessor.type === "query") contentAccessToken += req.query['cat'] || ""
|
||||
|
||||
if (contentAccessToken){
|
||||
if (note.contentAccessor.isTokenValid(contentAccessToken)){
|
||||
return note
|
||||
}
|
||||
res.status(401).send("Access is expired. Return back and update the page.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -124,9 +138,14 @@ function register(router: Router) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.isLabelTruthy("shareExclude")) {
|
||||
res.status(404);
|
||||
render404(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkNoteAccess(note.noteId, req, res)) {
|
||||
requestCredentials(res);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -138,6 +157,10 @@ function register(router: Router) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.contentAccessor && note.contentAccessor.type === "cookie") {
|
||||
res.cookie('trilium.cat', note.contentAccessor.getToken(), { maxAge: note.contentAccessor.getTokenExpiration() * 1000, httpOnly: true })
|
||||
}
|
||||
|
||||
res.send(renderNoteContent(note));
|
||||
}
|
||||
|
||||
@ -157,14 +180,29 @@ function register(router: Router) {
|
||||
renderNote(shaca.shareRootNote, req, res);
|
||||
});
|
||||
|
||||
router.get("/share/:parentShareId/:shareId", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
const { parentShareId, shareId } = req.params;
|
||||
|
||||
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
||||
if (note){
|
||||
note.parentId = parentShareId
|
||||
note.initContentAccessor()
|
||||
}
|
||||
|
||||
renderNote(note, req, res);
|
||||
});
|
||||
|
||||
router.get("/share/:shareId", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
const { shareId } = req.params;
|
||||
|
||||
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
||||
const parent = getSharedSubTreeRoot(note)
|
||||
|
||||
renderNote(note, req, res);
|
||||
res.redirect(`${parent?.note?.noteId}/${shareId}`)
|
||||
});
|
||||
|
||||
router.get("/share/api/notes/:noteId", (req, res) => {
|
||||
|
||||
81
apps/server/src/share/shaca/entities/content_accessor.ts
Normal file
81
apps/server/src/share/shaca/entities/content_accessor.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import crypto from "crypto";
|
||||
import SNote from "./snote";
|
||||
import utils from "../../../services/utils";
|
||||
|
||||
const DefaultAccessTimeoutSec = 10 * 60; // 10 minutes
|
||||
|
||||
export class ContentAccessor {
|
||||
note: SNote;
|
||||
token: string;
|
||||
timestamp: number;
|
||||
type: string;
|
||||
timeout: number;
|
||||
key: Buffer;
|
||||
|
||||
constructor(note: SNote) {
|
||||
this.note = note;
|
||||
this.key = crypto.randomBytes(32);
|
||||
this.token = "";
|
||||
this.timestamp = 0;
|
||||
this.timeout = Number(this.note.getAttributeValue("label", "shareAccessTokenTimeout") || DefaultAccessTimeoutSec)
|
||||
|
||||
switch (this.note.getAttributeValue("label", "shareContentAccess")) {
|
||||
case "basic": this.type = "basic"; break
|
||||
case "query": this.type = "query"; break
|
||||
default: this.type = "cookie"; break
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
__encrypt(text: string) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv);
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
return iv.toString('hex') + encrypted;
|
||||
}
|
||||
|
||||
__decrypt(encryptedText: string) {
|
||||
try {
|
||||
const iv = Buffer.from(encryptedText.slice(0, 32), 'hex');
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv);
|
||||
let decrypted = decipher.update(encryptedText.slice(32), 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
__compare(originalText: string, encryptedText: string) {
|
||||
return originalText === this.__decrypt(encryptedText)
|
||||
}
|
||||
|
||||
update() {
|
||||
if (new Date().getTime() < this.timestamp + this.getTimeout() * 1000) return
|
||||
this.token = utils.randomString(36);
|
||||
this.key = crypto.randomBytes(32);
|
||||
this.timestamp = new Date().getTime();
|
||||
}
|
||||
|
||||
isTokenValid(encToken: string) {
|
||||
return this.__compare(this.token, encToken) && new Date().getTime() < this.timestamp + this.getTimeout() * 1000;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this.__encrypt(this.token);
|
||||
}
|
||||
|
||||
getTokenExpiration() {
|
||||
return (this.timestamp + (this.timeout * 1000) - new Date().getTime()) /1000;
|
||||
}
|
||||
|
||||
getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
getContentAccessType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
@ -10,6 +10,7 @@ import type SAttribute from "./sattribute.js";
|
||||
import type SBranch from "./sbranch.js";
|
||||
import type { SNoteRow } from "./rows.js";
|
||||
import { NOTE_TYPE_ICONS } from "../../../becca/entities/bnote.js";
|
||||
import { ContentAccessor } from "./content_accessor.js";
|
||||
|
||||
const LABEL = "label";
|
||||
const RELATION = "relation";
|
||||
@ -19,6 +20,7 @@ const isCredentials = (attr: SAttribute) => attr.type === "label" && attr.name =
|
||||
|
||||
class SNote extends AbstractShacaEntity {
|
||||
noteId: string;
|
||||
parentId?: string | undefined;
|
||||
title: string;
|
||||
type: string;
|
||||
mime: string;
|
||||
@ -33,11 +35,13 @@ class SNote extends AbstractShacaEntity {
|
||||
private __inheritableAttributeCache: SAttribute[] | null;
|
||||
targetRelations: SAttribute[];
|
||||
attachments: SAttachment[];
|
||||
contentAccessor: ContentAccessor | undefined;
|
||||
|
||||
constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) {
|
||||
super();
|
||||
|
||||
this.noteId = noteId;
|
||||
this.parentId = undefined;
|
||||
this.title = isProtected ? "[protected]" : title;
|
||||
this.type = type;
|
||||
this.mime = mime;
|
||||
@ -59,6 +63,19 @@ class SNote extends AbstractShacaEntity {
|
||||
this.shaca.notes[this.noteId] = this;
|
||||
}
|
||||
|
||||
initContentAccessor(){
|
||||
if (!this.contentAccessor && this.getCredentials().length > 0) {
|
||||
this.contentAccessor = new ContentAccessor(this);
|
||||
}
|
||||
if (this.contentAccessor) {
|
||||
this.contentAccessor.update()
|
||||
}
|
||||
}
|
||||
|
||||
getParentId() {
|
||||
return this.parentId;
|
||||
}
|
||||
|
||||
getParentBranches() {
|
||||
return this.parentBranches;
|
||||
}
|
||||
@ -72,7 +89,7 @@ class SNote extends AbstractShacaEntity {
|
||||
}
|
||||
|
||||
getVisibleChildBranches() {
|
||||
return this.getChildBranches().filter((branch) => !branch.isHidden && !branch.getNote().isLabelTruthy("shareHiddenFromTree"));
|
||||
return this.getChildBranches().filter((branch) => !branch.isHidden && !branch.getNote().isLabelTruthy("shareHiddenFromTree") && !branch.getNote().isLabelTruthy("shareExclude"));
|
||||
}
|
||||
|
||||
getParentNotes() {
|
||||
@ -80,7 +97,7 @@ class SNote extends AbstractShacaEntity {
|
||||
}
|
||||
|
||||
getChildNotes() {
|
||||
return this.children;
|
||||
return this.children.filter((note) => !note.isLabelTruthy("shareExclude"));
|
||||
}
|
||||
|
||||
getVisibleChildNotes() {
|
||||
|
||||
@ -50,6 +50,90 @@
|
||||
"canvas_description": "Uspořádejte tvary, obrázky a text na nekonečném plátně pomocí stejné technologie, jaká se používá na webu excalidraw.com. Ideální pro diagramy, náčrtky a vizuální plánování.",
|
||||
"mermaid_title": "Mermaid diagramy",
|
||||
"mermaid_description": "Vytvářejte diagramy, jako jsou vývojové diagramy, diagramy tříd a sekvencí, Ganttovy diagramy a mnoho dalších, pomocí syntaxe Mermaid.",
|
||||
"mindmap_title": "Myšlenková mapa"
|
||||
"mindmap_title": "Myšlenková mapa",
|
||||
"mindmap_description": "Zorganizujte si myšlenky vizuálně nebo uspořádejte brainstorming.",
|
||||
"others_list": "a další: <0>mapa poznámek</0>, <1>mapa vztahů</1>, <2>uložená vyhledávání</2>, <3>zobrazení poznámky</3> a <4>webové zobrazení</4>."
|
||||
},
|
||||
"extensibility_benefits": {
|
||||
"title": "Sdílení a rozšiřitelnost",
|
||||
"import_export_title": "Import/export",
|
||||
"import_export_description": "Snadná interakce s jinými aplikacemi pomocí formátů Markdown, ENEX a OML.",
|
||||
"share_title": "Sdílet poznámky na webu",
|
||||
"share_description": "Pokud máte server, můžete jej použít ke sdílení části svých poznámek s ostatními lidmi.",
|
||||
"scripting_title": "Pokročilé skriptování",
|
||||
"scripting_description": "Vytvořte si vlastní integrace v rámci Trilium pomocí přizpůsobených widgetů nebo logiky na straně serveru.",
|
||||
"api_title": "REST API",
|
||||
"api_description": "Komunikujte s Trilium programově pomocí jeho vestavěného REST API."
|
||||
},
|
||||
"collections": {
|
||||
"title": "Kolekce",
|
||||
"calendar_title": "Kalendář",
|
||||
"calendar_description": "Organizujte své osobní nebo pracovní události pomocí kalendáře, který podporuje celodenní i vícedenní události. Zobrazte si své události na první pohled v týdenním, měsíčním a ročním přehledu. Snadná interakce pro přidávání nebo přetahování událostí.",
|
||||
"table_title": "Tabulka",
|
||||
"table_description": "Zobrazujte a upravujte informace o poznámkách v tabulkové struktuře s různými typy sloupců, jako jsou text, čísla, zaškrtávací políčka, datum a čas, odkazy a barvy, a podporou vztahů. Volitelně můžete poznámky zobrazit v hierarchické struktuře stromu uvnitř tabulky.",
|
||||
"board_title": "Kanbanová tabule",
|
||||
"board_description": "Uspořádejte si úkoly nebo stav projektu do tabule Kanban, kde můžete snadno vytvářet nové položky a sloupce a jednoduše měnit jejich stav přetahováním po tabuli.",
|
||||
"geomap_title": "Geomapa",
|
||||
"geomap_description": "Naplánujte si dovolenou nebo si označte místa, která vás zajímají, přímo na geografické mapě pomocí přizpůsobitelných značek. Zobrazte zaznamenané trasy GPX a sledujte itineráře.",
|
||||
"presentation_title": "Prezentace",
|
||||
"presentation_description": "Uspořádejte informace do snímků a prezentujte je na celé obrazovce s plynulými přechody. Snímky lze také exportovat do formátu PDF pro snadné sdílení."
|
||||
},
|
||||
"faq": {
|
||||
"title": "Často kladené otázky",
|
||||
"mobile_question": "Existuje mobilní aplikace?",
|
||||
"mobile_answer": "V současné době neexistuje žádná oficiální mobilní aplikace. Pokud však máte instanci serveru, můžete k ní přistupovat pomocí webového prohlížeče a dokonce ji nainstalovat jako PWA. Pro Android existuje neoficiální aplikace s názvem TriliumDroid, která funguje i offline (stejně jako desktopový klient).",
|
||||
"database_question": "Kde jsou má data uložena?",
|
||||
"database_answer": "Všechny vaše poznámky budou uloženy v databázi SQLite ve složce aplikace. Důvodem, proč Trilium používá databázi namísto prostých textových souborů, je jak výkon, tak i skutečnost, že některé funkce by byly mnohem obtížnější implementovat, například klony (stejná poznámka na více místech ve stromu). Chcete-li najít složku aplikace, stačí přejít do okna O aplikaci.",
|
||||
"server_question": "Potřebuju server pro používání Trilium?",
|
||||
"server_answer": "Ne, server umožňuje přístup přes webový prohlížeč a spravuje synchronizaci, pokud máte více zařízení. Chcete-li začít, stačí si stáhnout desktopovou aplikaci a začít ji používat.",
|
||||
"scaling_question": "Jak dobře se aplikace přizpůsobuje velkému množství poznámek?",
|
||||
"scaling_answer": "V závislosti na použití by aplikace měla být schopna bez problémů zpracovat alespoň 100 000 poznámek. Upozorňujeme, že proces synchronizace může někdy selhat, pokud nahráváte mnoho velkých souborů (1 GB na soubor), protože Trilium je spíše aplikací pro správu znalostí než úložištěm souborů (jako například NextCloud).",
|
||||
"network_share_question": "Mohu sdílet svou databázi přes síťový disk?",
|
||||
"network_share_answer": "Ne, sdílení databáze SQLite přes síťový disk obecně není dobrý nápad. I když to někdy může fungovat, existuje riziko, že se databáze poškodí kvůli nedokonalému zamykání souborů v síti.",
|
||||
"security_question": "Jak jsou má data chráněna?",
|
||||
"security_answer": "Ve výchozím nastavení nejsou poznámky šifrovány a lze je číst přímo z databáze. Jakmile je poznámka označena jako šifrovaná, je zašifrována pomocí AES-128-CBC."
|
||||
},
|
||||
"final_cta": {
|
||||
"title": "Jste připraveni začít používat Trilium Notes?",
|
||||
"description": "Vytvořte si svou osobní znalostní bázi s výkonnými funkcemi a plným soukromím.",
|
||||
"get_started": "Začít"
|
||||
},
|
||||
"components": {
|
||||
"link_learn_more": "Zjistit více..."
|
||||
},
|
||||
"download_now": {
|
||||
"text": "Stáhnout nyní ",
|
||||
"platform_big": "v{{version}} pro{{platform}}",
|
||||
"platform_small": "pro {{platform}}",
|
||||
"linux_big": "v{{version}} pro Linux",
|
||||
"linux_small": "pro Linux",
|
||||
"more_platforms": "Další platformy a nastavení serveru"
|
||||
},
|
||||
"header": {
|
||||
"get-started": "Začít",
|
||||
"documentation": "Dokumentace",
|
||||
"support-us": "Podpořte nás"
|
||||
},
|
||||
"footer": {
|
||||
"copyright_and_the": " a ",
|
||||
"copyright_community": "komunita"
|
||||
},
|
||||
"social_buttons": {
|
||||
"github": "GitHub",
|
||||
"github_discussions": "GitHub diskuze",
|
||||
"matrix": "Matrix",
|
||||
"reddit": "Reddit"
|
||||
},
|
||||
"support_us": {
|
||||
"title": "Podpořte nás",
|
||||
"financial_donations_title": "Finanční dary",
|
||||
"financial_donations_description": "Trilium je vyvíjeno a udržováno díky <Link>stovkám hodin práce</Link>. Vaše podpora zajišťuje, že zůstane open-source, vylepšuje jeho funkce a pokrývá náklady, jako je hosting.",
|
||||
"financial_donations_cta": "Zvažte podporu hlavního vývojáře (<Link>eliandoran</Link>) aplikace prostřednictvím:",
|
||||
"github_sponsors": "Sponzoři GitHubu",
|
||||
"paypal": "PayPal",
|
||||
"buy_me_a_coffee": "Buy Me A Coffee"
|
||||
},
|
||||
"contribute": {
|
||||
"title": "Další způsoby, jak přispět"
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ To do so, create a shared text note and apply the `shareIndex` label. When viewe
|
||||
|
||||
## Attribute reference
|
||||
|
||||
<table class="ck-table-resized"><colgroup><col style="width:18.38%;"><col style="width:81.62%;"></colgroup><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>#shareHiddenFromTree</code></td><td>this note is hidden from left navigation tree, but still accessible with its URL</td></tr><tr><td><code>#shareExternalLink</code></td><td>note will act as a link to an external website in the share tree</td></tr><tr><td><code>#shareAlias</code></td><td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code></td></tr><tr><td><code>#shareOmitDefaultCss</code></td><td>default share page CSS will be omitted. Use when you make extensive styling changes.</td></tr><tr><td><code>#shareRoot</code></td><td>marks note which is served on /share root.</td></tr><tr><td><code>#shareDescription</code></td><td>define text to be added to the HTML meta tag for description</td></tr><tr><td><code>#shareRaw</code></td><td>Note will be served in its raw format, without HTML wrapper. See also <a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a> for an alternative method without setting an attribute.</td></tr><tr><td><code>#shareDisallowRobotIndexing</code></td><td><p>Indicates to web crawlers that the page should not be indexed of this note by:</p><ul><li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li><li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li></ul></td></tr><tr><td><code>#shareCredentials</code></td><td>require credentials to access this shared note. Value is expected to be in format <code>username:password</code>. Don't forget to make this inheritable to apply to child-notes/images.</td></tr><tr><td><code>#shareIndex</code></td><td>Note with this label will list all roots of shared notes.</td></tr><tr><td><code>#shareHtmlLocation</code></td><td>defines where custom HTML injected via <code>~shareHtml</code> relation should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where location is <code>head</code>, <code>body</code>, or <code>content</code> and position is <code>start</code> or <code>end</code>. Defaults to <code>content:end</code>.</td></tr></tbody></table>
|
||||
<table class="ck-table-resized"><colgroup><col style="width:18.38%;"><col style="width:81.62%;"></colgroup><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>#shareHiddenFromTree</code></td><td>this note is hidden from left navigation tree, but still accessible with its URL</td></tr><tr><td><code>#shareTemplateNoPrevNext</code></td><td>hide bottom page navigation prev and next page.</td></tr><tr><td><code>#shareTemplateNoLeftPanel</code></td><td>hide left panel fully.</td></tr><tr><td><code>#shareExclude</code></td><td>this note will be excluded from share, not accessible via direct URL (implemented to hide scripts from share)</td></tr><tr><td><code>#shareContentAccess</code></td><td>method for attachments authorization in case when note protected with login and password (#shareCredentials). Could be cookie (the cookie will be provided when page loads) / query (every url will be updated with token) / basic (only basic header authorization)). By default for browser used cookie.</td></tr><tr><td><code>#shareAccessTokenTimeout</code></td><td>token expiration timeout in seconds, by default 10 minutes. While token not expired user could download attachment, after that he will get message `Access is expired. Return back and update the page.`</td></tr><tr><td><code>#shareExternalLink</code></td><td>note will act as a link to an external website in the share tree</td></tr><tr><td><code>#shareAlias</code></td><td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code></td></tr><tr><td><code>#shareOmitDefaultCss</code></td><td>default share page CSS will be omitted. Use when you make extensive styling changes.</td></tr><tr><td><code>#shareRoot</code></td><td>marks note which is served on /share root.</td></tr><tr><td><code>#shareDescription</code></td><td>define text to be added to the HTML meta tag for description</td></tr><tr><td><code>#shareRaw</code></td><td>Note will be served in its raw format, without HTML wrapper. See also <a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a> for an alternative method without setting an attribute.</td></tr><tr><td><code>#shareDisallowRobotIndexing</code></td><td><p>Indicates to web crawlers that the page should not be indexed of this note by:</p><ul><li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li><li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li></ul></td></tr><tr><td><code>#shareCredentials</code></td><td>require credentials to access this shared note. Value is expected to be in format <code>username:password</code>. Don't forget to make this inheritable to apply to child-notes/images.</td></tr><tr><td><code>#shareIndex</code></td><td>Note with this label will list all roots of shared notes.</td></tr><tr><td><code>#shareHtmlLocation</code></td><td>defines where custom HTML injected via <code>~shareHtml</code> relation should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where location is <code>head</code>, <code>body</code>, or <code>content</code> and position is <code>start</code> or <code>end</code>. Defaults to <code>content:end</code>.</td></tr></tbody></table>
|
||||
|
||||
### Customizing logo
|
||||
|
||||
|
||||
@ -2,9 +2,14 @@
|
||||
* https://github.com/TriliumNext/Trilium/issues/1002
|
||||
*/
|
||||
|
||||
import { Command, ModelDocumentSelection, ModelElement, ModelNode, Plugin, ModelRange } from 'ckeditor5';
|
||||
export default class MoveBlockUpDownPlugin extends Plugin {
|
||||
import { Command, ModelDocumentSelection, ModelElement, ModelNode, Plugin, ModelRange, _isMac, Editor } from 'ckeditor5';
|
||||
|
||||
const keyMap = {
|
||||
ArrowUp: 'moveBlockUp',
|
||||
ArrowDown: 'moveBlockDown'
|
||||
};
|
||||
|
||||
export default class MoveBlockUpDownPlugin extends Plugin {
|
||||
init() {
|
||||
const editor = this.editor;
|
||||
|
||||
@ -21,17 +26,14 @@ export default class MoveBlockUpDownPlugin extends Plugin {
|
||||
const domRoot = editor.editing.view.getDomRoot();
|
||||
if (!domRoot) return;
|
||||
|
||||
const isMac = _isMac(navigator.userAgent.toLowerCase());
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
const keyMap = {
|
||||
ArrowUp: 'moveBlockUp',
|
||||
ArrowDown: 'moveBlockDown'
|
||||
};
|
||||
|
||||
const command = keyMap[e.key];
|
||||
const isCtrl = e.ctrlKey || e.metaKey;
|
||||
const hasModifier = (isCtrl || e.altKey) && !(isCtrl && e.altKey);
|
||||
if (!command) return;
|
||||
const isOnlyMeta = (!e.ctrlKey && !e.altKey && e.metaKey);
|
||||
const isOnlyAlt = (!e.ctrlKey && e.altKey && !e.metaKey);
|
||||
|
||||
if (command && hasModifier) {
|
||||
if ((!isMac && isOnlyMeta) || isOnlyAlt) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
editor.execute(command);
|
||||
@ -100,8 +102,7 @@ abstract class MoveBlockUpDownCommand extends Command {
|
||||
}
|
||||
writer.setSelection(range);
|
||||
this.editor.editing.view.focus();
|
||||
|
||||
this.scrollToSelection();
|
||||
scrollToSelection(this.editor);
|
||||
});
|
||||
}
|
||||
|
||||
@ -129,13 +130,6 @@ abstract class MoveBlockUpDownCommand extends Command {
|
||||
// Deduplicate adjacent duplicates (e.g., nested selections resolving to same block)
|
||||
return resolved.filter((blk, idx) => idx === 0 || blk !== resolved[idx - 1]);
|
||||
}
|
||||
|
||||
scrollToSelection() {
|
||||
// Ensure scroll happens in sync with DOM updates
|
||||
requestAnimationFrame(() => {
|
||||
this.editor.editing.view.scrollToTheSelection();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
class MoveBlockUpCommand extends MoveBlockUpDownCommand {
|
||||
@ -162,3 +156,10 @@ class MoveBlockDownCommand extends MoveBlockUpDownCommand {
|
||||
return "after" as const;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToSelection(editor: Editor) {
|
||||
// Ensure scroll happens in sync with DOM updates
|
||||
requestAnimationFrame(() => {
|
||||
editor.editing.view.scrollToTheSelection();
|
||||
});
|
||||
};
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
let openGraphImage = subRoot.note.getLabelValue("shareOpenGraphImage");
|
||||
// Relation takes priority and requires some altering
|
||||
if (subRoot.note.hasRelation("shareOpenGraphImage")) {
|
||||
openGraphImage = `api/images/${subRoot.note.getRelation("shareOpenGraphImage").value}/image.png`;
|
||||
openGraphImage = `api/images/${subRoot.note.getRelation("shareOpenGraphImage").value}/image.png${addContentAccessQuery()}`;
|
||||
}
|
||||
%>
|
||||
<title><%= pageTitle %></title>
|
||||
@ -109,40 +109,43 @@ content = content.replaceAll(headingRe, (...match) => {
|
||||
<button aria-label="Show Mobile Menu" id="show-menu-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path></svg></button>
|
||||
</div>
|
||||
<div id="split-pane">
|
||||
<div id="left-pane">
|
||||
<div id="navigation">
|
||||
<div id="site-header">
|
||||
<a href="<%= shareRootLink %>">
|
||||
<img src="<%= logoUrl %>" width="<%= logoWidth %>" height="<%= logoHeight %>" alt="Logo" />
|
||||
<%= subRoot.note.title %>
|
||||
</a>
|
||||
<div class="theme-selection">
|
||||
<span id="sitetheme"><%= t("share_theme.site-theme") %></span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" aria-labelledby="sitetheme">
|
||||
<span class="slider"></span>
|
||||
<svg class="dark-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z"></path></svg>
|
||||
<svg class="light-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z"></path></svg>
|
||||
</label>
|
||||
<script>
|
||||
const el = document.querySelector(".theme-selection input");
|
||||
el.checked = (glob.theme === "dark");
|
||||
</script>
|
||||
</div>
|
||||
<% if (hasTree) { %>
|
||||
<div class="search-item">
|
||||
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M10 18a7.952 7.952 0 0 0 4.897-1.688l4.396 4.396 1.414-1.414-4.396-4.396A7.952 7.952 0 0 0 18 10c0-4.411-3.589-8-8-8s-8 3.589-8 8 3.589 8 8 8zm0-14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6z"></path></svg>
|
||||
<input type="text" class="search-input" placeholder="<%= t("share_theme.search_placeholder") %>">
|
||||
<% if (!note.isLabelTruthy("shareTemplateNoLeftPanel")) { %>
|
||||
<div id="left-pane">
|
||||
<div id="navigation">
|
||||
<div id="site-header">
|
||||
<a href="<%= shareRootLink %>">
|
||||
<img src="<%= logoUrl %>" width="<%= logoWidth %>" height="<%= logoHeight %>" alt="Logo" />
|
||||
<%= subRoot.note.title %>
|
||||
</a>
|
||||
<div class="theme-selection">
|
||||
<span id="sitetheme"><%= t("share_theme.site-theme") %></span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" aria-labelledby="sitetheme">
|
||||
<span class="slider"></span>
|
||||
<svg class="dark-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z"></path></svg>
|
||||
<svg class="light-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z"></path></svg>
|
||||
</label>
|
||||
<script>
|
||||
const el = document.querySelector(".theme-selection input");
|
||||
el.checked = (glob.theme === "dark");
|
||||
</script>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (hasTree) { %>
|
||||
<div class="search-item">
|
||||
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M10 18a7.952 7.952 0 0 0 4.897-1.688l4.396 4.396 1.414-1.414-4.396-4.396A7.952 7.952 0 0 0 18 10c0-4.411-3.589-8-8-8s-8 3.589-8 8 3.589 8 8 8zm0-14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6z"></path></svg>
|
||||
<input type="text" class="search-input" placeholder="<%= t("share_theme.search_placeholder") %>">
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (hasTree) { %>
|
||||
<nav id="menu">
|
||||
<%- include("tree_item", {note: subRoot.note, activeNote: note, subRoot: subRoot, ancestors}) %>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (hasTree) { %>
|
||||
<nav id="menu">
|
||||
<%- include("tree_item", {note: subRoot.note, activeNote: note, subRoot: subRoot, ancestors}) %>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div id="right-pane">
|
||||
<div id="main">
|
||||
<div id="content" class="type-<%= note.type %><% if (note.type === "text") { %> ck-content<% } %><% if (isEmpty) { %> no-content<% } %>">
|
||||
@ -152,7 +155,9 @@ content = content.replaceAll(headingRe, (...match) => {
|
||||
<p>This note has no content.</p>
|
||||
<% } else { %>
|
||||
<%
|
||||
content = content.replace(/<img /g, `<img alt="${t("share_theme.image_alt")}" loading="lazy" `);
|
||||
content = content
|
||||
.replace(/<img /g, `<img alt="${t("share_theme.image_alt")}" loading="lazy" `)
|
||||
.replace(/src="(api\/[^"]+)"/g, (m, url) => `src="../${url}${addContentAccessQuery(url.includes('?'))}"`);
|
||||
%>
|
||||
<%- content %>
|
||||
<% } %>
|
||||
@ -189,7 +194,7 @@ content = content.replaceAll(headingRe, (...match) => {
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (hasTree) { %>
|
||||
<% if (hasTree && !note.isLabelTruthy("shareTemplateNoPrevNext")) { %>
|
||||
<%- include("prev_next", { note: note, subRoot: subRoot }) %>
|
||||
<% } %>
|
||||
</footer>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user