Storyteller Suite - Core Plugin Architecture

Main Plugin Class

The plugin is defined in src/main.ts as StorytellerSuitePlugin:

export default class StorytellerSuitePlugin extends Plugin {
    settings: StorytellerSuiteSettings;
    
    async onload() {
        // Load settings
        await this.loadSettings();
        
        // Register ribbon icon
        this.addRibbonIcon('book-open', 'Storyteller Suite', () => {
            new DashboardModal(this.app, this).open();
        });
        
        // Register commands
        this.registerCommands();
        
        // Register views
        this.registerView(DASHBOARD_VIEW_TYPE, ...);
        this.registerView(TIMELINE_VIEW_TYPE, ...);
        this.registerView(NETWORK_GRAPH_VIEW_TYPE, ...);
        
        // Register code block processor for maps
        this.registerMarkdownCodeBlockProcessor('storyteller-map', ...);
        
        // Add settings tab
        this.addSettingTab(new StorytellerSuiteSettingTab(this.app, this));
    }
}

Core Types (src/types.ts)

Entity Interfaces

// Base entity fields
interface BaseEntity {
    id?: string;
    name: string;
    description?: string;
    tags?: string[];
    image?: string;          // Profile image path
    filePath?: string;       // Markdown file path
    customFields?: Record<string, any>;
}

// Character entity
interface Character extends BaseEntity {
    backstory?: string;
    traits?: string[];
    status?: string;
    relationships?: Relationship[];
    events?: string[];       // Event IDs
    locations?: string[];    // Location IDs
    groups?: string[];       // Group IDs
}

// Location entity
interface Location extends BaseEntity {
    history?: string;
    locationType?: string;
    parentLocationId?: string;
    childLocationIds?: string[];
    correspondingMapId?: string;
    coordinates?: { lat: number; lng: number };
}

// Event entity
interface Event extends BaseEntity {
    dateTime?: string;       // Natural language or ISO
    endDateTime?: string;
    outcome?: string;
    isMilestone?: boolean;
    progress?: number;       // 0-100
    dependencies?: string[]; // Event IDs
    charactersInvolved?: string[];
    location?: string;
}

// Scene entity
interface Scene extends BaseEntity {
    status?: 'draft' | 'wip' | 'review' | 'final';
    priority?: 'low' | 'medium' | 'high';
    chapterId?: string;
    content?: string;
    beats?: string[];        // Beat sheet items
    linkedCharacters?: string[];
    linkedLocations?: string[];
    linkedEvents?: string[];
    linkedItems?: string[];
}

// Chapter entity
interface Chapter extends BaseEntity {
    number?: number;
    summary?: string;
    sceneOrder?: string[];   // Scene IDs in order
}

// Item entity
interface Item extends BaseEntity {
    history?: string;
    currentOwner?: string;   // Character ID
    currentLocation?: string; // Location ID
    isPlotCritical?: boolean;
    associatedEvents?: string[];
}

// Group entity
interface Group extends BaseEntity {
    color?: string;          // Hex color
    groupType?: string;
    members?: GroupMember[];
    linkedEvents?: string[];
}

// Story Map
interface StoryMap extends BaseEntity {
    type?: 'image' | 'real';
    scale?: 'world' | 'region' | 'city' | 'building' | 'custom';
    backgroundImagePath?: string;
    lat?: number;
    lng?: number;
    zoom?: number;
    width?: number;
    height?: number;
    parentMapId?: string;
    childMapIds?: string[];
    correspondingLocationId?: string;
    markers?: MapMarker[];
    layers?: MapLayer[];
}

Relationship Types

interface Relationship {
    type: RelationshipType;
    targetId: string;
    label?: string;          // Custom label
}

type RelationshipType = 
    | 'ally'
    | 'enemy'
    | 'family'
    | 'rival'
    | 'romantic'
    | 'mentor'
    | 'acquaintance'
    | 'neutral'
    | 'custom';

Story Management

interface Story {
    id: string;
    name: string;
    created: string;         // ISO date
    folders?: StoryFolders;
}

interface StoryDraft {
    id: string;
    name: string;
    storyId: string;
    isActive?: boolean;
    sceneOrder: IndentedSceneRef[];
}

interface IndentedSceneRef {
    sceneId: string;
    indent: number;          // Hierarchy depth
    includeInCompile: boolean;
}

Settings Structure

interface StorytellerSuiteSettings {
    // Story Management
    stories: Story[];
    activeStoryId?: string;
    
    // Folder Configuration
    enableCustomEntityFolders: boolean;
    enableOneStoryMode: boolean;
    oneStoryBaseFolder: string;
    storyRootFolder: string;
    charactersFolder: string;
    locationsFolder: string;
    eventsFolder: string;
    itemsFolder: string;
    referencesFolder: string;
    scenesFolder: string;
    chaptersFolder: string;
    galleryFolder: string;
    mapsFolder: string;
    
    // Timeline Settings
    forwardDateBias: boolean;
    customToday: string;
    defaultTimelineGrouping: 'none' | 'byLocation' | 'byGroup' | 'byCharacter';
    defaultZoomPreset: 'none' | 'fit' | 'decade' | 'century';
    defaultStacking: boolean;
    defaultDensity: number;
    showLegendByDefault: boolean;
    timelineDefaultHeight: string;
    
    // Gantt View
    showProgressBarsInGantt: boolean;
    defaultGanttDuration: number;
    dependencyArrowStyle: 'solid' | 'dashed' | 'dotted';
    
