From 28cf8db82d1fa1d6c084d47041c89e90c2360699 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 04:18:20 +0000 Subject: [PATCH 1/2] feat(obsidian): add text search and browse features for note finding Enhanced the Obsidian note finding system with: **ObsidianBridge:** - searchNotes(query): Search vault by filename - listAllNotes(): Browse all notes in vault **Obsidian Notes Editor:** - Text search box with auto-populated state/province - "Browse All Notes" button to see full vault - Display burg's state and province in dialog - Pre-fill search with state name for easier finding - Enter key triggers search - Click any result to load that note **getElementData enhancement:** - Extract and include state/province names for burgs - Extract state/province from cell data for markers Now users can: 1. See which state/province the burg belongs to 2. Search by state name (pre-filled) 3. Search by any text in filename 4. Browse all notes manually 5. Click to select matching note This addresses the user's organization structure where notes are stored in State/Province folders matching the map structure. --- index.html | 4 +- modules/io/obsidian-bridge.js | 64 ++++++++- modules/ui/obsidian-notes-editor.js | 197 ++++++++++++++++++++++++++-- 3 files changed, 251 insertions(+), 14 deletions(-) diff --git a/index.html b/index.html index 1a7b2bd0..71b80876 100644 --- a/index.html +++ b/index.html @@ -8281,8 +8281,8 @@ - - + + diff --git a/modules/io/obsidian-bridge.js b/modules/io/obsidian-bridge.js index 1d8325fc..06643621 100644 --- a/modules/io/obsidian-bridge.js +++ b/modules/io/obsidian-bridge.js @@ -396,6 +396,66 @@ Add your lore here... `; } + // Search notes by text query (searches in filename and frontmatter) + async function searchNotes(query) { + if (!query || query.trim() === "") { + return []; + } + + const allFiles = await getVaultFiles(); + const searchTerm = query.toLowerCase(); + const results = []; + + for (const filePath of allFiles) { + const fileName = filePath.split("/").pop().replace(".md", "").toLowerCase(); + + // Check if filename matches + if (fileName.includes(searchTerm)) { + try { + const content = await getNote(filePath); + const {frontmatter} = parseFrontmatter(content); + + results.push({ + path: filePath, + name: filePath.split("/").pop().replace(".md", ""), + frontmatter, + matchType: "filename" + }); + } catch (error) { + WARN && console.warn(`Could not read file ${filePath}:`, error); + } + } + } + + return results; + } + + // List all notes with basic info + async function listAllNotes() { + const allFiles = await getVaultFiles(); + const notes = []; + + for (const filePath of allFiles) { + try { + const content = await getNote(filePath); + const {frontmatter} = parseFrontmatter(content); + + notes.push({ + path: filePath, + name: filePath.split("/").pop().replace(".md", ""), + frontmatter, + folder: filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : "" + }); + } catch (error) { + WARN && console.warn(`Could not read file ${filePath}:`, error); + } + } + + // Sort by path + notes.sort((a, b) => a.path.localeCompare(b.path)); + return notes; + } + return { init, config, @@ -408,7 +468,9 @@ Add your lore here... parseFrontmatter, findNotesByCoordinates, findNoteByFmgId, - generateNoteTemplate + generateNoteTemplate, + searchNotes, + listAllNotes }; })(); diff --git a/modules/ui/obsidian-notes-editor.js b/modules/ui/obsidian-notes-editor.js index 78fa133e..b034bb6a 100644 --- a/modules/ui/obsidian-notes-editor.js +++ b/modules/ui/obsidian-notes-editor.js @@ -153,21 +153,49 @@ async function promptCreateNewNote(elementId, elementType, coordinates) { const element = getElementData(elementId, elementType); const suggestedName = element.name || `${elementType}-${element.i}`; + // Build context info for the element + let contextInfo = ""; + if (element.state) { + contextInfo += `
State: ${element.state}
`; + } + if (element.province) { + contextInfo += `
Province: ${element.province}
`; + } + + // Pre-fill search with state or element name + const defaultSearch = element.state || element.name || ""; + alertMessage.innerHTML = ` -

No matching notes found. Create a new note in your Obsidian vault?

-
- - +
+

${element.name || elementId}

+ ${contextInfo} +

No matching notes found by coordinates.

-
- - + +
+ + + + +
+
+ +
+

Or create a new note:

+
+ + +
+
+ + +
`; $("#alert").dialog({ - title: "Create New Note", - width: "500px", + title: "Find or Create Note", + width: "600px", buttons: { Create: async function () { const name = byId("newNoteName").value.trim(); @@ -206,6 +234,128 @@ async function promptCreateNewNote(elementId, elementType, coordinates) { }, position: {my: "center", at: "center", of: "svg"} }); + + // Add event handlers for search and browse + const searchBtn = byId("obsidianSearchBtn"); + const browseBtn = byId("obsidianBrowseBtn"); + const searchInput = byId("obsidianSearch"); + const resultsDiv = byId("obsidianSearchResults"); + + const performSearch = async () => { + const query = searchInput.value.trim(); + if (!query) { + resultsDiv.innerHTML = "

Enter a search term

"; + return; + } + + resultsDiv.innerHTML = "

Searching...

"; + + try { + const results = await ObsidianBridge.searchNotes(query); + + if (results.length === 0) { + resultsDiv.innerHTML = "

No matching notes found

"; + return; + } + + resultsDiv.innerHTML = results + .map( + (note, index) => ` +
+
${note.name}
+
${note.path}
+
+ ` + ) + .join(""); + + // Add click handlers + document.querySelectorAll(".search-result").forEach((el, index) => { + el.addEventListener("click", async () => { + $("#alert").dialog("close"); + try { + const note = results[index]; + const content = await ObsidianBridge.getNote(note.path); + resolve({ + path: note.path, + name: note.name, + content, + frontmatter: note.frontmatter + }); + } catch (error) { + reject(error); + } + }); + }); + } catch (error) { + resultsDiv.innerHTML = `

Search failed: ${error.message}

`; + } + }; + + const showBrowse = async () => { + resultsDiv.innerHTML = "

Loading all notes...

"; + + try { + const allNotes = await ObsidianBridge.listAllNotes(); + + if (allNotes.length === 0) { + resultsDiv.innerHTML = "

No notes in vault

"; + return; + } + + resultsDiv.innerHTML = allNotes + .map( + (note, index) => ` +
+
${note.name}
+
${note.path}
+
+ ` + ) + .join(""); + + // Add click handlers + document.querySelectorAll(".browse-result").forEach((el, index) => { + el.addEventListener("click", async () => { + $("#alert").dialog("close"); + try { + const note = allNotes[index]; + const content = await ObsidianBridge.getNote(note.path); + resolve({ + path: note.path, + name: note.name, + content, + frontmatter: note.frontmatter + }); + } catch (error) { + reject(error); + } + }); + }); + } catch (error) { + resultsDiv.innerHTML = `

Failed to load notes: ${error.message}

