Compare commits

..

3 Commits

Author SHA1 Message Date
Elian Doran
e6d728715f feat(geomap): support hiding labels 2026-02-12 19:47:24 +02:00
Elian Doran
54a52f0589 feat(geomap): support for custom tile URLs 2026-02-12 19:32:22 +02:00
Elian Doran
badfa23f86 refactor(geomap): delegate layer data handling to index 2026-02-12 19:12:56 +02:00
49 changed files with 765 additions and 879 deletions

View File

@@ -11,7 +11,7 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.29.2",
"devDependencies": {
"@redocly/cli": "2.18.0",
"@redocly/cli": "2.17.0",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"react": "19.2.4",

View File

@@ -23,6 +23,7 @@
"@fullcalendar/list": "6.1.20",
"@fullcalendar/multimonth": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
@@ -43,16 +44,17 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.1",
"globals": "17.3.0",
"i18next": "25.8.5",
"i18next": "25.8.4",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.28",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"maplibre-gl": "5.18.0",
"marked": "17.0.2",
"marked": "17.0.1",
"mermaid": "11.12.2",
"mind-elixir": "5.8.0",
"normalize.css": "8.0.1",
@@ -70,11 +72,13 @@
"@prefresh/vite": "2.4.11",
"@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.33",
"@types/leaflet": "1.9.21",
"@types/leaflet-gpx": "1.3.8",
"@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.6.1",
"happy-dom": "20.5.0",
"lightningcss": "1.31.1",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"

View File

@@ -1,4 +1,4 @@
import type { GeoMouseEvent } from "../widgets/collections/geomap/map.js";
import type { LeafletMouseEvent } from "leaflet";
import appContext, { type CommandNames } from "../components/app_context.js";
import { t } from "../services/i18n.js";
@@ -16,7 +16,7 @@ function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewS
});
}
function getItems(e: ContextMenuEvent | GeoMouseEvent): MenuItem<CommandNames>[] {
function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandNames>[] {
const ntxId = getNtxId(e);
const isMobileSplitOpen = isMobile() && appContext.tabManager.getNoteContextById(ntxId).getMainContext().getSubContexts().length > 1;
@@ -28,7 +28,7 @@ function getItems(e: ContextMenuEvent | GeoMouseEvent): MenuItem<CommandNames>[]
];
}
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | GeoMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | LeafletMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
}
@@ -52,7 +52,7 @@ function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEv
return false;
}
function getNtxId(e: ContextMenuEvent | GeoMouseEvent) {
function getNtxId(e: ContextMenuEvent | LeafletMouseEvent) {
if (utils.isDesktop()) {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
if (!subContexts) return null;

View File

@@ -1180,6 +1180,9 @@
"note_not_found": "الملاحظة {{noteId}} غير موجودة!",
"cannot_match_transform": "تعذر مطابقة التحويل: {{transform}}"
},
"web_view": {
"web_view": "عرض الويب"
},
"consistency_checks": {
"title": "فحوصات التناسق"
},

View File

@@ -1068,6 +1068,11 @@
"note_detail_render_help_1": "之所以显示此帮助说明,是因为这个类型为渲染 HTML 的笔记没有正常工作所需的关系。",
"note_detail_render_help_2": "渲染 HTML 笔记类型用于<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">编写脚本</a>。简而言之,您有一份 HTML 代码笔记(可包含一些 JavaScript然后这个笔记会把页面渲染出来。要使其正常工作您需要定义一个名为 \"renderNote\" 的<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">关系</a>指向要渲染的 HTML 笔记。"
},
"web_view": {
"web_view": "网页视图",
"embed_websites": "网页视图类型的笔记允许您将网站嵌入到 Trilium 中。",
"create_label": "首先,请创建一个带有您要嵌入的 URL 地址的标签,例如 #webViewSrc=\"https://www.bing.com\""
},
"backend_log": {
"refresh": "刷新"
},
@@ -1416,8 +1421,7 @@
"description": "描述",
"reload_app": "重载应用以应用更改",
"set_all_to_default": "将所有快捷键重置为默认值",
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?",
"no_results": "未找到与“{{filter}}”匹配的快捷方式"
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?"
},
"spellcheck": {
"title": "拼写检查",
@@ -1618,9 +1622,7 @@
"print_report_title": "打印报告",
"print_report_collection_content_other": "集合中的 {{count}} 篇笔记无法打印,因为它们不受支持或受到保护。",
"print_report_collection_details_button": "查看详情",
"print_report_collection_details_ignored_notes": "忽略的笔记",
"print_report_error_title": "打印失败",
"print_report_stack_trace": "堆栈跟踪"
"print_report_collection_details_ignored_notes": "忽略的笔记"
},
"note_title": {
"placeholder": "请输入笔记标题...",
@@ -2266,12 +2268,5 @@
},
"bookmark_buttons": {
"bookmarks": "书签"
},
"web_view_setup": {
"title": "直接在 Trilium 中创建网页的实时视图",
"url_placeholder": "输入或粘贴网站地址,例如 https://triliumnotes.org",
"create_button": "创建网页视图",
"invalid_url_title": "无效的地址",
"invalid_url_message": "请输入有效的网址,例如 https://triliumnotes.org。"
}
}

View File

@@ -1067,6 +1067,11 @@
"note_detail_render_help_1": "Diese Hilfesnotiz wird angezeigt, da diese Notiz vom Typ „HTML rendern“ nicht über die erforderliche Beziehung verfügt, um ordnungsgemäß zu funktionieren.",
"note_detail_render_help_2": "Render-HTML-Notiztyp wird benutzt für <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. Kurzgesagt, du hast ein HTML-Code-Notiz (optional mit JavaScript) und diese Notiz rendert es. Damit es funktioniert, musst du eine a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">Beziehung</a> namens \"renderNote\" zeigend auf die HTML-Notiz zum rendern definieren."
},
"web_view": {
"web_view": "Webansicht",
"embed_websites": "Notiz vom Typ Web View ermöglicht das Einbetten von Websites in Trilium.",
"create_label": "Um zu beginnen, erstelle bitte ein Label mit einer URL-Adresse, die eingebettet werden soll, z. B. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Aktualisieren"
},
@@ -1382,8 +1387,7 @@
"description": "Beschreibung",
"reload_app": "Lade die App neu, um die Änderungen zu übernehmen",
"set_all_to_default": "Setze alle Verknüpfungen auf die Standardeinstellungen",
"confirm_reset": "Möchtest du wirklich alle Tastaturkürzel auf die Standardeinstellungen zurücksetzen?",
"no_results": "Keine Tastenkürzel für '{{filter}}' gefunden"
"confirm_reset": "Möchtest du wirklich alle Tastaturkürzel auf die Standardeinstellungen zurücksetzen?"
},
"spellcheck": {
"title": "Rechtschreibprüfung",
@@ -1587,9 +1591,7 @@
"print_report_collection_details_button": "Details anzeigen",
"print_report_collection_details_ignored_notes": "Ignorierte Notizen",
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt oder geschützt ist.",
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind.",
"print_report_error_title": "Druck fehlgeschlagen",
"print_report_stack_trace": "Stapelzurückverfolgung"
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind."
},
"note_title": {
"placeholder": "Titel der Notiz hier eingeben…",
@@ -2281,12 +2283,5 @@
},
"bookmark_buttons": {
"bookmarks": "Lesezeichen"
},
"web_view_setup": {
"title": "Erstelle eine Live-Ansicht einer Webseite direkt in Trilium",
"url_placeholder": "Gib oder füge die Adresse der Webseite ein, zum Beispiel https://triliumnotes.org",
"create_button": "Erstelle Web Ansicht",
"invalid_url_title": "Ungültige Adresse",
"invalid_url_message": "Füge eine valide Webadresse ein, zum Beispiel https://triliumnotes.org."
}
}

View File

@@ -2104,7 +2104,8 @@
"raster": "Raster",
"vector_light": "Vector (Light)",
"vector_dark": "Vector (Dark)",
"show-scale": "Show scale"
"show-scale": "Show scale",
"show-labels": "Show marker names"
},
"table_context_menu": {
"delete_row": "Delete row"

View File

@@ -1072,6 +1072,11 @@
"note_detail_render_help_1": "Esta nota de ayuda se muestra porque esta nota de tipo Renderizar HTML no tiene la relación requerida para funcionar correctamente.",
"note_detail_render_help_2": "El tipo de nota Render HTML es usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. De forma resumida, tiene una nota con código HTML (opcionalmente con algo de JavaScript) y esta nota la renderizará. Para que funcione, es necesario definir una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relación</a> llamada \"renderNote\" apuntando a la nota HTML nota a renderizar."
},
"web_view": {
"web_view": "Vista web",
"embed_websites": "La nota de tipo Web View le permite insertar sitios web en Trilium.",
"create_label": "Para comenzar, por favor cree una etiqueta con una dirección URL que desee empotrar, e.g. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Refrescar"
},
@@ -1572,8 +1577,7 @@
"description": "Descripción",
"reload_app": "Vuelva a cargar la aplicación para aplicar los cambios",
"set_all_to_default": "Establecer todos los accesos directos al valor predeterminado",
"confirm_reset": "¿Realmente desea restablecer todos los atajos de teclado a sus valores predeterminados?",
"no_results": "No se encontraron atajos que coincidan con '{{filter}} '"
"confirm_reset": "¿Realmente desea restablecer todos los atajos de teclado a sus valores predeterminados?"
},
"spellcheck": {
"title": "Revisión ortográfica",
@@ -1780,9 +1784,7 @@
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_title": "Imprimir informe",
"print_report_collection_details_button": "Ver detalles",
"print_report_collection_details_ignored_notes": "Notas ignoradas",
"print_report_stack_trace": "Rastreo de pila",
"print_report_error_title": "Fallo al imprimir"
"print_report_collection_details_ignored_notes": "Notas ignoradas"
},
"note_title": {
"placeholder": "escriba el título de la nota aquí...",
@@ -2296,12 +2298,5 @@
},
"bookmark_buttons": {
"bookmarks": "Marcadores"
},
"web_view_setup": {
"title": "Crear una vista en vivo de una página web directamente en Trilium",
"url_placeholder": "Ingresar o pegar la dirección del sitio web, por ejemplo https://triliumnotes.org",
"create_button": "Crear Vista Web",
"invalid_url_title": "Dirección inválida",
"invalid_url_message": "Ingrese una dirección web válida, por ejemplo https://triliumnotes.org."
}
}

