Für Entwickler

Plugins entwickeln

Ein Plugin ist eine einzelne .js-Datei. Sie verändert Aussehen oder Funktionen von mind3D, ohne den Kern der App anzufassen. Plugins lassen sich in der App über den 🧩-Button im Startbildschirm ein- und ausschalten – ohne Neustart. Alles, was ein Plugin über die API registriert, wird beim Ausschalten automatisch wieder entfernt.

Gilt für beide Versionen (Android und USB/Desktop) – der Plugin-Code ist identisch.

Vorlage zum Kopieren: plugin-vorlage.js (zum Herunterladen) Ausführliche Beispiele in den App-Assets: plugins/farbschema.js (Design), plugins/projekte.js (eigene Ansichten, Daten im Plugin-Bereich der Mindmap), plugins/ansicht3d.js (Vollbild-Ansicht mit nachgeladener Bibliothek) und plugins/markdown-import.js (Import-Format)

Grundgerüst

mind3D.plugins.register({
  id: "mein-plugin",          // eindeutig, nur a-z 0-9 . _ -
  name: "Mein Plugin",
  version: "1.0.0",
  author: "Name",
  icon: "✨",                  // Emoji in der Plugin-Liste
  description: "Was das Plugin macht.",
  apiVersion: 1,              // API-Version, für die das Plugin geschrieben ist

  settings: [ /* optional, siehe unten */ ],

  activate(api) {
    // Hier alles registrieren. Läuft beim Einschalten und bei jedem Seitenstart.
  },

  deactivate(api) {
    // Optional. Registrierungen über die API werden automatisch entfernt.
  },
});

activate läuft auf beiden Seiten der App. api.page sagt, wo:

api.pageSeite
"menu"Startbildschirm / Projektliste
"map"geöffnete Mindmap

Funktionen, die auf einer Seite keinen Sinn haben (z. B. Toolbar-Button im Menü), tun dort einfach nichts.

API-Übersicht

Alle Funktionen, die etwas registrieren, geben eine Funktion zurück, mit der man die Registrierung vorzeitig wieder entfernen kann. Normalerweise braucht man das nicht.

Aussehen

AufrufWirkung
api.theme.set({ accent, accent2, colors, css, nodeTypes, palette })Farben ändern, erneut aufrufen = ersetzen. Details unten.
api.theme.clear()eigene Farben entfernen
api.ui.addStyle(css)eigenes CSS einfügen (z. B. Buttons, Dialoge)
api.background.replace(factory)Hintergrund austauschen (siehe unten)

Theme im Detail

api.theme.set({
  accent:  "#a07cff",   // färbt das GESAMTE Design: Buttons, Logo, Rahmen, Knoten, Verbindungen
  accent2: "#ff4fd8",   // optional: Farbverlauf accent → accent2 (null = einfarbig)
  colors:  { text: "#ffffff" },              // einzelne Canvas-Farben aus COLORS
  css:     { "--accent-border": "#333" },     // einzelne CSS-Variablen
  nodeTypes: { text: { card: "#101010", hdr: "#181818" } },
  palette: ["#ff0000", "#00ff00"],          // Farbauswahl für Rahmen (ersetzt sie)
  paletteExtra: ["#a07cff"],                // zusätzliche Farben vorne in der Farbauswahl
});

Aus accent leitet die App automatisch alle Abstufungen ab (hell, gedämpft, dunkel, getönte Rahmen/Hover-Flächen) und wählt Schwarz oder Weiß als Textfarbe auf Akzent-Buttons. Die CSS-Variablen dafür sind --accent, --accent-rgb, --accent-bright, --accent-deep, --accent-border, --on-accent, --accent-2 und --accent-gradient – eigenes Plugin-CSS kann sie ebenfalls verwenden. Die zuletzt gesetzten Werte werden beim nächsten Start sofort angewendet (kein Aufblitzen).

Zeichnen auf der Mindmap

