refactor: rename plugin and update description; remove unused CSS file
This commit is contained in:
parent
6d09ce3e39
commit
cbc9d095d6
|
@ -0,0 +1,162 @@
|
|||
# MOC System Plugin
|
||||
|
||||
## Overview
|
||||
|
||||
This is a custom Obsidian plugin designed to automate and streamline a MOC (Map of Content) based note-taking system. The plugin focuses on efficiency by providing context-aware commands and automatic organization of notes into a hierarchical structure.
|
||||
|
||||
## Goals
|
||||
|
||||
The primary goal of this plugin is to automate the user's MOC-based system for organizing notes in Obsidian, with these specific objectives:
|
||||
|
||||
1. **Single-command note creation** - One keyboard shortcut handles all note creation needs based on context
|
||||
2. **Dynamic content organization** - MOCs only show sections that contain content, maintaining clean and minimal structure
|
||||
3. **Efficient prompt management** - Specialized system for managing LLM prompts with versioning and multi-chat link support
|
||||
4. **Automated maintenance** - Auto-cleanup of broken links and automatic folder structure creation
|
||||
|
||||
## System Design
|
||||
|
||||
### File Organization Structure
|
||||
|
||||
- **Top-level MOCs**: Created in vault root directory
|
||||
- **Sub-MOCs**: Stored in `MOCs/` folder
|
||||
- **Notes**: Stored in `Notes/` folder
|
||||
- **Resources**: Stored in `Resources/` folder
|
||||
- **Prompts**: Stored in `Prompts/` folder (includes both hubs and iterations)
|
||||
|
||||
### MOC Structure
|
||||
|
||||
MOCs are identified by the `#moc` tag in their frontmatter. They start empty and dynamically display only the sections that contain content, in this fixed order:
|
||||
|
||||
1. MOCs (sub-MOCs)
|
||||
2. Notes
|
||||
3. Resources
|
||||
4. Prompts
|
||||
|
||||
### Prompt System
|
||||
|
||||
The prompt system is designed for iterative LLM conversations:
|
||||
|
||||
- **Prompt Hub**: Main note for a prompt topic (e.g., `AI Assistant.md`)
|
||||
- Contains links to all iterations
|
||||
- Includes `llm-links` code block for storing chat URLs
|
||||
- **Iterations**: Individual versions (e.g., `AI Assistant v1.md`, `AI Assistant v2 - Added error handling.md`)
|
||||
- Can be duplicated from any version
|
||||
- Automatically increments to next available version number
|
||||
- Optional description can be added to title
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Context-Aware Creation Command
|
||||
**Command**: "Create MOC or add content"
|
||||
|
||||
- When not in a MOC: Creates a new top-level MOC
|
||||
- When in a MOC: Shows modal with options to create:
|
||||
- Sub-MOC
|
||||
- Note
|
||||
- Resource
|
||||
- Prompt
|
||||
|
||||
### 2. Prompt Iteration Duplication
|
||||
**Command**: "Duplicate prompt iteration"
|
||||
|
||||
- Works when viewing any prompt iteration file
|
||||
- Creates copy with next version number
|
||||
- Shows modal for optional description
|
||||
- Updates the prompt hub automatically
|
||||
|
||||
### 3. Multi-Link Opening
|
||||
**Command**: "Open all LLM links"
|
||||
|
||||
- Works when viewing a prompt hub
|
||||
- Parses `llm-links` code block
|
||||
- Opens all URLs in new browser tabs
|
||||
|
||||
### 4. Automatic Features
|
||||
|
||||
- **Folder Structure**: Creates required folders on plugin load
|
||||
- **Section Management**: Adds sections to MOCs only when first item is created
|
||||
- **Link Cleanup**: Removes broken links when files are deleted
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Core Architecture
|
||||
|
||||
The plugin extends Obsidian's Plugin class with these key components:
|
||||
|
||||
```typescript
|
||||
export default class MOCSystemPlugin extends Plugin {
|
||||
// Main plugin class
|
||||
}
|
||||
```
|
||||
|
||||
### Key Methods
|
||||
|
||||
#### Content Creation Methods
|
||||
- `createMOC()`: Creates top-level MOC with frontmatter tags
|
||||
- `createSubMOC()`: Creates MOC in MOCs/ folder and links from parent
|
||||
- `createNote()`: Creates note in Notes/ folder and links from parent MOC
|
||||
- `createResource()`: Creates resource in Resources/ folder and links from parent
|
||||
- `createPrompt()`: Creates prompt hub with first iteration and LLM links block
|
||||
|
||||
#### Section Management
|
||||
- `addToMOCSection()`: Intelligently adds links to MOC sections
|
||||
- Creates section if it doesn't exist
|
||||
- Maintains proper section ordering
|
||||
- Inserts links at appropriate position
|
||||
|
||||
#### Prompt System
|
||||
- `duplicatePromptIteration()`:
|
||||
- Parses filename to extract base name and version
|
||||
- Finds highest existing version number
|
||||
- Creates new file with incremented version
|
||||
- Updates prompt hub with new iteration link
|
||||
- `updatePromptHub()`: Adds new iteration links to hub file
|
||||
- `openLLMLinks()`: Extracts URLs from code block and opens in browser
|
||||
|
||||
#### Maintenance
|
||||
- `cleanupBrokenLinks()`: Removes references to deleted files
|
||||
- `ensureFolderStructure()`: Creates required folders if missing
|
||||
|
||||
### File Detection Methods
|
||||
- `isMOC()`: Checks for `#moc` tag in frontmatter
|
||||
- `isPromptIteration()`: Detects files with version pattern (v1, v2, etc.)
|
||||
- `isPromptHub()`: Identifies prompt files that aren't iterations
|
||||
|
||||
### Modal Dialogs
|
||||
|
||||
The plugin includes several custom modals for user input:
|
||||
|
||||
1. **CreateMOCModal**: For creating new top-level MOCs
|
||||
2. **AddToMOCModal**: Shows options when adding content to existing MOC
|
||||
3. **CreateItemModal**: Generic input for creating notes/resources/etc.
|
||||
4. **PromptDescriptionModal**: Optional description when duplicating prompts
|
||||
|
||||
### Event Handling
|
||||
|
||||
- Registers file deletion event to trigger automatic link cleanup
|
||||
- Uses command callbacks to check active file context
|
||||
- Implements keyboard shortcuts (Enter key) in all modals
|
||||
|
||||
## Technical Decisions
|
||||
|
||||
1. **Frontend-only approach**: All logic in main.ts, no settings or complex state management
|
||||
2. **Tag-based MOC identification**: Uses frontmatter tags instead of naming conventions for flexibility
|
||||
3. **Dynamic sections**: Sections only appear when needed, keeping MOCs clean
|
||||
4. **Regex-based parsing**: For version detection and link patterns
|
||||
5. **Batch link opening**: Uses window.open() in a loop for multi-link functionality
|
||||
|
||||
## Current Status
|
||||
|
||||
The plugin has been fully implemented with all requested features:
|
||||
- ✅ Context-aware creation command
|
||||
- ✅ Prompt iteration system with versioning
|
||||
- ✅ Multi-link opening for LLM chats
|
||||
- ✅ Dynamic section management
|
||||
- ✅ Automatic link cleanup
|
||||
- ✅ Folder structure creation
|
||||
|
||||
The plugin has been built and is ready for testing in Obsidian.
|
||||
|
||||
## History
|
||||
|
||||
*Initial implementation completed in first session - no previous history*
|
14
README.md
14
README.md
|
@ -1,16 +1,6 @@
|
|||
# Obsidian Sample Plugin
|
||||
# My Obsidian System Plugin
|
||||
|
||||
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||
|
||||
This project uses TypeScript to provide type checking and documentation.
|
||||
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
|
||||
|
||||
This sample plugin demonstrates some of the basic functionality the plugin API can do.
|
||||
- Adds a ribbon icon, which shows a Notice when clicked.
|
||||
- Adds a command "Open Sample Modal" which opens a Modal.
|
||||
- Adds a plugin setting tab to the settings page.
|
||||
- Registers a global click event and output 'click' to the console.
|
||||
- Registers a global interval which logs 'setInterval' to the console.
|
||||
An Obsidian plugin for system integration and automation.
|
||||
|
||||
## First time developing plugins?
|
||||
|
||||
|
|
572
main.ts
572
main.ts
|
@ -1,81 +1,77 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { App, Modal, Notice, Plugin, TFile, TFolder, normalizePath, MarkdownView } from 'obsidian';
|
||||
|
||||
// Remember to rename these classes and interfaces!
|
||||
interface PluginSettings {
|
||||
|
||||
interface MyPluginSettings {
|
||||
mySetting: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
mySetting: 'default'
|
||||
const DEFAULT_SETTINGS: PluginSettings = {
|
||||
|
||||
}
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
const FOLDERS = {
|
||||
MOCs: 'MOCs',
|
||||
Notes: 'Notes',
|
||||
Resources: 'Resources',
|
||||
Prompts: 'Prompts'
|
||||
} as const;
|
||||
|
||||
const SECTION_ORDER = ['MOCs', 'Notes', 'Resources', 'Prompts'] as const;
|
||||
type SectionType = typeof SECTION_ORDER[number];
|
||||
|
||||
export default class MOCSystemPlugin extends Plugin {
|
||||
settings: PluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
await this.ensureFolderStructure();
|
||||
|
||||
// This creates an icon in the left ribbon.
|
||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
new Notice('This is a notice!');
|
||||
// Main command for context-aware creation
|
||||
this.addCommand({
|
||||
id: 'moc-context-create',
|
||||
name: 'Create MOC or add content',
|
||||
callback: () => this.handleContextCreate()
|
||||
});
|
||||
// Perform additional things with the ribbon
|
||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText('Status Bar Text');
|
||||
|
||||
// This adds a simple command that can be triggered anywhere
|
||||
// Command to duplicate prompt iteration
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-simple',
|
||||
name: 'Open sample modal (simple)',
|
||||
callback: () => {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
});
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'sample-editor-command',
|
||||
name: 'Sample editor command',
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection('Sample Editor Command');
|
||||
}
|
||||
});
|
||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-complex',
|
||||
name: 'Open sample modal (complex)',
|
||||
id: 'duplicate-prompt-iteration',
|
||||
name: 'Duplicate prompt iteration',
|
||||
checkCallback: (checking: boolean) => {
|
||||
// Conditions to check
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
// If checking is true, we're simply "checking" if the command can be run.
|
||||
// If checking is false, then we want to actually perform the operation.
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile && this.isPromptIteration(activeFile)) {
|
||||
if (!checking) {
|
||||
new SampleModal(this.app).open();
|
||||
this.duplicatePromptIteration(activeFile);
|
||||
}
|
||||
|
||||
// This command will only show up in Command Palette when the check function returns true
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
console.log('click', evt);
|
||||
// Command to open all LLM links
|
||||
this.addCommand({
|
||||
id: 'open-llm-links',
|
||||
name: 'Open all LLM links',
|
||||
checkCallback: (checking: boolean) => {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile && this.isPromptHub(activeFile)) {
|
||||
if (!checking) {
|
||||
this.openLLMLinks(activeFile);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
// Auto-cleanup on file deletion
|
||||
this.registerEvent(
|
||||
this.app.vault.on('delete', (file) => {
|
||||
if (file instanceof TFile) {
|
||||
this.cleanupBrokenLinks(file);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
@ -89,46 +85,464 @@ export default class MyPlugin extends Plugin {
|
|||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
async ensureFolderStructure() {
|
||||
for (const folder of Object.values(FOLDERS)) {
|
||||
const folderPath = normalizePath(folder);
|
||||
if (!this.app.vault.getAbstractFileByPath(folderPath)) {
|
||||
await this.app.vault.createFolder(folderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleContextCreate() {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
|
||||
if (!activeFile || !this.isMOC(activeFile)) {
|
||||
// Not in a MOC, create new MOC
|
||||
new CreateMOCModal(this.app, async (name: string) => {
|
||||
await this.createMOC(name);
|
||||
}).open();
|
||||
} else {
|
||||
// In a MOC, show options to add content
|
||||
new AddToMOCModal(this.app, activeFile, this).open();
|
||||
}
|
||||
}
|
||||
|
||||
async createMOC(name: string): Promise<TFile> {
|
||||
const fileName = `${name}.md`;
|
||||
const content = `---\ntags:\n - moc\n---\n`;
|
||||
|
||||
const file = await this.app.vault.create(fileName, content);
|
||||
await this.app.workspace.getLeaf().openFile(file);
|
||||
new Notice(`Created MOC: ${name}`);
|
||||
return file;
|
||||
}
|
||||
|
||||
async createSubMOC(parentMOC: TFile, name: string): Promise<TFile> {
|
||||
const fileName = `${FOLDERS.MOCs}/${name}.md`;
|
||||
const content = `---\ntags:\n - moc\n---\n`;
|
||||
|
||||
const file = await this.app.vault.create(normalizePath(fileName), content);
|
||||
await this.addToMOCSection(parentMOC, 'MOCs', file);
|
||||
new Notice(`Created sub-MOC: ${name}`);
|
||||
return file;
|
||||
}
|
||||
|
||||
async createNote(parentMOC: TFile, name: string): Promise<TFile> {
|
||||
const fileName = `${FOLDERS.Notes}/${name}.md`;
|
||||
const content = '';
|
||||
|
||||
const file = await this.app.vault.create(normalizePath(fileName), content);
|
||||
await this.addToMOCSection(parentMOC, 'Notes', file);
|
||||
new Notice(`Created note: ${name}`);
|
||||
return file;
|
||||
}
|
||||
|
||||
async createResource(parentMOC: TFile, name: string): Promise<TFile> {
|
||||
const fileName = `${FOLDERS.Resources}/${name}.md`;
|
||||
const content = '';
|
||||
|
||||
const file = await this.app.vault.create(normalizePath(fileName), content);
|
||||
await this.addToMOCSection(parentMOC, 'Resources', file);
|
||||
new Notice(`Created resource: ${name}`);
|
||||
return file;
|
||||
}
|
||||
|
||||
async createPrompt(parentMOC: TFile, name: string): Promise<TFile> {
|
||||
// Create prompt hub
|
||||
const hubFileName = `${FOLDERS.Prompts}/${name}.md`;
|
||||
const hubContent = `# ${name}\n\n## Iterations\n\n- [[${name} v1]]\n\n## LLM Links\n\n\`\`\`llm-links\n\n\`\`\`\n`;
|
||||
|
||||
const hubFile = await this.app.vault.create(normalizePath(hubFileName), hubContent);
|
||||
|
||||
// Create first iteration
|
||||
const iterationFileName = `${FOLDERS.Prompts}/${name} v1.md`;
|
||||
const iterationContent = '';
|
||||
await this.app.vault.create(normalizePath(iterationFileName), iterationContent);
|
||||
|
||||
await this.addToMOCSection(parentMOC, 'Prompts', hubFile);
|
||||
new Notice(`Created prompt: ${name}`);
|
||||
return hubFile;
|
||||
}
|
||||
|
||||
async addToMOCSection(moc: TFile, section: SectionType, newFile: TFile) {
|
||||
const content = await this.app.vault.read(moc);
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Find or create section
|
||||
let sectionIndex = -1;
|
||||
let insertIndex = lines.length;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim() === `## ${section}`) {
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionIndex === -1) {
|
||||
// Section doesn't exist, find where to insert it
|
||||
const currentSectionIndices: Map<SectionType, number> = new Map();
|
||||
|
||||
// Find existing sections
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
for (const sectionName of SECTION_ORDER) {
|
||||
if (lines[i].trim() === `## ${sectionName}`) {
|
||||
currentSectionIndices.set(sectionName, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find where to insert new section
|
||||
insertIndex = lines.length;
|
||||
for (let i = SECTION_ORDER.indexOf(section) + 1; i < SECTION_ORDER.length; i++) {
|
||||
if (currentSectionIndices.has(SECTION_ORDER[i])) {
|
||||
insertIndex = currentSectionIndices.get(SECTION_ORDER[i])!;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert section header
|
||||
const newSection = [`## ${section}`, '', `- [[${newFile.basename}]]`, ''];
|
||||
lines.splice(insertIndex, 0, ...newSection);
|
||||
} else {
|
||||
// Section exists, add link to it
|
||||
let linkInsertIndex = sectionIndex + 1;
|
||||
|
||||
// Skip empty lines after header
|
||||
while (linkInsertIndex < lines.length && lines[linkInsertIndex].trim() === '') {
|
||||
linkInsertIndex++;
|
||||
}
|
||||
|
||||
// Find end of section
|
||||
while (linkInsertIndex < lines.length &&
|
||||
!lines[linkInsertIndex].startsWith('## ') &&
|
||||
lines[linkInsertIndex].trim() !== '') {
|
||||
linkInsertIndex++;
|
||||
}
|
||||
|
||||
// Insert before empty line or next section
|
||||
lines.splice(linkInsertIndex, 0, `- [[${newFile.basename}]]`);
|
||||
}
|
||||
|
||||
await this.app.vault.modify(moc, lines.join('\n'));
|
||||
}
|
||||
|
||||
async duplicatePromptIteration(file: TFile) {
|
||||
const match = file.basename.match(/^(.+?)\s*v(\d+)(?:\s*-\s*(.+))?$/);
|
||||
if (!match) return;
|
||||
|
||||
const [, baseName, currentVersion] = match;
|
||||
|
||||
// Find all iterations to get next available version
|
||||
const promptFiles = this.app.vault.getMarkdownFiles()
|
||||
.filter(f => f.path.startsWith(FOLDERS.Prompts) && f.basename.startsWith(baseName));
|
||||
|
||||
let maxVersion = 0;
|
||||
for (const pFile of promptFiles) {
|
||||
const vMatch = pFile.basename.match(/v(\d+)/);
|
||||
if (vMatch) {
|
||||
maxVersion = Math.max(maxVersion, parseInt(vMatch[1]));
|
||||
}
|
||||
}
|
||||
|
||||
const nextVersion = maxVersion + 1;
|
||||
|
||||
// Ask for description
|
||||
new PromptDescriptionModal(this.app, async (description: string) => {
|
||||
const newName = description
|
||||
? `${baseName} v${nextVersion} - ${description}`
|
||||
: `${baseName} v${nextVersion}`;
|
||||
|
||||
const newPath = `${FOLDERS.Prompts}/${newName}.md`;
|
||||
const content = await this.app.vault.read(file);
|
||||
|
||||
const newFile = await this.app.vault.create(normalizePath(newPath), content);
|
||||
|
||||
// Update hub file
|
||||
await this.updatePromptHub(baseName, newFile);
|
||||
|
||||
await this.app.workspace.getLeaf().openFile(newFile);
|
||||
new Notice(`Created iteration: ${newName}`);
|
||||
}).open();
|
||||
}
|
||||
|
||||
async updatePromptHub(baseName: string, newIteration: TFile) {
|
||||
const hubPath = `${FOLDERS.Prompts}/${baseName}.md`;
|
||||
const hubFile = this.app.vault.getAbstractFileByPath(normalizePath(hubPath));
|
||||
|
||||
if (hubFile instanceof TFile) {
|
||||
const content = await this.app.vault.read(hubFile);
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Find iterations section
|
||||
let iterIndex = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim() === '## Iterations') {
|
||||
iterIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (iterIndex !== -1) {
|
||||
// Find where to insert
|
||||
let insertIndex = iterIndex + 1;
|
||||
while (insertIndex < lines.length &&
|
||||
!lines[insertIndex].startsWith('## ') &&
|
||||
lines[insertIndex].trim() !== '') {
|
||||
insertIndex++;
|
||||
}
|
||||
|
||||
// Insert before empty line or next section
|
||||
lines.splice(insertIndex, 0, `- [[${newIteration.basename}]]`);
|
||||
await this.app.vault.modify(hubFile, lines.join('\n'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async openLLMLinks(file: TFile) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const linkBlockMatch = content.match(/```llm-links\n([\s\S]*?)\n```/);
|
||||
|
||||
if (linkBlockMatch) {
|
||||
const links = linkBlockMatch[1]
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.startsWith('http'));
|
||||
|
||||
if (links.length === 0) {
|
||||
new Notice('No links found in llm-links block');
|
||||
return;
|
||||
}
|
||||
|
||||
// Open all links
|
||||
for (const link of links) {
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
|
||||
new Notice(`Opened ${links.length} links`);
|
||||
} else {
|
||||
new Notice('No llm-links block found');
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupBrokenLinks(deletedFile: TFile) {
|
||||
const allFiles = this.app.vault.getMarkdownFiles();
|
||||
|
||||
for (const file of allFiles) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const linkPattern = new RegExp(`\\[\\[${deletedFile.basename}\\]\\]`, 'g');
|
||||
|
||||
if (linkPattern.test(content)) {
|
||||
const lines = content.split('\n');
|
||||
const newLines = lines.filter(line => !line.includes(`[[${deletedFile.basename}]]`));
|
||||
|
||||
if (lines.length !== newLines.length) {
|
||||
await this.app.vault.modify(file, newLines.join('\n'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isMOC(file: TFile): boolean {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
return cache?.frontmatter?.tags?.includes('moc') ?? false;
|
||||
}
|
||||
|
||||
isPromptIteration(file: TFile): boolean {
|
||||
return file.path.startsWith(FOLDERS.Prompts) && /v\d+/.test(file.basename);
|
||||
}
|
||||
|
||||
isPromptHub(file: TFile): boolean {
|
||||
return file.path.startsWith(FOLDERS.Prompts) && !this.isPromptIteration(file);
|
||||
}
|
||||
}
|
||||
|
||||
class SampleModal extends Modal {
|
||||
constructor(app: App) {
|
||||
class CreateMOCModal extends Modal {
|
||||
constructor(app: App, private onSubmit: (name: string) => void) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.setText('Woah!');
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h2', { text: 'Create new MOC' });
|
||||
|
||||
const inputEl = contentEl.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'MOC name...'
|
||||
});
|
||||
inputEl.style.width = '100%';
|
||||
inputEl.focus();
|
||||
|
||||
inputEl.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && inputEl.value) {
|
||||
this.onSubmit(inputEl.value);
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
const buttonEl = contentEl.createEl('button', { text: 'Create' });
|
||||
buttonEl.addEventListener('click', () => {
|
||||
if (inputEl.value) {
|
||||
this.onSubmit(inputEl.value);
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
class AddToMOCModal extends Modal {
|
||||
constructor(
|
||||
app: App,
|
||||
private moc: TFile,
|
||||
private plugin: MOCSystemPlugin
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h2', { text: 'Add to MOC' });
|
||||
|
||||
containerEl.empty();
|
||||
const options: Array<{ type: SectionType, label: string }> = [
|
||||
{ type: 'MOCs', label: 'Sub-MOC' },
|
||||
{ type: 'Notes', label: 'Note' },
|
||||
{ type: 'Resources', label: 'Resource' },
|
||||
{ type: 'Prompts', label: 'Prompt' }
|
||||
];
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Setting #1')
|
||||
.setDesc('It\'s a secret')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your secret')
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
options.forEach(option => {
|
||||
const button = contentEl.createEl('button', {
|
||||
text: `Create ${option.label}`,
|
||||
cls: 'mod-cta'
|
||||
});
|
||||
button.style.display = 'block';
|
||||
button.style.width = '100%';
|
||||
button.style.marginBottom = '10px';
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.close();
|
||||
new CreateItemModal(this.app, option.label, async (name: string) => {
|
||||
switch (option.type) {
|
||||
case 'MOCs':
|
||||
await this.plugin.createSubMOC(this.moc, name);
|
||||
break;
|
||||
case 'Notes':
|
||||
await this.plugin.createNote(this.moc, name);
|
||||
break;
|
||||
case 'Resources':
|
||||
await this.plugin.createResource(this.moc, name);
|
||||
break;
|
||||
case 'Prompts':
|
||||
await this.plugin.createPrompt(this.moc, name);
|
||||
break;
|
||||
}
|
||||
}).open();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class CreateItemModal extends Modal {
|
||||
constructor(
|
||||
app: App,
|
||||
private itemType: string,
|
||||
private onSubmit: (name: string) => void
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h2', { text: `Create ${this.itemType}` });
|
||||
|
||||
const inputEl = contentEl.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: `${this.itemType} name...`
|
||||
});
|
||||
inputEl.style.width = '100%';
|
||||
inputEl.focus();
|
||||
|
||||
inputEl.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && inputEl.value) {
|
||||
this.onSubmit(inputEl.value);
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
const buttonEl = contentEl.createEl('button', { text: 'Create' });
|
||||
buttonEl.addEventListener('click', () => {
|
||||
if (inputEl.value) {
|
||||
this.onSubmit(inputEl.value);
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class PromptDescriptionModal extends Modal {
|
||||
constructor(app: App, private onSubmit: (description: string) => void) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('h2', { text: 'Add iteration description (optional)' });
|
||||
|
||||
const inputEl = contentEl.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Description (optional)...'
|
||||
});
|
||||
inputEl.style.width = '100%';
|
||||
inputEl.focus();
|
||||
|
||||
const submitFn = () => {
|
||||
this.onSubmit(inputEl.value);
|
||||
this.close();
|
||||
};
|
||||
|
||||
inputEl.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitFn();
|
||||
}
|
||||
});
|
||||
|
||||
const buttonContainer = contentEl.createDiv();
|
||||
buttonContainer.style.display = 'flex';
|
||||
buttonContainer.style.gap = '10px';
|
||||
buttonContainer.style.marginTop = '10px';
|
||||
|
||||
const skipButton = buttonContainer.createEl('button', { text: 'Skip' });
|
||||
skipButton.addEventListener('click', () => {
|
||||
this.onSubmit('');
|
||||
this.close();
|
||||
});
|
||||
|
||||
const addButton = buttonContainer.createEl('button', {
|
||||
text: 'Add Description',
|
||||
cls: 'mod-cta'
|
||||
});
|
||||
addButton.addEventListener('click', submitFn);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
{
|
||||
"id": "sample-plugin",
|
||||
"name": "Sample Plugin",
|
||||
"id": "my-obsidian-system-plugin",
|
||||
"name": "MOC System Plugin",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Demonstrates some of the capabilities of the Obsidian API.",
|
||||
"author": "Obsidian",
|
||||
"authorUrl": "https://obsidian.md",
|
||||
"fundingUrl": "https://obsidian.md/pricing",
|
||||
"description": "Automated MOC-based note management system",
|
||||
"author": "Your Name",
|
||||
"authorUrl": "",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"name": "my-obsidian-system-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"description": "An Obsidian plugin for system integration and automation",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
|
||||
This CSS file will be included with your plugin, and
|
||||
available in the app when your plugin is enabled.
|
||||
|
||||
If your plugin does not need CSS, delete this file.
|
||||
|
||||
*/
|
Loading…
Reference in New Issue