mirror of
https://github.com/zadam/trilium.git
synced 2026-02-15 02:47:05 +01:00
Compare commits
43 Commits
copilot/sw
...
feature/ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ef4eb7eae | ||
|
|
7b230706cb | ||
|
|
5a2b04adba | ||
|
|
50dcd3ba44 | ||
|
|
740b02952f | ||
|
|
866d3110da | ||
|
|
7a3e7fccec | ||
|
|
3107bc8840 | ||
|
|
2d34cdef5e | ||
|
|
bd1f0909a2 | ||
|
|
ef75de63fe | ||
|
|
a739d28563 | ||
|
|
66ff009b72 | ||
|
|
a68e82c1c8 | ||
|
|
46556c1c14 | ||
|
|
7be637798f | ||
|
|
9b3396349e | ||
|
|
ccff210b4c | ||
|
|
a2264847b6 | ||
|
|
7d103f8c50 | ||
|
|
f3dccc0aec | ||
|
|
5da9963f31 | ||
|
|
34e885812f | ||
|
|
a9ac11452d | ||
|
|
04b91308b1 | ||
|
|
b09ef222f5 | ||
|
|
2d0ed06d50 | ||
|
|
8dd7cf6085 | ||
|
|
4999bd4f1e | ||
|
|
22f408addb | ||
|
|
9ca1dbe638 | ||
|
|
26662952e3 | ||
|
|
f0c9fa4ca3 | ||
|
|
b51aa1dd71 | ||
|
|
846253c9e3 | ||
|
|
6ab6ea97ac | ||
|
|
bf41f70b98 | ||
|
|
67ddbedd08 | ||
|
|
2573e219dc | ||
|
|
7e368678ab | ||
|
|
4a9fcf7ab6 | ||
|
|
65856c61c5 | ||
|
|
b5a97bffab |
@@ -9,7 +9,7 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"packageManager": "pnpm@10.29.3",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.18.0",
|
||||
"archiver": "7.0.1",
|
||||
|
||||
@@ -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,15 +44,16 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.1",
|
||||
"globals": "17.3.0",
|
||||
"i18next": "25.8.5",
|
||||
"i18next": "25.8.6",
|
||||
"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",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.8.0",
|
||||
@@ -70,6 +72,8 @@
|
||||
"@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",
|
||||
|
||||
@@ -700,6 +700,10 @@ export default class FNote {
|
||||
return this.hasAttribute(LABEL, name);
|
||||
}
|
||||
|
||||
hasLabelOrDisabled(name: string) {
|
||||
return this.hasLabel(name) || this.hasLabel(`disabled:${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns true if label exists (including inherited) and does not have "false" value.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1336,15 +1336,12 @@ body.desktop .dropdown-submenu > .dropdown-menu {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.dropdown-submenu.dropstart > .dropdown-menu {
|
||||
.dropdown-submenu.dropstart > .dropdown-menu,
|
||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: calc(100% - 2px);
|
||||
}
|
||||
|
||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
inset-inline-start: calc(-100% + 10px);
|
||||
}
|
||||
|
||||
.right-dropdown-widget {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
--badge-share-background-color: #4d4d4d;
|
||||
--badge-clipped-note-background-color: #295773;
|
||||
--badge-execute-background-color: #604180;
|
||||
--badge-active-content-background-color: rgb(12, 68, 70);
|
||||
|
||||
--note-icon-background-color: #444444;
|
||||
--note-icon-color: #d4d4d4;
|
||||
@@ -238,9 +239,9 @@
|
||||
|
||||
--bottom-panel-background-color: #11111180;
|
||||
--bottom-panel-title-bar-background-color: #3F3F3F80;
|
||||
|
||||
|
||||
--status-bar-border-color: var(--main-border-color);
|
||||
|
||||
|
||||
--scrollbar-thumb-color: #fdfdfd5c;
|
||||
--scrollbar-thumb-hover-color: #ffffff7d;
|
||||
--scrollbar-background-color: transparent;
|
||||
@@ -351,4 +352,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
.note-split.with-hue *::selection,
|
||||
.quick-edit-dialog-wrapper.with-hue *::selection {
|
||||
--selection-background-color: hsl(var(--custom-color-hue), 49.2%, 35%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
--badge-share-background-color: #6b6b6b;
|
||||
--badge-clipped-note-background-color: #2284c0;
|
||||
--badge-execute-background-color: #7b47af;
|
||||
--badge-active-content-background-color: rgb(27, 164, 168);
|
||||
|
||||
--note-icon-background-color: #4f4f4f;
|
||||
--note-icon-color: white;
|
||||
@@ -322,4 +323,4 @@
|
||||
.note-split.with-hue *::selection,
|
||||
.quick-edit-dialog-wrapper.with-hue *::selection {
|
||||
--selection-background-color: hsl(var(--custom-color-hue), 60%, 90%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,46 @@
|
||||
"critical-error": {
|
||||
"title": "Κρίσιμο σφάλμα",
|
||||
"message": "Συνέβη κάποιο κρίσιμο σφάλμα, το οποίο δεν επιτρέπει στην εφαρμογή χρήστη να ξεκινήσει:\n\n{{message}}\n\nΤο πιθανότερο είναι να προκλήθηκε από κάποιο script που απέτυχε απρόοπτα. Δοκιμάστε να ξεκινήσετε την εφαρμογή σε ασφαλή λειτουργία για να λύσετε το πρόβλημα."
|
||||
}
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Δεν ήταν δυνατή η αρχικοποίηση του widget",
|
||||
"message-custom": "Προσαρμοσμένο widget της σημείωσης με ID \"{{id}}\", με τίτλο \"{{title}}\", δεν ήταν δυνατό να αρχικοποιηθεί λόγω:\n\n{{message}}",
|
||||
"message-unknown": "Άγνωστο widget δεν ήταν δυνατό να αρχικοποιηθεί λόγω:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Δεν ήταν δυνατή η φόρτωση προσαρμοσμένου script",
|
||||
"message": "Το script δεν ήταν δυνατό να εκτελεστεί λόγω:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Δεν ήταν δυνατή η λήψη της λίστας των widgets από τον server"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Δεν ήταν δυνατή η απόδοση προσαρμοσμένου React widget"
|
||||
},
|
||||
"widget-missing-parent": "Το προσαρμοσμένο widget δεν έχει ορισμένη την υποχρεωτική ιδιότητα '{{property}}'.\n\nΕάν το script προορίζεται για εκτέλεση χωρίς UI element, χρησιμοποιήστε '#run=frontendStartup' αντί για αυτό.",
|
||||
"open-script-note": "Άνοιγμα σημείωσης script",
|
||||
"scripting-error": "Σφάλμα προσαρμοσμένου script: {{title}}"
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Σελιδοδείκτες"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Προσθήκη συνδέσμου",
|
||||
"help_on_links": "Βοήθεια για συνδέσμους",
|
||||
"note": "Σημείωση",
|
||||
"search_note": "Αναζήτηση σημείωσης με βάση το όνομά της",
|
||||
"link_title_mirrors": "Ο τίτλος του συνδέσμου αντικατοπτρίζει τον τρέχοντα τίτλο της σημείωσης",
|
||||
"link_title_arbitrary": "Ο τίτλος του συνδέσμου μπορεί να τροποποιηθεί ελεύθερα",
|
||||
"link_title": "Τίτλος συνδέσμου",
|
||||
"button_add_link": "Προσθήκη συνδέσμου"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"edit_branch_prefix": "Επεξεργασία προθέματος κλάδου",
|
||||
"edit_branch_prefix_multiple": "Επεξεργασία προθέματος κλάδου για {{count}} κλάδους",
|
||||
"help_on_tree_prefix": "Βοήθεια για πρόθεμα δέντρου",
|
||||
"prefix": "Πρόθεμα: ",
|
||||
"save": "Αποθήκευση",
|
||||
"branch_prefix_saved": "Το πρόθεμα κλάδου αποθηκεύτηκε.",
|
||||
"branch_prefix_saved_multiple": "Το πρόθεμα κλάδου αποθηκεύτηκε για {{count}} κλάδους."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2288,5 +2288,27 @@
|
||||
},
|
||||
"bookmark_buttons": {
|
||||
"bookmarks": "Bookmarks"
|
||||
},
|
||||
"active_content_badges": {
|
||||
"type_icon_pack": "Icon pack",
|
||||
"type_backend_script": "Backend script",
|
||||
"type_frontend_script": "Frontend script",
|
||||
"type_widget": "Widget",
|
||||
"type_app_css": "Custom CSS",
|
||||
"type_render_note": "Render note",
|
||||
"type_web_view": "Web view",
|
||||
"toggle_tooltip_enable_tooltip": "Click to enable this {{type}}.",
|
||||
"toggle_tooltip_disable_tooltip": "Click to disable this {{type}}.",
|
||||
"menu_docs": "Open documentation",
|
||||
"menu_execute_now": "Execute script now",
|
||||
"menu_run": "Run automatically",
|
||||
"menu_run_disabled": "Manually",
|
||||
"menu_run_backend_startup": "When the backend starts up",
|
||||
"menu_run_hourly": "Hourly",
|
||||
"menu_run_daily": "Daily",
|
||||
"menu_run_frontend_startup": "When the desktop frontend starts up",
|
||||
"menu_run_mobile_startup": "When the mobile frontend starts up",
|
||||
"menu_change_to_widget": "Change to widget",
|
||||
"menu_change_to_frontend_script": "Change to frontend script"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,8 @@
|
||||
"reload_app": "リロードして変更を適用する",
|
||||
"set_all_to_default": "すべてのショートカットをデフォルトに戻す",
|
||||
"confirm_reset": "キーボードショートカットをすべてデフォルトにリセットしますか?",
|
||||
"keyboard_shortcuts": "キーボードショートカット"
|
||||
"keyboard_shortcuts": "キーボードショートカット",
|
||||
"no_results": "'{{filter}}' に一致するショートカットが見つかりません"
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "確認",
|
||||
|
||||
20
apps/client/src/types-lib.d.ts
vendored
20
apps/client/src/types-lib.d.ts
vendored
@@ -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. */
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
/* #endregion */
|
||||
|
||||
/* #region Geo map buttons */
|
||||
.maplibregl-canvas-container {
|
||||
.leaflet-pane {
|
||||
z-index: 50;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 Map from "./map";
|
||||
import { DEFAULT_MAP_LAYER_NAME } 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;
|
||||
};
|
||||
}
|
||||
@@ -87,7 +89,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 +97,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 +120,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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -199,8 +201,8 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
|
||||
const [ archived ] = useNoteLabelBoolean(note, "archived");
|
||||
|
||||
const title = useNoteProperty(note, "title");
|
||||
const iconHtml = useMemo(() => {
|
||||
return buildIconHtml(note.getIcon(), note.getColorClass() ?? undefined, title, note.noteId, archived);
|
||||
const icon = useMemo(() => {
|
||||
return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, title, note.noteId, archived);
|
||||
}, [ iconClass, color, title, note.noteId, archived]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
@@ -216,17 +218,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}
|
||||
@@ -254,40 +254,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(), note.title),
|
||||
endIcon: buildIcon("bxs-flag-checkered"),
|
||||
wptIcons: {
|
||||
"": buildIcon("bx bx-pin")
|
||||
}
|
||||
},
|
||||
polyline_options: {
|
||||
color: note.getLabelValue("color") ?? "blue"
|
||||
}
|
||||
}), [ color, iconClass ]);
|
||||
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 +299,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 && (
|
||||
|
||||
@@ -1,210 +1,139 @@
|
||||
import { useEffect, useImperativeHandle, useRef } from "preact/hooks";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks";
|
||||
import L, { control, LatLng, Layer, LeafletMouseEvent } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { MAP_LAYERS } 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;
|
||||
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);
|
||||
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() {
|
||||
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");
|
||||
|
||||
setLayer(L.maplibreGL({
|
||||
style: style as any
|
||||
}));
|
||||
} else {
|
||||
setLayer(L.tileLayer(layerData.url, {
|
||||
attribution: layerData.attribution,
|
||||
detectRetina: true,
|
||||
noWrap: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
}, [ layerName ]);
|
||||
|
||||
// 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 (
|
||||
|
||||
@@ -5,8 +5,7 @@ export interface MapLayer {
|
||||
|
||||
interface VectorLayer extends MapLayer {
|
||||
type: "vector";
|
||||
style: string | (() => Promise<{}>);
|
||||
styleFallback: {};
|
||||
style: string | (() => Promise<{}>)
|
||||
}
|
||||
|
||||
interface RasterLayer extends MapLayer {
|
||||
@@ -15,9 +14,6 @@ interface RasterLayer extends MapLayer {
|
||||
attribution: string;
|
||||
}
|
||||
|
||||
// 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> = {
|
||||
"openstreetmap": {
|
||||
name: "OpenStreetMap",
|
||||
@@ -28,33 +24,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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
308
apps/client/src/widgets/layout/ActiveContentBadges.tsx
Normal file
308
apps/client/src/widgets/layout/ActiveContentBadges.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import { BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { BadgeWithDropdown } from "../react/Badge";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks";
|
||||
|
||||
const DANGEROUS_ATTRIBUTES = BUILTIN_ATTRIBUTES.filter(a => a.isDangerous || a.name === "appCss");
|
||||
const activeContentLabels = [ "iconPack", "widget", "appCss" ] as const;
|
||||
|
||||
interface ActiveContentInfo {
|
||||
type: "iconPack" | "backendScript" | "frontendScript" | "widget" | "appCss" | "renderNote" | "webView";
|
||||
isEnabled: boolean;
|
||||
canToggleEnabled: boolean;
|
||||
}
|
||||
|
||||
const typeMappings: Record<ActiveContentInfo["type"], {
|
||||
icon: string;
|
||||
helpPage: string;
|
||||
apiDocsPage?: string;
|
||||
isExecutable?: boolean
|
||||
}> = {
|
||||
iconPack: {
|
||||
icon: "bx bx-package",
|
||||
helpPage: "g1mlRoU8CsqC",
|
||||
},
|
||||
backendScript: {
|
||||
icon: "bx bx-server",
|
||||
helpPage: "SPirpZypehBG",
|
||||
apiDocsPage: "MEtfsqa5VwNi",
|
||||
isExecutable: true,
|
||||
},
|
||||
frontendScript: {
|
||||
icon: "bx bx-window",
|
||||
helpPage: "yIhgI5H7A2Sm",
|
||||
apiDocsPage: "Q2z6av6JZVWm",
|
||||
isExecutable: true
|
||||
},
|
||||
widget: {
|
||||
icon: "bx bxs-widget",
|
||||
helpPage: "MgibgPcfeuGz"
|
||||
},
|
||||
appCss: {
|
||||
icon: "bx bxs-file-css",
|
||||
helpPage: "AlhDUqhENtH7"
|
||||
},
|
||||
renderNote: {
|
||||
icon: "bx bx-extension",
|
||||
helpPage: "HcABDtFCkbFN"
|
||||
},
|
||||
webView: {
|
||||
icon: "bx bx-globe",
|
||||
helpPage: "1vHRoWCEjj0L"
|
||||
}
|
||||
};
|
||||
|
||||
export function ActiveContentBadges() {
|
||||
const { note } = useNoteContext();
|
||||
const info = useActiveContentInfo(note);
|
||||
|
||||
return (note && info &&
|
||||
<>
|
||||
{info.canToggleEnabled && <ActiveContentToggle info={info} note={note} />}
|
||||
<ActiveContentBadge info={info} note={note} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ActiveContentBadge({ info, note }: { note: FNote, info: ActiveContentInfo }) {
|
||||
const { icon, helpPage, apiDocsPage, isExecutable } = typeMappings[info.type];
|
||||
return (
|
||||
<BadgeWithDropdown
|
||||
className={clsx("active-content-badge", !!info.isEnabled && "disabled")}
|
||||
icon={icon}
|
||||
text={getTranslationForType(info.type)}
|
||||
>
|
||||
{isExecutable && (
|
||||
<>
|
||||
<FormListItem
|
||||
icon="bx bx-play"
|
||||
triggerCommand="runActiveNote"
|
||||
>{t("active_content_badges.menu_execute_now")}</FormListItem>
|
||||
<ScriptRunOptions note={note} info={info} />
|
||||
<FormDropdownDivider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{(info.type === "frontendScript" || info.type === "widget") && (
|
||||
<>
|
||||
<WidgetSwitcher note={note} />
|
||||
<FormDropdownDivider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormListItem
|
||||
icon="bx bx-help-circle"
|
||||
onClick={() => openInAppHelpFromUrl(helpPage)}
|
||||
>{t("active_content_badges.menu_docs")}</FormListItem>
|
||||
|
||||
{apiDocsPage && <FormListItem
|
||||
icon="bx bx-book-content"
|
||||
onClick={() => openInAppHelpFromUrl(apiDocsPage)}
|
||||
>{t("code_buttons.trilium_api_docs_button_title")}</FormListItem>}
|
||||
</BadgeWithDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function ScriptRunOptions({ info, note }: { note: FNote, info: ActiveContentInfo }) {
|
||||
const [ run, setRun ] = useNoteLabel(note, "run");
|
||||
|
||||
const options: {
|
||||
title: string;
|
||||
value: string | null;
|
||||
type: "both" | "backendScript" | "frontendScript";
|
||||
}[] = ([
|
||||
{
|
||||
title: t("active_content_badges.menu_run_disabled"),
|
||||
value: null,
|
||||
type: "both"
|
||||
},
|
||||
{
|
||||
title: t("active_content_badges.menu_run_backend_startup"),
|
||||
value: "backendStartup",
|
||||
type: "backendScript"
|
||||
},
|
||||
{
|
||||
title: t("active_content_badges.menu_run_daily"),
|
||||
value: "daily",
|
||||
type: "backendScript"
|
||||
},
|
||||
{
|
||||
title: t("active_content_badges.menu_run_hourly"),
|
||||
value: "hourly",
|
||||
type: "backendScript"
|
||||
},
|
||||
{
|
||||
title: t("active_content_badges.menu_run_frontend_startup"),
|
||||
value: "frontendStartup",
|
||||
type: "frontendScript"
|
||||
},
|
||||
{
|
||||
title: t("active_content_badges.menu_run_mobile_startup"),
|
||||
value: "mobileStartup",
|
||||
type: "frontendScript"
|
||||
}
|
||||
] as const).filter(option => option.type === "both" || option.type === info.type);
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu title={t("active_content_badges.menu_run")} icon="bx bx-rss" dropStart>
|
||||
{options.map(({ title, value }) => (
|
||||
<FormListItem
|
||||
key={value}
|
||||
onClick={() => setRun(value)}
|
||||
checked={run ? run === value : value === null }
|
||||
>{title}</FormListItem>
|
||||
))}
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
function WidgetSwitcher({ note }: { note: FNote }) {
|
||||
const [ widget, setWidget ] = useNoteLabelBoolean(note, "widget");
|
||||
const [ disabledWidget, setDisabledWidget ] = useNoteLabelBoolean(note, "disabled:widget");
|
||||
|
||||
return (widget || disabledWidget)
|
||||
? <FormListItem
|
||||
icon="bx bx-window"
|
||||
onClick={() => {
|
||||
setWidget(false);
|
||||
setDisabledWidget(false);
|
||||
}}
|
||||
>{t("active_content_badges.menu_change_to_frontend_script")}</FormListItem>
|
||||
: <FormListItem
|
||||
icon={widget ? "bx bx-window" : "bx bxs-widget"}
|
||||
onClick={() => {
|
||||
setWidget(true);
|
||||
}}
|
||||
>{t("active_content_badges.menu_change_to_widget")}</FormListItem>;
|
||||
|
||||
}
|
||||
|
||||
function getTranslationForType(type: ActiveContentInfo["type"]) {
|
||||
switch (type) {
|
||||
case "iconPack":
|
||||
return t("active_content_badges.type_icon_pack");
|
||||
case "backendScript":
|
||||
return t("active_content_badges.type_backend_script");
|
||||
case "frontendScript":
|
||||
return t("active_content_badges.type_frontend_script");
|
||||
case "widget":
|
||||
return t("active_content_badges.type_widget");
|
||||
case "appCss":
|
||||
return t("active_content_badges.type_app_css");
|
||||
case "renderNote":
|
||||
return t("active_content_badges.type_render_note");
|
||||
case "webView":
|
||||
return t("note_types.web-view");
|
||||
}
|
||||
}
|
||||
|
||||
function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentInfo }) {
|
||||
const typeTranslation = getTranslationForType(info.type);
|
||||
|
||||
return info && <FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={info.isEnabled}
|
||||
switchOnTooltip={t("active_content_badges.toggle_tooltip_disable_tooltip", { type: typeTranslation })}
|
||||
switchOffTooltip={t("active_content_badges.toggle_tooltip_enable_tooltip", { type: typeTranslation })}
|
||||
onChange={async (willEnable) => {
|
||||
const attrs = note.getOwnedAttributes()
|
||||
.filter(attr => {
|
||||
if (attr.isInheritable) return false;
|
||||
const baseName = getNameWithoutPrefix(attr.name);
|
||||
return DANGEROUS_ATTRIBUTES.some(item => item.name === baseName && item.type === attr.type);
|
||||
});
|
||||
|
||||
for (const attr of attrs) {
|
||||
const baseName = getNameWithoutPrefix(attr.name);
|
||||
const newName = willEnable ? baseName : `disabled:${baseName}`;
|
||||
if (newName === attr.name) continue;
|
||||
|
||||
// We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically.
|
||||
if (attr.type === "label") {
|
||||
await attributes.addLabel(note.noteId, newName, attr.value);
|
||||
} else {
|
||||
await attributes.setRelation(note.noteId, newName, attr.value);
|
||||
}
|
||||
await attributes.removeAttributeById(note.noteId, attr.attributeId);
|
||||
}
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
function getNameWithoutPrefix(name: string) {
|
||||
return name.startsWith("disabled:") ? name.substring(9) : name;
|
||||
}
|
||||
|
||||
function useActiveContentInfo(note: FNote | null | undefined) {
|
||||
const [ info, setInfo ] = useState<ActiveContentInfo | null>(null);
|
||||
|
||||
function refresh() {
|
||||
let type: ActiveContentInfo["type"] | null = null;
|
||||
let isEnabled = true;
|
||||
let canToggleEnabled = false;
|
||||
|
||||
if (!note) {
|
||||
setInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.type === "render") {
|
||||
type = "renderNote";
|
||||
isEnabled = note.hasRelation("renderNote");
|
||||
canToggleEnabled = note.hasRelation("renderNote") || note.hasRelation("disabled:renderNote");
|
||||
} else if (note.type === "webView") {
|
||||
type = "webView";
|
||||
isEnabled = note.hasLabel("webViewSrc");
|
||||
canToggleEnabled = note.hasLabelOrDisabled("webViewSrc");
|
||||
} else if (note.type === "code" && note.mime === "application/javascript;env=backend") {
|
||||
type = "backendScript";
|
||||
for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) {
|
||||
isEnabled ||= note.hasLabel(backendLabel);
|
||||
|
||||
if (!canToggleEnabled && note.hasLabelOrDisabled(backendLabel)) {
|
||||
canToggleEnabled = true;
|
||||
}
|
||||
}
|
||||
} else if (note.type === "code" && note.mime === "application/javascript;env=frontend") {
|
||||
type = "frontendScript";
|
||||
isEnabled = note.hasLabel("widget") || note.hasLabel("run");
|
||||
canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run");
|
||||
}
|
||||
|
||||
for (const labelToCheck of activeContentLabels ) {
|
||||
if (note.hasLabel(labelToCheck)) {
|
||||
type = labelToCheck;
|
||||
break;
|
||||
} else if (note.hasLabel(`disabled:${labelToCheck}`)) {
|
||||
type = labelToCheck;
|
||||
isEnabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type) {
|
||||
setInfo({ type, isEnabled, canToggleEnabled });
|
||||
} else {
|
||||
setInfo(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh on note change.
|
||||
useEffect(refresh, [ note ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return info;
|
||||
}
|
||||
@@ -37,6 +37,10 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
&.active-content-badge { --color: var(--badge-active-content-background-color); }
|
||||
&.active-content-badge.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
min-width: 0;
|
||||
|
||||
@@ -45,6 +49,11 @@
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.switch-button {
|
||||
--switch-track-height: 8px;
|
||||
--switch-track-width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-badge {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
|
||||
import { useShareState } from "../ribbon/BasicPropertiesTab";
|
||||
import { useShareInfo } from "../shared_info";
|
||||
import { ActiveContentBadges } from "./ActiveContentBadges";
|
||||
|
||||
export default function NoteBadges() {
|
||||
return (
|
||||
@@ -19,6 +20,7 @@ export default function NoteBadges() {
|
||||
<ShareBadge />
|
||||
<ClippedNoteBadge />
|
||||
<ExecuteBadge />
|
||||
<ActiveContentBadges />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ export default function NoteActionsCustom(props: NoteActionsCustomProps) {
|
||||
>
|
||||
<AddChildButton {...innerProps} />
|
||||
<RunActiveNoteButton {...innerProps } />
|
||||
<OpenTriliumApiDocsButton {...innerProps} />
|
||||
<SwitchSplitOrientationButton {...innerProps} />
|
||||
<ToggleReadOnlyButton {...innerProps} />
|
||||
<SaveToNoteButton {...innerProps} />
|
||||
@@ -230,15 +229,6 @@ function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
|
||||
/>;
|
||||
}
|
||||
|
||||
function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) {
|
||||
const isEnabled = noteMime.startsWith("application/javascript;env=");
|
||||
return isEnabled && <NoteAction
|
||||
icon="bx bx-help-circle"
|
||||
text={t("code_buttons.trilium_api_docs_button_title")}
|
||||
onClick={() => openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
|
||||
/>;
|
||||
}
|
||||
|
||||
function InAppHelpButton({ note }: NoteActionsCustomInnerProps) {
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
const isEnabled = !!helpUrl;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AttributeType } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { ComponentChildren, VNode } from "preact";
|
||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||
|
||||
@@ -7,6 +8,7 @@ import FNote from "../../entities/fnote";
|
||||
import { removeOwnedAttributesByNameOrType } from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import server from "../../services/server";
|
||||
import Admonition from "../react/Admonition";
|
||||
import FormSelect from "../react/FormSelect";
|
||||
import FormTextArea from "../react/FormTextArea";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
@@ -105,8 +107,9 @@ export const SEARCH_OPTIONS: SearchOption[] = [
|
||||
}
|
||||
];
|
||||
|
||||
function SearchOption({ note, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: {
|
||||
function SearchOption({ note, className, title, titleIcon, children, help, attributeName, attributeType, additionalAttributesToDelete }: {
|
||||
note: FNote;
|
||||
className?: string;
|
||||
title: string,
|
||||
titleIcon?: string,
|
||||
children?: ComponentChildren,
|
||||
@@ -116,7 +119,7 @@ function SearchOption({ note, title, titleIcon, children, help, attributeName, a
|
||||
additionalAttributesToDelete?: { type: "label" | "relation", name: string }[]
|
||||
}) {
|
||||
return (
|
||||
<tr className={attributeName}>
|
||||
<tr className={clsx(attributeName, className)}>
|
||||
<td className="title-column">
|
||||
{titleIcon && <><Icon icon={titleIcon} />{" "}</>}
|
||||
{title}
|
||||
@@ -154,64 +157,57 @@ function SearchStringOption({ note, refreshResults, error, ...restProps }: Searc
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// React to errors
|
||||
const { showTooltip, hideTooltip } = useTooltip(inputRef, {
|
||||
trigger: "manual",
|
||||
title: `${t("search_string.error", { error: error?.message })}`,
|
||||
html: true,
|
||||
placement: "bottom"
|
||||
});
|
||||
|
||||
// Auto-focus.
|
||||
useEffect(() => inputRef.current?.focus(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
showTooltip();
|
||||
setTimeout(() => hideTooltip(), 4000);
|
||||
} else {
|
||||
hideTooltip();
|
||||
}
|
||||
}, [ error ]);
|
||||
return <>
|
||||
<SearchOption
|
||||
title={t("search_string.title_column")}
|
||||
className={clsx({ "has-error": !!error })}
|
||||
help={<>
|
||||
<strong>{t("search_string.search_syntax")}</strong> - {t("search_string.also_see")} <a href="#" data-help-page="search.html">{t("search_string.complete_help")}</a>
|
||||
<ul style="marigin-bottom: 0;">
|
||||
<li>{t("search_string.full_text_search")}</li>
|
||||
<li><code>#abc</code> - {t("search_string.label_abc")}</li>
|
||||
<li><code>#year = 2019</code> - {t("search_string.label_year")}</li>
|
||||
<li><code>#rock #pop</code> - {t("search_string.label_rock_pop")}</li>
|
||||
<li><code>#rock or #pop</code> - {t("search_string.label_rock_or_pop")}</li>
|
||||
<li><code>#year <= 2000</code> - {t("search_string.label_year_comparison")}</li>
|
||||
<li><code>note.dateCreated >= MONTH-1</code> - {t("search_string.label_date_created")}</li>
|
||||
</ul>
|
||||
</>}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<FormTextArea
|
||||
inputRef={inputRef}
|
||||
className="search-string"
|
||||
placeholder={t("search_string.placeholder")}
|
||||
currentValue={searchString ?? ""}
|
||||
onChange={text => {
|
||||
currentValue.current = text;
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
return <SearchOption
|
||||
title={t("search_string.title_column")}
|
||||
help={<>
|
||||
<strong>{t("search_string.search_syntax")}</strong> - {t("search_string.also_see")} <a href="#" data-help-page="search.html">{t("search_string.complete_help")}</a>
|
||||
<ul style="marigin-bottom: 0;">
|
||||
<li>{t("search_string.full_text_search")}</li>
|
||||
<li><code>#abc</code> - {t("search_string.label_abc")}</li>
|
||||
<li><code>#year = 2019</code> - {t("search_string.label_year")}</li>
|
||||
<li><code>#rock #pop</code> - {t("search_string.label_rock_pop")}</li>
|
||||
<li><code>#rock or #pop</code> - {t("search_string.label_rock_or_pop")}</li>
|
||||
<li><code>#year <= 2000</code> - {t("search_string.label_year_comparison")}</li>
|
||||
<li><code>note.dateCreated >= MONTH-1</code> - {t("search_string.label_date_created")}</li>
|
||||
</ul>
|
||||
</>}
|
||||
note={note} {...restProps}
|
||||
>
|
||||
<FormTextArea
|
||||
inputRef={inputRef}
|
||||
className="search-string"
|
||||
placeholder={t("search_string.placeholder")}
|
||||
currentValue={searchString ?? ""}
|
||||
onChange={text => {
|
||||
currentValue.current = text;
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
// this also in effect disallows new lines in query string.
|
||||
// on one hand, this makes sense since search string is a label
|
||||
// on the other hand, it could be nice for structuring long search string. It's probably a niche case though.
|
||||
await spacedUpdate.updateNowIfNecessary();
|
||||
refreshResults();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SearchOption>;
|
||||
// this also in effect disallows new lines in query string.
|
||||
// on one hand, this makes sense since search string is a label
|
||||
// on the other hand, it could be nice for structuring long search string. It's probably a niche case though.
|
||||
await spacedUpdate.updateNowIfNecessary();
|
||||
refreshResults();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SearchOption>
|
||||
{error?.message && (
|
||||
<tr>
|
||||
<td colspan={3}>
|
||||
<Admonition type="caution">{error.message}</Admonition>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>;
|
||||
}
|
||||
|
||||
function SearchScriptOption({ note, ...restProps }: SearchOptionProps) {
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 10px;
|
||||
|
||||
.admonition {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 1em;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.search-setting-table div {
|
||||
@@ -141,20 +147,26 @@ body.mobile .search-definition-widget {
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.search-setting-table tr.searchString td:nth-of-type(2) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.search-setting-table tr.searchString {
|
||||
td:nth-of-type(2) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.search-setting-table tr.searchString .button-column {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 64px;
|
||||
.button-column {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-setting-table tr.ancestor > td > div {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions tr {
|
||||
border-bottom: 0;
|
||||
@@ -171,4 +183,4 @@ body.mobile .search-definition-widget {
|
||||
overflow: unset;
|
||||
height: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function RelationMap({ note, noteContext, ntxId, parentComponent
|
||||
};
|
||||
},
|
||||
onContentChange(content) {
|
||||
let newData: MapData | null = null;
|
||||
let newData: Partial<MapData> | null = null;
|
||||
|
||||
if (content) {
|
||||
try {
|
||||
@@ -75,7 +75,7 @@ export default function RelationMap({ note, noteContext, ntxId, parentComponent
|
||||
}
|
||||
}
|
||||
|
||||
if (!newData) {
|
||||
if (!newData || !newData.notes || !newData.transform) {
|
||||
newData = {
|
||||
notes: [],
|
||||
// it is important to have this exact value here so that initial transform is the same as this
|
||||
@@ -90,8 +90,8 @@ export default function RelationMap({ note, noteContext, ntxId, parentComponent
|
||||
};
|
||||
}
|
||||
|
||||
setData(newData);
|
||||
mapApiRef.current = new RelationMapApi(note, newData, (newData, refreshUi) => {
|
||||
setData(newData as MapData);
|
||||
mapApiRef.current = new RelationMapApi(note, newData as MapData, (newData, refreshUi) => {
|
||||
if (refreshUi) {
|
||||
setData(newData);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"e2e": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "17.2.4"
|
||||
"dotenv": "17.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.0-bullseye-slim AS builder
|
||||
FROM node:24.13.1-bullseye-slim AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.13.0-bullseye-slim
|
||||
FROM node:24.13.1-bullseye-slim
|
||||
# Install only runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.0-alpine AS builder
|
||||
FROM node:24.13.1-alpine AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.13.0-alpine
|
||||
FROM node:24.13.1-alpine
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache su-exec shadow
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.0-alpine AS builder
|
||||
FROM node:24.13.1-alpine AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.13.0-alpine
|
||||
FROM node:24.13.1-alpine
|
||||
# Create a non-root user with configurable UID/GID
|
||||
ARG USER=trilium
|
||||
ARG UID=1001
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.13.0-bullseye-slim AS builder
|
||||
FROM node:24.13.1-bullseye-slim AS builder
|
||||
RUN corepack enable
|
||||
|
||||
# Install native dependencies since we might be building cross-platform.
|
||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
|
||||
# We have to use --no-frozen-lockfile due to CKEditor patches
|
||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
|
||||
|
||||
FROM node:24.13.0-bullseye-slim
|
||||
FROM node:24.13.1-bullseye-slim
|
||||
# Create a non-root user with configurable UID/GID
|
||||
ARG USER=trilium
|
||||
ARG UID=1001
|
||||
|
||||
@@ -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.6",
|
||||
"i18next-fs-backend": "2.6.1",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "6.0.0",
|
||||
|
||||
@@ -3,6 +3,37 @@
|
||||
"back-in-note-history": "Μετάβαση στην προηγούμενη σημείωση στο ιστορικό",
|
||||
"forward-in-note-history": "Μεταβείτε στην επόμενη σημείωση στο ιστορικό",
|
||||
"open-jump-to-note-dialog": "Ανοίξτε το παράθυρο διαλόγου \"Μετάβαση στη σημείωση\"",
|
||||
"open-command-palette": "Άνοιγμα παλέτας εντολών"
|
||||
"open-command-palette": "Άνοιγμα παλέτας εντολών",
|
||||
"scroll-to-active-note": "Μετακίνηση του δέντρου σημειώσεων στην ενεργή σημείωση",
|
||||
"quick-search": "Ενεργοποίηση γραμμής γρήγορης αναζήτησης",
|
||||
"search-in-subtree": "Αναζήτηση σημειώσεων στο υποδέντρο της ενεργής σημείωσης",
|
||||
"expand-subtree": "Ανάπτυξη υποδέντρου της τρέχουσας σημείωσης",
|
||||
"collapse-tree": "Σύμπτυξη ολόκληρου του δέντρου σημειώσεων",
|
||||
"collapse-subtree": "Σύμπτυξη υποδέντρου της τρέχουσας σημείωσης",
|
||||
"sort-child-notes": "Ταξινόμηση υποσημειώσεων",
|
||||
"creating-and-moving-notes": "Δημιουργία και μετακίνηση σημειώσεων",
|
||||
"create-note-after": "Δημιουργία σημείωσης μετά την ενεργή σημείωση",
|
||||
"create-note-into": "Δημιουργία σημείωσης ως υποσημείωσης της ενεργής σημείωσης",
|
||||
"create-note-into-inbox": "Δημιουργία σημείωσης στα Εισερχόμενα (εάν έχουν οριστεί) ή στη σημείωση ημέρας",
|
||||
"delete-note": "Διαγραφή σημείωσης",
|
||||
"move-note-up": "Μετακίνηση σημείωσης προς τα επάνω",
|
||||
"move-note-down": "Μετακίνηση σημείωσης προς τα κάτω",
|
||||
"move-note-up-in-hierarchy": "Μετακίνηση σημείωσης προς τα επάνω στην ιεραρχία",
|
||||
"move-note-down-in-hierarchy": "Μετακίνηση σημείωσης προς τα κάτω στην ιεραρχία",
|
||||
"edit-note-title": "Μετάβαση από το δέντρο στις λεπτομέρειες της σημείωσης και επεξεργασία τίτλου",
|
||||
"edit-branch-prefix": "Εμφάνιση παραθύρου «Επεξεργασία προθέματος κλάδου»",
|
||||
"clone-notes-to": "Κλωνοποίηση επιλεγμένων σημειώσεων",
|
||||
"move-notes-to": "Μετακίνηση επιλεγμένων σημειώσεων",
|
||||
"note-clipboard": "Πρόχειρο σημειώσεων",
|
||||
"copy-notes-to-clipboard": "Αντιγραφή επιλεγμένων σημειώσεων στο πρόχειρο",
|
||||
"paste-notes-from-clipboard": "Επικόλληση σημειώσεων από το πρόχειρο στην ενεργή σημείωση",
|
||||
"cut-notes-to-clipboard": "Αποκοπή επιλεγμένων σημειώσεων στο πρόχειρο",
|
||||
"select-all-notes-in-parent": "Επιλογή όλων των σημειώσεων από το τρέχον επίπεδο σημείωσης",
|
||||
"add-note-above-to-the-selection": "Προσθήκη της παραπάνω σημείωσης στην επιλογή",
|
||||
"add-note-below-to-selection": "Προσθήκη της παρακάτω σημείωσης στην επιλογή",
|
||||
"duplicate-subtree": "Αντιγραφή υποδέντρου",
|
||||
"tabs-and-windows": "Καρτέλες & Παράθυρα",
|
||||
"open-new-tab": "Άνοιγμα νέας καρτέλας",
|
||||
"close-active-tab": "Κλείσιμο ενεργής καρτέλας"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import BUILTIN_ATTRIBUTES from "./builtin_attributes.js";
|
||||
import { AnonymizedDbResponse, BUILTIN_ATTRIBUTES, DatabaseAnonymizeResponse } from "@triliumnext/commons";
|
||||
import Database from "better-sqlite3";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import dataDir from "./data_dir.js";
|
||||
import dateUtils from "./date_utils.js";
|
||||
import Database from "better-sqlite3";
|
||||
import sql from "./sql.js";
|
||||
import path from "path";
|
||||
import { AnonymizedDbResponse, DatabaseAnonymizeResponse } from "@triliumnext/commons";
|
||||
|
||||
function getFullAnonymizationScript() {
|
||||
// we want to delete all non-builtin attributes because they can contain sensitive names and values
|
||||
@@ -86,7 +86,7 @@ function getExistingAnonymizedDatabases() {
|
||||
.readdirSync(dataDir.ANONYMIZED_DB_DIR)
|
||||
.filter((fileName) => fileName.includes("anonymized"))
|
||||
.map((fileName) => ({
|
||||
fileName: fileName,
|
||||
fileName,
|
||||
filePath: path.resolve(dataDir.ANONYMIZED_DB_DIR, fileName)
|
||||
})) satisfies AnonymizedDbResponse[];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"use strict";
|
||||
import { type AttributeRow, BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
|
||||
|
||||
import searchService from "./search/services/search.js";
|
||||
import sql from "./sql.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import BAttribute from "../becca/entities/battribute.js";
|
||||
import attributeFormatter from "./attribute_formatter.js";
|
||||
import BUILTIN_ATTRIBUTES from "./builtin_attributes.js";
|
||||
import type BNote from "../becca/entities/bnote.js";
|
||||
import type { AttributeRow } from "@triliumnext/commons";
|
||||
import attributeFormatter from "./attribute_formatter.js";
|
||||
import searchService from "./search/services/search.js";
|
||||
import sql from "./sql.js";
|
||||
|
||||
const ATTRIBUTE_TYPES = new Set(["label", "relation"]);
|
||||
|
||||
@@ -41,18 +39,18 @@ function getNoteWithLabel(name: string, value?: string): BNote | null {
|
||||
|
||||
function createLabel(noteId: string, name: string, value: string = "") {
|
||||
return createAttribute({
|
||||
noteId: noteId,
|
||||
noteId,
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value
|
||||
name,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
function createRelation(noteId: string, name: string, targetNoteId: string) {
|
||||
return createAttribute({
|
||||
noteId: noteId,
|
||||
noteId,
|
||||
type: "relation",
|
||||
name: name,
|
||||
name,
|
||||
value: targetNoteId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"keywords": [],
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"packageManager": "pnpm@10.29.3",
|
||||
"devDependencies": {
|
||||
"@wxt-dev/auto-icons": "1.1.0",
|
||||
"wxt": "0.20.15"
|
||||
"wxt": "0.20.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"cash-dom": "8.1.5"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"preview": "pnpm build && vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "25.8.5",
|
||||
"i18next": "25.8.6",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"preact": "10.28.3",
|
||||
"preact-iso": "2.11.1",
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
import { ComponentChildren } from 'preact';
|
||||
import Card from '../../components/Card.js';
|
||||
import Section from '../../components/Section.js';
|
||||
import DownloadButton from '../../components/DownloadButton.js';
|
||||
import "./index.css";
|
||||
import { useColorScheme, usePageTitle } from '../../hooks.js';
|
||||
import Button, { Link } from '../../components/Button.js';
|
||||
import gitHubIcon from "../../assets/boxicons/bx-github.svg?raw";
|
||||
import dockerIcon from "../../assets/boxicons/bx-docker.svg?raw";
|
||||
import noteStructureIcon from "../../assets/boxicons/bx-folder.svg?raw";
|
||||
import attributesIcon from "../../assets/boxicons/bx-tag.svg?raw";
|
||||
import hoistingIcon from "../../assets/boxicons/bx-chevrons-up.svg?raw";
|
||||
import revisionsIcon from "../../assets/boxicons/bx-history.svg?raw";
|
||||
import syncIcon from "../../assets/boxicons/bx-refresh-cw.svg?raw";
|
||||
import protectedNotesIcon from "../../assets/boxicons/bx-shield.svg?raw";
|
||||
import jumpToIcon from "../../assets/boxicons/bx-send-alt.svg?raw";
|
||||
import searchIcon from "../../assets/boxicons/bx-search.svg?raw";
|
||||
import webClipperIcon from "../../assets/boxicons/bx-paperclip.svg?raw";
|
||||
import importExportIcon from "../../assets/boxicons/bx-swap-horizontal.svg?raw";
|
||||
import shareIcon from "../../assets/boxicons/bx-globe.svg?raw";
|
||||
import codeIcon from "../../assets/boxicons/bx-code.svg?raw";
|
||||
import restApiIcon from "../../assets/boxicons/bx-extension.svg?raw";
|
||||
import textNoteIcon from "../../assets/boxicons/bx-note.svg?raw";
|
||||
import fileIcon from "../../assets/boxicons/bx-file.svg?raw";
|
||||
import canvasIcon from "../../assets/boxicons/bx-pen.svg?raw";
|
||||
import mermaidIcon from "../../assets/boxicons/bx-vector-square.svg?raw";
|
||||
import mindmapIcon from "../../assets/boxicons/bx-network-chart.svg?raw";
|
||||
import calendarIcon from "../../assets/boxicons/bx-calendar.svg?raw";
|
||||
import tableIcon from "../../assets/boxicons/bx-table.svg?raw";
|
||||
import boardIcon from "../../assets/boxicons/bx-columns-3.svg?raw";
|
||||
import geomapIcon from "../../assets/boxicons/bx-map.svg?raw";
|
||||
import presentationIcon from "../../assets/boxicons/bx-slideshow.svg?raw";
|
||||
import { getPlatform } from '../../download-helper.js';
|
||||
|
||||
import { ComponentChildren } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import calendarIcon from "../../assets/boxicons/bx-calendar.svg?raw";
|
||||
import hoistingIcon from "../../assets/boxicons/bx-chevrons-up.svg?raw";
|
||||
import codeIcon from "../../assets/boxicons/bx-code.svg?raw";
|
||||
import boardIcon from "../../assets/boxicons/bx-columns-3.svg?raw";
|
||||
import dockerIcon from "../../assets/boxicons/bx-docker.svg?raw";
|
||||
import restApiIcon from "../../assets/boxicons/bx-extension.svg?raw";
|
||||
import fileIcon from "../../assets/boxicons/bx-file.svg?raw";
|
||||
import noteStructureIcon from "../../assets/boxicons/bx-folder.svg?raw";
|
||||
import gitHubIcon from "../../assets/boxicons/bx-github.svg?raw";
|
||||
import shareIcon from "../../assets/boxicons/bx-globe.svg?raw";
|
||||
import revisionsIcon from "../../assets/boxicons/bx-history.svg?raw";
|
||||
import geomapIcon from "../../assets/boxicons/bx-map.svg?raw";
|
||||
import mindmapIcon from "../../assets/boxicons/bx-network-chart.svg?raw";
|
||||
import textNoteIcon from "../../assets/boxicons/bx-note.svg?raw";
|
||||
import webClipperIcon from "../../assets/boxicons/bx-paperclip.svg?raw";
|
||||
import canvasIcon from "../../assets/boxicons/bx-pen.svg?raw";
|
||||
import syncIcon from "../../assets/boxicons/bx-refresh-cw.svg?raw";
|
||||
import searchIcon from "../../assets/boxicons/bx-search.svg?raw";
|
||||
import jumpToIcon from "../../assets/boxicons/bx-send-alt.svg?raw";
|
||||
import protectedNotesIcon from "../../assets/boxicons/bx-shield.svg?raw";
|
||||
import presentationIcon from "../../assets/boxicons/bx-slideshow.svg?raw";
|
||||
import importExportIcon from "../../assets/boxicons/bx-swap-horizontal.svg?raw";
|
||||
import tableIcon from "../../assets/boxicons/bx-table.svg?raw";
|
||||
import attributesIcon from "../../assets/boxicons/bx-tag.svg?raw";
|
||||
import mermaidIcon from "../../assets/boxicons/bx-vector-square.svg?raw";
|
||||
import Button, { Link } from '../../components/Button.js';
|
||||
import Card from '../../components/Card.js';
|
||||
import DownloadButton from '../../components/DownloadButton.js';
|
||||
import Section from '../../components/Section.js';
|
||||
import { getPlatform } from '../../download-helper.js';
|
||||
import { useColorScheme, usePageTitle } from '../../hooks.js';
|
||||
|
||||
export function Home() {
|
||||
usePageTitle("");
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<HeroSection />
|
||||
<OrganizationBenefitsSection />
|
||||
@@ -48,7 +50,7 @@ export function Home() {
|
||||
<FaqSection />
|
||||
<FinalCta />
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
function HeroSection() {
|
||||
@@ -93,7 +95,7 @@ function HeroSection() {
|
||||
{screenshotUrl && <img class="screenshot" src={screenshotUrl} alt={t("hero_section.screenshot_alt")} />}
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function OrganizationBenefitsSection() {
|
||||
@@ -122,7 +124,7 @@ function ProductivityBenefitsSection() {
|
||||
<Card iconSvg={protectedNotesIcon} title={t("productivity_benefits.protected_notes_title")} moreInfoUrl="https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes">{t("productivity_benefits.protected_notes_content")}</Card>
|
||||
<Card iconSvg={jumpToIcon} title={t("productivity_benefits.jump_to_title")} moreInfoUrl="https://docs.triliumnotes.org/user-guide/concepts/navigation/jump-to">{t("productivity_benefits.jump_to_content")}</Card>
|
||||
<Card iconSvg={searchIcon} title={t("productivity_benefits.search_title")} moreInfoUrl="https://docs.triliumnotes.org/user-guide/concepts/navigation/search">{t("productivity_benefits.search_content")}</Card>
|
||||
<Card iconSvg={webClipperIcon} title={t("productivity_benefits.web_clipper_title")} moreInfoUrl="docs.triliumnotes.org/user-guide/setup/web-clipper">{t("productivity_benefits.web_clipper_content")}</Card>
|
||||
<Card iconSvg={webClipperIcon} title={t("productivity_benefits.web_clipper_title")} moreInfoUrl="https://docs.triliumnotes.org/user-guide/setup/web-clipper">{t("productivity_benefits.web_clipper_content")}</Card>
|
||||
</div>
|
||||
</Section>
|
||||
</>
|
||||
@@ -275,7 +277,7 @@ function ListWithScreenshot({ items, cardExtra }: {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FaqSection() {
|
||||
@@ -299,7 +301,7 @@ function FaqItem({ question, children }: { question: string; children: Component
|
||||
<Card title={question}>
|
||||
{children}
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FinalCta() {
|
||||
@@ -312,5 +314,5 @@ function FinalCta() {
|
||||
<Button href="./get-started/" text={t("final_cta.get_started")} />
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
13
docs/README-el.md
vendored
13
docs/README-el.md
vendored
@@ -97,8 +97,8 @@ script)](./README-ZH_TW.md) | [English](../README.md) | [French](./README-fr.md)
|
||||
ασφαλή σύνδεση
|
||||
* [Συγχρονισμός](https://docs.triliumnotes.org/user-guide/setup/synchronization)
|
||||
με self-hosted διακομιστή συγχρονισμού
|
||||
* there are [3rd party services for hosting synchronisation
|
||||
server](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
|
||||
* Υπάρχουν [υπηρεσίες τρίτων για φιλοξενία διακομιστή
|
||||
συγχρονισμού](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
|
||||
* [Κοινή χρήση](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing)
|
||||
(δημοσίευση) σημειώσεων στο δημόσιο διαδίκτυο
|
||||
* Ισχυρή [κρυπτογράφηση
|
||||
@@ -106,10 +106,11 @@ script)](./README-ZH_TW.md) | [English](../README.md) | [French](./README-fr.md)
|
||||
με υποδιαίρεση ανά σημείωση
|
||||
* Σχεδίαση διαγραμμάτων, με βάση το [Excalidraw](https://excalidraw.com/) (τύπος
|
||||
σημείωσης "καμβάς")
|
||||
* [Relation
|
||||
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
|
||||
[note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map)
|
||||
for visualizing notes and their relations
|
||||
* [Χάρτες
|
||||
συσχετίσεων](https://docs.triliumnotes.org/user-guide/note-types/relation-map)
|
||||
και [χάρτες
|
||||
σημειώσεων/συνδέσμων](https://docs.triliumnotes.org/user-guide/note-types/note-map)
|
||||
για την οπτικοποίηση σημειώσεων και των συσχετίσεών τους
|
||||
* Νοητικοί χάρτες, βασισμένοι στο [Mind Elixir](https://docs.mind-elixir.com/)
|
||||
* [Γεωγραφικοί
|
||||
χάρτες](https://docs.triliumnotes.org/user-guide/collections/geomap) με
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.10.10",
|
||||
"@types/node": "24.10.13",
|
||||
"@vitest/browser-webdriverio": "4.0.18",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
"@vitest/ui": "4.0.18",
|
||||
@@ -93,7 +93,7 @@
|
||||
"url": "https://github.com/TriliumNext/Trilium/issues"
|
||||
},
|
||||
"homepage": "https://triliumnotes.org",
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"packageManager": "pnpm@10.29.3",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",
|
||||
|
||||
@@ -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.14",
|
||||
"@fsegurai/codemirror-theme-abcdef": "6.2.3",
|
||||
"@fsegurai/codemirror-theme-abyss": "6.2.3",
|
||||
"@fsegurai/codemirror-theme-android-studio": "6.2.3",
|
||||
|
||||
@@ -13,3 +13,4 @@ export * from "./lib/attribute_names.js";
|
||||
export * from "./lib/utils.js";
|
||||
export * from "./lib/dayjs.js";
|
||||
export * from "./lib/notes.js";
|
||||
export { default as BUILTIN_ATTRIBUTES } from "./lib/builtin_attributes.js";
|
||||
|
||||
@@ -22,6 +22,11 @@ type Labels = {
|
||||
pageUrl: string;
|
||||
dateNote: string;
|
||||
|
||||
// Scripting
|
||||
run: string;
|
||||
widget: boolean;
|
||||
"disabled:widget": boolean;
|
||||
|
||||
// Tree specific
|
||||
subtreeHidden: boolean;
|
||||
|
||||
|
||||
@@ -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.3.1",
|
||||
"esbuild": "0.27.3",
|
||||
"eslint": "10.0.0",
|
||||
"highlight.js": "11.11.1",
|
||||
|
||||
771
pnpm-lock.yaml
generated
771
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user