api.render.before((ctx, info) => { … });          // vor allem anderen
api.render.after((ctx, info) => { … });           // über allem
api.render.nodeBefore((ctx, node, rect, info) => { … });  // hinter jedem Knoten
api.render.nodeAfter((ctx, node, rect, info) => { … });   // über jedem Knoten
api.render.redraw();                               // Neuzeichnen anfordern
  • ctx – Canvas-2D-Kontext in CSS-Pixeln (Zustand wird automatisch gesichert/zurückgesetzt)
  • info – { cssW, cssH, viewport, nodes, selectedId }
  • rect – Bildschirm-Rechteck des Knotens { x1, y1, x2, y2, z, selected } (z = Zoom)

Die Mindmap zeichnet nur neu, wenn sich etwas ändert. Für Animationen regelmäßig api.render.redraw() aufrufen (z. B. mit api.every(33, …)).

Eigener Hintergrund

api.background.replace((canvas, { viewport, page }) => {
  const ctx = canvas.getContext("2d");      // oder "webgl"
  return {
    resize(w, h, dpr) { … },   // Pflicht: Größe in CSS-Pixeln + Pixeldichte
    start() { … },             // Animation starten
    destroy() { … },           // Animation beenden (oder stop())
  };
});

Der Canvas ist bei jedem Austausch frisch. viewport (zoom, panX, panY) gibt es nur auf der Mindmap-Seite. Beim Ausschalten kommt der Standard-Weltraum zurück.

Eigene Knotentypen

api.nodes.registerType({
  type: "checkliste",               // nicht: mindmap, text, image, sound, document
  label: "Checkliste",              // Name im ➕-Menü und im Editor
  icon: "☑",
  colors: { card: "#0a1a10", hdr: "#0d2818" },
  create(node) { api.data.set(node, { items: [] }); },   // beim Anlegen
  draw(ctx, node, rect) { … },      // Inhaltsbereich zeichnen (rect wie oben, ohne Kopf)
  open(node) { return false; },     // Doppelklick; true = selbst erledigt, sonst Editor
  editor: {
    render(container, node) { … },  // eigene Felder in den Knoten-Editor einfügen
    save(container, node) { … },    // beim Speichern Werte übernehmen
  },
});

Der Typ erscheint automatisch im ➕-Menü. Ist das Plugin später aus, bleiben diese Knoten samt Daten erhalten und erscheinen als Platzhalter „Plugin nicht aktiv“.

Daten

AufrufGespeichert
api.data.get(node) / api.data.set(node, obj)am Knoten, im Projekt (wandert mit Export/Import). Nach set mit api.map.save() übernehmen.
api.storage.get(key, fallback) / api.storage.set(key, value)pro Gerät, projektübergreifend
api.settings.get(key) / api.settings.set(key, value)Einstellungen (siehe unten)

Sicherheitsmodus (api.security, nur lesen): mode() → "standard" | "global" | "tresor", protectsMindmaps() → true im Modus „Alles verschlüsseln“, isUnlocked(). api.storage und api.settings liegen unverschlüsselt in localStorage. Gibt protectsMindmaps() true zurück, dürfen dort keine Inhalte oder Namen von Mindmaps landen. Zwischenspeicher dann nur im Arbeitsspeicher halten (Beispiel: plugins/projekte.js). Mindmaps selbst immer über api.mindmaps/api.map lesen und schreiben: Diese Wege verschlüsseln automatisch. Tresor-Mindmaps sind für Plugins nicht sichtbar.

Die geöffnete Mindmap (api.map, nur auf "map", sonst null)

Aufruf
api.map.nodes()Knoten der aktuellen Ebene
api.map.allNodes()alle Knoten des Projekts
api.map.selected() / api.map.select(node)Auswahl
api.map.addNode(type, text, x?, y?)Knoten anlegen (ohne Position: Bildschirmmitte)
api.map.save()Änderung übernehmen: Undo-Schritt + Autosave + Neuzeichnen
api.map.viewport(){ zoom, panX, panY, worldToScreen(), screenToWorld() }
api.map.navigateTo(id)direkt auf die Ebene einer Node springen und sie auswählen (Zoom-Hysterese wird korrekt gesetzt)
api.map.suspend() / resume()2D-Ansicht pausieren/fortsetzen (für Vollbild-Ansichten wie die 3D-Ansicht)
api.map.state / api.map.appinterner Zugriff – kein stabiler Vertrag, nur im Notfall

Mindmaps verwalten (api.mindmaps, auf beiden Seiten)

Lesen und Schreiben über die normale Speicherlogik der App – im JSON-Format der Mindmaps.

Aufruf
await api.mindmaps.list()Mindmaps des angemeldeten Benutzers ([{ name, last_modified, saves }])
await api.mindmaps.load(name) / save(name, data)Mindmap-JSON laden / speichern
await api.mindmaps.create(name)neue Mindmap, gibt den tatsächlichen (ggf. nummerierten) Namen zurück
api.mindmaps.open(name)im Editor öffnen (nur Startbildschirm)
api.mindmaps.root(data) / children(data, id)Knoten finden
api.mindmaps.addNode(data, { text, parent_id, node_type, body_text, plugin_data })Knoten anlegen (Position automatisch)
api.mindmaps.removeNode(data, id)Knoten samt Unterknoten und Verbindungen entfernen
api.mindmaps.meta(data) / setMeta(data, obj)Plugin-Bereich im JSON: data.plugins[pluginId]

Grundregel für Daten: Alles, was auch ohne das Plugin sichtbar sein soll, gehört in normale Knoten (Text, Unterknoten). Plugin-eigene Zusatzinfos gehören in den Plugin-Bereich plugins.<id>. Die App reicht diesen Bereich beim Laden und Speichern unverändert durch, auch wenn das Plugin gerade aus ist. So bleibt jede Mindmap ohne Plugin lesbar, und mit Plugin geht nichts verloren.

In der geöffneten Mindmap: api.map.meta() / api.map.setMeta(obj), danach api.map.save().

Ereignisse

api.events.on("map:loaded",    (state) => …);
api.events.on("map:changed",   (state) => …);  // nach jeder Änderung
api.events.on("map:saved",     (state) => …);
api.events.on("node:created",  (node) => …);
api.events.on("node:deleted",  (node) => …);
api.events.on("node:selected", (nodeOrNull) => …);
api.events.on("level:changed", (contextNodeOrNull) => …);  // Ebene betreten/verlassen
api.events.on("menu:projectsShown", (username) => …);      // Projektliste angezeigt

Oberfläche

Aufruf
api.ui.toast(text, "ok" | "error")kurzer Hinweis
api.ui.dialog({ title, message, content, buttons })Dialog → Promise mit dem value des Buttons. buttons: [{ label, value, primary?, danger? }], content: eigenes DOM-Element
api.ui.confirm(title, message, okLabel)Ja/Nein → Promise<boolean>
api.ui.addToolbarButton({ icon, title, onClick })Button in der Mindmap-Toolbar
api.ui.addStartButton({ icon, title, onClick })runder Button im Startbildschirm
api.ui.addProjectTab({ id, label, icon, render(el, { username }) })eigener Tab neben „Mindmaps“ in der Projektliste
api.ui.selectProjectTab(id)Tab wählen (null = „Mindmaps“)
api.ui.decorateMindmapCard((cardEl, eintrag, username) => …)Karten der Mindmap-Liste ergänzen
api.ui.addImportOption({ icon, label, accept, directory, onFile(file, files) })eigenes Import-Format im „Neues Projekt“-Dialog; onFile liefert { name, data } (Mindmap-JSON). directory: true = Ordnerauswahl (alle Dateien in files, auf Android automatisch ausgeblendet)
api.ui.onBack(() => true/false)Android-Zurück-Taste abfangen; true = erledigt
api.ui.addElement(el, parent?)beliebiges Element einhängen
api.keys.add("ctrl+k", fn)Tastenkürzel (ctrl, shift, alt; nicht während Texteingabe)

Aufräumen automatisch

Aufruf
api.every(ms, fn) / api.after(ms, fn)Timer, die beim Ausschalten gestoppt werden
api.listen(target, type, fn)DOM-Listener, der beim Ausschalten entfernt wird
api.onDispose(fn)eigenes Aufräumen

Wichtig für das Ein-/Ausschalten ohne Neustart: Keine eigenen setInterval, addEventListener usw. direkt verwenden, sondern die Varianten oben – sonst laufen sie nach dem Ausschalten weiter.

Sprache

Die App spricht Deutsch und Englisch. Die aktuelle Sprache steht in document.documentElement.lang ("de" oder "en"). Gib die Texte deines Plugins in beiden Sprachen an und wähle sie nach diesem Wert; die App übersetzt nur ihre eigenen Texte, nicht die deines Plugins.

Einstellungen

Die App baut den Einstellungsdialog (⚙ in der Plugin-Liste) automatisch:

settings: [
  { key: "farbe",   type: "color",   label: "Farbe",       default: "#00d4ff" },  // Wähler + Hex-Feld
  { key: "vorgabe", type: "swatches", label: "Vorgabe",    default: "blau",       // Farbfelder
    options: [{ value: "blau", label: "Blau", color: "#4d8dff" },
              { value: "rot",  label: "Rot",  color: "#ff4d5e" }] },
  { key: "staerke", type: "range",   label: "Stärke",      default: 0.5, min: 0, max: 1, step: 0.05 },
  { key: "anzahl",  type: "number",  label: "Anzahl",      default: 3, min: 1 },
  { key: "an",      type: "boolean", label: "Aktiviert",   default: true },          // Schalter
  { key: "modus",   type: "select",  label: "Modus",       default: "a",
    options: [{ value: "a", label: "Variante A" }, { value: "b", label: "Variante B" }] },
  { key: "text",    type: "text",    label: "Beschriftung", default: "", hint: "Optionaler Hinweis" },
]

Änderungen wirken sofort: api.settings.onChange((key, value, all) => …).

  • color zeigt einen Farbwähler und ein Hex-Feld (#rrggbb, auch rgb-Kurzform; ungültige Eingaben werden markiert und nicht übernommen).
  • swatches zeigt abgerundete Farbfelder; gespeichert wird der value des gewählten Feldes.
  • boolean erscheint als Schalter.

Ein Feld nur unter einer Bedingung anzeigen: { key: "farbe2", type: "color", label: "Zweite Farbe", showIf: { verlauf: true } }. Ändert das Plugin selbst eine Einstellung (api.settings.set), zieht der offene Dialog mit.

Sicherheitsregeln der App (gelten auch für Plugins)

  • Kein Netz: Die Content Security Policy blockiert jedes Laden oder Senden ins Internet (fetch, Bilder, Skripte von Servern). Plugins arbeiten nur mit lokalen Daten und Dateien aus plugins/. Bilder/Töne als data: oder blob: gehen.
  • Erlaubt sind eigene Skripte, eval/new Function und Inline-Styles (api.ui.addStyle).
  • Nutzertext nie ungeprüft als HTML einsetzen: textContent verwenden oder maskieren – auch Anführungszeichen, wenn der Wert in einem Attribut landet (value="…").
  • Keine Mindmap-Daten am Kern vorbei speichern (kein eigenes localStorage/IndexedDB für Inhalte). Sonst landen sie im Hochsicherheitsmodus als Klartext auf dem Gerät.
  • Eigene Import-Formate liefern ihre Daten an den Kern; dort laufen sie durch die Import-Prüfung (js/map-validate.js) und Bilder durch die Metadaten-Bereinigung (js/media-clean.js).

Stabilität

  • Jeder Aufruf eines Plugins ist abgesichert. Ein Fehler bringt die App nicht zum Absturz.
  • Nach 5 Fehlern wird ein Plugin automatisch ausgeschaltet (mit Hinweis).
  • Schlägt activate fehl, wird alles bereits Registrierte zurückgenommen.
  • apiVersion höher als die der App → Plugin wird als „benötigt neuere Version“ angezeigt.

Installieren und verteilen

  • Aus Datei: 🧩 → „Plugin aus Datei installieren“ → .js wählen. Gleiche id erneut installieren = Update.