mirror of
https://github.com/zadam/trilium.git
synced 2026-02-13 01:46:54 +01:00
Compare commits
35 Commits
feature/cu
...
feature/tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ae10dd82 | ||
|
|
0619bcf35a | ||
|
|
0d37f7419a | ||
|
|
04b2ed9633 | ||
|
|
cf88cc4990 | ||
|
|
de2937af86 | ||
|
|
4dac65d2e4 | ||
|
|
1b9e58a8b0 | ||
|
|
d179616702 | ||
|
|
f53c64f76d | ||
|
|
537b468714 | ||
|
|
6dcef0b1e5 | ||
|
|
622fe33264 | ||
|
|
8ee81b2607 | ||
|
|
620f2e93a4 | ||
|
|
47cb6531ff | ||
|
|
5ed13ff68c | ||
|
|
0f62f864c8 | ||
|
|
47b53335af | ||
|
|
b875924c8e | ||
|
|
7c002e2871 | ||
|
|
666d0e7c08 | ||
|
|
737ea34a24 | ||
|
|
49bff10ac5 | ||
|
|
1b4c01015b | ||
|
|
995cdf330e | ||
|
|
cbea727f08 | ||
|
|
152ec404bf | ||
|
|
5f6b324c00 | ||
|
|
395aa410f2 | ||
|
|
1f81e864bc | ||
|
|
8849d1f4f2 | ||
|
|
ada530cfef | ||
|
|
1057d55e36 | ||
|
|
cafe4254f9 |
@@ -9,9 +9,9 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.17.0",
|
||||
"@redocly/cli": "2.15.1",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mind-elixir/node-menu": "5.0.1",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@preact/signals": "2.7.1",
|
||||
"@preact/signals": "2.6.2",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
@@ -44,7 +44,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.1",
|
||||
"globals": "17.3.0",
|
||||
"i18next": "25.8.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -56,7 +56,7 @@
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.8.0",
|
||||
"mind-elixir": "5.7.1",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.3",
|
||||
|
||||
@@ -13,12 +13,13 @@ import type LoadResults from "../services/load_results.js";
|
||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import options from "../services/options.js";
|
||||
import toast from "../services/toast.js";
|
||||
import utils, { hasTouchBar } from "../services/utils.js";
|
||||
import utils, { dynamicRequire, hasTouchBar } from "../services/utils.js";
|
||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||
import type RootContainer from "../widgets/containers/root_container.js";
|
||||
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
||||
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import { ImportPreviewData } from "../widgets/dialogs/import_preview.jsx";
|
||||
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
||||
import type { InfoProps } from "../widgets/dialogs/info.jsx";
|
||||
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
||||
@@ -130,6 +131,7 @@ export type CommandMappings = {
|
||||
showConfirmDialog: ConfirmWithMessageOptions;
|
||||
showRecentChanges: CommandData & { ancestorNoteId: string };
|
||||
showImportDialog: CommandData & { noteId: string };
|
||||
showImportPreviewDialog: CommandData & ImportPreviewData;
|
||||
openNewNoteSplit: NoteCommandData;
|
||||
openInWindow: NoteCommandData;
|
||||
openInPopup: CommandData & { noteIdOrPath: string; };
|
||||
@@ -639,6 +641,10 @@ export class AppContext extends Component {
|
||||
this.child(rootWidget as Component);
|
||||
|
||||
this.triggerEvent("initialRenderComplete", {});
|
||||
if (utils.isElectron()) {
|
||||
const { ipcRenderer } = dynamicRequire('electron');
|
||||
ipcRenderer.send("initial-render-complete");
|
||||
}
|
||||
}
|
||||
|
||||
triggerEvent<K extends EventNames>(name: K, data: EventData<K>) {
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import type RootContainer from "../widgets/containers/root_container.js";
|
||||
|
||||
import AboutDialog from "../widgets/dialogs/about.js";
|
||||
import HelpDialog from "../widgets/dialogs/help.js";
|
||||
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
|
||||
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
|
||||
import PromptDialog from "../widgets/dialogs/prompt.js";
|
||||
import AddLinkDialog from "../widgets/dialogs/add_link.js";
|
||||
import IncludeNoteDialog from "../widgets/dialogs/include_note.js";
|
||||
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
|
||||
import BranchPrefixDialog from "../widgets/dialogs/branch_prefix.js";
|
||||
import SortChildNotesDialog from "../widgets/dialogs/sort_child_notes.js";
|
||||
import NoteTypeChooserDialog from "../widgets/dialogs/note_type_chooser.js";
|
||||
import MoveToDialog from "../widgets/dialogs/move_to.js";
|
||||
import CloneToDialog from "../widgets/dialogs/clone_to.js";
|
||||
import ImportDialog from "../widgets/dialogs/import.js";
|
||||
import ExportDialog from "../widgets/dialogs/export.js";
|
||||
import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js";
|
||||
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
|
||||
import ConfirmDialog from "../widgets/dialogs/confirm.js";
|
||||
import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||
import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
|
||||
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
|
||||
import CloneToDialog from "../widgets/dialogs/clone_to.js";
|
||||
import ConfirmDialog from "../widgets/dialogs/confirm.js";
|
||||
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
|
||||
import ExportDialog from "../widgets/dialogs/export.js";
|
||||
import HelpDialog from "../widgets/dialogs/help.js";
|
||||
import ImportDialog from "../widgets/dialogs/import.js";
|
||||
import ImportPreviewDialog from "../widgets/dialogs/import_preview.jsx";
|
||||
import IncludeNoteDialog from "../widgets/dialogs/include_note.js";
|
||||
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
|
||||
import InfoDialog from "../widgets/dialogs/info.js";
|
||||
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
|
||||
import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js";
|
||||
import MoveToDialog from "../widgets/dialogs/move_to.js";
|
||||
import NoteTypeChooserDialog from "../widgets/dialogs/note_type_chooser.js";
|
||||
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
|
||||
import PromptDialog from "../widgets/dialogs/prompt.js";
|
||||
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
|
||||
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
|
||||
import RevisionsDialog from "../widgets/dialogs/revisions.js";
|
||||
import SortChildNotesDialog from "../widgets/dialogs/sort_child_notes.js";
|
||||
import ToastContainer from "../widgets/Toast.jsx";
|
||||
|
||||
export function applyModals(rootContainer: RootContainer) {
|
||||
@@ -52,5 +52,6 @@ export function applyModals(rootContainer: RootContainer) {
|
||||
.child(<IncorrectCpuArchDialog />)
|
||||
.child(<PopupEditorDialog />)
|
||||
.child(<CallToActionDialog />)
|
||||
.child(<ToastContainer />);
|
||||
.child(<ToastContainer />)
|
||||
.child(<ImportPreviewDialog />);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ export type PrintReport = {
|
||||
} | {
|
||||
type: "collection";
|
||||
ignoredNoteIds: string[];
|
||||
} | {
|
||||
type: "error";
|
||||
message: string;
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import server from "./server.js";
|
||||
import ws from "./ws.js";
|
||||
import utils from "./utils.js";
|
||||
import { ImportPreviewResponse, WebSocketMessage } from "@triliumnext/commons";
|
||||
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { WebSocketMessage } from "@triliumnext/commons";
|
||||
import server from "./server.js";
|
||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import utils from "./utils.js";
|
||||
import ws from "./ws.js";
|
||||
|
||||
type BooleanLike = boolean | "true" | "false";
|
||||
type BooleanLike = "true" | "false";
|
||||
|
||||
export interface UploadFilesOptions {
|
||||
safeImport?: BooleanLike;
|
||||
@@ -48,7 +49,7 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file
|
||||
dataType: "json",
|
||||
type: "POST",
|
||||
timeout: 60 * 60 * 1000,
|
||||
error: function (xhr) {
|
||||
error (xhr) {
|
||||
toastService.showError(t("import.failed", { message: xhr.responseText }));
|
||||
},
|
||||
contentType: false, // NEEDED, DON'T REMOVE THIS
|
||||
@@ -57,6 +58,64 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFilesWithPreview(files: string[] | File[]) {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = utils.randomString(10);
|
||||
const results: ImportPreviewResponse[] = [];
|
||||
for (const file of files) {
|
||||
const formData = new FormData();
|
||||
formData.append("upload", file);
|
||||
formData.append("taskId", taskId);
|
||||
|
||||
results.push(await $.ajax({
|
||||
url: `${window.glob.baseApiUrl}notes/preview-import`,
|
||||
headers: await server.getHeaders(),
|
||||
data: formData,
|
||||
dataType: "json",
|
||||
type: "POST",
|
||||
timeout: 60 * 60 * 1000,
|
||||
error (xhr) {
|
||||
toastService.showError(t("import.failed", { message: xhr.responseText }));
|
||||
},
|
||||
contentType: false, // NEEDED, DON'T REMOVE THIS
|
||||
processData: false // NEEDED, DON'T REMOVE THIS
|
||||
}));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function executeUploadWithPreview(parentNoteId: string, files: ImportPreviewResponse[], options: UploadFilesOptions) {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = utils.randomString(10);
|
||||
let counter = 0;
|
||||
|
||||
for (const file of files) {
|
||||
counter++;
|
||||
|
||||
server.post(
|
||||
`notes/${parentNoteId}/execute-import`,
|
||||
{
|
||||
...options,
|
||||
id: file.id,
|
||||
taskId,
|
||||
last: counter === files.length ? "true" : "false"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelUploadWithPreview(files: ImportPreviewResponse[]) {
|
||||
for (const file of files) {
|
||||
server.remove(`notes/preview-import/${file.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
function makeToast(id: string, message: string): ToastOptionsWithRequiredId {
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -913,6 +913,10 @@ export function handleRightToLeftPlacement<T extends string>(placement: T) {
|
||||
return placement;
|
||||
}
|
||||
|
||||
export function boolToString(value: boolean) {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadFrontendApp,
|
||||
restartDesktopApp,
|
||||
|
||||
@@ -800,18 +800,3 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
background: var(--hover-item-background-color);
|
||||
color: var(--hover-item-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
* Alert bars
|
||||
*/
|
||||
|
||||
div.alert {
|
||||
margin-bottom: 8px;
|
||||
background: var(--alert-bar-background) !important;
|
||||
border-radius: 8px;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
div.alert p + p {
|
||||
margin-block: 1em 0;
|
||||
}
|
||||
@@ -84,22 +84,6 @@ button.btn.btn-success kbd {
|
||||
letter-spacing: 0.5pt;
|
||||
}
|
||||
|
||||
/*
|
||||
* Low profile buttons
|
||||
*/
|
||||
|
||||
button.tn-low-profile {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button.tn-low-profile:hover {
|
||||
background-color: var(--icon-button-hover-background);
|
||||
}
|
||||
|
||||
/*
|
||||
* Icon buttons
|
||||
*/
|
||||
@@ -810,35 +794,3 @@ input[type="range"] {
|
||||
scrollbar-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Centered forms
|
||||
*/
|
||||
|
||||
.tn-centered-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 20vh;
|
||||
}
|
||||
|
||||
.tn-centered-form .form-group {
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.tn-centered-form .form-icon {
|
||||
font-size: 140px;
|
||||
color: var(--main-border-color);
|
||||
}
|
||||
|
||||
.tn-centered-form .protected-session-password {
|
||||
margin-inline: auto;
|
||||
max-width: 350px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tn-centered-form input,
|
||||
.tn-centered-form button {
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -265,6 +265,13 @@ body.desktop .options-section:not(.tn-no-card) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.options-section .alert {
|
||||
margin-bottom: 8px;
|
||||
background: var(--alert-bar-background) !important;
|
||||
border-radius: 8px;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
nav.options-section-tabs {
|
||||
min-width: var(--options-card-min-width);
|
||||
max-width: var(--options-card-max-width);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Über Trilium Notes",
|
||||
"title": "Über Trilium Notizen",
|
||||
"homepage": "Startseite:",
|
||||
"app_version": "App-Version:",
|
||||
"db_version": "DB-Version:",
|
||||
@@ -662,8 +662,7 @@
|
||||
"show-cheatsheet": "Cheatsheet anzeigen",
|
||||
"toggle-zen-mode": "Zen Modus",
|
||||
"new-version-available": "Neues Update verfügbar",
|
||||
"download-update": "Version {{latestVersion}} herunterladen",
|
||||
"search_notes": "Notizen durchsuchen"
|
||||
"download-update": "Version {{latestVersion}} herunterladen"
|
||||
},
|
||||
"sync_status": {
|
||||
"unknown": "<p>Der Synchronisations-Status wird bekannt, sobald der nächste Synchronisierungsversuch gestartet wird.</p><p>Klicke, um eine Synchronisierung jetzt auszulösen.</p>",
|
||||
@@ -759,8 +758,7 @@
|
||||
"error_cannot_get_branch_id": "BranchId für notePath „{{notePath}}“ kann nicht abgerufen werden",
|
||||
"error_unrecognized_command": "Unbekannter Befehl {{command}}",
|
||||
"note_revisions": "Notiz Revisionen",
|
||||
"backlinks": "Rücklinks",
|
||||
"content_language_switcher": "Inhaltssprache: {{language}}"
|
||||
"backlinks": "Rücklinks"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Notiz-Icon ändern",
|
||||
@@ -912,8 +910,7 @@
|
||||
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
|
||||
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
|
||||
"actions_executed": "Aktionen wurden ausgeführt.",
|
||||
"view_options": "Optionen anzeigen:",
|
||||
"option": "Option"
|
||||
"view_options": "Optionen anzeigen:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Ähnliche Notizen",
|
||||
@@ -2280,8 +2277,5 @@
|
||||
"title_one": "{{count}} Tab",
|
||||
"title_other": "{{count}} Tabs",
|
||||
"more_options": "Weitere Optionen"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Lesezeichen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1070,12 +1070,10 @@
|
||||
"note_detail_render_help_1": "This help note is shown because this note of type Render HTML doesn't have required relation to function properly.",
|
||||
"note_detail_render_help_2": "Render HTML note type is used for <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In short, you have a HTML code note (optionally with some JavaScript) and this note will render it. To make it work, you need to define a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> called \"renderNote\" pointing to the HTML note to render."
|
||||
},
|
||||
"web_view_setup": {
|
||||
"title": "Create a live view of a webpage directly into Trilium",
|
||||
"url_placeholder": "Enter or paste the website address, for example https://triliumnotes.org",
|
||||
"create_button": "Create Web View",
|
||||
"invalid_url_title": "Invalid address",
|
||||
"invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org."
|
||||
"web_view": {
|
||||
"web_view": "Web View",
|
||||
"embed_websites": "Note of type Web View allows you to embed websites into Trilium.",
|
||||
"create_label": "To start, please create a label with a URL address you want to embed, e.g. #webViewSrc=\"https://www.google.com\""
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Refresh"
|
||||
@@ -1591,8 +1589,7 @@
|
||||
"description": "Description",
|
||||
"reload_app": "Reload app to apply changes",
|
||||
"set_all_to_default": "Set all shortcuts to the default",
|
||||
"confirm_reset": "Do you really want to reset all keyboard shortcuts to the default?",
|
||||
"no_results": "No shortcuts found matching '{{filter}}'"
|
||||
"confirm_reset": "Do you really want to reset all keyboard shortcuts to the default?"
|
||||
},
|
||||
"spellcheck": {
|
||||
"title": "Spell Check",
|
||||
@@ -1798,8 +1795,6 @@
|
||||
"printing": "Printing in progress...",
|
||||
"printing_pdf": "Exporting to PDF in progress...",
|
||||
"print_report_title": "Print report",
|
||||
"print_report_error_title": "Failed to print",
|
||||
"print_report_stack_trace": "Stack trace",
|
||||
"print_report_collection_content_one": "{{count}} note in the collection could not be printed because they are not supported or they are protected.",
|
||||
"print_report_collection_content_other": "{{count}} notes in the collection could not be printed because they are not supported or they are protected.",
|
||||
"print_report_collection_details_button": "See details",
|
||||
@@ -2104,8 +2099,7 @@
|
||||
"raster": "Raster",
|
||||
"vector_light": "Vector (Light)",
|
||||
"vector_dark": "Vector (Dark)",
|
||||
"show-scale": "Show scale",
|
||||
"show-labels": "Show marker names"
|
||||
"show-scale": "Show scale"
|
||||
},
|
||||
"table_context_menu": {
|
||||
"delete_row": "Delete row"
|
||||
@@ -2289,5 +2283,37 @@
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Bookmarks"
|
||||
},
|
||||
"import_preview": {
|
||||
"intro_safe_one": "You are about to import an archive with no unsafe content.",
|
||||
"intro_safe_other": "You are about to import {{count}} archives with no unsafe content.",
|
||||
"intro_unsafe_one": "You are about to import an archive with active content.",
|
||||
"intro_unsafe_other": "You are about to import {{count}} archives, some of which have active content.",
|
||||
|
||||
"title": "Import preview",
|
||||
"notes_count_one": "{{count}} note",
|
||||
"notes_count_other": "{{count}} notes",
|
||||
"attributes_count_one": "{{count}} attribute",
|
||||
"attributes_count_other": "{{count}} attributes",
|
||||
"attachments_count_one": "{{count}} attachment",
|
||||
"attachments_count_other": "{{count}} attachments",
|
||||
"cancel": "Cancel",
|
||||
"import": "Import",
|
||||
"import_with_timeout": "Import ({{timeout}})",
|
||||
"import_safely": "Import safely (recommended)",
|
||||
"import_safely_description": "Scripts, widgets and icon packs will be disabled.",
|
||||
"import_trust": "Trust and enable active content",
|
||||
"import_trust_description": "Only do this if you trust the source.",
|
||||
"parent_note": "Parent note",
|
||||
"badge_client_side_scripting_title": "Client-side scripting",
|
||||
"badge_client_side_scripting_tooltip": "Can modify the application's interface and send requests to the server. Malicious scripts can read or change your notes.",
|
||||
"badge_server_side_scripting_title": "Server-side scripting",
|
||||
"badge_server_side_scripting_tooltip": "Runs on the server with access to your database and local files. Malicious scripts could read, modify, or delete your data.",
|
||||
"badge_code_execution_title": "Code execution",
|
||||
"badge_code_execution_description": "Allows running arbitrary programs on your system or server. This can fully compromise your data and environment.",
|
||||
"badge_icon_pack_title": "Icon pack",
|
||||
"badge_icon_pack_description": "Provides custom icons. Usually safe, but malformed or very large icon packs may cause performance or stability issues.",
|
||||
"badge_web_view_title": "Web view",
|
||||
"badge_web_view_description": "Displays external web pages inside Trilium. These pages may track activity or receive information about your notes."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,8 +662,7 @@
|
||||
"show-cheatsheet": "Mostrar hoja de trucos",
|
||||
"toggle-zen-mode": "Modo Zen",
|
||||
"new-version-available": "Nueva actualización disponible",
|
||||
"download-update": "Obtener versión {{latestVersion}}",
|
||||
"search_notes": "Buscar notas"
|
||||
"download-update": "Obtener versión {{latestVersion}}"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Salir del modo Zen"
|
||||
@@ -763,8 +762,7 @@
|
||||
"error_cannot_get_branch_id": "No se puede obtener el branchID del notePath '{{notePath}}'",
|
||||
"error_unrecognized_command": "Comando no reconocido {{command}}",
|
||||
"note_revisions": "Revisiones de notas",
|
||||
"backlinks": "Vínculos de retroceso",
|
||||
"content_language_switcher": "Idioma de contenido: {{language}}"
|
||||
"backlinks": "Vínculos de retroceso"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Cambiar icono de nota",
|
||||
@@ -917,8 +915,7 @@
|
||||
"unknown_search_option": "Opción de búsqueda desconocida {{searchOptionName}}",
|
||||
"search_note_saved": "La nota de búsqueda se ha guardado en {{- notePathTitle}}",
|
||||
"actions_executed": "Las acciones han sido ejecutadas.",
|
||||
"view_options": "Ver opciones:",
|
||||
"option": "opción"
|
||||
"view_options": "Ver opciones:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Notas similares",
|
||||
@@ -2295,8 +2292,5 @@
|
||||
"title_many": "{{count}} pestañas",
|
||||
"title_other": "{{count}} pestañas",
|
||||
"more_options": "Más opciones"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Marcadores"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
"show-cheatsheet": "Taispeáin Bileog leideanna",
|
||||
"toggle-zen-mode": "Mód Zen",
|
||||
"new-version-available": "Nuashonrú Nua ar Fáil",
|
||||
"download-update": "Faigh Leagan {{latestVersion}}",
|
||||
"search_notes": "Cuardaigh nótaí"
|
||||
"download-update": "Faigh Leagan {{latestVersion}}"
|
||||
},
|
||||
"about": {
|
||||
"title": "Maidir le Trilium Notes",
|
||||
@@ -765,8 +764,7 @@
|
||||
"note_revisions": "Athbhreithnithe nóta",
|
||||
"error_cannot_get_branch_id": "Ní féidir aitheantas brainse a fháil do NotePad '{{notePath}}'",
|
||||
"error_unrecognized_command": "Ordú gan aitheantas {{command}}",
|
||||
"backlinks": "Naisc ar ais",
|
||||
"content_language_switcher": "Teanga an ábhair: {{language}}"
|
||||
"backlinks": "Naisc ar ais"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Deilbhín nóta athraithe",
|
||||
@@ -921,8 +919,7 @@
|
||||
"unknown_search_option": "Rogha cuardaigh anaithnid {{searchOptionName}}",
|
||||
"search_note_saved": "Tá an nóta cuardaigh sábháilte i {{- notePathTitle}}",
|
||||
"actions_executed": "Tá gníomhartha curtha i gcrích.",
|
||||
"view_options": "Roghanna féachana:",
|
||||
"option": "rogha"
|
||||
"view_options": "Roghanna féachana:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Nótaí Comhchosúla",
|
||||
@@ -2325,8 +2322,5 @@
|
||||
"title_many": "{{count}} cluaisíní",
|
||||
"title_other": "{{count}} cluaisíní",
|
||||
"more_options": "Tuilleadh roghanna"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Leabharmharcanna"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Nie udało się załadować niestandardowego skryptu",
|
||||
"message": "Skrypt nie mógł zostać wykonany z powodu:\n\n{{message}}"
|
||||
"message": "Skrypt z notatki o ID \"{{id}}\", zatytułowany \"{{title}}\", nie mógł zostać wykonany z powodu:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Nie udało się pobrać listy widżetów z serwera"
|
||||
@@ -29,9 +29,8 @@
|
||||
"widget-render-error": {
|
||||
"title": "Nie udało się wyrenderować niestandardowego widżetu React"
|
||||
},
|
||||
"widget-missing-parent": "Niestandardowy widżet nie ma zdefiniowanej obowiązkowej właściwości „{{property}}”.\nJeśli skrypt ma działać bez interfejsu użytkownika (UI) wyłącz go: '#run=frontendStartup'.",
|
||||
"open-script-note": "Otwórz notatkę ze skryptem",
|
||||
"scripting-error": "Błąd skryptu użytkownika: {{title}}"
|
||||
"widget-missing-parent": "Niestandardowy widżet nie ma zdefiniowanej obowiązkowej właściwości „{{property}}”.",
|
||||
"open-script-note": "Otwórz notatkę ze skryptem"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Dodaj link",
|
||||
@@ -192,8 +191,7 @@
|
||||
"expand_tooltip": "Rozwija bezpośrednie elementy podrzędne tej kolekcji (o jeden poziom). Aby uzyskać więcej opcji, naciśnij strzałkę po prawej.",
|
||||
"expand_first_level": "Rozwiń bezpośrednie elementy podrzędne",
|
||||
"expand_nth_level": "Rozwiń {{depth}} poziomów",
|
||||
"expand_all_levels": "Rozwiń wszystkie poziomy",
|
||||
"hide_child_notes": "Ukryj notatki podrzędne w derzwie"
|
||||
"expand_all_levels": "Rozwiń wszystkie poziomy"
|
||||
},
|
||||
"board_view": {
|
||||
"move-to": "Przenieś do",
|
||||
@@ -242,7 +240,7 @@
|
||||
"background_effects_title": "Efekty tła są teraz stabilne",
|
||||
"dismiss": "Odrzuć",
|
||||
"background_effects_button": "Włącz efekty tła",
|
||||
"background_effects_message": "Na urządzeniach z systemem Windows i macOS efekty tła są stabilne. Efekty tła dodają odrobinę koloru do interfejsu użytkownika poprzez rozmycie tła za nim.",
|
||||
"background_effects_message": "Na urządzeniach z systemem Windows efekty tła są teraz w pełni stabilne. Efekty tła dodają odrobinę koloru do interfejsu użytkownika poprzez rozmycie tła za nim. Ta technika jest również stosowana w innych aplikacjach, takich jak Eksplorator Windows.",
|
||||
"new_layout_title": "Nowy układ",
|
||||
"new_layout_message": "Wprowadziliśmy zmodernizowany układ interfejsu dla Trilium. Wstążka została usunięta i płynnie zintegrowana z głównym interfejsem, a jej kluczowe funkcje przejęły nowy pasek stanu i rozwijane sekcje (takie jak promowane atrybuty).\n\nNowy układ jest domyślnie włączony i można go tymczasowo wyłączyć w Ustawienia → Wygląd.",
|
||||
"new_layout_button": "Szczegóły"
|
||||
@@ -522,8 +520,7 @@
|
||||
"action": "akcja",
|
||||
"search_button": "Szukaj",
|
||||
"search_execute": "Szukaj i wykonaj akcje",
|
||||
"view_options": "Ustawienia widoku:",
|
||||
"option": "opcja"
|
||||
"view_options": "Ustawienia widoku:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Podobne notatki",
|
||||
@@ -605,8 +602,8 @@
|
||||
"desktop-application": "Aplikacja desktopowa",
|
||||
"native-title-bar": "Natywny pasek tytułu",
|
||||
"native-title-bar-description": "Dla Windows i macOS, wyłączenie natywnego paska tytułu sprawia, że aplikacja wygląda bardziej kompaktowo. Na Linuxie, włączenie natywnego paska tytułu lepiej integruje się z resztą systemu.",
|
||||
"background-effects": "Włącz efekty tła",
|
||||
"background-effects-description": "Dodaje rozmyte, stylowe tło do okien aplikacji, tworząc głębię i nowoczesny wygląd. \"Natywny pasek tytułu\" musi być wyłączony.",
|
||||
"background-effects": "Włącz efekty tła (tylko Windows 11)",
|
||||
"background-effects-description": "Efekt Mica dodaje rozmyte, stylowe tło do okien aplikacji, tworząc głębię i nowoczesny wygląd. \"Natywny pasek tytułu\" musi być wyłączony.",
|
||||
"restart-app-button": "Zrestartuj aplikację, aby zobaczyć zmiany",
|
||||
"zoom-factor": "Współczynnik powiększenia"
|
||||
},
|
||||
@@ -1185,8 +1182,7 @@
|
||||
"show-cheatsheet": "Pokaż ściągawkę",
|
||||
"toggle-zen-mode": "Tryb Zen",
|
||||
"new-version-available": "Dostępna nowa aktualizacja",
|
||||
"download-update": "Pobierz wersję {{latestVersion}}",
|
||||
"search_notes": "Przeszukaj notatki"
|
||||
"download-update": "Pobierz wersję {{latestVersion}}"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Wyjdź z trybu Zen"
|
||||
@@ -1269,7 +1265,7 @@
|
||||
"button_title": "Eksportuj diagram jako SVG"
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"create_child_note_title": "Utwórz notatkę podrzędną i dodaj ją do mapy",
|
||||
"create_child_note_title": "Utwórz nową notatkę podrzędną i dodaj ją do tej mapy relacji",
|
||||
"reset_pan_zoom_title": "Zresetuj przesunięcie i powiększenie do początkowych współrzędnych i powiększenia",
|
||||
"zoom_in_title": "Powiększ",
|
||||
"zoom_out_title": "Pomniejsz"
|
||||
@@ -1285,23 +1281,12 @@
|
||||
"delete_this_note": "Usuń tę notatkę",
|
||||
"note_revisions": "Wersje notatki",
|
||||
"error_cannot_get_branch_id": "Nie można pobrać branchId dla ścieżki notatki '{{notePath}}'",
|
||||
"error_unrecognized_command": "Nierozpoznane polecenie {{command}}",
|
||||
"backlinks": "Linki zwrotne",
|
||||
"content_language_switcher": "Język treści: {{language}}"
|
||||
"error_unrecognized_command": "Nierozpoznane polecenie {{command}}"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Zmień ikonę notatki",
|
||||
"search": "Szukaj:",
|
||||
"reset-default": "Przywróć domyślną ikonę",
|
||||
"search_placeholder_one": "Znaleziono {{number}} ikonę w {{count}} pakietach",
|
||||
"search_placeholder_few": "Znaleziono {{number}} ikon w {{count}} pakietach",
|
||||
"search_placeholder_many": "Znaleziono {{number}} ikon w {{count}} pakietach",
|
||||
"search_placeholder_filtered": "Wyszukaj {{number}} ikon w {{name}}",
|
||||
"filter": "Filtr",
|
||||
"filter-none": "Wszystkie ikony",
|
||||
"filter-default": "Domyślne ikony",
|
||||
"icon_tooltip": "{{name}}\npakiet ikon: {{iconPack}}",
|
||||
"no_results": "Nie znaleziono ikon."
|
||||
"reset-default": "Przywróć domyślną ikonę"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Typ notatki",
|
||||
@@ -1841,7 +1826,7 @@
|
||||
"will_be_deleted_in": "Ten załącznik zostanie automatycznie usunięty za {{time}}",
|
||||
"will_be_deleted_soon": "Ten załącznik zostanie wkrótce automatycznie usunięty",
|
||||
"deletion_reason": ", ponieważ załącznik nie jest podlinkowany w treści notatki. Aby zapobiec usunięciu, dodaj link do załącznika z powrotem do treści lub przekonwertuj załącznik na notatkę.",
|
||||
"role_and_size": "Rola: {{role}}, Rozmiar: {{size}}, MIME: {{- mimeType}}",
|
||||
"role_and_size": "Rola: {{role}}, Rozmiar: {{size}}",
|
||||
"link_copied": "Link do załącznika skopiowany do schowka.",
|
||||
"unrecognized_role": "Nierozpoznana rola załącznika '{{role}}'."
|
||||
},
|
||||
@@ -1895,10 +1880,7 @@
|
||||
"apply-bulk-actions": "Zastosuj akcje masowe",
|
||||
"converted-to-attachments": "{{count}} notatek zostało przekonwertowanych na załączniki.",
|
||||
"convert-to-attachment-confirm": "Czy na pewno chcesz przekonwertować wybrane notatki na załączniki ich notatek nadrzędnych? Ta operacja dotyczy tylko notatek Obrazów, inne notatki zostaną pominięte.",
|
||||
"open-in-popup": "Szybka edycja",
|
||||
"open-in-a-new-window": "Otwórz w nowym oknie",
|
||||
"hide-subtree": "Ukryj gałąź",
|
||||
"show-subtree": "Rozwiń gałąź"
|
||||
"open-in-popup": "Szybka edycja"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "Ta notatka jest udostępniona publicznie pod adresem {{- link}}.",
|
||||
@@ -1989,17 +1971,7 @@
|
||||
"create-child-note": "Utwórz notatkę podrzędną",
|
||||
"unhoist": "Cofnij zawężenie",
|
||||
"toggle-sidebar": "Przełącz pasek boczny",
|
||||
"dropping-not-allowed": "Upuszczanie notatek w tej lokalizacji jest niedozwolone.",
|
||||
"clone-indicator-tooltip": "Ta notatka ma {{- count}} notatek nadrzędnych: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "Ta notatka jest sklonowana (1 dodatkowa notatka nadrzędna: {{- parent}})",
|
||||
"shared-indicator-tooltip": "Ta notatka jest udostępniona publicznie",
|
||||
"shared-indicator-tooltip-with-url": "Ta notatka jest udostępniana publicznie jako: {{- url}}",
|
||||
"subtree-hidden-tooltip_one": "{{count}} notatka podrzędna ukryta w drzewie",
|
||||
"subtree-hidden-tooltip_few": "{{count}} notatek podrzędnych ukrytych w drzewie",
|
||||
"subtree-hidden-tooltip_many": "{{count}} notatek podrzędnych ukrytych w drzewie",
|
||||
"subtree-hidden-moved-title": "Dodano do {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "Ta kolekcja ukrywa swoje notatki podrzędne w drzewie.",
|
||||
"subtree-hidden-moved-description-other": "Notatki podrzędne są ukryte w drzewie tej notatki."
|
||||
"dropping-not-allowed": "Upuszczanie notatek w tej lokalizacji jest niedozwolone."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Utrzymuj okno na wierzchu"
|
||||
@@ -2007,13 +1979,7 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Nie można znaleźć widżetu typu dla typu '{{type}}'",
|
||||
"printing": "Drukowanie w toku...",
|
||||
"printing_pdf": "Eksportowanie do PDF w toku...",
|
||||
"print_report_title": "Wydrukuj raport",
|
||||
"print_report_collection_content_one": "Nie można wydrukować {{count}} notatki w kolekcji, ponieważ nie jest ona obsługiwana lub jest chroniona.",
|
||||
"print_report_collection_content_few": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.",
|
||||
"print_report_collection_content_many": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.",
|
||||
"print_report_collection_details_button": "Zobacz szczegóły",
|
||||
"print_report_collection_details_ignored_notes": "Zignorowane notatki"
|
||||
"printing_pdf": "Eksportowanie do PDF w toku..."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "wpisz tytuł notatki tutaj...",
|
||||
@@ -2023,8 +1989,7 @@
|
||||
"note_type_switcher_others": "Inny typ notatki",
|
||||
"note_type_switcher_templates": "Szablon",
|
||||
"note_type_switcher_collection": "Kolekcja",
|
||||
"edited_notes": "Notatki edytowane dzisiaj",
|
||||
"promoted_attributes": "Sugerowane atrybuty"
|
||||
"edited_notes": "Edytowane notatki"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Nie znaleziono notatek dla podanych parametrów wyszukiwania.",
|
||||
@@ -2034,11 +1999,7 @@
|
||||
"configure_launchbar": "Konfiguruj pasek szybkiego dostępu"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "Dla tego zapytania nie zwrócono żadnych wierszy",
|
||||
"not_executed": "Zapytanie nie zostało jeszcze wykonane.",
|
||||
"failed": "Wykonanie zapytania SQL nie powiodło się",
|
||||
"statement_result": "Wynik wyrażenia",
|
||||
"execute_now": "Wykonaj teraz"
|
||||
"no_rows": "Dla tego zapytania nie zwrócono żadnych wierszy"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tabele"
|
||||
@@ -2155,8 +2116,7 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Utwórz nową notatkę podrzędną i dodaj ją do mapy",
|
||||
"create-child-note-instruction": "Kliknij na mapie, aby utworzyć nową notatkę w tej lokalizacji lub naciśnij Escape, aby anulować.",
|
||||
"unable-to-load-map": "Nie można załadować mapy.",
|
||||
"create-child-note-text": "Dodaj zaznaczenie"
|
||||
"unable-to-load-map": "Nie można załadować mapy."
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Otwórz lokalizację",
|
||||
@@ -2223,14 +2183,7 @@
|
||||
"execute_sql_description": "Ta notatka jest notatką SQL. Kliknij, aby wykonać zapytanie SQL.",
|
||||
"shared_copy_to_clipboard": "Kopiuj link do schowka",
|
||||
"shared_open_in_browser": "Otwórz link w przeglądarce",
|
||||
"shared_unshare": "Usuń udostępnienie",
|
||||
"save_status_saved": "Zapisane",
|
||||
"save_status_saving": "Zapisywanie...",
|
||||
"save_status_unsaved": "Niezapisane",
|
||||
"save_status_error": "Zapis nie powiódł się",
|
||||
"save_status_saving_tooltip": "Zmiany zostały zapisane.",
|
||||
"save_status_unsaved_tooltip": "Są niezapisane zmiany. Zostaną one zapisane automatycznie za chwilę.",
|
||||
"save_status_error_tooltip": "Wystąpił błąd podczas zapisywania notatki. Spróbuj skopiować treść notatki w inne miejsce i ponownie załadować aplikację."
|
||||
"shared_unshare": "Usuń udostępnienie"
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Zmień język treści",
|
||||
@@ -2273,30 +2226,5 @@
|
||||
"empty_button": "Ukryj panel",
|
||||
"toggle": "Pokaż/ukryj prawy panel",
|
||||
"custom_widget_go_to_source": "Przejdź do kodu źródłowego"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} załącznik",
|
||||
"attachments_few": "{{count}} załączniki",
|
||||
"attachments_many": "{{count}} załączników",
|
||||
"layers_one": "{{count}} warstwa",
|
||||
"layers_few": "{{count}} warstw",
|
||||
"layers_many": "{{count}} warstw",
|
||||
"pages_one": "{{count}} strona",
|
||||
"pages_few": "{{count}} stron",
|
||||
"pages_many": "{{count}} stron",
|
||||
"pages_alt": "Strona {{pageNumber}}",
|
||||
"pages_loading": "Wczytuję..."
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Dostępne na {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} zakładka",
|
||||
"title_few": "{{count}} zakładki",
|
||||
"title_many": "{{count}} zakładek",
|
||||
"more_options": "Więcej opcji"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Zakładki"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,33 +370,7 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) {
|
||||
}
|
||||
|
||||
function handlePrintReport(printReport?: PrintReport) {
|
||||
if (!printReport) return;
|
||||
|
||||
if (printReport.type === "error") {
|
||||
toast.showPersistent({
|
||||
id: "print-error",
|
||||
icon: "bx bx-error-circle",
|
||||
title: t("note_detail.print_report_error_title"),
|
||||
message: printReport.message,
|
||||
buttons: printReport.stack ? [
|
||||
{
|
||||
text: t("note_detail.print_report_collection_details_button"),
|
||||
onClick(api) {
|
||||
api.dismissToast();
|
||||
dialog.info(<>
|
||||
<p>{printReport.message}</p>
|
||||
<details>
|
||||
<summary>{t("note_detail.print_report_stack_trace")}</summary>
|
||||
<pre style="font-size: 0.85em; overflow-x: auto;">{printReport.stack}</pre>
|
||||
</details>
|
||||
</>, {
|
||||
title: t("note_detail.print_report_error_title")
|
||||
});
|
||||
}
|
||||
}
|
||||
] : undefined
|
||||
});
|
||||
} else if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) {
|
||||
if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) {
|
||||
toast.showPersistent({
|
||||
id: "print-report",
|
||||
icon: "bx bx-collection",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
> .collection-properties {
|
||||
position: relative;
|
||||
z-index: 998;
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
z-index: 997 !important;
|
||||
z-index: 997;
|
||||
}
|
||||
|
||||
.geo-view.placing-note .geo-map-container {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ViewModeProps } from "../interface";
|
||||
import { createNewNote, moveMarker } from "./api";
|
||||
import openContextMenu, { openMapContextMenu } from "./context_menu";
|
||||
import Map from "./map";
|
||||
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, MapLayer } from "./map_layer";
|
||||
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
|
||||
import Marker, { GpxTrack } from "./marker";
|
||||
|
||||
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
|
||||
@@ -45,11 +45,10 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
const [ state, setState ] = useState(State.Normal);
|
||||
const [ coordinates, setCoordinates ] = useState(viewConfig?.view?.center);
|
||||
const [ zoom, setZoom ] = useState(viewConfig?.view?.zoom);
|
||||
const [ layerName ] = useNoteLabel(note, "map:style");
|
||||
const [ hasScale ] = useNoteLabelBoolean(note, "map:scale");
|
||||
const [ hideLabels ] = useNoteLabelBoolean(note, "map:hideLabels");
|
||||
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const [ notes, setNotes ] = useState<FNote[]>([]);
|
||||
const layerData = useLayerData(note);
|
||||
const spacedUpdate = useSpacedUpdate(() => {
|
||||
if (viewConfig) {
|
||||
saveConfig(viewConfig);
|
||||
@@ -153,7 +152,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
apiRef={apiRef} containerRef={containerRef}
|
||||
coordinates={coordinates}
|
||||
zoom={zoom}
|
||||
layerData={layerData}
|
||||
layerName={layerName ?? DEFAULT_MAP_LAYER_NAME}
|
||||
viewportChanged={(coordinates, zoom) => {
|
||||
if (!viewConfig) viewConfig = {};
|
||||
viewConfig.view = { center: coordinates, zoom };
|
||||
@@ -163,35 +162,13 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
|
||||
onContextMenu={onContextMenu}
|
||||
scale={hasScale}
|
||||
>
|
||||
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} hideLabels={hideLabels} />)}
|
||||
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} />)}
|
||||
</Map>}
|
||||
<GeoMapTouchBar state={state} map={apiRef.current} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useLayerData(note: FNote) {
|
||||
const [ layerName ] = useNoteLabel(note, "map:style");
|
||||
// Memo is needed because it would generate unnecessary reloads due to layer change.
|
||||
const layerData = useMemo(() => {
|
||||
// Custom layers.
|
||||
if (layerName?.startsWith("http")) {
|
||||
return {
|
||||
name: "Custom",
|
||||
type: "raster",
|
||||
url: layerName,
|
||||
attribution: ""
|
||||
} satisfies MapLayer;
|
||||
}
|
||||
|
||||
// Built-in layers.
|
||||
const layerData = MAP_LAYERS[layerName ?? ""] ?? MAP_LAYERS[DEFAULT_MAP_LAYER_NAME];
|
||||
return layerData;
|
||||
}, [ layerName ]);
|
||||
|
||||
return layerData;
|
||||
}
|
||||
|
||||
function ToggleReadOnlyButton({ note }: { note: FNote }) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
|
||||
@@ -202,26 +179,22 @@ function ToggleReadOnlyButton({ note }: { note: FNote }) {
|
||||
/>;
|
||||
}
|
||||
|
||||
function NoteWrapper({ note, isReadOnly, hideLabels }: {
|
||||
note: FNote,
|
||||
isReadOnly: boolean,
|
||||
hideLabels: boolean
|
||||
}) {
|
||||
function NoteWrapper({ note, isReadOnly }: { note: FNote, isReadOnly: boolean }) {
|
||||
const mime = useNoteProperty(note, "mime");
|
||||
const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE);
|
||||
|
||||
if (mime === "application/gpx+xml") {
|
||||
return <NoteGpxTrack note={note} hideLabels={hideLabels} />;
|
||||
return <NoteGpxTrack note={note} />;
|
||||
}
|
||||
|
||||
if (location) {
|
||||
const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined;
|
||||
if (!latLng) return;
|
||||
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} hideLabels={hideLabels} />;
|
||||
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} />;
|
||||
}
|
||||
}
|
||||
|
||||
function NoteMarker({ note, editable, latLng, hideLabels }: { note: FNote, editable: boolean, latLng: [number, number], hideLabels: boolean }) {
|
||||
function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean, latLng: [number, number] }) {
|
||||
// React to changes
|
||||
const [ color ] = useNoteLabel(note, "color");
|
||||
const [ iconClass ] = useNoteLabel(note, "iconClass");
|
||||
@@ -229,9 +202,8 @@ function NoteMarker({ note, editable, latLng, hideLabels }: { note: FNote, edita
|
||||
|
||||
const title = useNoteProperty(note, "title");
|
||||
const icon = useMemo(() => {
|
||||
const titleOrNone = hideLabels ? undefined : title;
|
||||
return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, titleOrNone, note.noteId, archived);
|
||||
}, [ iconClass, color, title, note.noteId, archived, hideLabels ]);
|
||||
return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, title, note.noteId, archived);
|
||||
}, [ iconClass, color, title, note.noteId, archived]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId });
|
||||
@@ -263,7 +235,7 @@ function NoteMarker({ note, editable, latLng, hideLabels }: { note: FNote, edita
|
||||
/>;
|
||||
}
|
||||
|
||||
function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean }) {
|
||||
function NoteGpxTrack({ note }: { note: FNote }) {
|
||||
const [ xmlString, setXmlString ] = useState<string>();
|
||||
const blob = useNoteBlob(note);
|
||||
|
||||
@@ -284,7 +256,7 @@ function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean
|
||||
|
||||
const options = useMemo<GPXOptions>(() => ({
|
||||
markers: {
|
||||
startIcon: buildIcon(note.getIcon(), note.getColorClass(), hideLabels ? undefined : note.title),
|
||||
startIcon: buildIcon(note.getIcon(), note.getColorClass(), note.title),
|
||||
endIcon: buildIcon("bxs-flag-checkered"),
|
||||
wptIcons: {
|
||||
"": buildIcon("bx bx-pin")
|
||||
@@ -293,7 +265,7 @@ function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean
|
||||
polyline_options: {
|
||||
color: note.getLabelValue("color") ?? "blue"
|
||||
}
|
||||
}), [ color, iconClass, hideLabels ]);
|
||||
}), [ color, iconClass ]);
|
||||
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks";
|
||||
import L, { control, LatLng, Layer, LeafletMouseEvent } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { MAP_LAYERS, type MapLayer } from "./map_layer";
|
||||
import { MAP_LAYERS } from "./map_layer";
|
||||
import { ComponentChildren, createContext, RefObject } from "preact";
|
||||
import { useElementSize, useSyncedRef } from "../../react/hooks";
|
||||
|
||||
@@ -12,7 +12,7 @@ interface MapProps {
|
||||
containerRef?: RefObject<HTMLDivElement>;
|
||||
coordinates: LatLng | [number, number];
|
||||
zoom: number;
|
||||
layerData: MapLayer;
|
||||
layerName: string;
|
||||
viewportChanged: (coordinates: LatLng, zoom: number) => void;
|
||||
children: ComponentChildren;
|
||||
onClick?: (e: LeafletMouseEvent) => void;
|
||||
@@ -21,7 +21,7 @@ interface MapProps {
|
||||
scale: boolean;
|
||||
}
|
||||
|
||||
export default function Map({ coordinates, zoom, layerData, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
|
||||
export default function Map({ coordinates, zoom, layerName, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
|
||||
const mapRef = useRef<L.Map>(null);
|
||||
const containerRef = useSyncedRef<HTMLDivElement>(_containerRef);
|
||||
|
||||
@@ -49,6 +49,8 @@ export default function Map({ coordinates, zoom, layerData, viewportChanged, chi
|
||||
const [ layer, setLayer ] = useState<Layer>();
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const layerData = MAP_LAYERS[layerName];
|
||||
|
||||
if (layerData.type === "vector") {
|
||||
const style = (typeof layerData.style === "string" ? layerData.style : await layerData.style());
|
||||
await import("@maplibre/maplibre-gl-leaflet");
|
||||
@@ -66,7 +68,7 @@ export default function Map({ coordinates, zoom, layerData, viewportChanged, chi
|
||||
}
|
||||
|
||||
load();
|
||||
}, [ layerData ]);
|
||||
}, [ layerName ]);
|
||||
|
||||
// Attach layer to the map.
|
||||
useEffect(() => {
|
||||
@@ -137,7 +139,7 @@ export default function Map({ coordinates, zoom, layerData, viewportChanged, chi
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`geo-map-container ${layerData.isDarkTheme ? "dark" : ""}`}
|
||||
className={`geo-map-container ${MAP_LAYERS[layerName].isDarkTheme ? "dark" : ""}`}
|
||||
>
|
||||
<ParentMap.Provider value={mapRef.current}>
|
||||
{children}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
export type MapLayer = ({
|
||||
export interface MapLayer {
|
||||
name: string;
|
||||
isDarkTheme?: boolean;
|
||||
}
|
||||
|
||||
interface VectorLayer extends MapLayer {
|
||||
type: "vector";
|
||||
style: string | (() => Promise<{}>)
|
||||
} | {
|
||||
}
|
||||
|
||||
interface RasterLayer extends MapLayer {
|
||||
type: "raster";
|
||||
url: string;
|
||||
attribution: string;
|
||||
}) & {
|
||||
// Common properties
|
||||
name: string;
|
||||
isDarkTheme?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const MAP_LAYERS: Record<string, MapLayer> = {
|
||||
export const MAP_LAYERS: Record<string, VectorLayer | RasterLayer> = {
|
||||
"openstreetmap": {
|
||||
name: "OpenStreetMap",
|
||||
type: "raster",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Modal from "../react/Modal.jsx";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { ComponentChildren } from "preact";
|
||||
import appContext, { CommandNames } from "../../components/app_context.js";
|
||||
import RawHtml from "../react/RawHtml.jsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandNames } from "../../components/app_context.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import keyboard_actions from "../../services/keyboard_actions.js";
|
||||
import { Card } from "../react/Card.jsx";
|
||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||
import Modal from "../react/Modal.jsx";
|
||||
import RawHtml from "../react/RawHtml.jsx";
|
||||
|
||||
export default function HelpDialog() {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
@@ -110,7 +111,7 @@ export default function HelpDialog() {
|
||||
|
||||
function KeyboardShortcut({ commands, description }: { commands: CommandNames | CommandNames[], description: string }) {
|
||||
const [ shortcuts, setShortcuts ] = useState<string[]>([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const shortcuts: string[] = [];
|
||||
@@ -148,20 +149,6 @@ function FixedKeyboardShortcut({ keys, description }: { keys?: string[], descrip
|
||||
);
|
||||
}
|
||||
|
||||
function Card({ title, children }: { title: string, children: ComponentChildren }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{title}</h5>
|
||||
|
||||
<p className="card-text">
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function editShortcuts() {
|
||||
appContext.tabManager.openContextWithNote("_optionsShortcuts", { activate: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import importService, { UploadFilesOptions } from "../../services/import";
|
||||
import tree from "../../services/tree";
|
||||
import { boolToString } from "../../services/utils";
|
||||
import Button from "../react/Button";
|
||||
import FormCheckbox from "../react/FormCheckbox";
|
||||
import FormFileUpload from "../react/FormFileUpload";
|
||||
import FormGroup, { FormMultiGroup } from "../react/FormGroup";
|
||||
import { useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import importService, { UploadFilesOptions } from "../../services/import";
|
||||
import { useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
|
||||
|
||||
export default function ImportDialog() {
|
||||
const [ compressImages ] = useTriliumOptionBool("compressImages");
|
||||
@@ -98,6 +100,4 @@ export default function ImportDialog() {
|
||||
);
|
||||
}
|
||||
|
||||
function boolToString(value: boolean) {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
|
||||
47
apps/client/src/widgets/dialogs/import_preview.css
Normal file
47
apps/client/src/widgets/dialogs/import_preview.css
Normal file
@@ -0,0 +1,47 @@
|
||||
.modal.import-preview-dialog {
|
||||
.stats {
|
||||
color: var(--muted-text-color);
|
||||
font-size: 0.9em;
|
||||
|
||||
span:after {
|
||||
content: " • "
|
||||
}
|
||||
|
||||
span:last-of-type:after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.critical { --color: #ff7979; }
|
||||
.warning { --color: #e2b11d; }
|
||||
.safe { --color: var(--admonition-tip-accent-color); }
|
||||
|
||||
.card {
|
||||
margin-bottom: 1em;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-inline: 0.75em;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
.dangerous-categories {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
--badge-radius: 1000px;
|
||||
|
||||
.ext-badge {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
240
apps/client/src/widgets/dialogs/import_preview.tsx
Normal file
240
apps/client/src/widgets/dialogs/import_preview.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import "./import_preview.css";
|
||||
|
||||
import { DangerousAttributeCategory, ImportPreviewResponse } from "@triliumnext/commons";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import { cancelUploadWithPreview, executeUploadWithPreview } from "../../services/import";
|
||||
import { boolToString, dynamicRequire, isElectron } from "../../services/utils";
|
||||
import { Badge } from "../react/Badge";
|
||||
import Button from "../react/Button";
|
||||
import { Card } from "../react/Card";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
import FormRadioGroup from "../react/FormRadioGroup";
|
||||
import { useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
|
||||
export interface ImportPreviewData {
|
||||
parentNoteId?: string;
|
||||
previews: ImportPreviewResponse[];
|
||||
}
|
||||
|
||||
type DangerousCategory = "critical" | "warning";
|
||||
const DANGEROUS_CATEGORIES_MAPPINGS: Record<DangerousAttributeCategory, {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
category: DangerousCategory;
|
||||
}> = {
|
||||
clientSideScripting: {
|
||||
icon: "bx bx-window-alt",
|
||||
title: t("import_preview.badge_client_side_scripting_title"),
|
||||
description: t("import_preview.badge_client_side_scripting_tooltip"),
|
||||
category: "critical"
|
||||
},
|
||||
serverSideScripting: {
|
||||
icon: "bx bx-server",
|
||||
title: t("import_preview.badge_server_side_scripting_title"),
|
||||
description: t("import_preview.badge_server_side_scripting_tooltip"),
|
||||
category: "critical"
|
||||
},
|
||||
codeExecution: {
|
||||
icon: "bx bx-terminal",
|
||||
title: t("import_preview.badge_code_execution_title"),
|
||||
description: t("import_preview.badge_code_execution_description"),
|
||||
category: "critical"
|
||||
},
|
||||
iconPack: {
|
||||
icon: "bx bx-package",
|
||||
title: t("import_preview.badge_icon_pack_title"),
|
||||
description: t("import_preview.badge_icon_pack_description"),
|
||||
category: "warning"
|
||||
},
|
||||
webview: {
|
||||
icon: "bx bx-globe",
|
||||
title: t("import_preview.badge_web_view_title"),
|
||||
description: t("import_preview.badge_web_view_description"),
|
||||
category: "warning"
|
||||
}
|
||||
};
|
||||
|
||||
const SEVERITY_ORDER: Record<DangerousCategory, number> = {
|
||||
critical: 0,
|
||||
warning: 1
|
||||
};
|
||||
|
||||
const IMPORT_BUTTON_TIMEOUT = 3;
|
||||
|
||||
export default function ImportPreviewDialog() {
|
||||
const [ data, setData ] = useState<ImportPreviewData | null>(null);
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const [ importMethod, setImportMethod ] = useState<string>("safe");
|
||||
const isDangerousImport = data?.previews.some(preview => preview.isDangerous);
|
||||
const [ importButtonTimeout, setImportButtonTimeout ] = useState(0);
|
||||
const [ compressImages ] = useTriliumOptionBool("compressImages");
|
||||
const hasSubmittedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// If safe → reset and do nothing
|
||||
if (!isDangerousImport) {
|
||||
setImportButtonTimeout(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start countdown
|
||||
setImportButtonTimeout(IMPORT_BUTTON_TIMEOUT);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setImportButtonTimeout(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isDangerousImport]);
|
||||
|
||||
useTriliumEvent("showImportPreviewDialog", (data) => {
|
||||
setData(data);
|
||||
setShown(true);
|
||||
setImportButtonTimeout(IMPORT_BUTTON_TIMEOUT);
|
||||
hasSubmittedRef.current = false;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron()) return;
|
||||
const { ipcRenderer } = dynamicRequire("electron");
|
||||
const onShow = (_event: any, data: ImportPreviewData) => {
|
||||
setData(data);
|
||||
setShown(true);
|
||||
setImportButtonTimeout(IMPORT_BUTTON_TIMEOUT);
|
||||
hasSubmittedRef.current = false;
|
||||
};
|
||||
ipcRenderer.on("show-import-preview-dialog", onShow);
|
||||
return () => {
|
||||
ipcRenderer.removeListener("show-import-preview-dialog", onShow);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="import-preview-dialog"
|
||||
size="lg"
|
||||
title={t("import_preview.title")}
|
||||
footer={<>
|
||||
<Button text={t("import_preview.cancel")} onClick={() => setShown(false)}/>
|
||||
<Button
|
||||
text={importButtonTimeout
|
||||
? t("import_preview.import_with_timeout", { timeout: importButtonTimeout })
|
||||
: t("import_preview.import")}
|
||||
disabled={importButtonTimeout > 0}
|
||||
primary
|
||||
/>
|
||||
</>}
|
||||
show={shown}
|
||||
onSubmit={() => {
|
||||
if (!data) return;
|
||||
hasSubmittedRef.current = true;
|
||||
executeUploadWithPreview(data.parentNoteId, data.previews, {
|
||||
shrinkImages: boolToString(compressImages),
|
||||
safeImport: boolToString(importMethod === "safe")
|
||||
});
|
||||
setShown(false);
|
||||
}}
|
||||
onHidden={() => {
|
||||
setShown(false);
|
||||
setData(null);
|
||||
setImportButtonTimeout(3);
|
||||
if (!hasSubmittedRef.current) {
|
||||
hasSubmittedRef.current = true;
|
||||
if (data?.previews) {
|
||||
cancelUploadWithPreview(data.previews);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p>{isDangerousImport
|
||||
? t("import_preview.intro_unsafe", { count: data?.previews.length })
|
||||
: t("import_preview.intro_safe", { count: data?.previews.length })}</p>
|
||||
|
||||
{data?.previews.map(preview => <SinglePreview key={preview.id} preview={preview} />)}
|
||||
|
||||
<div className="import-options">
|
||||
<FormGroup name="parent-note" label={t("import_preview.parent_note")}>
|
||||
<NoteAutocomplete
|
||||
noteId={data?.parentNoteId}
|
||||
noteIdChanged={noteId => {
|
||||
if (!data) return;
|
||||
setData({
|
||||
...data,
|
||||
parentNoteId: noteId
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormRadioGroup
|
||||
name="import-method"
|
||||
currentValue={importMethod} onChange={setImportMethod}
|
||||
values={[
|
||||
{ value: "safe", label: t("import_preview.import_safely"), inlineDescription: t("import_preview.import_safely_description") },
|
||||
{ value: "unsafe", label: t("import_preview.import_trust"), inlineDescription: t("import_preview.import_trust_description") }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function SinglePreview({ preview }: { preview: ImportPreviewResponse }) {
|
||||
const categories = sortDangerousAttributeCategoryBySeverity(preview.dangerousAttributeCategories);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={preview.fileName}
|
||||
className={DANGEROUS_CATEGORIES_MAPPINGS[categories[0]]?.category ?? "safe"}
|
||||
>
|
||||
<div className="stats">
|
||||
<span>{t("import_preview.notes_count", { count: preview.numNotes })}</span>
|
||||
<span>{t("import_preview.attributes_count", { count: preview.numAttributes })}</span>
|
||||
<span>{t("import_preview.attachments_count", { count: preview.numAttachments })}</span>
|
||||
</div>
|
||||
|
||||
<div className="dangerous-categories">
|
||||
{categories.length > 0
|
||||
? categories.map(dangerousCategory => {
|
||||
const mapping = DANGEROUS_CATEGORIES_MAPPINGS[dangerousCategory];
|
||||
return (
|
||||
<Badge
|
||||
key={dangerousCategory}
|
||||
className={mapping.category}
|
||||
icon={mapping.icon}
|
||||
text={mapping.title}
|
||||
tooltip={mapping.description}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: (
|
||||
<Badge
|
||||
className="safe"
|
||||
icon="bx bx-check"
|
||||
text="Safe"
|
||||
tooltip="This archive has no active content such as scripts or widgets that could affect your knowledge base or access sensitive data."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function sortDangerousAttributeCategoryBySeverity(categories: string[]) {
|
||||
return categories.toSorted((a, b) => {
|
||||
const aLevel = DANGEROUS_CATEGORIES_MAPPINGS[a].category;
|
||||
const bLevel = DANGEROUS_CATEGORIES_MAPPINGS[b].category;
|
||||
return SEVERITY_ORDER[aLevel] - SEVERITY_ORDER[bLevel];
|
||||
});
|
||||
}
|
||||
@@ -37,10 +37,6 @@
|
||||
&:hover {
|
||||
background: var(--input-background-color);
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.status-bar-dropdown-button {
|
||||
|
||||
@@ -226,8 +226,8 @@ function CheckBoxPropertyView({ note, property }: { note: FNote, property: Check
|
||||
<FormListToggleableItem
|
||||
icon={property.icon}
|
||||
title={property.label}
|
||||
currentValue={ property.reverseValue ? !value : value }
|
||||
onChange={newValue => setValue(property.reverseValue ? !newValue : newValue)}
|
||||
currentValue={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
border-radius: 0.5em;
|
||||
font-size: 0.7rem;
|
||||
font-weight: normal;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -523,46 +523,59 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
const dataTransfer = data.dataTransfer;
|
||||
|
||||
if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
|
||||
const files = [...dataTransfer.files]; // chrome has issue that dataTransfer.files empties after async operation
|
||||
const files: File[] = [...dataTransfer.files]; // chrome has issue that dataTransfer.files empties after async operation
|
||||
|
||||
const importService = await import("../services/import.js");
|
||||
const isTriliumImport = files.every(f => f.name.endsWith(".trilium"));
|
||||
const parentNoteId = node.data.noteId;
|
||||
|
||||
importService.uploadFiles("notes", node.data.noteId, files, {
|
||||
safeImport: true,
|
||||
shrinkImages: true,
|
||||
textImportedAsText: true,
|
||||
codeImportedAsCode: true,
|
||||
explodeArchives: true,
|
||||
replaceUnderscoresWithSpaces: true
|
||||
});
|
||||
} else {
|
||||
const jsonStr = dataTransfer.getData("text");
|
||||
let notes: BranchRow[];
|
||||
|
||||
try {
|
||||
notes = JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
logError(`Cannot parse JSON '${jsonStr}' into notes for drop`);
|
||||
if (!isTriliumImport) {
|
||||
importService.uploadFiles("notes", parentNoteId, files, {
|
||||
safeImport: true,
|
||||
shrinkImages: true,
|
||||
textImportedAsText: true,
|
||||
codeImportedAsCode: true,
|
||||
explodeArchives: true,
|
||||
replaceUnderscoresWithSpaces: true
|
||||
});
|
||||
} else {
|
||||
importService.uploadFilesWithPreview(files).then((previews) => {
|
||||
if (!previews) return;
|
||||
this.triggerCommand("showImportPreviewDialog", {
|
||||
parentNoteId,
|
||||
previews
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// This function MUST be defined to enable dropping of items on the tree.
|
||||
// data.hitMode is 'before', 'after', or 'over'.
|
||||
|
||||
const selectedBranchIds = notes
|
||||
.map((note) => note.branchId)
|
||||
.filter((branchId) => branchId) as string[];
|
||||
|
||||
if (data.hitMode === "before") {
|
||||
branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId);
|
||||
} else if (data.hitMode === "after") {
|
||||
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
|
||||
} else if (data.hitMode === "over") {
|
||||
branchService.moveToParentNote(selectedBranchIds, node.data.branchId, this.componentId);
|
||||
} else {
|
||||
throw new Error(`Unknown hitMode '${data.hitMode}'`);
|
||||
}
|
||||
}
|
||||
const jsonStr = dataTransfer.getData("text");
|
||||
let notes: BranchRow[];
|
||||
|
||||
try {
|
||||
notes = JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
logError(`Cannot parse JSON '${jsonStr}' into notes for drop`);
|
||||
return;
|
||||
}
|
||||
|
||||
// This function MUST be defined to enable dropping of items on the tree.
|
||||
// data.hitMode is 'before', 'after', or 'over'.
|
||||
|
||||
const selectedBranchIds = notes
|
||||
.map((note) => note.branchId)
|
||||
.filter((branchId) => branchId) as string[];
|
||||
|
||||
if (data.hitMode === "before") {
|
||||
branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId);
|
||||
} else if (data.hitMode === "after") {
|
||||
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
|
||||
} else if (data.hitMode === "over") {
|
||||
branchService.moveToParentNote(selectedBranchIds, node.data.branchId, this.componentId);
|
||||
} else {
|
||||
throw new Error(`Unknown hitMode '${data.hitMode}'`);
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
lazyLoad: (event, data) => {
|
||||
@@ -1992,7 +2005,7 @@ function buildEnhanceTitle() {
|
||||
if (isSubtreeHidden && count > 0) {
|
||||
const $badge = $(`<span class="note-indicator-icon subtree-hidden-badge">${count}</span>`);
|
||||
$badge.attr("title", t("note_tree.subtree-hidden-tooltip", { count }));
|
||||
$span.append($badge);
|
||||
$span.find(".fancytree-title").append($badge);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,8 +43,7 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
|
||||
},
|
||||
protectedSession: {
|
||||
view: () => import("./type_widgets/ProtectedSession"),
|
||||
className: "protected-session-password-component",
|
||||
isFullHeight: true
|
||||
className: "protected-session-password-component"
|
||||
},
|
||||
book: {
|
||||
view: () => import("./type_widgets/Book"),
|
||||
|
||||
20
apps/client/src/widgets/react/Card.tsx
Normal file
20
apps/client/src/widgets/react/Card.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import clsx from "clsx";
|
||||
import type { ComponentChildren } from "preact";
|
||||
|
||||
export function Card({ title, className, children }: {
|
||||
title: string;
|
||||
children: ComponentChildren;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx("card", className)}>
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{title}</h5>
|
||||
|
||||
<p className="card-text">
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,10 @@
|
||||
line-height: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline-end: 12px;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
|
||||
.arrow {
|
||||
font-size: 1.3em;
|
||||
|
||||
@@ -57,7 +57,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
|
||||
"with-transition": transitionEnabled
|
||||
})}>
|
||||
<button
|
||||
className="collapsible-title tn-low-profile"
|
||||
className="collapsible-title"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={contentId}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.similar-notes-widget > .similar-notes-wrapper {
|
||||
/* The font size of the links with the highest similarity score */
|
||||
font-size: 17px;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import "./SimilarNotesTab.css";
|
||||
|
||||
import { SimilarNoteResponse } from "@triliumnext/commons";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
@@ -35,7 +33,7 @@ export default function SimilarNotesTab({ note }: Pick<TabContext, "note">) {
|
||||
notePath={notePath}
|
||||
noTnLink
|
||||
style={{
|
||||
"font-size": (1 - 1 / (1 + score)) + "em"
|
||||
"font-size": 20 * (1 - 1 / (1 + score))
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -20,8 +20,6 @@ export interface CheckBoxProperty {
|
||||
label: string;
|
||||
bindToLabel: FilterLabelsByType<boolean>;
|
||||
icon?: string;
|
||||
/** When true, the checkbox will be checked when the label value is false. Useful when the label represents a "hide" action, without exposing double negatives to the user. */
|
||||
reverseValue?: boolean;
|
||||
}
|
||||
|
||||
export interface ButtonProperty {
|
||||
@@ -158,13 +156,6 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
icon: "bx bx-ruler",
|
||||
type: "checkbox",
|
||||
bindToLabel: "map:scale"
|
||||
},
|
||||
{
|
||||
label: t("book_properties_config.show-labels"),
|
||||
icon: "bx bx-label",
|
||||
type: "checkbox",
|
||||
bindToLabel: "map:hideLabels",
|
||||
reverseValue: true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
.attachment-list .links-wrapper {
|
||||
font-size: larger;
|
||||
margin-block: 12px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
.type-contentWidget .note-detail {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-content-widget {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-content-widget-content {
|
||||
padding: 15px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail.full-height .note-detail-content-widget-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
.protected-session-password-component {
|
||||
display: flex;
|
||||
margin-inline: 40px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 300px;
|
||||
margin: 30px auto auto;
|
||||
}
|
||||
|
||||
.protected-session-password-component input,
|
||||
.protected-session-password-component button {
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -20,9 +20,7 @@ export default function ProtectedSession() {
|
||||
}, [ passwordRef ]);
|
||||
|
||||
return (
|
||||
<form class="protected-session-password-form tn-centered-form" onSubmit={submitCallback}>
|
||||
<span class="form-icon bx bx-key" />
|
||||
|
||||
<form class="protected-session-password-form" onSubmit={submitCallback}>
|
||||
<FormGroup name="protected-session-password-in-detail" label={t("protected_session.enter_password_instruction")}>
|
||||
<FormTextBox
|
||||
type="password"
|
||||
@@ -39,4 +37,4 @@ export default function ProtectedSession() {
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,20 +16,3 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.web-view-setup-form {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-inline: 40px;
|
||||
|
||||
.form-icon {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import utils from "../../services/utils";
|
||||
import Alert from "../react/Alert";
|
||||
import { useNoteLabel } from "../react/hooks";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
import "./WebView.css";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
import toast from "../../services/toast";
|
||||
import Button from "../react/Button";
|
||||
|
||||
const isElectron = utils.isElectron();
|
||||
|
||||
@@ -16,7 +12,7 @@ export default function WebView({ note }: TypeWidgetProps) {
|
||||
|
||||
return (webViewSrc
|
||||
? <WebViewContent src={webViewSrc} />
|
||||
: <SetupWebView note={note} />
|
||||
: <WebViewHelp />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,41 +24,12 @@ function WebViewContent({ src }: { src: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
function SetupWebView({note}: {note: FNote}) {
|
||||
const [srcLabel, setSrcLabel] = useNoteLabel(note, "webViewSrc");
|
||||
const [src, setSrc] = useState("");
|
||||
|
||||
const submit = useCallback((url: string) => {
|
||||
try {
|
||||
// Validate URL
|
||||
new URL(url);
|
||||
} catch (ex) {
|
||||
toast.showErrorTitleAndMessage(t("web_view_setup.invalid_url_title"),
|
||||
t("web_view_setup.invalid_url_message"));
|
||||
return;
|
||||
}
|
||||
|
||||
setSrcLabel(url);
|
||||
}, [note]);
|
||||
|
||||
return <div class="web-view-setup-form">
|
||||
<form class="tn-centered-form" onSubmit={() => submit(src)}>
|
||||
<span className="bx bx-globe-alt form-icon" />
|
||||
|
||||
<FormGroup name="web-view-src-detail" label={t("web_view_setup.title")}>
|
||||
<input className="form-control"
|
||||
type="text"
|
||||
value={src}
|
||||
placeholder={t("web_view_setup.url_placeholder")}
|
||||
onChange={(e) => {setSrc((e.target as HTMLInputElement)?.value)}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<Button
|
||||
text={t("web_view_setup.create_button")}
|
||||
primary
|
||||
keyboardShortcut="Enter"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
function WebViewHelp() {
|
||||
return (
|
||||
<Alert className="note-detail-web-view-help" type="warning">
|
||||
<h4>{t("web_view.web_view")}</h4>
|
||||
<p>{t("web_view.embed_websites")}</p>
|
||||
<p>{t("web_view.create_label")}</p>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
.shortcuts-options-section {
|
||||
> header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--main-background-color);
|
||||
padding-block: 0.5em;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
> footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--main-background-color);
|
||||
padding-block: 0.5em;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.separator {
|
||||
background-color: var(--accented-background-color);
|
||||
font-weight: bold;
|
||||
|
||||
&:first-of-type {
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { ActionKeyboardShortcut, KeyboardShortcut, OptionNames } from "@triliumn
|
||||
import { t } from "../../../services/i18n";
|
||||
import { arrayEqual, reloadFrontendApp } from "../../../services/utils";
|
||||
import Button from "../../react/Button";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import RawHtml from "../../react/RawHtml";
|
||||
@@ -11,8 +12,6 @@ import server from "../../../services/server";
|
||||
import options from "../../../services/options";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { useTriliumEvent } from "../../react/hooks";
|
||||
import "./shortcuts.css";
|
||||
import NoItems from "../../react/NoItems";
|
||||
|
||||
export default function ShortcutSettings() {
|
||||
const [ keyboardShortcuts, setKeyboardShortcuts ] = useState<KeyboardShortcut[]>([]);
|
||||
@@ -71,29 +70,29 @@ export default function ShortcutSettings() {
|
||||
options.saveMany(optionsToSet);
|
||||
}, [ keyboardShortcuts ]);
|
||||
|
||||
const filterLowerCase = filter?.toLowerCase() ?? "";
|
||||
const filteredKeyboardShortcuts = filter ? keyboardShortcuts.filter((action) => filterKeyboardAction(action, filterLowerCase)) : keyboardShortcuts;
|
||||
|
||||
return (
|
||||
<OptionsSection
|
||||
className="shortcuts-options-section"
|
||||
style={{ display: "flex", flexDirection: "column", height: "100%" }}
|
||||
noCard
|
||||
>
|
||||
<FormText>
|
||||
{t("shortcuts.multiple_shortcuts")}{" "}
|
||||
{t("shortcuts.multiple_shortcuts")}
|
||||
<RawHtml html={t("shortcuts.electron_documentation")} />
|
||||
</FormText>
|
||||
|
||||
<header>
|
||||
<FormGroup name="keyboard-shortcut-filter">
|
||||
<FormTextBox
|
||||
placeholder={t("shortcuts.type_text_to_filter")}
|
||||
currentValue={filter} onChange={(value) => setFilter(value)}
|
||||
currentValue={filter} onChange={(value) => setFilter(value.toLowerCase())}
|
||||
/>
|
||||
</header>
|
||||
</FormGroup>
|
||||
|
||||
<KeyboardShortcutTable filteredKeyboardActions={filteredKeyboardShortcuts} filter={filter} />
|
||||
<div style={{overflow: "auto", flexGrow: 1, flexShrink: 1}}>
|
||||
<KeyboardShortcutTable keyboardShortcuts={keyboardShortcuts} filter={filter} />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", margin: "15px 15px 0 15px"}}>
|
||||
<Button
|
||||
text={t("shortcuts.reload_app")}
|
||||
onClick={reloadFrontendApp}
|
||||
@@ -103,17 +102,12 @@ export default function ShortcutSettings() {
|
||||
text={t("shortcuts.set_all_to_default")}
|
||||
onClick={resetShortcuts}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
function filterKeyboardAction(action: KeyboardShortcut, filter: string) {
|
||||
// Hide separators when filtering is active.
|
||||
if ("separator" in action) {
|
||||
return !filter;
|
||||
}
|
||||
|
||||
function filterKeyboardAction(action: ActionKeyboardShortcut, filter: string) {
|
||||
return action.actionName.toLowerCase().includes(filter) ||
|
||||
(action.friendlyName && action.friendlyName.toLowerCase().includes(filter)) ||
|
||||
(action.defaultShortcuts ?? []).some((shortcut) => shortcut.toLowerCase().includes(filter)) ||
|
||||
@@ -121,7 +115,7 @@ function filterKeyboardAction(action: KeyboardShortcut, filter: string) {
|
||||
(action.description && action.description.toLowerCase().includes(filter));
|
||||
}
|
||||
|
||||
function KeyboardShortcutTable({ filteredKeyboardActions, filter }: { filteredKeyboardActions: KeyboardShortcut[], filter: string | undefined }) {
|
||||
function KeyboardShortcutTable({ filter, keyboardShortcuts }: { filter?: string, keyboardShortcuts: KeyboardShortcut[] }) {
|
||||
return (
|
||||
<table class="keyboard-shortcut-table" cellPadding="10">
|
||||
<thead>
|
||||
@@ -133,17 +127,16 @@ function KeyboardShortcutTable({ filteredKeyboardActions, filter }: { filteredKe
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredKeyboardActions.length > 0
|
||||
? filteredKeyboardActions.map(action => (
|
||||
{keyboardShortcuts.map(action => (
|
||||
<tr>
|
||||
{"separator" in action ?
|
||||
{"separator" in action ? ( !filter &&
|
||||
<td class="separator" colspan={4} style={{
|
||||
backgroundColor: "var(--accented-background-color)",
|
||||
fontWeight: "bold"
|
||||
}}>
|
||||
{action.separator}
|
||||
</td>
|
||||
: (
|
||||
) : ( (!filter || filterKeyboardAction(action, filter)) &&
|
||||
<>
|
||||
<td>{action.friendlyName}</td>
|
||||
<td>
|
||||
@@ -154,17 +147,7 @@ function KeyboardShortcutTable({ filteredKeyboardActions, filter }: { filteredKe
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
))
|
||||
: (
|
||||
<tr>
|
||||
<td colspan={4} class="text-center">
|
||||
<NoItems
|
||||
icon="bx bx-filter-alt"
|
||||
text={t("shortcuts.no_results", { filter })}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
@@ -95,7 +95,7 @@ export default defineConfig(() => ({
|
||||
output: {
|
||||
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" || chunk.name === "print") {
|
||||
if (chunk.name === "index") {
|
||||
return "src/[name]-[hash].js";
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "40.2.1",
|
||||
"electron": "40.1.0",
|
||||
"@electron-forge/cli": "7.11.1",
|
||||
"@electron-forge/maker-deb": "7.11.1",
|
||||
"@electron-forge/maker-dmg": "7.11.1",
|
||||
|
||||
@@ -12,6 +12,8 @@ import { PRODUCT_NAME } from "./app-info";
|
||||
import port from "@triliumnext/server/src/services/port.js";
|
||||
import { join } from "path";
|
||||
import { deferred, LOCALES } from "../../../packages/commons/src";
|
||||
import previewZipForImport from "@triliumnext/server/src/services/import/zip_preview";
|
||||
import { ipcMain } from "electron/main";
|
||||
|
||||
async function main() {
|
||||
const userDataPath = getUserData();
|
||||
@@ -115,7 +117,8 @@ async function onReady() {
|
||||
if (sqlInit.isDbInitialized()) {
|
||||
await sqlInit.dbReady;
|
||||
|
||||
await windowService.createMainWindow(app);
|
||||
const mainWindow = await windowService.createMainWindow(app);
|
||||
handleImportArguments(process.argv, mainWindow);
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
app.on("activate", async () => {
|
||||
@@ -133,6 +136,20 @@ async function onReady() {
|
||||
await windowService.registerGlobalShortcuts();
|
||||
}
|
||||
|
||||
async function handleImportArguments(args: string[], window: BrowserWindow) {
|
||||
const filesToImport = args.filter(arg => !arg.startsWith("-") && arg.endsWith(".trilium"));
|
||||
if (filesToImport.length === 0) return;
|
||||
|
||||
const previews = await Promise.all(filesToImport.map(file => previewZipForImport(file)));
|
||||
console.log("Sendimg import preview data to window", window);
|
||||
ipcMain.on("initial-render-complete", () => {
|
||||
console.log("READYYY!!!");
|
||||
window.webContents.send("show-import-preview-dialog", {
|
||||
previews
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getElectronLocale() {
|
||||
const uiLocale = options.getOptionOrNull("locale");
|
||||
const formattingLocale = options.getOptionOrNull("formattingLocale");
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@triliumnext/desktop": "workspace:*",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "40.2.1",
|
||||
"electron": "40.1.0",
|
||||
"fs-extra": "11.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"sucrase": "3.35.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.74.0",
|
||||
"@anthropic-ai/sdk": "0.72.1",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@electron/remote": "2.1.3",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
@@ -70,7 +70,7 @@
|
||||
"@types/xml2js": "0.4.14",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"axios": "1.13.5",
|
||||
"axios": "1.13.4",
|
||||
"bindings": "1.5.0",
|
||||
"bootstrap": "5.3.8",
|
||||
"chardet": "2.1.1",
|
||||
@@ -83,7 +83,7 @@
|
||||
"debounce": "3.0.0",
|
||||
"debug": "4.4.3",
|
||||
"ejs": "4.0.1",
|
||||
"electron": "40.2.1",
|
||||
"electron": "40.1.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.8.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-fs-backend": "2.6.1",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "6.0.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
"multer": "2.0.2",
|
||||
"normalize-strings": "1.1.1",
|
||||
"ollama": "0.6.3",
|
||||
"openai": "6.21.0",
|
||||
"openai": "6.17.0",
|
||||
"rand-token": "1.0.1",
|
||||
"safe-compare": "1.1.4",
|
||||
"sanitize-filename": "1.6.3",
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
<aside class="admonition important">
|
||||
<p>This feature is still in preview and is available only in the <a href="https://docs.triliumnotes.org/user-guide/advanced-usage/nightly-release">nightly release</a>.</p>
|
||||
</aside>
|
||||
<figure class="image image-style-align-right image_resized" style="width:45.14%;">
|
||||
<img style="aspect-ratio:854/649;" src="Icon Packs_image.png"
|
||||
width="854" height="649">
|
||||
|
||||
@@ -317,8 +317,7 @@
|
||||
"other": "Inne",
|
||||
"visible-launchers-title": "Widoczne launchery",
|
||||
"user-guide": "Podręcznik użytkownika",
|
||||
"inbox-title": "Skrzynka odbiorcza",
|
||||
"tab-switcher-title": "Przełącznik kart"
|
||||
"inbox-title": "Skrzynka odbiorcza"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "Nowa notatka",
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
import enexImportService from "../../services/import/enex.js";
|
||||
import opmlImportService from "../../services/import/opml.js";
|
||||
import zipImportService from "../../services/import/zip.js";
|
||||
import singleImportService from "../../services/import/single.js";
|
||||
import cls from "../../services/cls.js";
|
||||
|
||||
import { ImportPreviewResponse } from "@triliumnext/commons";
|
||||
import type { Request } from "express";
|
||||
import path from "path";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import beccaLoader from "../../becca/becca_loader.js";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import ValidationError from "../../errors/validation_error.js";
|
||||
import cls from "../../services/cls.js";
|
||||
import enexImportService from "../../services/import/enex.js";
|
||||
import importStore from "../../services/import/import_store.js";
|
||||
import opmlImportService from "../../services/import/opml.js";
|
||||
import singleImportService from "../../services/import/single.js";
|
||||
import zipImportService from "../../services/import/zip.js";
|
||||
import previewZipForImport from "../../services/import/zip_preview.js";
|
||||
import log from "../../services/log.js";
|
||||
import TaskContext from "../../services/task_context.js";
|
||||
import ValidationError from "../../errors/validation_error.js";
|
||||
import type { Request } from "express";
|
||||
import type BNote from "../../becca/entities/bnote.js";
|
||||
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
|
||||
|
||||
async function importNotesToBranch(req: Request) {
|
||||
@@ -79,6 +83,10 @@ async function importNotesToBranch(req: Request) {
|
||||
return [500, message];
|
||||
}
|
||||
|
||||
onImportDone(note, last, taskContext, parentNoteId);
|
||||
}
|
||||
|
||||
function onImportDone(note: BNote | null, last: "true" | "false", taskContext: TaskContext<"importNotes">, parentNoteId: string) {
|
||||
if (!note) {
|
||||
return [500, "No note was generated as a result of the import."];
|
||||
}
|
||||
@@ -88,7 +96,7 @@ async function importNotesToBranch(req: Request) {
|
||||
setTimeout(
|
||||
() =>
|
||||
taskContext.taskSucceeded({
|
||||
parentNoteId: parentNoteId,
|
||||
parentNoteId,
|
||||
importedNoteId: note?.noteId
|
||||
}),
|
||||
1000
|
||||
@@ -138,14 +146,83 @@ function importAttachmentsToNote(req: Request) {
|
||||
setTimeout(
|
||||
() =>
|
||||
taskContext.taskSucceeded({
|
||||
parentNoteId: parentNoteId
|
||||
parentNoteId
|
||||
}),
|
||||
1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function importPreview(req: Request) {
|
||||
const file = req.file;
|
||||
if (!file) {
|
||||
throw new ValidationError("No file has been uploaded");
|
||||
}
|
||||
|
||||
if (!file.originalname.endsWith(".trilium")) {
|
||||
throw new ValidationError("Preview supports only .trilium files.");
|
||||
}
|
||||
|
||||
try {
|
||||
const previewInfo = await previewZipForImport(file.path);
|
||||
const id = file.filename;
|
||||
|
||||
importStore.set(id, {
|
||||
path: file.path
|
||||
});
|
||||
|
||||
return {
|
||||
...previewInfo,
|
||||
fileName: file.originalname,
|
||||
id
|
||||
} satisfies ImportPreviewResponse;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
throw new ValidationError("Error while generating the preview.");
|
||||
}
|
||||
}
|
||||
|
||||
async function importExecute(req: Request) {
|
||||
const { id } = req.body;
|
||||
|
||||
const importRecord = importStore.get(id);
|
||||
if (!importRecord) throw new ValidationError("Unable to find a record of the upload, maybe it expired or the ID is missing or incorrect.");
|
||||
|
||||
try {
|
||||
const { taskId, last } = req.body;
|
||||
const options = {
|
||||
safeImport: req.body.safeImport !== "false",
|
||||
shrinkImages: req.body.shrinkImages !== "false",
|
||||
textImportedAsText: req.body.textImportedAsText !== "false",
|
||||
codeImportedAsCode: req.body.codeImportedAsCode !== "false",
|
||||
explodeArchives: req.body.explodeArchives !== "false",
|
||||
replaceUnderscoresWithSpaces: req.body.replaceUnderscoresWithSpaces !== "false"
|
||||
};
|
||||
|
||||
const taskContext = TaskContext.getInstance(taskId, "importNotes", options);
|
||||
const { parentNoteId } = req.params;
|
||||
const parentNote = becca.getNoteOrThrow(parentNoteId);
|
||||
|
||||
const note = await zipImportService.importZip(taskContext, importRecord.path, parentNote);
|
||||
onImportDone(note, last, taskContext, parentNoteId);
|
||||
} finally {
|
||||
importStore.remove(id);
|
||||
}
|
||||
|
||||
return importRecord;
|
||||
}
|
||||
|
||||
async function importCancel(req: Request) {
|
||||
const { id } = req.params;
|
||||
if (!id) throw new ValidationError("Missing ID to cancel.");
|
||||
|
||||
importStore.remove(id);
|
||||
}
|
||||
|
||||
export default {
|
||||
importNotesToBranch,
|
||||
importAttachmentsToNote
|
||||
importAttachmentsToNote,
|
||||
importPreview,
|
||||
importExecute,
|
||||
importCancel
|
||||
};
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import express, { type RequestHandler } from "express";
|
||||
import { mkdirSync } from "fs";
|
||||
import multer from "multer";
|
||||
import log from "../services/log.js";
|
||||
import cls from "../services/cls.js";
|
||||
import sql from "../services/sql.js";
|
||||
import entityChangesService from "../services/entity_changes.js";
|
||||
import { join } from "path";
|
||||
|
||||
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
|
||||
import NotFoundError from "../errors/not_found_error.js";
|
||||
import ValidationError from "../errors/validation_error.js";
|
||||
import auth from "../services/auth.js";
|
||||
import cls from "../services/cls.js";
|
||||
import dataDirs from "../services/data_dir.js";
|
||||
import entityChangesService from "../services/entity_changes.js";
|
||||
import import_store from "../services/import/import_store.js";
|
||||
import log from "../services/log.js";
|
||||
import sql from "../services/sql.js";
|
||||
import { randomString, safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
|
||||
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
|
||||
const MAX_ALLOWED_FILE_SIZE_MB = 250;
|
||||
export const router = express.Router();
|
||||
@@ -67,9 +72,9 @@ export function apiResultHandler(req: express.Request, res: express.Response, re
|
||||
return send(res, statusCode, response);
|
||||
} else if (result === undefined) {
|
||||
return send(res, 204, "");
|
||||
} else {
|
||||
return send(res, 200, result);
|
||||
}
|
||||
return send(res, 200, result);
|
||||
|
||||
}
|
||||
|
||||
function send(res: express.Response, statusCode: number, response: unknown) {
|
||||
@@ -81,14 +86,14 @@ function send(res: express.Response, statusCode: number, response: unknown) {
|
||||
res.status(statusCode).send(response);
|
||||
|
||||
return response.length;
|
||||
} else {
|
||||
const json = JSON.stringify(response);
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.status(statusCode).send(json);
|
||||
|
||||
return json.length;
|
||||
}
|
||||
const json = JSON.stringify(response);
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.status(statusCode).send(json);
|
||||
|
||||
return json.length;
|
||||
|
||||
}
|
||||
|
||||
export function apiRoute(method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) {
|
||||
@@ -190,10 +195,49 @@ export function createUploadMiddleware(): RequestHandler {
|
||||
return multer(multerOptions).single("upload");
|
||||
}
|
||||
|
||||
export function createImportUploadMiddleware(): RequestHandler {
|
||||
const uploadDir = import_store.uploadDir;
|
||||
const multerOptions: multer.Options = {
|
||||
storage: multer.diskStorage({
|
||||
destination(req, file, cb) {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename(req, file, cb) {
|
||||
cb(null, `${randomString(13)}.trilium`);
|
||||
}
|
||||
}),
|
||||
fileFilter: (_req: express.Request, file, cb) => {
|
||||
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
|
||||
// See https://github.com/expressjs/multer/pull/1102.
|
||||
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
|
||||
cb(null, true);
|
||||
}
|
||||
};
|
||||
|
||||
if (!process.env.TRILIUM_NO_UPLOAD_LIMIT) {
|
||||
multerOptions.limits = {
|
||||
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
|
||||
};
|
||||
}
|
||||
|
||||
return multer(multerOptions).single("upload");
|
||||
}
|
||||
|
||||
const uploadMiddleware = createUploadMiddleware();
|
||||
const importUploadMiddleware = createImportUploadMiddleware();
|
||||
|
||||
export const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
uploadMiddleware(req, res, function (err) {
|
||||
uploadMiddleware(req, res, (err) => {
|
||||
if (err?.code === "LIMIT_FILE_SIZE") {
|
||||
res.setHeader("Content-Type", "text/plain").status(400).send(`Cannot upload file because it excceeded max allowed file size of ${MAX_ALLOWED_FILE_SIZE_MB} MiB`);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const importMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
importUploadMiddleware(req, res, (err) => {
|
||||
if (err?.code === "LIMIT_FILE_SIZE") {
|
||||
res.setHeader("Content-Type", "text/plain").status(400).send(`Cannot upload file because it excceeded max allowed file size of ${MAX_ALLOWED_FILE_SIZE_MB} MiB`);
|
||||
} else {
|
||||
|
||||
@@ -10,9 +10,9 @@ import etapiBackupRoute from "../etapi/backup.js";
|
||||
import etapiBranchRoutes from "../etapi/branches.js";
|
||||
import etapiMetricsRoute from "../etapi/metrics.js";
|
||||
import etapiNoteRoutes from "../etapi/notes.js";
|
||||
import etapiRevisionsRoutes from "../etapi/revisions.js";
|
||||
import etapiSpecRoute from "../etapi/spec.js";
|
||||
import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
|
||||
import etapiRevisionsRoutes from "../etapi/revisions.js";
|
||||
import auth from "../services/auth.js";
|
||||
import openID from '../services/open_id.js';
|
||||
import { isElectron } from "../services/utils.js";
|
||||
@@ -66,7 +66,7 @@ import treeApiRoute from "./api/tree.js";
|
||||
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
|
||||
import * as indexRoute from "./index.js";
|
||||
import loginRoute from "./login.js";
|
||||
import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js";
|
||||
import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, importMiddlewareWithErrorHandling, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js";
|
||||
// page routes
|
||||
import setupRoute from "./setup.js";
|
||||
|
||||
@@ -195,7 +195,11 @@ function register(app: express.Application) {
|
||||
|
||||
route(GET, "/api/branches/:branchId/export/:type/:format/:version/:taskId", [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||
asyncRoute(PST, "/api/notes/:parentNoteId/notes-import", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importNotesToBranch, apiResultHandler);
|
||||
route(PST, "/api/notes/:parentNoteId/attachments-import", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importAttachmentsToNote, apiResultHandler);
|
||||
route(PST, "/api/notes/:parentNoteId/attachments-import", [auth.checkApiAuthOrElectron,
|
||||
uploadMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importAttachmentsToNote, apiResultHandler);
|
||||
asyncRoute(PST, "/api/notes/:parentNoteId/execute-import", [auth.checkApiAuthOrElectron, csrfMiddleware], importRoute.importExecute, apiResultHandler);
|
||||
asyncRoute(PST, "/api/notes/preview-import", [auth.checkApiAuthOrElectron, importMiddlewareWithErrorHandling, csrfMiddleware], importRoute.importPreview, apiResultHandler);
|
||||
asyncRoute(DEL, "/api/notes/preview-import/:id", [auth.checkApiAuthOrElectron, csrfMiddleware], importRoute.importCancel, apiResultHandler);
|
||||
|
||||
apiRoute(GET, "/api/notes/:noteId/attributes", attributesRoute.getEffectiveNoteAttributes);
|
||||
apiRoute(PST, "/api/notes/:noteId/attributes", attributesRoute.addNoteAttribute);
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
export default [
|
||||
import type { DangerousAttributeCategory } from "@triliumnext/commons";
|
||||
|
||||
type AttributeInfo = {
|
||||
type: "label" | "relation";
|
||||
name: string;
|
||||
isDangerous?: false;
|
||||
} | DangerousAttributeInfo;
|
||||
|
||||
export type DangerousAttributeInfo = {
|
||||
type: "label" | "relation";
|
||||
name: string;
|
||||
isDangerous: true;
|
||||
dangerCategory: DangerousAttributeCategory;
|
||||
};
|
||||
|
||||
const builtinAttributes: AttributeInfo[] = [
|
||||
// label names
|
||||
{ type: "label", name: "inbox" },
|
||||
{ type: "label", name: "disableVersioning" },
|
||||
@@ -15,12 +30,12 @@ export default [
|
||||
{ type: "label", name: "cssClass" },
|
||||
{ type: "label", name: "iconClass" },
|
||||
{ type: "label", name: "keyboardShortcut" },
|
||||
{ type: "label", name: "run", isDangerous: true },
|
||||
{ type: "label", name: "run", isDangerous: true, dangerCategory: "codeExecution" },
|
||||
{ type: "label", name: "runOnInstance", isDangerous: false },
|
||||
{ type: "label", name: "runAtHour", isDangerous: false },
|
||||
{ type: "label", name: "customRequestHandler", isDangerous: true },
|
||||
{ type: "label", name: "customResourceProvider", isDangerous: true },
|
||||
{ type: "label", name: "widget", isDangerous: true },
|
||||
{ type: "label", name: "customRequestHandler", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "label", name: "customResourceProvider", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "label", name: "widget", isDangerous: true, dangerCategory: "clientSideScripting" },
|
||||
{ type: "label", name: "noteInfoWidgetDisabled" },
|
||||
{ type: "label", name: "linkMapWidgetDisabled" },
|
||||
{ type: "label", name: "revisionsWidgetDisabled" },
|
||||
@@ -69,7 +84,7 @@ export default [
|
||||
{ type: "label", name: "shareHtmlLocation" },
|
||||
{ type: "label", name: "displayRelations" },
|
||||
{ type: "label", name: "hideRelations" },
|
||||
{ type: "label", name: "titleTemplate", isDangerous: true },
|
||||
{ type: "label", name: "titleTemplate", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "label", name: "template" },
|
||||
{ type: "label", name: "toc" },
|
||||
{ type: "label", name: "color" },
|
||||
@@ -78,9 +93,9 @@ export default [
|
||||
{ type: "label", name: "executeDescription" },
|
||||
{ type: "label", name: "newNotesOnTop" },
|
||||
{ type: "label", name: "clipperInbox" },
|
||||
{ type: "label", name: "webViewSrc", isDangerous: true },
|
||||
{ type: "label", name: "webViewSrc", isDangerous: true, dangerCategory: "webview" },
|
||||
{ type: "label", name: "hideHighlightWidget" },
|
||||
{ type: "label", name: "iconPack", isDangerous: true },
|
||||
{ type: "label", name: "iconPack", isDangerous: true, dangerCategory: "iconPack" },
|
||||
|
||||
{ type: "label", name: "printLandscape" },
|
||||
{ type: "label", name: "printPageSize" },
|
||||
@@ -90,24 +105,26 @@ export default [
|
||||
{ type: "relation", name: "imageLink" },
|
||||
{ type: "relation", name: "relationMapLink" },
|
||||
{ type: "relation", name: "includeMapLink" },
|
||||
{ type: "relation", name: "runOnNoteCreation", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteTitleChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteContentChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteDeletion", isDangerous: true },
|
||||
{ type: "relation", name: "runOnBranchCreation", isDangerous: true },
|
||||
{ type: "relation", name: "runOnBranchChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnBranchDeletion", isDangerous: true },
|
||||
{ type: "relation", name: "runOnChildNoteCreation", isDangerous: true },
|
||||
{ type: "relation", name: "runOnAttributeCreation", isDangerous: true },
|
||||
{ type: "relation", name: "runOnAttributeChange", isDangerous: true },
|
||||
{ type: "relation", name: "runOnNoteCreation", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnNoteTitleChange", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnNoteChange", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnNoteContentChange", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnNoteDeletion", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnBranchCreation", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnBranchChange", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnBranchDeletion", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnChildNoteCreation", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnAttributeCreation", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "runOnAttributeChange", isDangerous: true, dangerCategory: "serverSideScripting" },
|
||||
{ type: "relation", name: "template" },
|
||||
{ type: "relation", name: "inherit" },
|
||||
{ type: "relation", name: "widget", isDangerous: true },
|
||||
{ type: "relation", name: "renderNote", isDangerous: true },
|
||||
{ type: "relation", name: "widget", isDangerous: true, dangerCategory: "clientSideScripting" },
|
||||
{ type: "relation", name: "renderNote", isDangerous: true, dangerCategory: "clientSideScripting" },
|
||||
{ type: "relation", name: "shareCss" },
|
||||
{ type: "relation", name: "shareJs" },
|
||||
{ type: "relation", name: "shareHtml" },
|
||||
{ type: "relation", name: "shareTemplate" },
|
||||
{ type: "relation", name: "shareFavicon" }
|
||||
];
|
||||
|
||||
export default builtinAttributes;
|
||||
|
||||
90
apps/server/src/services/import/import_store.ts
Normal file
90
apps/server/src/services/import/import_store.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { mkdir, readdir, rm, } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
import dataDirs from "../data_dir";
|
||||
|
||||
export interface ImportRecord {
|
||||
path: string;
|
||||
created: number;
|
||||
}
|
||||
|
||||
const MAX_AGE = 30 * 60_000;
|
||||
|
||||
abstract class ImportStoreBase {
|
||||
|
||||
private importStore: Map<string, ImportRecord> = new Map();
|
||||
|
||||
get(id: string) {
|
||||
return this.importStore.get(id);
|
||||
}
|
||||
|
||||
set(id: string, record: Omit<ImportRecord, "created">) {
|
||||
this.importStore.set(id, {
|
||||
...record,
|
||||
created: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const record = this.get(id);
|
||||
if (!record) return;
|
||||
|
||||
this.onRecordRemoved(record);
|
||||
this.importStore.delete(id);
|
||||
}
|
||||
|
||||
startCleanupTimer() {
|
||||
setInterval(async () => {
|
||||
const now = Date.now();
|
||||
|
||||
for (const [id, record] of this.importStore.entries()) {
|
||||
if (now - record.created > MAX_AGE) {
|
||||
await this.remove(id);
|
||||
}
|
||||
}
|
||||
}, 5 * 60_000);
|
||||
}
|
||||
|
||||
public abstract onRecordRemoved(record: ImportRecord): Promise<void>;
|
||||
|
||||
}
|
||||
|
||||
class DiskImportStore extends ImportStoreBase {
|
||||
|
||||
constructor(private _uploadDir: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
get uploadDir() {
|
||||
return this._uploadDir;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Ensure the import directory exists.
|
||||
await mkdir(this._uploadDir, { recursive: true });
|
||||
|
||||
// Delete all files at start-up.
|
||||
const files = await readdir(this._uploadDir);
|
||||
await Promise.all(files.map(async file => {
|
||||
try {
|
||||
await rm(join(this._uploadDir, file));
|
||||
} catch (e) {
|
||||
console.error(`Error while deleting import: ${file}`, e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public async onRecordRemoved(record: ImportRecord): Promise<void> {
|
||||
try {
|
||||
await rm(record.path);
|
||||
} catch (e) {
|
||||
console.error(`Unable to delete file from import store: ${record.path}.`, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const store = new DiskImportStore(join(dataDirs.TMP_DIR, "upload"));
|
||||
store.init();
|
||||
store.startCleanupTimer();
|
||||
export default store;
|
||||
@@ -31,7 +31,7 @@ interface ImportZipOpts {
|
||||
preserveIds?: boolean;
|
||||
}
|
||||
|
||||
async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Buffer, importRootNote: BNote, opts?: ImportZipOpts): Promise<BNote> {
|
||||
async function importZip(taskContext: TaskContext<"importNotes">, bufferOrPath: string | Buffer, importRootNote: BNote, opts?: ImportZipOpts): Promise<BNote> {
|
||||
/** maps from original noteId (in ZIP file) to newly generated noteId */
|
||||
const noteIdMap: Record<string, string> = {};
|
||||
/** type maps from original attachmentId (in ZIP file) to newly generated attachmentId */
|
||||
@@ -295,12 +295,12 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
attachmentId: getNewAttachmentId(attachmentMeta.attachmentId),
|
||||
noteId: getNewNoteId(noteMeta.noteId)
|
||||
};
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
@@ -313,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
|
||||
}
|
||||
}
|
||||
return `<h2>${text}</h2>`;
|
||||
|
||||
|
||||
});
|
||||
|
||||
if (taskContext.data?.safeImport) {
|
||||
@@ -348,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)}"`;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
|
||||
|
||||
});
|
||||
|
||||
content = content.replace(/href="([^"]*)"/g, (match, url) => {
|
||||
@@ -374,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}"`;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
|
||||
|
||||
});
|
||||
|
||||
if (noteMeta) {
|
||||
@@ -559,7 +559,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
|
||||
// we're running two passes in order to obtain critical information first (meta file and root)
|
||||
const topLevelItems = new Set<string>();
|
||||
await readZipFile(fileBuffer, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
|
||||
await readZipFile(bufferOrPath, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
|
||||
const filePath = normalizeFilePath(entry.fileName);
|
||||
|
||||
// make sure that the meta file is loaded before the rest of the files is processed.
|
||||
@@ -579,7 +579,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
|
||||
topLevelPath = (topLevelItems.size > 1 ? "" : topLevelItems.values().next().value ?? "");
|
||||
|
||||
await readZipFile(fileBuffer, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
|
||||
await readZipFile(bufferOrPath, async (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => {
|
||||
const filePath = normalizeFilePath(entry.fileName);
|
||||
|
||||
if (/\/$/.test(entry.fileName)) {
|
||||
@@ -626,7 +626,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
|
||||
}
|
||||
|
||||
/** @returns path without leading or trailing slash and backslashes converted to forward ones */
|
||||
function normalizeFilePath(filePath: string): string {
|
||||
export function normalizeFilePath(filePath: string): string {
|
||||
filePath = filePath.replace(/\\/g, "/");
|
||||
|
||||
if (filePath.startsWith("/")) {
|
||||
@@ -658,22 +658,41 @@ export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise
|
||||
});
|
||||
}
|
||||
|
||||
export function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => Promise<void>) {
|
||||
export function readZipFile(bufferOrPath: Buffer | string, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => Promise<void>) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, (err, zipfile) => {
|
||||
if (err) rej(err);
|
||||
if (!zipfile) throw new Error("Unable to read zip file.");
|
||||
const options: yauzl.Options = { lazyEntries: true, validateEntrySizes: false };
|
||||
const callback = (err, zipfile) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
return;
|
||||
}
|
||||
if (!zipfile) {
|
||||
rej("Unable to read zip file.");
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile.readEntry();
|
||||
try {
|
||||
zipfile.readEntry();
|
||||
} catch (err) {
|
||||
rej(err);
|
||||
return;
|
||||
}
|
||||
zipfile.on("entry", async (entry) => {
|
||||
try {
|
||||
await processEntryCallback(zipfile, entry);
|
||||
} catch (e) {
|
||||
rej(e);
|
||||
return;
|
||||
}
|
||||
});
|
||||
zipfile.on("end", res);
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof bufferOrPath === "string") {
|
||||
yauzl.open(bufferOrPath, options, callback);
|
||||
} else {
|
||||
yauzl.fromBuffer(bufferOrPath, options, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -692,9 +711,9 @@ function resolveNoteType(type: string | undefined): NoteType {
|
||||
|
||||
if (type && (ALLOWED_NOTE_TYPES as readonly string[]).includes(type)) {
|
||||
return type as NoteType;
|
||||
}
|
||||
}
|
||||
return "text";
|
||||
|
||||
|
||||
}
|
||||
|
||||
export function removeTriliumTags(content: string) {
|
||||
|
||||
73
apps/server/src/services/import/zip_preview.spec.ts
Normal file
73
apps/server/src/services/import/zip_preview.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import NoteMeta, { NoteMetaFile } from "../meta/note_meta";
|
||||
import { previewMeta } from "./zip_preview";
|
||||
|
||||
describe("Preview meta", () => {
|
||||
it("identifies dangerous attributes", () => {
|
||||
const meta = wrapMeta({
|
||||
title: "First unsafe note",
|
||||
attributes: [
|
||||
{
|
||||
type: "label",
|
||||
name: "widget",
|
||||
value: ""
|
||||
}
|
||||
],
|
||||
children: [
|
||||
{
|
||||
title: "Sub unsafe note",
|
||||
attributes: [
|
||||
{
|
||||
type: "relation",
|
||||
name: "runOnBranchCreation",
|
||||
value: ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}, {
|
||||
title: "Second unsafe note",
|
||||
attributes: [
|
||||
{
|
||||
type: "label",
|
||||
name: "customRequestHandler",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
type: "label",
|
||||
name: "safe",
|
||||
value: ""
|
||||
}
|
||||
],
|
||||
attachments: [
|
||||
{
|
||||
attachmentId: "YRAEUXCDKNtn",
|
||||
title: "icon-color.svg",
|
||||
role: "image",
|
||||
mime: "image/svg+xml",
|
||||
position: 10,
|
||||
dataFileName: "Trilium Demo_icon-color.svg"
|
||||
}
|
||||
]
|
||||
});
|
||||
const result = previewMeta(meta);
|
||||
expect(result.numNotes).toBe(3);
|
||||
expect(result.numAttributes).toBe(4);
|
||||
expect(result.numAttachments).toBe(1);
|
||||
expect(result.isDangerous).toBe(true);
|
||||
expect(result.dangerousAttributes).toContain("widget");
|
||||
expect(result.dangerousAttributes).toContain("customRequestHandler");
|
||||
expect(result.dangerousAttributes).toContain("runOnBranchCreation");
|
||||
expect(result.dangerousAttributeCategories).toContain("serverSideScripting");
|
||||
expect(result.dangerousAttributeCategories).toContain("clientSideScripting");
|
||||
});
|
||||
});
|
||||
|
||||
function wrapMeta(...noteMeta: NoteMeta[]): NoteMetaFile {
|
||||
return {
|
||||
formatVersion: 2,
|
||||
appVersion: "0.101.3-test-260207-031832",
|
||||
files: noteMeta
|
||||
};
|
||||
};
|
||||
86
apps/server/src/services/import/zip_preview.ts
Normal file
86
apps/server/src/services/import/zip_preview.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { DangerousAttributeCategory, ImportPreviewResponse } from "@triliumnext/commons";
|
||||
|
||||
import ValidationError from "../../errors/validation_error";
|
||||
import { DangerousAttributeInfo } from "../builtin_attributes";
|
||||
import BUILTIN_ATTRIBUTES from "../builtin_attributes.js";
|
||||
import NoteMeta, { NoteMetaFile } from "../meta/note_meta";
|
||||
import { normalizeFilePath, readContent, readZipFile } from "./zip";
|
||||
|
||||
export default async function previewZipForImport(bufferOrPath: string | Buffer) {
|
||||
let metaFile: NoteMetaFile | null = null;
|
||||
|
||||
await readZipFile(bufferOrPath, async (zipfile, entry) => {
|
||||
const filePath = normalizeFilePath(entry.fileName);
|
||||
|
||||
if (filePath === "!!!meta.json") {
|
||||
const content = await readContent(zipfile, entry);
|
||||
metaFile = JSON.parse(content.toString("utf-8")) as NoteMetaFile;
|
||||
}
|
||||
|
||||
zipfile.readEntry();
|
||||
});
|
||||
|
||||
if (!metaFile) {
|
||||
throw new ValidationError("Missing meta file.");
|
||||
}
|
||||
|
||||
const previewResults = previewMeta(metaFile);
|
||||
return previewResults;
|
||||
}
|
||||
|
||||
interface PreviewContext {
|
||||
dangerousAttributes: Set<string>;
|
||||
dangerousAttributeCategories: Set<DangerousAttributeCategory>;
|
||||
numNotes: number;
|
||||
numAttributes: number;
|
||||
numAttachments: number;
|
||||
}
|
||||
|
||||
export function previewMeta(meta: NoteMetaFile): Omit<ImportPreviewResponse, "id" | "fileName"> {
|
||||
const context: PreviewContext = {
|
||||
dangerousAttributes: new Set<string>(),
|
||||
dangerousAttributeCategories: new Set<DangerousAttributeCategory>(),
|
||||
numNotes: 0,
|
||||
numAttributes: 0,
|
||||
numAttachments: 0
|
||||
};
|
||||
previewMetaInternal(meta.files, context);
|
||||
|
||||
return {
|
||||
isDangerous: context.dangerousAttributes.size > 0,
|
||||
dangerousAttributes: Array.from(context.dangerousAttributes),
|
||||
dangerousAttributeCategories: Array.from(context.dangerousAttributeCategories),
|
||||
numNotes: context.numNotes,
|
||||
numAttributes: context.numAttributes,
|
||||
numAttachments: context.numAttachments
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function previewMetaInternal(metaFiles: NoteMeta[], context: PreviewContext) {
|
||||
for (const metaFile of metaFiles) {
|
||||
context.numNotes++;
|
||||
|
||||
// Look through the attributes for dangerous ones.
|
||||
if (metaFile.attributes) {
|
||||
for (const { name, type } of metaFile.attributes) {
|
||||
context.numAttributes++;
|
||||
const dangerousAttribute = BUILTIN_ATTRIBUTES.find((attr) =>
|
||||
attr.type === type &&
|
||||
attr.name.toLowerCase() === name.trim().toLowerCase() && attr.isDangerous) as DangerousAttributeInfo | undefined;
|
||||
if (!dangerousAttribute) continue;
|
||||
|
||||
context.dangerousAttributes.add(name);
|
||||
context.dangerousAttributeCategories.add(dangerousAttribute.dangerCategory);
|
||||
}
|
||||
}
|
||||
|
||||
if (metaFile.attachments) {
|
||||
context.numAttachments += metaFile.attachments.length;
|
||||
}
|
||||
|
||||
if (metaFile.children) {
|
||||
previewMetaInternal(metaFile.children, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,82 +81,66 @@ interface ExportAsPdfOpts {
|
||||
}
|
||||
|
||||
electron.ipcMain.on("print-note", async (e, { notePath }: PrintOpts) => {
|
||||
try {
|
||||
const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "printing");
|
||||
browserWindow.webContents.print({}, (success, failureReason) => {
|
||||
if (!success && failureReason !== "Print job canceled") {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-print"), failureReason);
|
||||
}
|
||||
e.sender.send("print-done", printReport);
|
||||
browserWindow.destroy();
|
||||
});
|
||||
} catch (err) {
|
||||
e.sender.send("print-done", {
|
||||
type: "error",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
stack: err instanceof Error ? err.stack : undefined
|
||||
});
|
||||
}
|
||||
const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "printing");
|
||||
browserWindow.webContents.print({}, (success, failureReason) => {
|
||||
if (!success && failureReason !== "Print job canceled") {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-print"), failureReason);
|
||||
}
|
||||
e.sender.send("print-done", printReport);
|
||||
browserWindow.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
electron.ipcMain.on("export-as-pdf", async (e, { title, notePath, landscape, pageSize }: ExportAsPdfOpts) => {
|
||||
try {
|
||||
const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "exporting_pdf");
|
||||
const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "exporting_pdf");
|
||||
|
||||
async function print() {
|
||||
const filePath = electron.dialog.showSaveDialogSync(browserWindow, {
|
||||
defaultPath: formatDownloadTitle(title, "file", "application/pdf"),
|
||||
filters: [
|
||||
{
|
||||
name: t("pdf.export_filter"),
|
||||
extensions: ["pdf"]
|
||||
}
|
||||
]
|
||||
async function print() {
|
||||
const filePath = electron.dialog.showSaveDialogSync(browserWindow, {
|
||||
defaultPath: formatDownloadTitle(title, "file", "application/pdf"),
|
||||
filters: [
|
||||
{
|
||||
name: t("pdf.export_filter"),
|
||||
extensions: ["pdf"]
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!filePath) return;
|
||||
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await browserWindow.webContents.printToPDF({
|
||||
landscape,
|
||||
pageSize,
|
||||
generateDocumentOutline: true,
|
||||
generateTaggedPDF: true,
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: `<div></div>`,
|
||||
footerTemplate: `
|
||||
<div class="pageNumber" style="width: 100%; text-align: center; font-size: 10pt;">
|
||||
</div>
|
||||
`
|
||||
});
|
||||
if (!filePath) return;
|
||||
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await browserWindow.webContents.printToPDF({
|
||||
landscape,
|
||||
pageSize,
|
||||
generateDocumentOutline: true,
|
||||
generateTaggedPDF: true,
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: `<div></div>`,
|
||||
footerTemplate: `
|
||||
<div class="pageNumber" style="width: 100%; text-align: center; font-size: 10pt;">
|
||||
</div>
|
||||
`
|
||||
});
|
||||
} catch (_e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, buffer);
|
||||
} catch (_e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
electron.shell.openPath(filePath);
|
||||
} catch (_e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await print();
|
||||
} finally {
|
||||
e.sender.send("print-done", printReport);
|
||||
browserWindow.destroy();
|
||||
await fs.writeFile(filePath, buffer);
|
||||
} catch (_e) {
|
||||
electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
e.sender.send("print-done", {
|
||||
type: "error",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
stack: err instanceof Error ? err.stack : undefined
|
||||
});
|
||||
|
||||
electron.shell.openPath(filePath);
|
||||
}
|
||||
|
||||
try {
|
||||
await print();
|
||||
} finally {
|
||||
e.sender.send("print-done", printReport);
|
||||
browserWindow.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -167,7 +151,6 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
offscreen: true,
|
||||
devTools: false,
|
||||
session: e.sender.session
|
||||
},
|
||||
});
|
||||
@@ -175,78 +158,13 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
|
||||
const progressCallback = (_e, progress: number) => e.sender.send("print-progress", { progress, action });
|
||||
ipcMain.on("print-progress", progressCallback);
|
||||
|
||||
// Capture ALL console output (including errors) for debugging
|
||||
browserWindow.webContents.on("console-message", (e, message, line, sourceId) => {
|
||||
if (e.level === "debug") return;
|
||||
if (e.level === "error") {
|
||||
log.error(`[Print Window ${sourceId}:${line}] ${message}`);
|
||||
return;
|
||||
}
|
||||
log.info(`[Print Window ${sourceId}:${line}] ${message}`);
|
||||
});
|
||||
|
||||
try {
|
||||
await browserWindow.loadURL(`http://127.0.0.1:${port}/?print#${notePath}`);
|
||||
} catch (err) {
|
||||
log.error(`Failed to load print window: ${err}`);
|
||||
ipcMain.off("print-progress", progressCallback);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Set up error tracking and logging in the renderer process
|
||||
await browserWindow.webContents.executeJavaScript(`
|
||||
(function() {
|
||||
window._printWindowErrors = [];
|
||||
window.addEventListener("error", (e) => {
|
||||
const errorMsg = "Uncaught error: " + e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno;
|
||||
console.error(errorMsg);
|
||||
if (e.error?.stack) console.error(e.error.stack);
|
||||
window._printWindowErrors.push({
|
||||
type: 'error',
|
||||
message: errorMsg,
|
||||
stack: e.error?.stack
|
||||
});
|
||||
});
|
||||
window.addEventListener("unhandledrejection", (e) => {
|
||||
const errorMsg = "Unhandled rejection: " + String(e.reason);
|
||||
console.error(errorMsg);
|
||||
if (e.reason?.stack) console.error(e.reason.stack);
|
||||
window._printWindowErrors.push({
|
||||
type: 'rejection',
|
||||
message: errorMsg,
|
||||
stack: e.reason?.stack
|
||||
});
|
||||
});
|
||||
})();
|
||||
`).catch(err => log.error(`Failed to set up error handlers in print window: ${err}`));
|
||||
|
||||
let printReport;
|
||||
try {
|
||||
printReport = await browserWindow.webContents.executeJavaScript(`
|
||||
new Promise((resolve, reject) => {
|
||||
if (window._noteReady) return resolve(window._noteReady);
|
||||
|
||||
// Check for errors periodically
|
||||
const errorChecker = setInterval(() => {
|
||||
if (window._printWindowErrors && window._printWindowErrors.length > 0) {
|
||||
clearInterval(errorChecker);
|
||||
const errors = window._printWindowErrors.map(e => e.message).join('; ');
|
||||
reject(new Error("Print window errors: " + errors));
|
||||
}
|
||||
}, 100);
|
||||
|
||||
window.addEventListener("note-ready", (data) => {
|
||||
clearInterval(errorChecker);
|
||||
resolve(data.detail);
|
||||
});
|
||||
});
|
||||
`);
|
||||
} catch (err) {
|
||||
log.error(`Print window promise failed for ${notePath}: ${err}`);
|
||||
ipcMain.off("print-progress", progressCallback);
|
||||
throw err;
|
||||
}
|
||||
|
||||
await browserWindow.loadURL(`http://127.0.0.1:${port}/?print#${notePath}`);
|
||||
const printReport = await browserWindow.webContents.executeJavaScript(`
|
||||
new Promise(resolve => {
|
||||
if (window._noteReady) return resolve(window._noteReady);
|
||||
window.addEventListener("note-ready", (data) => resolve(data.detail));
|
||||
});
|
||||
`);
|
||||
ipcMain.off("print-progress", progressCallback);
|
||||
return { browserWindow, printReport };
|
||||
}
|
||||
@@ -303,6 +221,7 @@ async function createMainWindow(app: App) {
|
||||
|
||||
configureWebContents(mainWindow.webContents, spellcheckEnabled);
|
||||
trackWindowFocus(mainWindow);
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function getWindowExtraOpts() {
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"keywords": [],
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"devDependencies": {
|
||||
"@wxt-dev/auto-icons": "1.1.0",
|
||||
"wxt": "0.20.14"
|
||||
"wxt": "0.20.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"cash-dom": "8.1.5"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"preview": "pnpm build && vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "25.8.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"preact": "10.28.3",
|
||||
"preact-iso": "2.11.1",
|
||||
|
||||
@@ -27,10 +27,6 @@ export default function Resources() {
|
||||
<Section className="icon-packs fill">
|
||||
<h2>{t("resources.icon_packs")}</h2>
|
||||
|
||||
<div>
|
||||
<p>Note: This feature is still in preview and is available only in the <a href="https://docs.triliumnotes.org/user-guide/advanced-usage/nightly-release" target="_blank" rel="noopener noreferrer">nightly release</a>.</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="resources.icon_packs_intro"
|
||||
|
||||
@@ -154,8 +154,7 @@
|
||||
"header": {
|
||||
"get-started": "Loslegen",
|
||||
"documentation": "Dokumentation",
|
||||
"support-us": "Unterstützt uns",
|
||||
"resources": "Ressourcen"
|
||||
"support-us": "Unterstützt uns"
|
||||
},
|
||||
"footer": {
|
||||
"copyright_and_the": " und die ",
|
||||
@@ -197,12 +196,5 @@
|
||||
"download_exe": "Installationsdatei herunterladen (.exe)",
|
||||
"download_zip": "Portable (.zip)",
|
||||
"download_scoop": "Scoop"
|
||||
},
|
||||
"resources": {
|
||||
"icon_packs": "Symbolpakete",
|
||||
"download": "Herunterladen",
|
||||
"website": "Webseite",
|
||||
"title": "Ressourcen",
|
||||
"icon_packs_intro": "Erweitere die Auswahl an verfügbaren Symbolen für deine Notizen, indem du ein Symbolpaket verwendest. Für weitere Informationen zu Symbolpaketen, schaue in die <DocumentationLink>offizielle Dokumentation</DocumentationLink>."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,14 +195,6 @@
|
||||
"header": {
|
||||
"get-started": "Comencemos",
|
||||
"documentation": "Documentación",
|
||||
"support-us": "Apóyanos",
|
||||
"resources": "Recursos"
|
||||
},
|
||||
"resources": {
|
||||
"title": "Recursos",
|
||||
"icon_packs": "Paquetes de iconos",
|
||||
"icon_packs_intro": "Ampliar la selección de iconos disponibles para sus notas utilizando un paquete de iconos. Para más información acerca de paquetes de icono, vea la <DocumentationLink>documentación oficial</DocumentationLink>.",
|
||||
"download": "Descargar",
|
||||
"website": "Sitio web"
|
||||
"support-us": "Apóyanos"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +112,7 @@
|
||||
"header": {
|
||||
"get-started": "Tosaigh",
|
||||
"documentation": "Doiciméadú",
|
||||
"support-us": "Tacaigh linn",
|
||||
"resources": "Acmhainní"
|
||||
"support-us": "Tacaigh linn"
|
||||
},
|
||||
"footer": {
|
||||
"copyright_and_the": " agus an ",
|
||||
@@ -197,12 +196,5 @@
|
||||
"description": "Nótaí Trilium atá á n-óstáil ar PikaPods, seirbhís íoctha le haghaidh rochtana agus bainistíochta éasca. Níl baint dhíreach aige le foireann Trilium.",
|
||||
"download_pikapod": "Socraigh ar PikaPods",
|
||||
"download_triliumcc": "Nó féach ar trilium.cc"
|
||||
},
|
||||
"resources": {
|
||||
"title": "Acmhainní",
|
||||
"icon_packs": "Pacáistí deilbhín",
|
||||
"icon_packs_intro": "Leathnaigh rogha na ndeilbhíní atá ar fáil le do nótaí trí phacáiste deilbhíní a úsáid. Le haghaidh tuilleadh eolais faoi phacáistí deilbhíní, féach ar an <DocumentationLink>dhoiciméadú oifigiúil</DocumentationLink>.",
|
||||
"download": "Íoslódáil",
|
||||
"website": "Suíomh Gréasáin"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,14 +195,6 @@
|
||||
"header": {
|
||||
"get-started": "はじめる",
|
||||
"documentation": "ドキュメント",
|
||||
"support-us": "サポート",
|
||||
"resources": "リソース"
|
||||
},
|
||||
"resources": {
|
||||
"title": "リソース",
|
||||
"icon_packs": "アイコンパック",
|
||||
"icon_packs_intro": "アイコンパックを使用すると、ノートで使用できるアイコンの選択肢が広がります。アイコンパックの詳細については、<DocumentationLink>公式ドキュメント</DocumentationLink>をご覧ください。",
|
||||
"download": "ダウンロード",
|
||||
"website": "Webサイト"
|
||||
"support-us": "サポート"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,14 +195,6 @@
|
||||
"header": {
|
||||
"get-started": "Start",
|
||||
"documentation": "Dokumentacja",
|
||||
"support-us": "Wesprzyj nas",
|
||||
"resources": "Zasoby"
|
||||
},
|
||||
"resources": {
|
||||
"title": "Zasoby",
|
||||
"icon_packs": "Paczki ikon",
|
||||
"icon_packs_intro": "Rozszerz wybór dostępnych ikon dla swoich notatek, korzystając z pakietu ikon. Więcej informacji na temat pakietów ikon znajdziesz w <DocumentationLink> dokumentacji </DocumentationLink>.",
|
||||
"download": "Pobieranie",
|
||||
"website": "Strona internetowa"
|
||||
"support-us": "Wesprzyj nas"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +131,7 @@
|
||||
"header": {
|
||||
"get-started": "开始使用",
|
||||
"documentation": "文档",
|
||||
"support-us": "支持我们",
|
||||
"resources": "资源"
|
||||
"support-us": "支持我们"
|
||||
},
|
||||
"footer": {
|
||||
"copyright_and_the": " 以及 ",
|
||||
@@ -197,12 +196,5 @@
|
||||
"download_tar_x64": "64 位 (.tar.xz)",
|
||||
"download_tar_arm64": "ARM (.tar.xz)",
|
||||
"download_nixos": "NixOS 模块"
|
||||
},
|
||||
"resources": {
|
||||
"icon_packs": "图标包",
|
||||
"icon_packs_intro": "使用图标包可以扩展笔记中可用的图标选择。有关图标包的更多信息,请参阅<DocumentationLink>官方文档</DocumentationLink>。",
|
||||
"download": "下载",
|
||||
"website": "网站",
|
||||
"title": "资源"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,7 @@ export default defineConfig({
|
||||
prerender: {
|
||||
enabled: true,
|
||||
renderTarget: '#app',
|
||||
additionalPrerenderRoutes: [
|
||||
'/404',
|
||||
'/get-started',
|
||||
'/resources',
|
||||
'/support-us'
|
||||
],
|
||||
additionalPrerenderRoutes: ['/404'],
|
||||
previewMiddlewareEnabled: true,
|
||||
previewMiddlewareFallback: '/404',
|
||||
},
|
||||
|
||||
21
docs/README-pl.md
vendored
21
docs/README-pl.md
vendored
@@ -35,7 +35,7 @@ wiedzy.
|
||||
|
||||
<img src="./app.png" alt="Trilium Screenshot" width="1000">
|
||||
|
||||
## ⏬ Pobieranie
|
||||
## ⏬ Pobierz
|
||||
- [Ostatnie wydanie](https://github.com/TriliumNext/Trilium/releases/latest) –
|
||||
stabilna wersja, polecane dla większości użytkowników.
|
||||
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) -
|
||||
@@ -48,7 +48,7 @@ wiedzy.
|
||||
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
|
||||
|
||||
Nasza dokumentacja jest dostępna w wielu formatach:
|
||||
- **Dokumentacja Online**: Pełna dokumentacja dostępna na
|
||||
- **Dokumentacja Online**: Pełna dokumentacja dostępna pod
|
||||
[docs.triliumnotes.org](https://docs.triliumnotes.org/)
|
||||
- **Pomoc w aplikacji**: Naciśnij `F1` w Trilium, aby uzyskać dostęp do tej
|
||||
samej dokumentacji bezpośrednio w aplikacji
|
||||
@@ -64,15 +64,15 @@ Nasza dokumentacja jest dostępna w wielu formatach:
|
||||
TriliumNext](https://docs.triliumnotes.org/user-guide/setup/upgrading)
|
||||
- [Podstawowe koncepcje i
|
||||
funkcjonalność](https://docs.triliumnotes.org/user-guide/concepts/notes)
|
||||
- [Wzorce osobistej Bazy
|
||||
Wiedzy](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)
|
||||
- [Wzorce Bazy Wiedzy
|
||||
Osobistej](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)
|
||||
|
||||
## 🎁 Funkcjonalność
|
||||
|
||||
* Notatki mogą być zorganizowane w drzewa dowolnej głębokości. Pojedyncza
|
||||
notatka może być umieszczona w wielu miejscach w drzewie (patrz
|
||||
[klonowanie](https://docs.triliumnotes.org/user-guide/concepts/notes/cloning))
|
||||
* Bogato wyposażony edytor notatek WYSIWYG, zawierający np. tabele, obrazy i
|
||||
* Bogaty edytor notatek WYSIWYG, zawierający np. tabele, obrazy i
|
||||
[matematykę](https://docs.triliumnotes.org/user-guide/note-types/text) z
|
||||
[autoformat](https://docs.triliumnotes.org/user-guide/note-types/text/markdown-formatting)
|
||||
Markdown
|
||||
@@ -83,7 +83,7 @@ Nasza dokumentacja jest dostępna w wielu formatach:
|
||||
notatkach](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation),
|
||||
wyszukiwanie po pełnym tekście i[wyróżnienie
|
||||
notatki](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
|
||||
* Bezproblemowe [aktualizowanie wersji
|
||||
* Płynne [aktualizowanie wersji
|
||||
notatki](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
|
||||
* [Atrybuty](https://docs.triliumnotes.org/user-guide/advanced-usage/attributes)
|
||||
notatki mogą być użyte dla jej organizacji, wyszukiwania i użycia
|
||||
@@ -113,7 +113,7 @@ Nasza dokumentacja jest dostępna w wielu formatach:
|
||||
* [Mapy
|
||||
geograficzne](https://docs.triliumnotes.org/user-guide/collections/geomap) z
|
||||
oznaczeniami lokalizacji i trasami GPX
|
||||
* [Skrypty](https://docs.triliumnotes.org/user-guide/scripts) – patrz
|
||||
* [Skryptowanie](https://docs.triliumnotes.org/user-guide/scripts) – patrz
|
||||
[zaawansowane
|
||||
przykłady](https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases)
|
||||
* [REST API](https://docs.triliumnotes.org/user-guide/advanced-usage/etapi) do
|
||||
@@ -209,7 +209,7 @@ interfejsu webowego, niemal identycznego z aplikacją desktopową.
|
||||
|
||||
Aktualnie wspierane i testowane są tylko najnowsze wersje Chrome i Firefox.
|
||||
|
||||
### Urządzenia mobilne
|
||||
### Mobile
|
||||
|
||||
Aby korzystać z TriliumNext na urządzeniu mobilnym, możesz użyć mobilnej
|
||||
przeglądarki internetowej, aby uzyskać dostęp do mobilnego interfejsu instalacji
|
||||
@@ -295,8 +295,9 @@ W razie dodatkowych pytań możesz skorzystać z linków podanych w sekcji
|
||||
implementację aplikacji.
|
||||
* [Sarah Hussein](https://github.com/Sarah-Hussein) za zaprojektowanie ikony
|
||||
aplikacji.
|
||||
* [nriver](https://github.com/nriver) za prace nad wersjami językowymi.
|
||||
* [Thomas Frei](https://github.com/thfrei) za pierwotne prace nad Canvas.
|
||||
* [nriver](https://github.com/nriver) za prace nad internacjonalizacją.
|
||||
* [Thomas Frei](https://github.com/thfrei) za pierwotne prace nad Płótnem (
|
||||
Canvas ).
|
||||
* [antoniotejada](https://github.com/nriver) za pierwotny widżet podświetlania
|
||||
składni.
|
||||
* [Dosu](https://dosu.dev/) za stworzenie rozwiązania umożliwiającego
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# Icon Packs
|
||||
> [!IMPORTANT]
|
||||
> This feature is still in preview and is available only in the [nightly release](https://docs.triliumnotes.org/user-guide/advanced-usage/nightly-release).
|
||||
|
||||
<figure class="image image-style-align-right image_resized" style="width:45.14%;"><img style="aspect-ratio:854/649;" src="Icon Packs_image.png" width="854" height="649"></figure>
|
||||
|
||||
By default, Trilium comes with a set of icons called Boxicons v2. Since v0.102.0, custom icon packs allow a wider selection of icons for notes.
|
||||
|
||||
10
package.json
10
package.json
@@ -46,7 +46,7 @@
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "4.0.3",
|
||||
"@fast-csv/parse": "5.0.5",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@playwright/test": "1.58.1",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
@@ -57,7 +57,7 @@
|
||||
"chalk": "5.6.2",
|
||||
"cross-env": "10.1.0",
|
||||
"dpdm": "4.0.1",
|
||||
"esbuild": "0.27.3",
|
||||
"esbuild": "0.27.2",
|
||||
"eslint": "10.0.0",
|
||||
"eslint-config-preact": "2.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
@@ -69,11 +69,11 @@
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonc-eslint-parser": "2.4.2",
|
||||
"react-refresh": "0.18.0",
|
||||
"rollup-plugin-webpack-stats": "2.1.11",
|
||||
"rollup-plugin-webpack-stats": "2.1.10",
|
||||
"tslib": "2.8.1",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.55.0",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"upath": "2.0.1",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-dts": "4.5.4",
|
||||
@@ -93,7 +93,7 @@
|
||||
"url": "https://github.com/TriliumNext/Trilium/issues"
|
||||
},
|
||||
"homepage": "https://triliumnotes.org",
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.55.0",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
@@ -33,13 +33,13 @@
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"stylelint": "17.2.0",
|
||||
"stylelint": "17.1.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.24.0"
|
||||
"webdriverio": "9.23.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.55.0",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
@@ -34,13 +34,13 @@
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"stylelint": "17.2.0",
|
||||
"stylelint": "17.1.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.24.0"
|
||||
"webdriverio": "9.23.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.55.0",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
@@ -36,13 +36,13 @@
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"stylelint": "17.2.0",
|
||||
"stylelint": "17.1.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.24.0"
|
||||
"webdriverio": "9.23.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.55.0",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
@@ -36,13 +36,13 @@
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"stylelint": "17.2.0",
|
||||
"stylelint": "17.1.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.24.0"
|
||||
"webdriverio": "9.23.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "54.3.2",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.55.0",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/coverage-istanbul": "4.0.18",
|
||||
"ckeditor5": "47.4.0",
|
||||
@@ -36,13 +36,13 @@
|
||||
"eslint-config-ckeditor5": ">=9.1.0",
|
||||
"http-server": "14.1.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"stylelint": "17.2.0",
|
||||
"stylelint": "17.1.0",
|
||||
"stylelint-config-ckeditor5": ">=9.1.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.24.0"
|
||||
"webdriverio": "9.23.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"ckeditor5-premium-features": "47.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@smithy/middleware-retry": "4.4.31",
|
||||
"@smithy/middleware-retry": "4.4.30",
|
||||
"@types/jquery": "3.5.33"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/lang-css": "6.3.1",
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@codemirror/lang-javascript": "6.2.4",
|
||||
@@ -16,7 +16,7 @@
|
||||
"@codemirror/lang-xml": "6.1.0",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/view": "6.39.13",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@fsegurai/codemirror-theme-abcdef": "6.2.3",
|
||||
"@fsegurai/codemirror-theme-abyss": "6.2.3",
|
||||
"@fsegurai/codemirror-theme-android-studio": "6.2.3",
|
||||
@@ -50,6 +50,6 @@
|
||||
"codemirror-lang-elixir": "4.0.0",
|
||||
"codemirror-lang-hcl": "0.1.0",
|
||||
"codemirror-lang-mermaid": "0.5.0",
|
||||
"eslint-linter-browserify": "10.0.0"
|
||||
"eslint-linter-browserify": "9.39.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ type Labels = {
|
||||
"calendar:initialDate": string;
|
||||
"map:style": string;
|
||||
"map:scale": boolean;
|
||||
"map:hideLabels": boolean;
|
||||
"board:groupBy": string;
|
||||
maxNestingDepth: number;
|
||||
includeArchived: boolean;
|
||||
|
||||
@@ -298,3 +298,16 @@ export interface IconRegistry {
|
||||
}[]
|
||||
}[];
|
||||
}
|
||||
|
||||
export type DangerousAttributeCategory = "codeExecution" | "serverSideScripting" | "clientSideScripting" | "iconPack" | "webview";
|
||||
|
||||
export interface ImportPreviewResponse {
|
||||
id: string;
|
||||
fileName: string;
|
||||
numNotes: number;
|
||||
numAttributes: number;
|
||||
numAttachments: number;
|
||||
isDangerous: boolean;
|
||||
dangerousAttributeCategories: string[];
|
||||
dangerousAttributes: string[];
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
"devDependencies": {
|
||||
"@digitak/esrun": "3.2.26",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@typescript-eslint/eslint-plugin": "8.55.0",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"dotenv": "17.2.3",
|
||||
"esbuild": "0.27.3",
|
||||
"esbuild": "0.27.2",
|
||||
"eslint": "10.0.0",
|
||||
"highlight.js": "11.11.1",
|
||||
"typescript": "5.9.3"
|
||||
|
||||
1847
pnpm-lock.yaml
generated
1847
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user