View File

@@ -1057,6 +1057,11 @@
"note_detail_render_help_1": "Cette note d'aide s'affiche car cette note de type Rendu HTML n'a pas la relation requise pour fonctionner correctement.",
"note_detail_render_help_2": "Le type de note Rendu HTML est utilisé pour les <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripts</a>. En résumé, vous disposez d'une note de code HTML (éventuellement contenant JavaScript) et cette note affichera le rendu. Pour que cela fonctionne, vous devez définir une <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> appelée \"renderNote\" pointant vers la note HTML à rendre."
},
"web_view": {
"web_view": "Affichage Web",
"embed_websites": "Les notes de type Affichage Web vous permet d'intégrer des sites Web dans Trilium.",
"create_label": "Pour commencer, veuillez créer un label avec l'adresse URL que vous souhaitez intégrer, par ex. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Rafraîchir"
},

View File

@@ -1076,6 +1076,11 @@
"note_detail_render_help_1": "Taispeántar an nóta cabhrach seo mar nach bhfuil aon ghaol riachtanach ag an nóta seo den chineál Render HTML le go bhfeidhmeoidh sé i gceart.",
"note_detail_render_help_2": "Úsáidtear cineál nóta HTML rindreála le haghaidh <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scriptithe</a>. Go hachomair, tá nóta cóid HTML agat (le roinnt JavaScript más féidir) agus déanfaidh an nóta seo é a rindreáil. Chun go n-oibreoidh sé, ní mór duit <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">gaol</a> ar a dtugtar \"renderNote\" a shainiú ag pointeáil chuig an nóta HTML atá le rindreáil."
},
"web_view": {
"web_view": "Radharc Gréasáin",
"embed_websites": "Nóta den chineál Gréasáin a ligeann duit suíomhanna gréasáin a leabú i Trilium.",
"create_label": "Chun tús a chur leis, cruthaigh lipéad le seoladh URL ar mhaith leat a leabú, m.sh. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Athnuachan"
},
@@ -2323,12 +2328,5 @@
},
"bookmark_buttons": {
"bookmarks": "Leabharmharcanna"
},
"web_view_setup": {
"title": "Cruthaigh radharc beo de leathanach gréasáin go díreach isteach i Trilium",
"url_placeholder": "Cuir isteach nó greamaigh seoladh an tsuímh ghréasáin, mar shampla https://triliumnotes.org",
"create_button": "Cruthaigh Radharc Gréasáin",
"invalid_url_title": "Seoladh neamhbhailí",
"invalid_url_message": "Cuir isteach seoladh gréasáin bailí, mar shampla https://triliumnotes.org."
}
}

View File

@@ -50,8 +50,7 @@
"save": "Simpan",
"branch_prefix_saved": "Prefiks cabang telah disimpan.",
"branch_prefix_saved_multiple": "Prefix cabang telah disimpan pada {{count}} cabang.",
"affected_branches": "Cabang terdampak ({{count}}):",
"edit_branch_prefix": "Sunting awalan cabang"
"affected_branches": "Cabang terdampak ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Aksi borongan",
@@ -62,10 +61,7 @@
"execute_bulk_actions": "Eksekusi aksi borongan",
"bulk_actions_executed": "Aksi borongan telah di eksekusi dengan sukses.",
"none_yet": "Belum ada... tambahkan aksi dengan memilih salah satu dari aksi di atas.",
"labels": "Label-label",
"relations": "Hubungan",
"notes": "Catatan",
"other": "Lainnya"
"labels": "Label-label"
},
"confirm": {
"cancel": "Batal",
@@ -84,8 +80,6 @@
"no_note_to_delete": "Tidak ada Catatan yang akan dihapus (hanya duplikat)."
},
"clone_to": {
"clone_notes_to": "Duplikat catatan ke…",
"help_on_links": "Bantuan pada tautan",
"notes_to_clone": "Catatan untuk kloning"
"clone_notes_to": "Duplikat catatan ke…"
}
}

View File

@@ -186,8 +186,7 @@
"geo-map": {
"create-child-note-title": "Crea una nota figlia e aggiungila alla mappa",
"create-child-note-instruction": "Clicca sulla mappa per creare una nuova nota qui o premi Escape per uscire.",
"unable-to-load-map": "Impossibile caricare la mappa.",
"create-child-note-text": "Aggiungi indicatore"
"unable-to-load-map": "Impossibile caricare la mappa."
},
"geo-map-context": {
"open-location": "Apri la posizione",
@@ -423,8 +422,7 @@
"unknown_search_option": "Opzione di ricerca sconosciuta {{searchOptionName}}",
"search_note_saved": "La nota di ricerca è stata salvata in {{- notePathTitle}}",
"actions_executed": "Le azioni sono state eseguite.",
"view_options": "Opzioni di visualizzazione:",
"option": "opzione"
"view_options": "Opzioni di visualizzazione:"
},
"modal": {
"close": "Chiudi",
@@ -1243,8 +1241,7 @@
"show-cheatsheet": "Mostra il foglietto illustrativo",
"toggle-zen-mode": "Modalità Zen",
"new-version-available": "Nuovo aggiornamento disponibile",
"download-update": "Ottieni la versione {{latestVersion}}",
"search_notes": "Cerca note"
"download-update": "Ottieni la versione {{latestVersion}}"
},
"zen_mode": {
"button_exit": "Esci dalla modalità Zen"
@@ -1343,9 +1340,7 @@
"delete_this_note": "Elimina questa nota",
"note_revisions": "Revisioni delle note",
"error_cannot_get_branch_id": "Impossibile ottenere branchId per notePath '{{notePath}}'",
"error_unrecognized_command": "Comando non riconosciuto {{command}}",
"backlinks": "Backlinks",
"content_language_switcher": "Lingua dei contenuti: {{language}}"
"error_unrecognized_command": "Comando non riconosciuto {{command}}"
},
"note_icon": {
"change_note_icon": "Cambia icona nota",
@@ -1586,6 +1581,11 @@
"note_detail_render_help_1": "Questa nota di aiuto viene visualizzata perché questa nota di tipo Render HTML non ha la relazione richiesta per funzionare correttamente.",
"note_detail_render_help_2": "Il tipo di nota HTML Render viene utilizzato per lo <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In breve, si ottiene una nota in codice HTML (opzionalmente con un po' di JavaScript) che verrà visualizzata. Per farla funzionare, è necessario definire una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relazione</a> denominata \"renderNote\" che punti alla nota HTML da visualizzare."
},
"web_view": {
"web_view": "Visualizzazione Web",
"embed_websites": "La nota di tipo Web View consente di incorporare siti web in Trilium.",
"create_label": "Per iniziare, crea un'etichetta con l'indirizzo URL che desideri incorporare, ad esempio #webViewSrc=\"https://www.google.com\""
},
"vacuum_database": {
"title": "Pulizia del database",
"description": "Questa operazione ricostruirà il database, generando in genere un file di dimensioni inferiori. In realtà, nessun dato verrà modificato.",
@@ -2145,7 +2145,7 @@
"background_effects_title": "Gli effetti di sfondo sono ora stabili",
"background_effects_message": "Sui dispositivi Windows, gli effetti di sfondo sono ora completamente stabili. Gli effetti di sfondo aggiungono un tocco di colore all'interfaccia utente sfocando lo sfondo retrostante. Questa tecnica è utilizzata anche in altre applicazioni come Esplora risorse di Windows.",
"background_effects_button": "Abilita gli effetti di sfondo",
"dismiss": "Chiudi",
"dismiss": "Congedare",
"new_layout_title": "Nuovo layout",
"new_layout_message": "Abbiamo introdotto un layout modernizzato per Trilium. La barra multifunzione è stata rimossa e integrata perfettamente nell'interfaccia principale, con una nuova barra di stato e sezioni espandibili (come gli attributi promossi) che assumono le funzioni chiave.\n\nIl nuovo layout è abilitato di default e può essere temporaneamente disabilitato tramite Opzioni → Aspetto.",
"new_layout_button": "Maggiori informazioni"
@@ -2281,24 +2281,5 @@
"pages_other": "{{count}} pagine",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Caricamento in corso..."
},
"web_view_setup": {
"title": "Crea una visualizzazione live di una pagina web direttamente in Trilium",
"url_placeholder": "Inserisci o incolla l'indirizzo del sito web, ad esempio https://triliumnotes.org",
"create_button": "Crea vista Web",
"invalid_url_title": "Indirizzo non valido",
"invalid_url_message": "Inserisci un indirizzo web valido, ad esempio https://triliumnotes.org."
},
"platform_indicator": {
"available_on": "Disponibile su {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "Scheda {{count}}",
"title_many": "Schede {{count}}",
"title_other": "Schede {{count}}",
"more_options": "Altre opzioni"
},
"bookmark_buttons": {
"bookmarks": "Segnalibri"
}
}

View File

@@ -826,6 +826,11 @@
"error_no_path": "移動するパスがありません。",
"move_success_message": "選択したノートは以下に移動されました "
},
"web_view": {
"web_view": "Web ビュー",
"embed_websites": "Web ビュータイプでは、web サイトを Trilium に埋め込むことができます。",
"create_label": "まず始めに、埋め込みたいURLアドレスのラベルを作成してください。例: #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "リフレッシュ"
},
@@ -1954,9 +1959,7 @@
"print_report_title": "レポートを印刷",
"print_report_collection_content_other": "コレクション内の {{count}} 件のノートは、サポートされていないか保護されているため、印刷できませんでした。",
"print_report_collection_details_button": "詳細を見る",
"print_report_collection_details_ignored_notes": "無視されたノート",
"print_report_error_title": "印刷に失敗しました",
"print_report_stack_trace": "スタックトレース"
"print_report_collection_details_ignored_notes": "無視されたノート"
},
"watched_file_update_status": {
"ignore_this_change": "この変更を無視する",
@@ -2265,12 +2268,5 @@
},
"bookmark_buttons": {
"bookmarks": "ブックマーク"
},
"web_view_setup": {
"title": "Trilium に直接 Web ページのライブビューを作成",
"url_placeholder": "Web サイトのアドレスを入力または貼り付けて下さい。 例: https://triliumnotes.org",
"create_button": "Web ビューを作成",
"invalid_url_title": "無効なアドレス",
"invalid_url_message": "有効な Web アドレスを入力してください。 例: https://triliumnotes.org"
}
}

View File

@@ -1436,6 +1436,11 @@
"note_detail_render_help_1": "Ta notatka pomocy jest wyświetlana, ponieważ ta notatka typu Render HTML nie ma wymaganej relacji do poprawnego działania.",
"note_detail_render_help_2": "Typ notatki Render HTML jest używany do <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">skryptowania</a>. W skrócie, masz notatkę kodu HTML (opcjonalnie z JavaScript) i ta notatka ją wyrenderuje. Aby to zadziałało, musisz zdefiniować <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relację</a> o nazwie \"renderNote\" wskazującą na notatkę HTML do wyrenderowania."
},
"web_view": {
"web_view": "Widok WWW",
"embed_websites": "Notatka typu Widok WWW pozwala na osadzanie stron internetowych w Trilium.",
"create_label": "Aby rozpocząć, utwórz etykietę z adresem URL, który chcesz osadzić, np. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Odśwież"
},

View File

@@ -1068,6 +1068,11 @@
"note_detail_render_help_1": "Esta nota de ajuda é mostrada porque esta nota do tipo Renderizar HTML não possui a relação necessária para funcionar corretamente.",
"note_detail_render_help_2": "O tipo de nota Renderizar HTML é usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">automação</a>. Em suma, tem uma nota de código HTML (opcionalmente com algum JavaScript) e esta nota irá renderizá-la. Para fazê-lo funcionar, deve definir uma <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relação</a> chamada \"renderNote\" que aponta para a nota HTML a ser renderizada."
},
"web_view": {
"web_view": "Web View",
"embed_websites": "Nota do tipo Visualização Web permite que incorpore sites no Trilium.",
"create_label": "Para começar, crie uma etiqueta com um endereço URL que deseja incorporar, por exemplo, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Recarregar"
},

View File

@@ -1271,6 +1271,11 @@
"start_dragging_relations": "Comece arrastando as relações daqui e solte-as em outra nota.",
"cannot_match_transform": "Não foi possível combinar a transformação: {{transform}}"
},
"web_view": {
"web_view": "Web View",
"embed_websites": "Nota do tipo Visualização Web permite que você incorpore sites dentro do Trilium.",
"create_label": "Para começar, crie uma etiqueta com um endereço URL que deseja incorporar, por exemplo, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Recarregar"
},

View File

@@ -1376,6 +1376,11 @@
"enable_vim_keybindings": "Permite utilizarea combinațiilor de taste în stil Vim pentru notițele de tip cod (fără modul ex)",
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
},
"web_view": {
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
"web_view": "Vizualizare web"
},
"wrap_lines": {
"enable_line_wrap": "Activează trecerea automată pe rândul următor (poate necesita o reîncărcare a interfeței pentru a avea efect)",
"wrap_lines_in_code_notes": "Trecerea automată pe rândul următor în notițe de cod"

View File

@@ -668,8 +668,7 @@
"geo-map": {
"unable-to-load-map": "Не удалось загрузить карту.",
"create-child-note-instruction": "Щелкните по карте, чтобы создать новую заметку в этом месте, или нажмите Escape, чтобы закрыть ее.",
"create-child-note-title": "Создать новую дочернюю заметку и добавить ее на карту",
"create-child-note-text": "Добавить маркер"
"create-child-note-title": "Создать новую дочернюю заметку и добавить ее на карту"
},
"note_tooltip": {
"quick-edit": "Быстрое редактирование",
@@ -686,8 +685,8 @@
"electron_integration": {
"zoom-factor": "Коэффициент масштабирования",
"restart-app-button": "Применить изменения и перезапустить приложение",
"background-effects-description": "Добавляет размытый, стильный фон окнам приложений, создавая глубину и современный вид. Опция \"Системная строка заголовка\" должна быть отключена.",
"background-effects": "Включить фоновые эффекты",
"background-effects-description": "Эффект Mica добавляет размытый, стильный фон окнам приложений, создавая глубину и современный вид. Опция \"Системная строка заголовка\" должна быть отключена.",
"background-effects": "Включить фоновые эффекты (только Windows 11)",
"native-title-bar-description": "В Windows и macOS отключение системной строки заголовка делает приложение более компактным. В Linux включение системной строки заголовка улучшает интеграцию с остальной частью системы.",
"native-title-bar": "Системная панель заголовка",
"desktop-application": "Десктопное приложение"
@@ -777,11 +776,7 @@
"refresh-saved-search-results": "Обновить сохраненные результаты поиска",
"automatically-collapse-notes-title": "Заметки будут свернуты после определенного периода бездействия, чтобы навести порядок в дереве.",
"toggle-sidebar": "Переключить боковую панель",
"dropping-not-allowed": "Перетаскивание заметок в эту область не разрешено.",
"shared-indicator-tooltip": "Эта заметка опубликована",
"shared-indicator-tooltip-with-url": "Эта заметка доступно публично по адресу: {{- url}}",
"subtree-hidden-moved-description-other": "В дереве, к которому относится эта заметка, скрыты дочерние заметки.",
"subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве."
"dropping-not-allowed": "Перетаскивание заметок в эту область не разрешено."
},
"quick-search": {
"no-results": "Результаты не найдены",
@@ -861,10 +856,7 @@
"convert-to-attachment-confirm": "Вы уверены, что хотите преобразовать выбранные заметки во вложения их родительских заметок? Эта операция применяется только к заметкам в виде изображений; другие заметки будут пропущены.",
"converted-to-attachments": "{{count}} заметок были преобразованы во вложения.",
"archive": "Архивировать",
"unarchive": "Разархивировать",
"open-in-a-new-window": "Открыть в новом окне",
"hide-subtree": "Скрыть поддерево",
"show-subtree": "Показать поддерево"
"unarchive": "Разархивировать"
},
"info": {
"closeButton": "Закрыть",
@@ -1008,8 +1000,7 @@
"switch_to_mobile_version": "Перейти на мобильную версию",
"switch_to_desktop_version": "Переключиться на версию для ПК",
"new-version-available": "Доступно обновление",
"download-update": "Обновить до {{latestVersion}}",
"search_notes": "Поиск заметок"
"download-update": "Обновить до {{latestVersion}}"
},
"zpetne_odkazy": {
"relation": "отношение",
@@ -1056,8 +1047,7 @@
"expand_all_levels": "Развернуть все вложенные уровни",
"expand_nth_level": "Развернуть уровни: {{depth}} шт.",
"expand_first_level": "Развернуть прямые дочерние уровни",
"expand_tooltip": "Разщвернуть дочерние элементы этой коллекции (на один уровень вложенности). Для получения дополнительных параметров нажмите стрелку справа.",
"hide_child_notes": "Скрыть дочерние заметки в дереве"
"expand_tooltip": "Разщвернуть дочерние элементы этой коллекции (на один уровень вложенности). Для получения дополнительных параметров нажмите стрелку справа."
},
"edited_notes": {
"deleted": "(удалено)",
@@ -1702,7 +1692,7 @@
"zoom_in_title": "Увеличить масштаб",
"zoom_out_title": "Уменьшить масштаб",
"reset_pan_zoom_title": "Сбросить панорамирование и масштабирование",
"create_child_note_title": "Создать дочернюю заметку и добавить ее в карту"
"create_child_note_title": "Создать новую дочернюю заметку и добавить ее в эту карту связей"
},
"code_auto_read_only_size": {
"unit": "символов",
@@ -1855,8 +1845,7 @@
"error_cannot_get_branch_id": "Невозможно получить branchId для notePath '{{notePath}}'",
"delete_this_note": "Удалить эту заметку",
"insert_child_note": "Вставить дочернюю заметку",
"note_revisions": "История изменений",
"content_language_switcher": "Язык содержимого: {{language}}"
"note_revisions": "История изменений"
},
"svg_export_button": {
"button_title": "Экспортировать диаграмму как SVG"
@@ -1911,7 +1900,7 @@
"dismiss": "Отклонить",
"background_effects_button": "Включить эффекты фона",
"next_theme_button": "Попробовать новую тему",
"background_effects_message": "На устройствах с ОС Windows или macOS, фоновые эффекты теперь полностью стабильны. Они добавляют цвета в пользовательский интерфейс, размывая фон за ним.",
"background_effects_message": "На устройствах Windows фоновые эффекты теперь полностью стабильны. Они добавляют цвет в пользовательский интерфейс, размывая фон за ним. Этот приём также используется в других приложениях, например, в проводнике Windows.",
"background_effects_title": "Фоновые эффекты теперь стабильны",
"next_theme_title": "Попробуйте новую тему Trilium",
"new_layout_button": "Подробнее",
@@ -1999,6 +1988,11 @@
"attachment_deleted": "Это вложение было удалено.",
"you_can_also_open": ", вы также можете открыть "
},
"web_view": {
"web_view": "Веб-страница",
"create_label": "Для начала создайте метку с URL-адресом, который вы хотите встроить, например, #webViewSrc=\"https://www.google.com\"",
"embed_websites": "Заметки типа \"Веб-страница\" позволяет встраивать веб-сайты в Trilium."
},
"ribbon": {
"widgets": "Виджеты ленты",
"promoted_attributes_message": "Вкладка \"Продвигаемые атрибуты\" будет автоматически открыта, если таковые атрибуты установлены у заметки",
@@ -2100,11 +2094,7 @@
"ui": "Пользовательский интерфейс"
},
"sql_result": {
"no_rows": "По этому запросу не возвращено ни одной строки",
"not_executed": "Запрос еще не выполнен.",
"failed": "Выполнение SQL-запроса завершилось с ошибкой",
"statement_result": "Результат заявления",
"execute_now": "Выполнить сейчас"
"no_rows": "По этому запросу не возвращено ни одной строки"
},
"editable_code": {
"placeholder": "Введите содержимое для заметки с кодом..."
@@ -2199,14 +2189,7 @@
"read_only_auto_description": "Эта заметка была автоматически переведена в режим только для чтения по соображениям производительности. Это автоматическое ограничение можно изменить в настройках.\n\nНажмите, чтобы временно отредактировать её.",
"read_only_auto": "Автоматический режим \"только для чтения\"",
"read_only_explicit_description": "Эта заметка была вручную установлена в режим «только для чтения».\nНажмите, чтобы временно отредактировать её.",
"read_only_explicit": "Только для чтения",
"save_status_saving": "Сохранение...",
"save_status_saved": "Сохранение",
"save_status_unsaved": "Не сохранено",
"save_status_error": "Ошибка сохранения",
"save_status_saving_tooltip": "Изменения сохраняются.",
"save_status_unsaved_tooltip": "Есть несохраненные изменения. Они будут сохранены автоматически через некоторое время.",
"save_status_error_tooltip": "Произошла ошибка при сохранении заметки. Если возможно, попробуйте скопировать содержимое заметки в другое место и перезагрузить приложение."
"read_only_explicit": "Только для чтения"
},
"breadcrumb": {
"hoisted_badge_title": "Снять фокус",
@@ -2260,30 +2243,5 @@
},
"attributes_panel": {
"title": "Атрибуты заметки"
},
"bookmark_buttons": {
"bookmarks": "Закладки"
},
"mobile_tab_switcher": {
"more_options": "Показать больше",
"title_one": "{{count}} вкладка",
"title_few": "{{count}} вкладки",
"title_many": "{{count}} вкладок"
},
"pdf": {
"pages_loading": "Загрузка...",
"pages_alt": "Страница {{pageNumber}}",
"pages_one": "{{count}} страница",
"pages_few": "{{count}} страницы",
"pages_many": "{{count}} страниц",
"layers_one": "{{count}} слой",
"layers_few": "{{count}} слоя",
"layers_many": "{{count}} слоев",
"attachments_one": "{{count}} вложение",
"attachments_few": "{{count}} вложения",
"attachments_many": "{{count}} вложений"
},
"platform_indicator": {
"available_on": "Доступно для {{platform}}"
}
}

View File

@@ -662,8 +662,7 @@
"show-cheatsheet": "顯示快捷鍵說明",
"toggle-zen-mode": "禪模式",
"new-version-available": "發現新更新",
"download-update": "取得版本 {{latestVersion}}",
"search_notes": "搜尋筆記"
"download-update": "取得版本 {{latestVersion}}"
},
"sync_status": {
"unknown": "<p>同步狀態將在下一次同步嘗試開始後顯示。</p><p>點擊以立即觸發同步。</p>",
@@ -759,8 +758,7 @@
"error_cannot_get_branch_id": "無法獲取 notePath '{{notePath}}' 的 branchId",
"error_unrecognized_command": "無法識別的命令 {{command}}",
"note_revisions": "筆記歷史版本",
"backlinks": "反向連結",
"content_language_switcher": "內文語言:{{language}}"
"backlinks": "反向連結"
},
"note_icon": {
"change_note_icon": "更改筆記圖標",
@@ -912,8 +910,7 @@
"unknown_search_option": "未知的搜尋選項 {{searchOptionName}}",
"search_note_saved": "搜尋筆記已儲存至 {{- notePathTitle}}",
"actions_executed": "已執行操作。",
"view_options": "查看選項:",
"option": "選項"
"view_options": "查看選項:"
},
"similar_notes": {
"title": "相似筆記",
@@ -1067,6 +1064,11 @@
"note_detail_render_help_1": "之所以顯示此說明筆記,是因為該類型的渲染 HTML 沒有設定好必須的關聯。",
"note_detail_render_help_2": "渲染筆記類型用於編寫 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">腳本</a>。簡單說就是您可以寫HTML程式碼或者加上一些JavaScript程式碼 然後這個筆記會把頁面渲染出來。要使其正常工作,您需要定義一個名為 \"renderNote\" 的 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">關聯</a> 指向要呈現的 HTML 筆記。"
},
"web_view": {
"web_view": "網頁顯示",
"embed_websites": "網頁顯示類型的筆記允許您將網站嵌入至 Trilium 中。",
"create_label": "首先,請新增一個帶有您要嵌入的 URL 地址的標籤,例如 #webViewSrc=\"https://www.bing.com\""
},
"backend_log": {
"refresh": "重新整理"
},
@@ -2270,13 +2272,9 @@
},
"mobile_tab_switcher": {
"more_options": "更多選項",
"title_one": "{{count}} 個分頁",
"title_other": ""
"title_one": "{{count}} 個分頁"
},
"platform_indicator": {
"available_on": "可於 {{platform}} 使用"
},
"bookmark_buttons": {
"bookmarks": "書籤"
}
}

View File

@@ -1134,6 +1134,11 @@
"note_detail_render_help_1": "Ця довідка відображається, оскільки ця нотатка типу Render HTML не має необхідного зв'язку для належного функціонування.",
"note_detail_render_help_2": "Тип нотатки Render HTML використовується для <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">скриптів</a>. Коротше кажучи, у вас є нотатка з HTML-кодом (за бажанням з деяким JavaScript), і ця нотатка її відобразить. Щоб це запрацювало, вам потрібно визначити <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">відношення</a> під назвою \"renderNote\", яке вказує на нотатку HTML для відображення."
},
"web_view": {
"web_view": "Веб-перегляд",
"embed_websites": "Нотатка типу Веб-перегляд дозволяє вбудовувати веб-сайти в Trilium.",
"create_label": "Для початку створіть мітку з URL-адресою, яку ви хочете вбудувати, наприклад, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Оновити"
},

View File

@@ -32,6 +32,26 @@ declare module "katex/contrib/auto-render" {
export default renderMathInElement;
}
import * as L from "leaflet";
declare module "leaflet" {
interface GPXMarker {
startIcon?: DivIcon | Icon | string | undefined;
endIcon?: DivIcon | Icon | string | undefined;
wptIcons?: {
[key: string]: DivIcon | Icon | string;
};
wptTypeIcons?: {
[key: string]: DivIcon | Icon | string;
};
pointMatchers?: Array<{ regex: RegExp; icon: DivIcon | Icon | string}>;
}
interface GPXOptions {
markers?: GPXMarker | undefined;
}
}
declare global {
interface Navigator {
/** Returns a boolean indicating whether the browser is running in standalone mode. Available on Apple's iOS Safari only. */

View File

@@ -89,7 +89,7 @@
/* #endregion */
/* #region Geo map buttons */
.maplibregl-canvas-container {
.leaflet-pane {
z-index: 50;
}
/* #endregion */

View File

@@ -1,4 +1,4 @@
import type { GeoMouseEvent } from "./map";
import type { LatLng, LeafletMouseEvent } from "leaflet";
import { LOCATION_ATTRIBUTE } from ".";
import attributes from "../../../services/attributes";
import { prompt } from "../../../services/dialog";
@@ -8,12 +8,12 @@ import { CreateChildrenResponse } from "@triliumnext/commons";
const CHILD_NOTE_ICON = "bx bx-pin";
export async function moveMarker(noteId: string, latLng: { lat: number; lng: number } | null) {
export async function moveMarker(noteId: string, latLng: LatLng | null) {
const value = latLng ? [latLng.lat, latLng.lng].join(",") : "";
await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value);
}
export async function createNewNote(noteId: string, e: GeoMouseEvent) {
export async function createNewNote(noteId: string, e: LeafletMouseEvent) {
const title = await prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
if (title?.trim()) {

View File

@@ -1,4 +1,4 @@
import type { GeoMouseEvent } from "./map.js";
import type { LatLng, LeafletMouseEvent } from "leaflet";
import appContext, { type CommandMappings } from "../../../components/app_context.js";
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
import linkContextMenu from "../../../menus/link_context_menu.js";
@@ -8,7 +8,7 @@ import { createNewNote } from "./api.js";
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import link from "../../../services/link.js";
export default function openContextMenu(noteId: string, e: GeoMouseEvent, isEditable: boolean) {
export default function openContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
let items: MenuItem<keyof CommandMappings>[] = [
...buildGeoLocationItem(e),
{ kind: "separator" },
@@ -44,7 +44,7 @@ export default function openContextMenu(noteId: string, e: GeoMouseEvent, isEdit
});
}
export function openMapContextMenu(noteId: string, e: GeoMouseEvent, isEditable: boolean) {
export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
let items: MenuItem<keyof CommandMappings>[] = [
...buildGeoLocationItem(e)
];
@@ -71,8 +71,8 @@ export function openMapContextMenu(noteId: string, e: GeoMouseEvent, isEditable:
});
}
function buildGeoLocationItem(e: GeoMouseEvent) {
function formatGeoLocation(latlng: { lat: number; lng: number }, precision: number = 6) {
function buildGeoLocationItem(e: LeafletMouseEvent) {
function formatGeoLocation(latlng: LatLng, precision: number = 6) {
return `${latlng.lat.toFixed(precision)}, ${latlng.lng.toFixed(precision)}`;
}

View File

@@ -16,10 +16,12 @@
overflow: hidden;
}
.maplibregl-ctrl-top-left,
.maplibregl-ctrl-top-right,
.maplibregl-ctrl-bottom-left,
.maplibregl-ctrl-bottom-right {
.leaflet-pane {
z-index: 1;
}
.leaflet-top,
.leaflet-bottom {
z-index: 997 !important;
}
@@ -27,25 +29,28 @@
cursor: crosshair;
}
.geo-map-container .geo-marker {
.geo-map-container .marker-pin {
position: relative;
cursor: pointer;
}
.geo-map-container .leaflet-div-icon {
position: relative;
background: transparent;
border: 0;
overflow: visible;
}
.geo-map-container .geo-marker .marker-pin {
position: relative;
.geo-map-container .leaflet-div-icon .icon-shadow {
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: -1;
}
.geo-map-container .geo-marker .marker-pin svg {
display: block;
filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.3));
}
.geo-map-container .geo-marker .tn-icon {
.geo-map-container .leaflet-div-icon .tn-icon {
position: absolute;
top: 3px;
inset-inline-start: 4px;
inset-inline-start: 2px;
background-color: white;
color: var(--light-theme-custom-color, black);
padding: 2px;
@@ -53,7 +58,7 @@
font-size: 17px;
}
.geo-map-container .geo-marker .title-label {
.geo-map-container .leaflet-div-icon .title-label {
display: block;
position: absolute;
top: 100%;
@@ -66,19 +71,19 @@
text-align: center;
text-overflow: ellipsis;
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
white-space: nowrap;
white-space: no-wrap;
overflow: hidden;
}
body[dir=rtl] .geo-map-container .geo-marker .title-label {
body[dir=rtl] .geo-map-container .leaflet-div-icon .title-label {
transform: translateX(50%);
}
.geo-map-container .geo-marker .archived {
.geo-map-container .leaflet-div-icon .archived {
opacity: 0.5;
}
.geo-map-container.dark .geo-marker .title-label {
.geo-map-container.dark .leaflet-div-icon .title-label {
color: white;
text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black;
}

View File

@@ -1,5 +1,8 @@
import "./index.css";
import { divIcon, GPXOptions, LatLng, LeafletMouseEvent } from "leaflet";
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../../components/app_context";
@@ -18,10 +21,9 @@ import TouchBar, { TouchBarButton, TouchBarSlider } from "../../react/TouchBar";
import { ViewModeProps } from "../interface";
import { createNewNote, moveMarker } from "./api";
import openContextMenu, { openMapContextMenu } from "./context_menu";
import Map, { GeoMouseEvent } from "./map";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import Map from "./map";
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, MapLayer } from "./map_layer";
import Marker, { GpxTrack } from "./marker";
import type maplibregl from "maplibre-gl";
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
const DEFAULT_ZOOM = 2;
@@ -29,7 +31,7 @@ export const LOCATION_ATTRIBUTE = "geolocation";
interface MapData {
view?: {
center?: { lat: number; lng: number } | [number, number];
center?: LatLng | [number, number];
zoom?: number;
};
}
@@ -43,10 +45,11 @@ 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);
@@ -87,7 +90,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
moveMarker(noteId, null);
});
const onClick = useCallback(async (e: GeoMouseEvent) => {
const onClick = useCallback(async (e: LeafletMouseEvent) => {
if (state === State.NewNote) {
toast.closePersistent("geo-new-note");
await createNewNote(note.noteId, e);
@@ -95,13 +98,13 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
}
}, [ state ]);
const onContextMenu = useCallback((e: GeoMouseEvent) => {
const onContextMenu = useCallback((e: LeafletMouseEvent) => {
openMapContextMenu(note.noteId, e, !isReadOnly);
}, [ note.noteId, isReadOnly ]);
// Dragging
const containerRef = useRef<HTMLDivElement>(null);
const apiRef = useRef<maplibregl.Map>(null);
const apiRef = useRef<L.Map>(null);
useNoteTreeDrag(containerRef, {
dragEnabled: !isReadOnly,
dragNotEnabledMessage: {
@@ -118,15 +121,15 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
const offset = containerRef.current?.getBoundingClientRect();
const x = e.clientX - (offset?.left ?? 0);
const y = e.clientY - (offset?.top ?? 0);
const lngLat = api.unproject([x, y]);
const latlng = api.containerPointToLatLng([ x, y ]);
const targetNote = await froca.getNote(noteId, true);
const parents = targetNote?.getParentNoteIds();
if (parents?.includes(note.noteId)) {
await moveMarker(noteId, { lat: lngLat.lat, lng: lngLat.lng });
await moveMarker(noteId, latlng);
} else {
await branches.cloneNoteToParentNote(noteId, noteId);
await moveMarker(noteId, { lat: lngLat.lat, lng: lngLat.lng });
await moveMarker(noteId, latlng);
}
}
});
@@ -150,7 +153,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
apiRef={apiRef} containerRef={containerRef}
coordinates={coordinates}
zoom={zoom}
layerName={layerName ?? DEFAULT_MAP_LAYER_NAME}
layerData={layerData}
viewportChanged={(coordinates, zoom) => {
if (!viewConfig) viewConfig = {};
viewConfig.view = { center: coordinates, zoom };
@@ -160,13 +163,35 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
onContextMenu={onContextMenu}
scale={hasScale}
>
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} />)}
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} hideLabels={hideLabels} />)}
</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");
@@ -177,31 +202,36 @@ function ToggleReadOnlyButton({ note }: { note: FNote }) {
/>;
}
function NoteWrapper({ note, isReadOnly }: { note: FNote, isReadOnly: boolean }) {
function NoteWrapper({ note, isReadOnly, hideLabels }: {
note: FNote,
isReadOnly: boolean,
hideLabels: boolean
}) {
const mime = useNoteProperty(note, "mime");
const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE);
if (mime === "application/gpx+xml") {
return <NoteGpxTrack note={note} />;
return <NoteGpxTrack note={note} hideLabels={hideLabels} />;
}
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} />;
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} hideLabels={hideLabels} />;
}
}
function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean, latLng: [number, number] }) {
function NoteMarker({ note, editable, latLng, hideLabels }: { note: FNote, editable: boolean, latLng: [number, number], hideLabels: boolean }) {
// React to changes
const [ color ] = useNoteLabel(note, "color");
const [ iconClass ] = useNoteLabel(note, "iconClass");
const [ archived ] = useNoteLabelBoolean(note, "archived");
const title = useNoteProperty(note, "title");
const iconHtml = useMemo(() => {
return buildIconHtml(note.getIcon(), note.getColorClass() ?? undefined, title, note.noteId, archived);
}, [ iconClass, color, title, note.noteId, archived]);
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 ]);
const onClick = useCallback(() => {
appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId });
@@ -216,17 +246,15 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
}
}, [ note.noteId ]);
const onDragged = useCallback((newCoordinates: { lat: number; lng: number }) => {
const onDragged = useCallback((newCoordinates: LatLng) => {
moveMarker(note.noteId, newCoordinates);
}, [ note.noteId ]);
const onContextMenu = useCallback((e: GeoMouseEvent) => openContextMenu(note.noteId, e, editable), [ note.noteId, editable ]);
const onContextMenu = useCallback((e: LeafletMouseEvent) => openContextMenu(note.noteId, e, editable), [ note.noteId, editable ]);
return latLng && <Marker
coordinates={latLng}
iconHtml={iconHtml}
iconSize={[25, 41]}
iconAnchor={[12, 41]}
icon={icon}
draggable={editable}
onMouseDown={onMouseDown}
onDragged={editable ? onDragged : undefined}
@@ -235,7 +263,7 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
/>;
}
function NoteGpxTrack({ note }: { note: FNote }) {
function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean }) {
const [ xmlString, setXmlString ] = useState<string>();
const blob = useNoteBlob(note);
@@ -254,40 +282,40 @@ function NoteGpxTrack({ note }: { note: FNote }) {
const color = useNoteLabel(note, "color");
const iconClass = useNoteLabel(note, "iconClass");
const trackColor = useMemo(() => note.getLabelValue("color") ?? "blue", [ color ]);
const startIconHtml = useMemo(() => buildIconHtml(note.getIcon(), note.getColorClass() ?? undefined, note.title), [ iconClass, color ]);
const endIconHtml = useMemo(() => buildIconHtml("bxs-flag-checkered"), [ ]);
const waypointIconHtml = useMemo(() => buildIconHtml("bx bx-pin"), [ ]);
return xmlString && <GpxTrack
gpxXmlString={xmlString}
trackColor={trackColor}
startIconHtml={startIconHtml}
endIconHtml={endIconHtml}
waypointIconHtml={waypointIconHtml}
/>;
const options = useMemo<GPXOptions>(() => ({
markers: {
startIcon: buildIcon(note.getIcon(), note.getColorClass(), hideLabels ? undefined : note.title),
endIcon: buildIcon("bxs-flag-checkered"),
wptIcons: {
"": buildIcon("bx bx-pin")
}
},
polyline_options: {
color: note.getLabelValue("color") ?? "blue"
}
}), [ color, iconClass, hideLabels ]);
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />;
}
// SVG marker pin shape (replaces the Leaflet marker PNG).
const MARKER_SVG = `<svg width="25" height="41" viewBox="0 0 25 41" xmlns="http://www.w3.org/2000/svg">` +
`<path d="M12.5 0C5.6 0 0 5.6 0 12.5C0 21.9 12.5 41 12.5 41S25 21.9 25 12.5C25 5.6 19.4 0 12.5 0Z" fill="#2A81CB" />` +
`<circle cx="12.5" cy="12.5" r="8" fill="white" />` +
`</svg>`;
function buildIconHtml(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
let html = /*html*/`\
<div class="marker-pin">${MARKER_SVG}</div>
<span class="bx ${bxIconClass} tn-icon ${colorClass ?? ""}"></span>
<img class="icon" src="${markerIcon}" />
<img class="icon-shadow" src="${markerIconShadow}" />
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
<span class="title-label">${title ?? ""}</span>`;
if (noteIdLink) {
html = `<div data-href="#root/${noteIdLink}" class="${archived ? "archived" : ""}">${html}</div>`;
}
return html;
return divIcon({
html,
iconSize: [25, 41],
iconAnchor: [12, 41]
});
}
function GeoMapTouchBar({ state, map }: { state: State, map: maplibregl.Map | null | undefined }) {
function GeoMapTouchBar({ state, map }: { state: State, map: L.Map | null | undefined }) {
const [ currentZoom, setCurrentZoom ] = useState<number>();
const parentComponent = useContext(ParentComponent);
@@ -299,7 +327,7 @@ function GeoMapTouchBar({ state, map }: { state: State, map: maplibregl.Map | nu
}
map.on("zoom", onZoomChanged);
return () => { map.off("zoom", onZoomChanged); };
return () => map.off("zoom", onZoomChanged);
}, [ map ]);
return map && currentZoom && (

View File

@@ -1,216 +1,143 @@
import { useEffect, useImperativeHandle, useRef } from "preact/hooks";
import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { MAP_LAYERS } from "./map_layer";
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 { ComponentChildren, createContext, RefObject } from "preact";
import { useElementSize, useSyncedRef } from "../../react/hooks";
export interface GeoMouseEvent {
latlng: { lat: number; lng: number };
originalEvent: MouseEvent;
}
export const ParentMap = createContext<maplibregl.Map | null>(null);
export const ParentMap = createContext<L.Map | null>(null);
interface MapProps {
apiRef?: RefObject<maplibregl.Map | null>;
apiRef?: RefObject<L.Map | null>;
containerRef?: RefObject<HTMLDivElement>;
coordinates: { lat: number; lng: number } | [number, number];
coordinates: LatLng | [number, number];
zoom: number;
layerName: string;
viewportChanged: (coordinates: { lat: number; lng: number }, zoom: number) => void;
layerData: MapLayer;
viewportChanged: (coordinates: LatLng, zoom: number) => void;
children: ComponentChildren;
onClick?: (e: GeoMouseEvent) => void;
onContextMenu?: (e: GeoMouseEvent) => void;
onClick?: (e: LeafletMouseEvent) => void;
onContextMenu?: (e: LeafletMouseEvent) => void;
onZoom?: () => void;
scale: boolean;
}
function toMapLibreEvent(e: maplibregl.MapMouseEvent): GeoMouseEvent {
return {
latlng: { lat: e.lngLat.lat, lng: e.lngLat.lng },
originalEvent: e.originalEvent
};
}
export default function Map({ coordinates, zoom, layerName, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
const mapRef = useRef<maplibregl.Map>(null);
export default function Map({ coordinates, zoom, layerData, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
const mapRef = useRef<L.Map>(null);
const containerRef = useSyncedRef<HTMLDivElement>(_containerRef);
useImperativeHandle(apiRef ?? null, () => mapRef.current);
// Initialize the map.
useEffect(() => {
if (!containerRef.current) return;
const layerData = MAP_LAYERS[layerName];
let style: maplibregl.StyleSpecification | string;
if (layerData.type === "vector") {
style = typeof layerData.style === "string"
? layerData.style
: layerData.styleFallback;
} else {
style = {
version: 8,
sources: {
"raster-tiles": {
type: "raster",
tiles: [layerData.url],
tileSize: 256,
attribution: layerData.attribution
}
},
layers: [
{
id: "raster-layer",
type: "raster",
source: "raster-tiles"
}
]
};
}
const center = Array.isArray(coordinates)
? [coordinates[1], coordinates[0]] as [number, number]
: [coordinates.lng, coordinates.lat] as [number, number];
const mapInstance = new maplibregl.Map({
container: containerRef.current,
style,
center,
zoom,
minZoom: 2,
maxBounds: [[-180, -90], [180, 90]]
const mapInstance = L.map(containerRef.current, {
worldCopyJump: false,
maxBounds: [
[-90, -180],
[90, 180]
],
minZoom: 2
});
mapRef.current = mapInstance;
// Load async vector style if needed.
if (layerData.type === "vector" && typeof layerData.style !== "string") {
layerData.style().then(asyncStyle => {
mapInstance.setStyle(asyncStyle as maplibregl.StyleSpecification);
});
}
return () => {
mapInstance.off();
mapInstance.remove();
mapRef.current = null;
};
}, []);
// React to layer changes.
// Load the layer asynchronously.
const [ layer, setLayer ] = useState<Layer>();
useEffect(() => {
async function load() {
if (layerData.type === "vector") {
const style = (typeof layerData.style === "string" ? layerData.style : await layerData.style());
await import("@maplibre/maplibre-gl-leaflet");
setLayer(L.maplibreGL({
style: style as any
}));
} else {
setLayer(L.tileLayer(layerData.url, {
attribution: layerData.attribution,
detectRetina: true,
noWrap: true
}));
}
}
load();
}, [ layerData ]);
// Attach layer to the map.
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const layerData = MAP_LAYERS[layerName];
if (layerData.type === "vector") {
if (typeof layerData.style === "string") {
map.setStyle(layerData.style);
} else {
layerData.style().then(asyncStyle => {
map.setStyle(asyncStyle as maplibregl.StyleSpecification);
});
}
} else {
map.setStyle({
version: 8,
sources: {
"raster-tiles": {
type: "raster",
tiles: [layerData.url],
tileSize: 256,
attribution: layerData.attribution
}
},
layers: [
{
id: "raster-layer",
type: "raster",
source: "raster-tiles"
}
]
});
}
}, [ layerName ]);
const layerToAdd = layer;
if (!map || !layerToAdd) return;
layerToAdd.addTo(map);
return () => layerToAdd.removeFrom(map);
}, [ mapRef, layer ]);
// React to coordinate changes.
useEffect(() => {
if (!mapRef.current) return;
const center = Array.isArray(coordinates)
? [coordinates[1], coordinates[0]] as [number, number]
: [coordinates.lng, coordinates.lat] as [number, number];
mapRef.current.setCenter(center);
mapRef.current.setZoom(zoom);
}, [ coordinates, zoom ]);
mapRef.current.setView(coordinates, zoom);
}, [ mapRef, coordinates, zoom ]);
// Viewport callback.
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const updateFn = () => {
const center = map.getCenter();
viewportChanged({ lat: center.lat, lng: center.lng }, map.getZoom());
};
const updateFn = () => viewportChanged(map.getBounds().getCenter(), map.getZoom());
map.on("moveend", updateFn);
map.on("zoomend", updateFn);
return () => {
map.off("moveend", updateFn);
map.off("zoomend", updateFn);
};
}, [ viewportChanged ]);
}, [ mapRef, viewportChanged ]);
useEffect(() => {
const map = mapRef.current;
if (!onClick || !map) return;
const handler = (e: maplibregl.MapMouseEvent) => onClick(toMapLibreEvent(e));
map.on("click", handler);
return () => { map.off("click", handler); };
}, [ onClick ]);
if (onClick && mapRef.current) {
mapRef.current.on("click", onClick);
return () => mapRef.current?.off("click", onClick);
}
}, [ mapRef, onClick ]);
useEffect(() => {
const map = mapRef.current;
if (!onContextMenu || !map) return;
const handler = (e: maplibregl.MapMouseEvent) => {
e.preventDefault();
onContextMenu(toMapLibreEvent(e));
};
map.on("contextmenu", handler);
return () => { map.off("contextmenu", handler); };
}, [ onContextMenu ]);
if (onContextMenu && mapRef.current) {
mapRef.current.on("contextmenu", onContextMenu);
return () => mapRef.current?.off("contextmenu", onContextMenu);
}
}, [ mapRef, onContextMenu ]);
useEffect(() => {
const map = mapRef.current;
if (!onZoom || !map) return;
map.on("zoom", onZoom);
return () => { map.off("zoom", onZoom); };
}, [ onZoom ]);
if (onZoom && mapRef.current) {
mapRef.current.on("zoom", onZoom);
return () => mapRef.current?.off("zoom", onZoom);
}
}, [ mapRef, onZoom ]);
// Scale
useEffect(() => {
const map = mapRef.current;
if (!scale || !map) return;
const scaleControl = new maplibregl.ScaleControl();
map.addControl(scaleControl);
return () => { map.removeControl(scaleControl); };
}, [ scale ]);
const scaleControl = control.scale();
scaleControl.addTo(map);
return () => scaleControl.remove();
}, [ mapRef, scale ]);
// Adapt to container size changes.
const size = useElementSize(containerRef);
useEffect(() => {
mapRef.current?.resize();
mapRef.current?.invalidateSize();
}, [ size?.width, size?.height ]);
return (
<div
ref={containerRef}
className={`geo-map-container ${MAP_LAYERS[layerName].isDarkTheme ? "dark" : ""}`}
className={`geo-map-container ${layerData.isDarkTheme ? "dark" : ""}`}
>
<ParentMap.Provider value={mapRef.current}>
{children}

View File

@@ -1,24 +1,17 @@
export interface MapLayer {
name: string;
isDarkTheme?: boolean;
}
interface VectorLayer extends MapLayer {
export type MapLayer = ({
type: "vector";
style: string | (() => Promise<{}>);
styleFallback: {};
}
interface RasterLayer extends MapLayer {
style: string | (() => Promise<{}>)
} | {
type: "raster";
url: string;
attribution: string;
}
}) & {
// Common properties
name: string;
isDarkTheme?: boolean;
};
// Minimal empty style used as a placeholder while the real style loads asynchronously.
const EMPTY_STYLE = { version: 8, sources: {}, layers: [] };
export const MAP_LAYERS: Record<string, VectorLayer | RasterLayer> = {
export const MAP_LAYERS: Record<string, MapLayer> = {
"openstreetmap": {
name: "OpenStreetMap",
type: "raster",
@@ -28,33 +21,28 @@ export const MAP_LAYERS: Record<string, VectorLayer | RasterLayer> = {
"versatiles-colorful": {
name: "VersaTiles Colorful",
type: "vector",
style: async () => (await import("./styles/colorful/en.json")).default,
styleFallback: EMPTY_STYLE
style: async () => (await import("./styles/colorful/en.json")).default
},
"versatiles-eclipse": {
name: "VersaTiles Eclipse",
type: "vector",
style: async () => (await import("./styles/eclipse/en.json")).default,
styleFallback: EMPTY_STYLE,
isDarkTheme: true
},
"versatiles-graybeard": {
name: "VersaTiles Graybeard",
type: "vector",
style: async () => (await import("./styles/graybeard/en.json")).default,
styleFallback: EMPTY_STYLE
style: async () => (await import("./styles/graybeard/en.json")).default
},
"versatiles-neutrino": {
name: "VersaTiles Neutrino",
type: "vector",
style: async () => (await import("./styles/neutrino/en.json")).default,
styleFallback: EMPTY_STYLE
style: async () => (await import("./styles/neutrino/en.json")).default
},
"versatiles-shadow": {
name: "VersaTiles Shadow",
type: "vector",
style: async () => (await import("./styles/shadow/en.json")).default,
styleFallback: EMPTY_STYLE,
isDarkTheme: true
}
};

View File

@@ -1,207 +1,71 @@
import { useContext, useEffect, useRef } from "preact/hooks";
import { ParentMap, GeoMouseEvent } from "./map";
import maplibregl from "maplibre-gl";
import { useContext, useEffect } from "preact/hooks";
import { ParentMap } from "./map";
import { DivIcon, GPX, GPXOptions, Icon, LatLng, Marker as LeafletMarker, LeafletMouseEvent, marker, MarkerOptions } from "leaflet";
import "leaflet-gpx";
export interface MarkerProps {
coordinates: [ number, number ];
iconHtml?: string;
iconSize?: [number, number];
iconAnchor?: [number, number];
icon?: Icon | DivIcon;
onClick?: () => void;
onMouseDown?: (e: MouseEvent) => void;
onDragged?: ((newCoordinates: { lat: number; lng: number }) => void);
onContextMenu: (e: GeoMouseEvent) => void;
onDragged?: ((newCoordinates: LatLng) => void);
onContextMenu: (e: LeafletMouseEvent) => void;
draggable?: boolean;
}
export default function Marker({ coordinates, iconHtml, iconSize, iconAnchor, draggable, onClick, onDragged, onMouseDown, onContextMenu }: MarkerProps) {
export default function Marker({ coordinates, icon, draggable, onClick, onDragged, onMouseDown, onContextMenu }: MarkerProps) {
const parentMap = useContext(ParentMap);
const markerRef = useRef<maplibregl.Marker>(null);
useEffect(() => {
if (!parentMap) return;
const el = document.createElement("div");
el.className = "geo-marker";
if (iconHtml) {
el.innerHTML = iconHtml;
}
if (iconSize) {
el.style.width = `${iconSize[0]}px`;
el.style.height = `${iconSize[1]}px`;
const options: MarkerOptions = { icon };
if (draggable) {
options.draggable = true;
options.autoPan = true;
options.autoPanSpeed = 5;
}
const newMarker = new maplibregl.Marker({
element: el,
draggable: !!draggable,
anchor: "bottom"
})
.setLngLat([coordinates[1], coordinates[0]])
.addTo(parentMap);
markerRef.current = newMarker;
const newMarker = marker(coordinates, options);
if (onClick) {
el.addEventListener("click", (e) => {
e.stopPropagation();
onClick();
});
newMarker.on("click", () => onClick());
}
if (onMouseDown) {
el.addEventListener("mousedown", (e) => {
if (e.button === 1) {
e.stopPropagation();
onMouseDown(e);
}
});
newMarker.on("mousedown", e => onMouseDown(e.originalEvent));
}
if (onDragged) {
newMarker.on("dragend", () => {
const lngLat = newMarker.getLngLat();
onDragged({ lat: lngLat.lat, lng: lngLat.lng });
newMarker.on("moveend", e => {
const coordinates = (e.target as LeafletMarker).getLatLng();
onDragged(coordinates);
});
}
if (onContextMenu) {
el.addEventListener("contextmenu", (e) => {
e.stopPropagation();
e.preventDefault();
const lngLat = newMarker.getLngLat();
onContextMenu({
latlng: { lat: lngLat.lat, lng: lngLat.lng },
originalEvent: e
});
});
newMarker.on("contextmenu", e => onContextMenu(e))
}
return () => {
newMarker.remove();
markerRef.current = null;
};
}, [ parentMap, coordinates, onMouseDown, onDragged, iconHtml ]);
newMarker.addTo(parentMap);
return (<div />);
return () => newMarker.removeFrom(parentMap);
}, [ parentMap, coordinates, onMouseDown, onDragged, icon ]);
return (<div />)
}
export interface GpxTrackProps {
gpxXmlString: string;
trackColor?: string;
startIconHtml?: string;
endIconHtml?: string;
waypointIconHtml?: string;
}
export function GpxTrack({ gpxXmlString, trackColor, startIconHtml, endIconHtml, waypointIconHtml }: GpxTrackProps) {
export function GpxTrack({ gpxXmlString, options }: { gpxXmlString: string, options: GPXOptions }) {
const parentMap = useContext(ParentMap);
useEffect(() => {
if (!parentMap) return;
const markers: maplibregl.Marker[] = [];
const sourceId = `gpx-source-${Math.random().toString(36).slice(2)}`;
const layerId = `gpx-layer-${sourceId}`;
const track = new GPX(gpxXmlString, options);
track.addTo(parentMap);
function addGpxToMap() {
const parser = new DOMParser();
const gpxDoc = parser.parseFromString(gpxXmlString, "application/xml");
// Parse tracks.
const coordinates: [number, number][] = [];
const trackPoints = gpxDoc.querySelectorAll("trkpt, rtept");
for (const pt of trackPoints) {
const lat = parseFloat(pt.getAttribute("lat") ?? "0");
const lon = parseFloat(pt.getAttribute("lon") ?? "0");
coordinates.push([lon, lat]);
}
// Add GeoJSON line for the track.
if (coordinates.length > 0) {
parentMap.addSource(sourceId, {
type: "geojson",
data: {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates
}
}
});
parentMap.addLayer({
id: layerId,
type: "line",
source: sourceId,
paint: {
"line-color": trackColor ?? "blue",
"line-width": 3
}
});
// Start marker
if (startIconHtml) {
const startEl = document.createElement("div");
startEl.className = "geo-marker";
startEl.innerHTML = startIconHtml;
const startMarker = new maplibregl.Marker({ element: startEl, anchor: "bottom" })
.setLngLat(coordinates[0])
.addTo(parentMap);
markers.push(startMarker);
}
// End marker
if (endIconHtml && coordinates.length > 1) {
const endEl = document.createElement("div");
endEl.className = "geo-marker";
endEl.innerHTML = endIconHtml;
const endMarker = new maplibregl.Marker({ element: endEl, anchor: "bottom" })
.setLngLat(coordinates[coordinates.length - 1])
.addTo(parentMap);
markers.push(endMarker);
}
}
// Parse waypoints.
const waypoints = gpxDoc.querySelectorAll("wpt");
for (const wpt of waypoints) {
const lat = parseFloat(wpt.getAttribute("lat") ?? "0");
const lon = parseFloat(wpt.getAttribute("lon") ?? "0");
if (waypointIconHtml) {
const wptEl = document.createElement("div");
wptEl.className = "geo-marker";
wptEl.innerHTML = waypointIconHtml;
const wptMarker = new maplibregl.Marker({ element: wptEl, anchor: "bottom" })
.setLngLat([lon, lat])
.addTo(parentMap);
markers.push(wptMarker);
}
}
}
if (parentMap.isStyleLoaded()) {
addGpxToMap();
} else {
parentMap.once("style.load", addGpxToMap);
}
return () => {
for (const m of markers) {
m.remove();
}
try {
if (parentMap.getLayer(layerId)) {
parentMap.removeLayer(layerId);
}
if (parentMap.getSource(sourceId)) {
parentMap.removeSource(sourceId);
}
} catch {
// Map may be already removed.
}
};
}, [ parentMap, gpxXmlString, trackColor, startIconHtml, endIconHtml, waypointIconHtml ]);
return () => track.removeFrom(parentMap);
}, [ parentMap, gpxXmlString, options ]);
return <div />;
}

View File

@@ -226,8 +226,8 @@ function CheckBoxPropertyView({ note, property }: { note: FNote, property: Check
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
currentValue={ property.reverseValue ? !value : value }
onChange={newValue => setValue(property.reverseValue ? !newValue : newValue)}
/>
);
}

View File

@@ -20,6 +20,8 @@ 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 {
@@ -156,6 +158,13 @@ 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
}
]
},

View File

@@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "40.4.0",
"electron": "40.2.1",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",

View File

@@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "40.4.0",
"electron": "40.2.1",
"fs-extra": "11.3.3"
},
"scripts": {

View File

@@ -6,6 +6,6 @@
"e2e": "playwright test"
},
"devDependencies": {
"dotenv": "17.2.4"
"dotenv": "17.2.3"
}
}