    // Map Settings
    enableFrontmatterMarkers: boolean;
    enableDataViewMarkers: boolean;
    
    // Serialization
    customFieldsSerialization: 'flatten' | 'nested';
    
    // UI
    language: 'en' | 'zh' | string;
    showTutorialSection: boolean;
    hiddenDashboardTabs: string[];
    
    // Templates
    defaultTemplateId?: string;
    lastUsedTemplates: Record<string, string>;
    
    // Story Board
    storyBoardLayout: 'byChapters' | 'byTimeline' | 'byStatus';
    storyBoardColorBy: 'status' | 'chapter' | 'none';
    storyBoardShowEdges: boolean;
    storyBoardCardWidth: number;
    storyBoardCardHeight: number;
}

Plugin Methods

Entity CRUD Operations

// Characters
async saveCharacter(character: Character): Promise<void>
async listCharacters(): Promise<Character[]>
async deleteCharacter(character: Character): Promise<void>

// Locations
async saveLocation(location: Location): Promise<void>
async listLocations(): Promise<Location[]>
async deleteLocation(location: Location): Promise<void>

// Events
async saveEvent(event: Event): Promise<void>
async listEvents(): Promise<Event[]>
async deleteEvent(event: Event): Promise<void>

// Scenes
async saveScene(scene: Scene): Promise<void>
async listScenes(): Promise<Scene[]>
async deleteScene(scene: Scene): Promise<void>

// Chapters
async saveChapter(chapter: Chapter): Promise<void>
async listChapters(): Promise<Chapter[]>
async deleteChapter(chapter: Chapter): Promise<void>

// Items
async saveItem(item: Item): Promise<void>
async listItems(): Promise<Item[]>
async deleteItem(item: Item): Promise<void>

// Groups
async saveGroup(group: Group): Promise<void>
async listGroups(): Promise<Group[]>
async deleteGroup(group: Group): Promise<void>

// Maps
async saveMap(map: StoryMap): Promise<void>
async listMaps(): Promise<StoryMap[]>
async deleteMap(map: StoryMap): Promise<void>

// References
async saveReference(reference: Reference): Promise<void>
async listReferences(): Promise<Reference[]>
async deleteReference(reference: Reference): Promise<void>

Folder Resolution

// Get resolved folder path for entity type
getEntityFolder(entityType: EntityType): string

// Resolve placeholders in folder path
resolveFolder(folderPattern: string): string

// Supported placeholders:
// {storyName} - Active story name
// {storyId} - Active story ID
// {year} - Current year

Settings Tab

The settings UI is defined in StorytellerSuiteSettingTab:

class StorytellerSuiteSettingTab extends PluginSettingTab {
    plugin: StorytellerSuitePlugin;
    
    display(): void {
        // Language selection
        // Story management
        // Folder configuration
        // Timeline settings
        // Gantt view settings
        // Map settings
        // Dashboard tab visibility
        // Tutorial toggle
        // Support links
    }
}

Settings Sections

  1. Language - Select UI language
  2. Story Discovery - Manage multiple stories
  3. Gallery - Image upload folder
  4. Timeline & Parsing - Date parsing, grouping, zoom defaults
  5. Gantt View - Progress bars, durations, arrow styles
  6. Map Settings - Frontmatter/DataView marker detection
  7. Custom Folders - Per-entity folder paths
  8. One Story Mode - Simplified single-story layout
  9. Dashboard Tabs - Show/hide tabs
  10. Tutorial - Enable/disable help section
  11. About - Version info, support links

View Registration

// Dashboard view type
const DASHBOARD_VIEW_TYPE = 'storyteller-dashboard';

// Timeline view type
const TIMELINE_VIEW_TYPE = 'storyteller-timeline';

// Network graph view type
const NETWORK_GRAPH_VIEW_TYPE = 'storyteller-network-graph';

// Register views in onload()
this.registerView(DASHBOARD_VIEW_TYPE, (leaf) => 
    new AnalyticsDashboardView(leaf, this)
);

Command Registration

// Example commands registered
this.addCommand({
    id: 'open-dashboard',
    name: 'Open Dashboard',
    callback: () => new DashboardModal(this.app, this).open()
});

this.addCommand({
    id: 'create-character',
    name: 'Create Character',
    callback: () => new CharacterModal(this.app, this).open()
});

this.addCommand({
    id: 'open-timeline',
    name: 'Open Timeline',
    callback: () => this.activateView(TIMELINE_VIEW_TYPE)
});

this.addCommand({
    id: 'save-note-as-template',
    name: 'Save Note as Template',
    checkCallback: (checking) => { ... }
});

Data Persistence

All entities are persisted as Markdown files:

// Save entity to file
async saveEntity(entity: BaseEntity, folder: string): Promise<void> {
    const frontmatter = this.serializeFrontmatter(entity);
    const sections = this.serializeSections(entity);
    const content = `---\n${frontmatter}---\n\n${sections}`;
    
    const filePath = `${folder}/${entity.name}.md`;
    await this.app.vault.adapter.write(filePath, content);
}

// Load entity from file
async loadEntity(filePath: string): Promise<BaseEntity> {
    const content = await this.app.vault.cachedRead(file);
    const { frontmatter, sections } = this.parseMarkdown(content);
    return this.deserializeEntity(frontmatter, sections);
}

Event System

The plugin responds to Obsidian events:

// File changes
this.registerEvent(
    this.app.vault.on('modify', (file) => {
        // Refresh entity caches
    })
);

// Workspace layout changes
this.registerEvent(
    this.app.workspace.on('layout-change', () => {
        // Update views
    })
);