`; + } + }; + + searchBtn.addEventListener("click", performSearch); + browseBtn.addEventListener("click", showBrowse); + searchInput.addEventListener("keypress", e => { + if (e.key === "Enter") performSearch(); + }); }); } @@ -213,10 +363,35 @@ function getElementData(elementId, elementType) { // Extract element data based on type if (elementType === "burg") { const burgId = parseInt(elementId.replace("burg", "")); - return pack.burgs[burgId]; + const burg = pack.burgs[burgId]; + + // Enhance with state and province names + const stateId = burg.state; + const provinceId = burg.province; + + return { + ...burg, + state: stateId && pack.states[stateId] ? pack.states[stateId].name : null, + province: provinceId && pack.provinces[provinceId] ? pack.provinces[provinceId].name : null + }; } else if (elementType === "marker") { const markerId = parseInt(elementId.replace("marker", "")); - return pack.markers[markerId]; + const marker = pack.markers[markerId]; + + // Enhance with state and province if marker has a cell + if (marker.cell) { + const cell = pack.cells; + const stateId = cell.state[marker.cell]; + const provinceId = cell.province[marker.cell]; + + return { + ...marker, + state: stateId && pack.states[stateId] ? pack.states[stateId].name : null, + province: provinceId && pack.provinces[provinceId] ? pack.provinces[provinceId].name : null + }; + } + + return marker; } else { // Generic element const el = document.getElementById(elementId); From 154145a5181f25caab4665edd66cae82034bffef Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 04:22:24 +0000 Subject: [PATCH 2/2] feat(obsidian): persist FMG-to-note association with fmg-id in frontmatter When a user selects or creates a note for a burg/marker, the system now automatically links them together by adding/updating the note's frontmatter. **How it works:** 1. **When saving a note**: - Adds `fmg-id: burg123` to frontmatter - Updates `x:` and `y:` coordinates - Preserves existing frontmatter fields 2. **When creating a new note**: - Template includes fmg-id from the start - Uses the proper elementId (e.g., "burg123") 3. **Next time you edit**: - System finds note instantly by fmg-id - No need to search by coordinates again - Permanent two-way link established **Changes:** - obsidian-notes-editor.js: - Pass elementId and coordinates to showMarkdownEditor() - updateFrontmatterWithFmgData() injects fmg-id on save - Automatically updates coordinates to keep in sync - obsidian-bridge.js: - generateNoteTemplate() accepts elementId parameter - Uses elementId for fmg-id instead of element.id Users no longer need to manually match notes - once linked, the association is permanent in the note's frontmatter. --- index.html | 4 +-- modules/io/obsidian-bridge.js | 4 +-- modules/ui/obsidian-notes-editor.js | 51 +++++++++++++++++++++++++---- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/index.html b/index.html index 71b80876..68ad5fae 100644 --- a/index.html +++ b/index.html @@ -8281,8 +8281,8 @@ - - + + diff --git a/modules/io/obsidian-bridge.js b/modules/io/obsidian-bridge.js index 06643621..a2874726 100644 --- a/modules/io/obsidian-bridge.js +++ b/modules/io/obsidian-bridge.js @@ -343,13 +343,13 @@ const ObsidianBridge = (() => { } // Generate note template for FMG element - function generateNoteTemplate(element, type) { + function generateNoteTemplate(element, type, elementId) { 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-id": elementId || element.id || `${type}${element.i}`, "fmg-type": type, coordinates: {x, y, lat, lon}, tags: [type], diff --git a/modules/ui/obsidian-notes-editor.js b/modules/ui/obsidian-notes-editor.js index b034bb6a..60b93386 100644 --- a/modules/ui/obsidian-notes-editor.js +++ b/modules/ui/obsidian-notes-editor.js @@ -11,7 +11,7 @@ function editObsidianNote(elementId, elementType, coordinates) { // Try to find note by FMG ID first, then by coordinates findOrCreateNote(elementId, elementType, coordinates) .then(noteData => { - showMarkdownEditor(noteData, elementType); + showMarkdownEditor(noteData, elementType, elementId, coordinates); }) .catch(error => { ERROR && console.error("Failed to load note:", error); @@ -211,7 +211,7 @@ async function promptCreateNewNote(elementId, elementType, coordinates) { $(this).dialog("close"); try { - const template = ObsidianBridge.generateNoteTemplate(element, elementType); + const template = ObsidianBridge.generateNoteTemplate(element, elementType, elementId); await ObsidianBridge.createNote(notePath, template); const {frontmatter} = ObsidianBridge.parseFrontmatter(template); @@ -404,7 +404,7 @@ function getElementData(elementId, elementType) { } } -function showMarkdownEditor(noteData, elementType) { +function showMarkdownEditor(noteData, elementType, elementId, coordinates) { const {path, name, content, frontmatter, isNew} = noteData; // Extract frontmatter and body @@ -416,9 +416,12 @@ function showMarkdownEditor(noteData, elementType) { byId("obsidianMarkdownEditor").value = content; byId("obsidianMarkdownPreview").innerHTML = renderMarkdown(bodyContent); - // Store current note data + // Store current note data and FMG element info showMarkdownEditor.currentNote = noteData; showMarkdownEditor.originalContent = content; + showMarkdownEditor.elementId = elementId; + showMarkdownEditor.elementType = elementType; + showMarkdownEditor.coordinates = coordinates; $("#obsidianNotesEditor").dialog({ title: `Obsidian Note: ${name}`, @@ -491,19 +494,55 @@ async function saveObsidianNote() { return; } - const content = byId("obsidianMarkdownEditor").value; + let content = byId("obsidianMarkdownEditor").value; const {path} = showMarkdownEditor.currentNote; + const elementId = showMarkdownEditor.elementId; + const coordinates = showMarkdownEditor.coordinates; + + // Update/add frontmatter with FMG ID and coordinates + if (elementId && coordinates) { + content = updateFrontmatterWithFmgData(content, elementId, coordinates); + } try { await ObsidianBridge.updateNote(path, content); showMarkdownEditor.originalContent = content; - tip("Note saved to Obsidian vault", true, "success", 2000); + // Update the editor to show the new frontmatter + byId("obsidianMarkdownEditor").value = content; + tip("Note saved to Obsidian vault (linked to FMG element)", true, "success", 3000); } catch (error) { ERROR && console.error("Failed to save note:", error); tip("Failed to save note: " + error.message, true, "error", 5000); } } +function updateFrontmatterWithFmgData(content, elementId, coordinates) { + const {x, y} = coordinates; + const {frontmatter, content: bodyContent} = ObsidianBridge.parseFrontmatter(content); + + // Update frontmatter with FMG data + frontmatter["fmg-id"] = elementId; + frontmatter["x"] = Math.round(x * 100) / 100; + frontmatter["y"] = Math.round(y * 100) / 100; + + // Rebuild frontmatter + let frontmatterLines = ["---"]; + for (const [key, value] of Object.entries(frontmatter)) { + if (typeof value === "object" && value !== null) { + // Handle nested objects + frontmatterLines.push(`${key}:`); + for (const [nestedKey, nestedValue] of Object.entries(value)) { + frontmatterLines.push(` ${nestedKey}: ${nestedValue}`); + } + } else { + frontmatterLines.push(`${key}: ${value}`); + } + } + frontmatterLines.push("---"); + + return frontmatterLines.join("\n") + "\n" + bodyContent; +} + function openInObsidian() { if (!showMarkdownEditor.currentNote) return;