View File

@@ -83,7 +83,7 @@
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "4.0.1",
"electron": "40.4.0",
"electron": "40.2.1",
"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.5",
"i18next": "25.8.4",
"i18next-fs-backend": "2.6.1",
"image-type": "6.0.0",
"ini": "6.0.0",
@@ -107,7 +107,7 @@
"is-svg": "6.1.0",
"jimp": "1.6.0",
"lorem-ipsum": "2.0.8",
"marked": "17.0.2",
"marked": "17.0.1",
"mime-types": "3.0.2",
"multer": "2.0.2",
"normalize-strings": "1.1.1",

View File

@@ -149,8 +149,7 @@
"note-launcher-title": "Scorciatoie delle note",
"script-launcher-title": "Scorciatoie degli script",
"command-palette": "Apri tavolozza comandi",
"zen-mode": "Modalità Zen",
"tab-switcher-title": "Selettore scheda"
"zen-mode": "Modalità Zen"
},
"notes": {
"new-note": "Nuova nota",

View File

@@ -16,7 +16,7 @@
"packageManager": "pnpm@10.29.2",
"devDependencies": {
"@wxt-dev/auto-icons": "1.1.0",
"wxt": "0.20.15"
"wxt": "0.20.14"
},
"dependencies": {
"cash-dom": "8.1.5"

View File

@@ -9,7 +9,7 @@
"preview": "pnpm build && vite preview"
},
"dependencies": {
"i18next": "25.8.5",
"i18next": "25.8.4",
"i18next-http-backend": "3.0.2",
"preact": "10.28.3",
"preact-iso": "2.11.1",

View File

@@ -61,8 +61,7 @@
"geomap_title": "Peta Geo",
"geomap_description": "Rencanakan liburan Anda atau tandai titik minat langsung pada peta geografis menggunakan penanda titik yang dapat disesuaikan. Tampilkan rekaman jalur GPX untuk melacak rencana perjalanan.",
"presentation_title": "Presentasi",
"presentation_description": "Atur informasi ke dalam slide dan presentasikan dalam layar penuh dengan transisi yang mulus. Slide juga dapat diekspor ke PDF agar mudah dibagikan.",
"calendar_description": "Kelola acara pribadi atau profesional Anda menggunakan kalender, dengan dukungan untuk acara sepanjang hari dan beberapa hari. Lihat acara Anda sekilas dengan tampilan mingguan, bulanan, dan tahunan. Interaksi mudah untuk menambahkan atau menyeret acara."
"presentation_description": "Atur informasi ke dalam slide dan presentasikan dalam layar penuh dengan transisi yang mulus. Slide juga dapat diekspor ke PDF agar mudah dibagikan."
},
"faq": {
"title": "Tanya Jawab",
@@ -73,11 +72,6 @@
"server_question": "Apakah saya butuh server untuk menjalankan Trilium?"
},
"extensibility_benefits": {
"share_title": "Bagikan catatan di web",
"title": "Berbagi & perluasan",
"import_export_title": "Impor/ekspor",
"import_export_description": "Berinteraksi dengan mudah dengan aplikasi lain menggunakan format Markdown, ENEX, dan OML.",
"share_description": "Jika Anda memiliki server, server tersebut dapat digunakan untuk berbagi sebagian catatan Anda dengan orang lain.",
"scripting_title": "Pembuatan skrip tingkat lanjut"
"share_title": "Bagikan catatan di web"
}
}

