mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
Merge pull request #1 from n8k99/claude/claude-md-mhy85sj7tlvzwb5w-01QzBpdgGJXE5Qk3JaNupuxM
Claude/claude md mhy85sj7tlvzwb5w 01 qz bpdg gjxe5 qk3 ja nupux m
This commit is contained in:
commit
fe15bd0cf0
11 changed files with 2211 additions and 7 deletions
416
modules/io/obsidian-bridge.js
Normal file
416
modules/io/obsidian-bridge.js
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
"use strict";
|
||||
|
||||
// Obsidian Vault Integration for Fantasy Map Generator
|
||||
// Uses Obsidian Local REST API plugin
|
||||
// https://github.com/coddingtonbear/obsidian-local-rest-api
|
||||
|
||||
const ObsidianBridge = (() => {
|
||||
// Configuration
|
||||
const config = {
|
||||
apiUrl: "http://127.0.0.1:27123",
|
||||
apiKey: "", // Set via UI
|
||||
enabled: false,
|
||||
vaultName: ""
|
||||
};
|
||||
|
||||
// Initialize from localStorage
|
||||
function init() {
|
||||
const stored = localStorage.getItem("obsidianConfig");
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
Object.assign(config, parsed);
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to load Obsidian config:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
function saveConfig() {
|
||||
localStorage.setItem("obsidianConfig", JSON.stringify(config));
|
||||
}
|
||||
|
||||
// Test connection to Obsidian
|
||||
async function testConnection() {
|
||||
if (!config.apiKey) {
|
||||
throw new Error("API key not set. Please configure in Options.");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Connection failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
INFO && console.log("Obsidian connection successful:", data);
|
||||
config.enabled = true;
|
||||
saveConfig();
|
||||
return true;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Obsidian connection failed:", error);
|
||||
config.enabled = false;
|
||||
saveConfig();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all markdown files from vault
|
||||
async function getVaultFiles() {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Obsidian not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/vault/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch vault files: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.files || [];
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to get vault files:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get note content by path
|
||||
async function getNote(notePath) {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Obsidian not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
Accept: "text/markdown"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch note: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to get note:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Update note content
|
||||
async function updateNote(notePath, content) {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Obsidian not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
"Content-Type": "text/markdown"
|
||||
},
|
||||
body: content
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update note: ${response.status}`);
|
||||
}
|
||||
|
||||
INFO && console.log("Note updated successfully:", notePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to update note:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new note
|
||||
async function createNote(notePath, content) {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Obsidian not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
"Content-Type": "text/markdown"
|
||||
},
|
||||
body: content
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create note: ${response.status}`);
|
||||
}
|
||||
|
||||
INFO && console.log("Note created successfully:", notePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to create note:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse YAML frontmatter from markdown content
|
||||
function parseFrontmatter(content) {
|
||||
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
|
||||
const match = content.match(frontmatterRegex);
|
||||
|
||||
if (!match) {
|
||||
return {frontmatter: {}, content};
|
||||
}
|
||||
|
||||
const frontmatterText = match[1];
|
||||
const bodyContent = content.slice(match[0].length).trim();
|
||||
|
||||
// Simple YAML parser (handles basic key-value pairs and nested objects)
|
||||
const frontmatter = {};
|
||||
const lines = frontmatterText.split("\n");
|
||||
let currentKey = null;
|
||||
let currentObj = frontmatter;
|
||||
let indentLevel = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
|
||||
const indent = line.search(/\S/);
|
||||
const colonIndex = trimmed.indexOf(":");
|
||||
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const key = trimmed.slice(0, colonIndex).trim();
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
|
||||
if (indent === 0) {
|
||||
currentObj = frontmatter;
|
||||
currentKey = key;
|
||||
|
||||
if (value) {
|
||||
// Simple value
|
||||
frontmatter[key] = parseValue(value);
|
||||
} else {
|
||||
// Nested object or array
|
||||
frontmatter[key] = {};
|
||||
currentObj = frontmatter[key];
|
||||
}
|
||||
} else if (currentObj && key) {
|
||||
// Nested property
|
||||
currentObj[key] = parseValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
return {frontmatter, content: bodyContent};
|
||||
}
|
||||
|
||||
// Parse YAML value (handle strings, numbers, arrays)
|
||||
function parseValue(value) {
|
||||
if (!value) return null;
|
||||
|
||||
// Array
|
||||
if (value.startsWith("[") && value.endsWith("]")) {
|
||||
return value
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map(v => v.trim())
|
||||
.filter(v => v);
|
||||
}
|
||||
|
||||
// Number
|
||||
if (!isNaN(value) && value !== "") {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
// Boolean
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
|
||||
// String (remove quotes if present)
|
||||
return value.replace(/^["']|["']$/g, "");
|
||||
}
|
||||
|
||||
// Calculate distance between two coordinates
|
||||
function calculateDistance(x1, y1, x2, y2) {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
// Find notes by coordinates
|
||||
async function findNotesByCoordinates(x, y, limit = 8) {
|
||||
TIME && console.time("findNotesByCoordinates");
|
||||
|
||||
try {
|
||||
const files = await getVaultFiles();
|
||||
const mdFiles = files.filter(f => f.endsWith(".md"));
|
||||
|
||||
const matches = [];
|
||||
|
||||
for (const filePath of mdFiles) {
|
||||
try {
|
||||
const content = await getNote(filePath);
|
||||
const {frontmatter} = parseFrontmatter(content);
|
||||
|
||||
// Check for coordinates in frontmatter
|
||||
let noteX, noteY;
|
||||
|
||||
// Support various coordinate formats
|
||||
if (frontmatter.coordinates) {
|
||||
noteX = frontmatter.coordinates.x || frontmatter.coordinates.X;
|
||||
noteY = frontmatter.coordinates.y || frontmatter.coordinates.Y;
|
||||
} else {
|
||||
noteX = frontmatter.x || frontmatter.X;
|
||||
noteY = frontmatter.y || frontmatter.Y;
|
||||
}
|
||||
|
||||
if (noteX !== undefined && noteY !== undefined) {
|
||||
const distance = calculateDistance(x, y, noteX, noteY);
|
||||
|
||||
matches.push({
|
||||
path: filePath,
|
||||
name: filePath.replace(/\.md$/, "").split("/").pop(),
|
||||
frontmatter,
|
||||
distance,
|
||||
coordinates: {x: noteX, y: noteY}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip files that can't be read
|
||||
DEBUG && console.debug("Skipping file:", filePath, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by distance and return top matches
|
||||
matches.sort((a, b) => a.distance - b.distance);
|
||||
const results = matches.slice(0, limit);
|
||||
|
||||
TIME && console.timeEnd("findNotesByCoordinates");
|
||||
INFO && console.log(`Found ${results.length} nearby notes for (${x}, ${y})`);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to find notes by coordinates:", error);
|
||||
TIME && console.timeEnd("findNotesByCoordinates");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Find note by FMG ID in frontmatter
|
||||
async function findNoteByFmgId(fmgId) {
|
||||
try {
|
||||
const files = await getVaultFiles();
|
||||
const mdFiles = files.filter(f => f.endsWith(".md"));
|
||||
|
||||
for (const filePath of mdFiles) {
|
||||
try {
|
||||
const content = await getNote(filePath);
|
||||
const {frontmatter} = parseFrontmatter(content);
|
||||
|
||||
if (frontmatter["fmg-id"] === fmgId || frontmatter.fmgId === fmgId) {
|
||||
return {
|
||||
path: filePath,
|
||||
name: filePath.replace(/\.md$/, "").split("/").pop(),
|
||||
content,
|
||||
frontmatter
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
DEBUG && console.debug("Skipping file:", filePath, error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to find note by FMG ID:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate note template for FMG element
|
||||
function generateNoteTemplate(element, type) {
|
||||
const {x, y} = element;
|
||||
const lat = pack.cells.lat?.[element.cell] || 0;
|
||||
const lon = pack.cells.lon?.[element.cell] || 0;
|
||||
|
||||
const frontmatter = {
|
||||
"fmg-id": element.id || `${type}${element.i}`,
|
||||
"fmg-type": type,
|
||||
coordinates: {x, y, lat, lon},
|
||||
tags: [type],
|
||||
created: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (element.name) {
|
||||
frontmatter.aliases = [element.name];
|
||||
}
|
||||
|
||||
const yaml = Object.entries(frontmatter)
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
const nested = Object.entries(value)
|
||||
.map(([k, v]) => ` ${k}: ${v}`)
|
||||
.join("\n");
|
||||
return `${key}:\n${nested}`;
|
||||
} else if (Array.isArray(value)) {
|
||||
return `${key}:\n - ${value.join("\n - ")}`;
|
||||
}
|
||||
return `${key}: ${value}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const title = element.name || `${type} ${element.i}`;
|
||||
|
||||
return `---
|
||||
${yaml}
|
||||
---
|
||||
|
||||
# ${title}
|
||||
|
||||
*This note was created from Fantasy Map Generator*
|
||||
|
||||
## Description
|
||||
|
||||
Add your lore here...
|
||||
|
||||
## History
|
||||
|
||||
## Notable Features
|
||||
|
||||
## Related
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
config,
|
||||
saveConfig,
|
||||
testConnection,
|
||||
getVaultFiles,
|
||||
getNote,
|
||||
updateNote,
|
||||
createNote,
|
||||
parseFrontmatter,
|
||||
findNotesByCoordinates,
|
||||
findNoteByFmgId,
|
||||
generateNoteTemplate
|
||||
};
|
||||
})();
|
||||
|
||||
// Initialize on load
|
||||
ObsidianBridge.init();
|
||||
|
|
@ -167,6 +167,32 @@ async function saveToStorage(mapData, showTip = false) {
|
|||
showTip && tip("Map is saved to the browser storage", false, "success");
|
||||
}
|
||||
|
||||
// save current map as the default map
|
||||
async function saveAsDefaultMap() {
|
||||
if (customization) return tip("Map cannot be saved in EDIT mode, please complete the edit and retry", false, "error");
|
||||
|
||||
try {
|
||||
const mapData = prepareMapData();
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
await ldb.set("defaultMap", blob);
|
||||
tip("Map is set as default and will open on load", true, "success", 5000);
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
tip("Failed to set default map", true, "error", 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the default map setting
|
||||
async function clearDefaultMap() {
|
||||
try {
|
||||
await ldb.set("defaultMap", null);
|
||||
tip("Default map cleared", false, "success", 2000);
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
tip("Failed to clear default map", false, "error", 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// download map file
|
||||
function saveToMachine(mapData, filename) {
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
|
|
|
|||
73
modules/ui/obsidian-config.js
Normal file
73
modules/ui/obsidian-config.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
"use strict";
|
||||
|
||||
// Obsidian Configuration UI
|
||||
|
||||
function openObsidianConfig() {
|
||||
// Load current config
|
||||
const {apiUrl, apiKey, vaultName, enabled} = ObsidianBridge.config;
|
||||
|
||||
byId("obsidianApiUrl").value = apiUrl || "http://127.0.0.1:27123";
|
||||
byId("obsidianApiKey").value = apiKey || "";
|
||||
byId("obsidianVaultName").value = vaultName || "";
|
||||
|
||||
updateObsidianStatus(enabled);
|
||||
|
||||
$("#obsidianConfig").dialog({
|
||||
title: "Obsidian Vault Configuration",
|
||||
width: "600px",
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
||||
function updateObsidianStatus(enabled) {
|
||||
const statusEl = byId("obsidianStatus");
|
||||
if (enabled) {
|
||||
statusEl.textContent = "✅ Connected";
|
||||
statusEl.style.color = "#2ecc71";
|
||||
} else {
|
||||
statusEl.textContent = "❌ Not connected";
|
||||
statusEl.style.color = "#e74c3c";
|
||||
}
|
||||
}
|
||||
|
||||
async function testObsidianConnection() {
|
||||
const apiUrl = byId("obsidianApiUrl").value.trim();
|
||||
const apiKey = byId("obsidianApiKey").value.trim();
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
tip("Please enter both API URL and API Key", false, "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily set config for testing
|
||||
Object.assign(ObsidianBridge.config, {apiUrl, apiKey});
|
||||
|
||||
byId("obsidianStatus").textContent = "Testing connection...";
|
||||
byId("obsidianStatus").style.color = "#f39c12";
|
||||
|
||||
try {
|
||||
await ObsidianBridge.testConnection();
|
||||
updateObsidianStatus(true);
|
||||
tip("Successfully connected to Obsidian!", true, "success", 3000);
|
||||
} catch (error) {
|
||||
updateObsidianStatus(false);
|
||||
tip("Connection failed: " + error.message, true, "error", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function saveObsidianConfig() {
|
||||
const apiUrl = byId("obsidianApiUrl").value.trim();
|
||||
const apiKey = byId("obsidianApiKey").value.trim();
|
||||
const vaultName = byId("obsidianVaultName").value.trim();
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
tip("Please enter both API URL and API Key", false, "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(ObsidianBridge.config, {apiUrl, apiKey, vaultName});
|
||||
ObsidianBridge.saveConfig();
|
||||
|
||||
$("#obsidianConfig").dialog("close");
|
||||
tip("Obsidian configuration saved", true, "success", 2000);
|
||||
}
|
||||
358
modules/ui/obsidian-notes-editor.js
Normal file
358
modules/ui/obsidian-notes-editor.js
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
"use strict";
|
||||
|
||||
// Modern Markdown Notes Editor with Obsidian Integration
|
||||
|
||||
function editObsidianNote(elementId, elementType, coordinates) {
|
||||
const {x, y} = coordinates;
|
||||
|
||||
// Show loading dialog
|
||||
showLoadingDialog();
|
||||
|
||||
// Try to find note by FMG ID first, then by coordinates
|
||||
findOrCreateNote(elementId, elementType, coordinates)
|
||||
.then(noteData => {
|
||||
showMarkdownEditor(noteData, elementType);
|
||||
})
|
||||
.catch(error => {
|
||||
ERROR && console.error("Failed to load note:", error);
|
||||
tip("Failed to load Obsidian note: " + error.message, true, "error", 5000);
|
||||
closeDialogs("#obsidianNoteLoading");
|
||||
});
|
||||
}
|
||||
|
||||
async function findOrCreateNote(elementId, elementType, coordinates) {
|
||||
const {x, y} = coordinates;
|
||||
|
||||
// First try to find by FMG ID
|
||||
let note = await ObsidianBridge.findNoteByFmgId(elementId);
|
||||
|
||||
if (note) {
|
||||
INFO && console.log("Found note by FMG ID:", note.path);
|
||||
return note;
|
||||
}
|
||||
|
||||
// Find by coordinates
|
||||
const matches = await ObsidianBridge.findNotesByCoordinates(x, y, 8);
|
||||
|
||||
closeDialogs("#obsidianNoteLoading");
|
||||
|
||||
if (matches.length === 0) {
|
||||
// No matches - offer to create new note
|
||||
return await promptCreateNewNote(elementId, elementType, coordinates);
|
||||
}
|
||||
|
||||
if (matches.length === 1) {
|
||||
// Single match - load it
|
||||
const match = matches[0];
|
||||
const content = await ObsidianBridge.getNote(match.path);
|
||||
return {
|
||||
path: match.path,
|
||||
name: match.name,
|
||||
content,
|
||||
frontmatter: match.frontmatter
|
||||
};
|
||||
}
|
||||
|
||||
// Multiple matches - show selection dialog
|
||||
return await showNoteSelectionDialog(matches, elementId, elementType, coordinates);
|
||||
}
|
||||
|
||||
function showLoadingDialog() {
|
||||
alertMessage.innerHTML = `
|
||||
<div style="text-align: center; padding: 2em;">
|
||||
<div class="spinner" style="margin: 0 auto 1em;"></div>
|
||||
<p>Searching Obsidian vault for matching notes...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "Loading from Obsidian",
|
||||
width: "400px",
|
||||
closeOnEscape: false,
|
||||
buttons: {},
|
||||
dialogClass: "no-close",
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
||||
async function showNoteSelectionDialog(matches, elementId, elementType, coordinates) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const matchList = matches
|
||||
.map(
|
||||
(match, index) => `
|
||||
<div class="note-match" data-index="${index}" style="
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
" onmouseover="this.style.background='#f0f0f0'" onmouseout="this.style.background='white'">
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">${match.name}</div>
|
||||
<div style="font-size: 0.9em; color: #666;">
|
||||
Distance: ${match.distance.toFixed(1)} units<br/>
|
||||
Coordinates: (${match.coordinates.x}, ${match.coordinates.y})<br/>
|
||||
Path: ${match.path}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
alertMessage.innerHTML = `
|
||||
<div style="max-height: 60vh; overflow-y: auto;">
|
||||
<p style="margin-bottom: 1em;">Found ${matches.length} notes near this location. Select one:</p>
|
||||
${matchList}
|
||||
</div>
|
||||
`;
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "Select Obsidian Note",
|
||||
width: "600px",
|
||||
buttons: {
|
||||
"Create New": async function () {
|
||||
$(this).dialog("close");
|
||||
try {
|
||||
const newNote = await promptCreateNewNote(elementId, elementType, coordinates);
|
||||
resolve(newNote);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
reject(new Error("Cancelled"));
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
// Add click handlers to matches
|
||||
document.querySelectorAll(".note-match").forEach((el, index) => {
|
||||
el.addEventListener("click", async () => {
|
||||
$("#alert").dialog("close");
|
||||
try {
|
||||
const match = matches[index];
|
||||
const content = await ObsidianBridge.getNote(match.path);
|
||||
resolve({
|
||||
path: match.path,
|
||||
name: match.name,
|
||||
content,
|
||||
frontmatter: match.frontmatter
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function promptCreateNewNote(elementId, elementType, coordinates) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const element = getElementData(elementId, elementType);
|
||||
const suggestedName = element.name || `${elementType}-${element.i}`;
|
||||
|
||||
alertMessage.innerHTML = `
|
||||
<p>No matching notes found. Create a new note in your Obsidian vault?</p>
|
||||
<div style="margin: 1em 0;">
|
||||
<label for="newNoteName" style="display: block; margin-bottom: 0.5em;">Note name:</label>
|
||||
<input id="newNoteName" type="text" value="${suggestedName}" style="width: 100%; padding: 8px; font-size: 1em;"/>
|
||||
</div>
|
||||
<div style="margin: 1em 0;">
|
||||
<label for="newNotePath" style="display: block; margin-bottom: 0.5em;">Folder (optional):</label>
|
||||
<input id="newNotePath" type="text" placeholder="e.g., Locations/Cities" style="width: 100%; padding: 8px; font-size: 1em;"/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "Create New Note",
|
||||
width: "500px",
|
||||
buttons: {
|
||||
Create: async function () {
|
||||
const name = byId("newNoteName").value.trim();
|
||||
const folder = byId("newNotePath").value.trim();
|
||||
|
||||
if (!name) {
|
||||
tip("Please enter a note name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const notePath = folder ? `${folder}/${name}.md` : `${name}.md`;
|
||||
|
||||
$(this).dialog("close");
|
||||
|
||||
try {
|
||||
const template = ObsidianBridge.generateNoteTemplate(element, elementType);
|
||||
await ObsidianBridge.createNote(notePath, template);
|
||||
|
||||
const {frontmatter} = ObsidianBridge.parseFrontmatter(template);
|
||||
|
||||
resolve({
|
||||
path: notePath,
|
||||
name,
|
||||
content: template,
|
||||
frontmatter,
|
||||
isNew: true
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
reject(new Error("Cancelled"));
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getElementData(elementId, elementType) {
|
||||
// Extract element data based on type
|
||||
if (elementType === "burg") {
|
||||
const burgId = parseInt(elementId.replace("burg", ""));
|
||||
return pack.burgs[burgId];
|
||||
} else if (elementType === "marker") {
|
||||
const markerId = parseInt(elementId.replace("marker", ""));
|
||||
return pack.markers[markerId];
|
||||
} else {
|
||||
// Generic element
|
||||
const el = document.getElementById(elementId);
|
||||
return {
|
||||
id: elementId,
|
||||
name: elementId,
|
||||
x: parseFloat(el?.getAttribute("cx") || 0),
|
||||
y: parseFloat(el?.getAttribute("cy") || 0)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function showMarkdownEditor(noteData, elementType) {
|
||||
const {path, name, content, frontmatter, isNew} = noteData;
|
||||
|
||||
// Extract frontmatter and body
|
||||
const {content: bodyContent} = ObsidianBridge.parseFrontmatter(content);
|
||||
|
||||
// Set up the dialog
|
||||
byId("obsidianNotePath").textContent = path;
|
||||
byId("obsidianNoteName").value = name;
|
||||
byId("obsidianMarkdownEditor").value = content;
|
||||
byId("obsidianMarkdownPreview").innerHTML = renderMarkdown(bodyContent);
|
||||
|
||||
// Store current note data
|
||||
showMarkdownEditor.currentNote = noteData;
|
||||
showMarkdownEditor.originalContent = content;
|
||||
|
||||
$("#obsidianNotesEditor").dialog({
|
||||
title: `Obsidian Note: ${name}`,
|
||||
width: Math.min(svgWidth * 0.9, 1200),
|
||||
height: svgHeight * 0.85,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
close: () => {
|
||||
showMarkdownEditor.currentNote = null;
|
||||
showMarkdownEditor.originalContent = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Update preview on edit
|
||||
updateMarkdownPreview();
|
||||
|
||||
if (isNew) {
|
||||
tip("New note created in Obsidian vault", true, "success", 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMarkdownPreview() {
|
||||
const content = byId("obsidianMarkdownEditor").value;
|
||||
const {content: bodyContent} = ObsidianBridge.parseFrontmatter(content);
|
||||
byId("obsidianMarkdownPreview").innerHTML = renderMarkdown(bodyContent);
|
||||
}
|
||||
|
||||
function renderMarkdown(markdown) {
|
||||
// Simple Markdown renderer (will be replaced with marked.js)
|
||||
let html = markdown;
|
||||
|
||||
// Headers
|
||||
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
||||
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
||||
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
||||
|
||||
// Bold
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\_\_(.*?)\_\_/g, "<strong>$1</strong>");
|
||||
|
||||
// Italic
|
||||
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/\_(.*?)\_/g, "<em>$1</em>");
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
// Wikilinks [[Page]]
|
||||
html = html.replace(/\[\[([^\]]+)\]\]/g, '<span class="wikilink">$1</span>');
|
||||
|
||||
// Lists
|
||||
html = html.replace(/^\* (.*)$/gim, "<li>$1</li>");
|
||||
html = html.replace(/^\- (.*)$/gim, "<li>$1</li>");
|
||||
html = html.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>");
|
||||
|
||||
// Paragraphs
|
||||
html = html.replace(/\n\n/g, "</p><p>");
|
||||
html = "<p>" + html + "</p>";
|
||||
|
||||
// Clean up
|
||||
html = html.replace(/<p><\/p>/g, "");
|
||||
html = html.replace(/<p>(<h[1-6]>)/g, "$1");
|
||||
html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async function saveObsidianNote() {
|
||||
if (!showMarkdownEditor.currentNote) {
|
||||
tip("No note loaded", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const content = byId("obsidianMarkdownEditor").value;
|
||||
const {path} = showMarkdownEditor.currentNote;
|
||||
|
||||
try {
|
||||
await ObsidianBridge.updateNote(path, content);
|
||||
showMarkdownEditor.originalContent = content;
|
||||
tip("Note saved to Obsidian vault", true, "success", 2000);
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to save note:", error);
|
||||
tip("Failed to save note: " + error.message, true, "error", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function openInObsidian() {
|
||||
if (!showMarkdownEditor.currentNote) return;
|
||||
|
||||
const {path} = showMarkdownEditor.currentNote;
|
||||
const vaultName = ObsidianBridge.config.vaultName || "vault";
|
||||
const obsidianUrl = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(path)}`;
|
||||
|
||||
window.open(obsidianUrl, "_blank");
|
||||
tip("Opening in Obsidian app...", false, "success", 2000);
|
||||
}
|
||||
|
||||
function togglePreviewMode() {
|
||||
const editor = byId("obsidianMarkdownEditor");
|
||||
const preview = byId("obsidianMarkdownPreview");
|
||||
const isPreviewMode = editor.style.display === "none";
|
||||
|
||||
if (isPreviewMode) {
|
||||
editor.style.display = "block";
|
||||
preview.style.display = "none";
|
||||
byId("togglePreview").textContent = "👁 Preview";
|
||||
} else {
|
||||
updateMarkdownPreview();
|
||||
editor.style.display = "none";
|
||||
preview.style.display = "block";
|
||||
byId("togglePreview").textContent = "✏ Edit";
|
||||
}
|
||||
}
|
||||
|
|
@ -121,11 +121,16 @@ function editUnits() {
|
|||
|
||||
function addRuler() {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
|
||||
const width = Math.min(graphWidth, svgWidth);
|
||||
const height = Math.min(graphHeight, svgHeight);
|
||||
const pt = byId("map").createSVGPoint();
|
||||
(pt.x = graphWidth / 2), (pt.y = graphHeight / 4);
|
||||
pt.x = width / 2;
|
||||
pt.y = height / 4;
|
||||
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
|
||||
const dx = graphWidth / 4 / scale;
|
||||
const dy = (rulers.data.length * 40) % (graphHeight / 2);
|
||||
|
||||
const dx = width / 4 / scale;
|
||||
const dy = (rulers.data.length * 40) % (height / 2);
|
||||
const from = [(p.x - dx) | 0, (p.y + dy) | 0];
|
||||
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
|
||||
rulers.create(Ruler, [from, to]).draw();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue