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
- Language - Select UI language
- Story Discovery - Manage multiple stories
- Gallery - Image upload folder
- Timeline & Parsing - Date parsing, grouping, zoom defaults
- Gantt View - Progress bars, durations, arrow styles
- Map Settings - Frontmatter/DataView marker detection
- Custom Folders - Per-entity folder paths
- One Story Mode - Simplified single-story layout
- Dashboard Tabs - Show/hide tabs
- Tutorial - Enable/disable help section
- 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
})
);