View File

@@ -195,14 +195,6 @@
"header": {
"get-started": "Inizia",
"documentation": "Documentazione",
"support-us": "Sostienici",
"resources": "Risorse"
},
"resources": {
"title": "Risorse",
"icon_packs": "Pacchetti di icone",
"icon_packs_intro": "Ampliate la selezione di icone disponibili per le vostre note utilizzando un pacchetto di icone. Per ulteriori informazioni sui pacchetti di icone, consultate la<DocumentationLink>documentazione ufficiale</DocumentationLink>.",
"download": "Scarica",
"website": "Sito web"
"support-us": "Sostienici"
}
}

View File

@@ -70,8 +70,7 @@
"header": {
"get-started": "Начало работы",
"support-us": "Поддержите нас",
"documentation": "Документация",
"resources": "Ресурсы"
"documentation": "Документация"
},
"social_buttons": {
"github": "GitHub",
@@ -197,11 +196,5 @@
"404": {
"title": "404: Не найдено",
"description": "Страница, которую вы искали, не найдена. Возможно, она была удалена или URL-адрес указан неверно."
},
"resources": {
"title": "Ресурсы",
"icon_packs": "Наборы иконок",
"download": "Скачать",
"website": "Сайт"
}
}

View File

@@ -195,14 +195,6 @@
"header": {
"get-started": "開始使用",
"documentation": "文件",
"support-us": "支持我們",
"resources": "資源"
},
"resources": {
"title": "資源",
"icon_packs": "圖示包",
"icon_packs_intro": "使用圖示包以擴充筆記的可用圖示選擇。有關圖示包的詳細資訊,請參閱<DocumentationLink>官方文件</DocumentationLink>。",
"download": "下載",
"website": "網站"
"support-us": "支持我們"
}
}

96
docs/README-id.md vendored
View File

@@ -94,8 +94,8 @@ Dokumentasi kami tersedia dalam berbagai format:
* Integrasi [OpenID dan TOTP
langsung](https://docs.triliumnotes.org/user-guide/setup/server/mfa) untuk
login yang lebih aman
* [Sinkronisasi](https://docs.triliumnotes.org/user-guide/setup/synchronization)
dengan server hostingan pribadi
* [Synchronization](https://docs.triliumnotes.org/user-guide/setup/synchronization)
with self-hosted sync server
* ada [servis pihak ke-3 untuk server hostingan
sinkronisasi](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
* [Bagikan](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing)
@@ -130,28 +130,28 @@ Dokumentasi kami tersedia dalam berbagai format:
* [Penyemat Web](https://docs.triliumnotes.org/user-guide/setup/web-clipper)
untuk memudahkan pencatatan konten web
* "UI yang dapat dikustomisasi (tombol sidebar, widget kustom, ...)"
* [Berbagai
Metrik](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics) yang
dipadukan dengan Dashboard Grafana.
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics),
along with a Grafana Dashboard.
✨ Cek lebih lanjut sumber daya/komunitas pihak ke-tiga untuk menikmati lebih
lanjut TriliumNext:
✨ Check out the following third-party resources/communities for more TriliumNext
related goodies:
- [trilium-beken](https://github.com/Nriver/awesome-trilium) untuk banyak tema,
skrip, plugin pihak ke-3 dan lain-lain.
- [TriliumJaya!](https://trilium.rocks/) untuk tutorial, panduan dan lainnya.
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party
themes, scripts, plugins and more.
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
## ❓Mengapa TriliumNext?
## ❓Why TriliumNext?
Pengembang asli Trilium ([Zadam](https://github.com/zadam)) dengan murah hati
telah memberikan repositori Trilium kepada proyek komunitas yang berada di
https://github.com/TriliumNext
The original Trilium developer ([Zadam](https://github.com/zadam)) has
graciously given the Trilium repository to the community project which resides
at https://github.com/TriliumNext
### ⬆️ Memindahkan dari Zadam/Trilium?
### ⬆️Migrating from Zadam/Trilium?
Tidak ada langkah migrasi khusus untuk bermigrasi dari zadam/Trilium ke
TriliumNext/Trilium. Cukup [instal TriliumNext/Trilium](#-installation) seperti
biasa dan akan menggunakan basis data yang sudah ada.
There are no special migration steps to migrate from a zadam/Trilium instance to
a TriliumNext/Trilium instance. Simply [install
TriliumNext/Trilium](#-installation) as usual and it will use your existing
database.
Versions up to and including
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are
@@ -169,8 +169,8 @@ features, suggestions, or issues you may have!
discussions.)
- The `General` Matrix room is also bridged to
[XMPP](xmpp:discuss@trilium.thisgreat.party?join)
- [Diskusi Github](https://github.com/TriliumNext/Trilium/discussions) (Untuk
diskusi asinkron.)
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For
asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug
reports and feature requests.)
@@ -294,39 +294,39 @@ described in the "Discuss with us" section above.
GitHub issues and discussions.
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
Trilium tidak akan ada tanpa teknologi-teknologi di balik berikut:
Trilium would not be possible without the technologies behind it:
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - editor visual dibalik
catatan teks. Kami sangat berterima kasih diberikan fitur-fitur editor yang
premium.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - editor kode dengan
dukungan banyak bahasa pemrograman.
* [Excalidraw](https://github.com/excalidraw/excalidraw) - area catatan tanpa
batas yang dipakai di catatan Kanvas.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - memberikan
fungsionalitas peta pikiran(mind map).
* [Leaflet](https://github.com/Leaflet/Leaflet) - untuk render peta geografikal.
* [Tabulator](https://github.com/olifolkerd/tabulator) - untuk tabel interaktif
yang dipakai di koleksi catatan.
* [FancyTree](https://github.com/mar10/fancytree) - library pohon yang kaya akan
fitur tanpa ada saingan.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - library konektivitas visual.
Dipakai di [peta
relasi](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[peta
hubungan](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind
text notes. We are grateful for being offered a set of the premium features.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with
support for huge amount of languages.
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite
whiteboard used in Canvas notes.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the
mind map functionality.
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical
maps.
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive
table used in collections.
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library
without real competition.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library.
Used in [relation
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[link
maps](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
## 🤝 Dukungan
## 🤝 Support
Trilium dibangun dan diperlihara oleh [banyak developer dan
waktu](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Dukungan
Anda yang membuat Trilium open-source, menambah dan mengembangkan fitur, juga
menutupi beban biaya hosting kami.
Trilium is built and maintained with [hundreds of hours of
work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your
support keeps it open-source, improves features, and covers costs such as
hosting.
Berikan dukungan ke developer utama
([eliandoran](https://github.com/eliandoran)) melalui:
Consider supporting the main developer
([eliandoran](https://github.com/eliandoran)) of the application via:
- [Sponsor-Sponsor GitHub](https://github.com/sponsors/eliandoran)
- [GitHub Sponsors](https://github.com/sponsors/eliandoran)
- [PayPal](https://paypal.me/eliandoran)
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)

14
docs/README-sv.md vendored
View File

@@ -11,13 +11,13 @@
# Trilium Notes
![GitHub Sponsorer](https://img.shields.io/github/sponsors/eliandoran)
![LiberalPay patrons](https://img.shields.io/liberapay/patrons/ElianDoran)\
![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran)
![LiberaPay patrons](https://img.shields.io/liberapay/patrons/ElianDoran)\
![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/trilium)
![GitHub Nedladdningar (alla resurser, alla
utgåvor)](https://img.shields.io/github/downloads/triliumnext/trilium/total)\
![GitHub Downloads (all assets, all
releases)](https://img.shields.io/github/downloads/triliumnext/trilium/total)\
[![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
[![Översättning
[![Translation
status](https://hosted.weblate.org/widget/trilium/svg-badge.svg)](https://hosted.weblate.org/engage/trilium/)
<!-- translate:off -->
@@ -29,8 +29,8 @@ script)](./README-ZH_TW.md) | [English](../README.md) | [French](./README-fr.md)
[Spanish](./README-es.md)
<!-- translate:on -->
Trilium Notes är fritt med öppen källkod, plattformsoberoende hierarkisk
antecknings app med fokus på att bygga en stor personlig kunskapsbas.
Trilium Notes is a free and open-source, cross-platform hierarchical note taking
application with focus on building large personal knowledge bases.
<img src="./app.png" alt="Trilium Screenshot" width="1000">

View File

@@ -63,7 +63,7 @@
"eslint-config-prettier": "10.1.8",
"eslint-plugin-playwright": "2.5.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"happy-dom": "20.6.1",
"happy-dom": "20.5.0",
"http-server": "14.1.1",
"jiti": "2.6.1",
"js-yaml": "4.1.1",

View File

@@ -71,6 +71,6 @@
},
"dependencies": {
"@ckeditor/ckeditor5-icons": "47.4.0",
"mathlive": "0.108.3"
"mathlive": "0.108.2"
}
}

View File

@@ -48,6 +48,7 @@ type Labels = {
"calendar:initialDate": string;
"map:style": string;
"map:scale": boolean;
"map:hideLabels": boolean;
"board:groupBy": string;
maxNestingDepth: number;
includeArchived: boolean;

View File

@@ -33,7 +33,7 @@
"@triliumnext/ckeditor5": "workspace:*",
"@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.55.0",
"dotenv": "17.2.4",
"dotenv": "17.2.3",
"esbuild": "0.27.3",
"eslint": "10.0.0",
"highlight.js": "11.11.1",

496
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff