The Orca Note plugin system is a powerful extension mechanism that allows developers to add new features, customize interface components, or integrate external services. Through plugins, you can:
This guide will help you quickly get started with Orca Note plugin development, from setting up your environment to developing your first plugin.
To develop Orca Note plugins, you'll need the following environment and tools:
A typical Orca Note plugin project structure is as follows:
my-orca-plugin/
├── dist/ # Compiled code
│ ├── index.js # Compiled plugin file
├── src/
│ ├── main.ts # Entry file, contains plugin registration and initialization logic
│ ├── orca.d.ts # Plugin API type definition file
│ └── styles/ # CSS style files
├── icon.png # Plugin icon image
├── package.json # Project configuration
├── tsconfig.json # TypeScript configuration
├── vite.config.js # Vite build configuration (if using Vite)
└── README.md # Plugin documentation
The plugin name is the name of its containing folder. To deploy a plugin, place the plugin folder containing the above files into the orca/plugins
directory.
The following files are the minimum required for a functional Orca Note plugin:
dist/index.js
: The compiled JavaScript file containing the plugin logic.icon.png
: An icon representing the plugin in the Orca Note interface.Ensure these files are present in your plugin folder before deployment.
{
"name": "my-orca-plugin",
"version": "1.0.0",
"description": "My Orca Note Plugin",
"peerDependencies": {
"react": "^18.2.0",
"valtio": "^1.13.2"
}
}
The entry file needs to expose the following functions:
load
: Called when the plugin is enabledunload
: Called when the plugin is disabledFor example:
export async function load(pluginName: string) {
// Plugin enable logic
console.log("Plugin enabled")
}
export async function unload() {
// Plugin disable logic
console.log("Plugin disabled")
}
Orca Note plugins follow a simple and clear lifecycle pattern, mainly including the following phases:
The plugin package is discovered and loaded into Orca Note but not yet enabled. Plugin metadata is parsed at this time.
When a user enables the plugin or the application automatically enables it at startup, the plugin's load
function is called. This is the main entry point for plugin initialization, typically used to:
export async function load(pluginName: string) {
// Register command
orca.commands.registerCommand(
`${pluginName}.helloWorld`,
() => {
orca.notify("info", "Hello from My Plugin!")
},
"Show welcome message",
)
// Register block renderer
orca.renderers.registerBlock("myblock", false, MyCustomBlockRenderer)
// Add toolbar button
orca.toolbar.registerToolbarButton(`${pluginName}.toolbarButton`, {
icon: "ti ti-star",
tooltip: "My Tool Button",
command: `${pluginName}.helloWorld`,
})
}
When a user disables the plugin or the application closes, the plugin's unload
function is called. At this time, you should:
export async function unload() {
// Unregister command
orca.commands.unregisterCommand(`${pluginName}.helloWorld`)
// Unregister block renderer
orca.renderers.unregisterBlock("myblock")
// Remove toolbar button
orca.toolbar.unregisterToolbarButton(`${pluginName}.toolbarButton`)
}
Plugins can define their own settings and provide an UI interface for user configuration:
// Define settings schema
const settingsSchema = {
enableFeatureX: {
label: "Enable Feature X",
type: "boolean",
defaultValue: true,
},
userName: {
label: "Username",
type: "string",
defaultValue: "",
},
}
export async function load(pluginName: string) {
// Register settings schema
await orca.plugins.setSettingsSchema(pluginName, settingsSchema)
// Get settings value
const settings = orca.state.plugins[pluginName]?.settings
if (settings?.enableFeatureX) {
// Execute related logic
}
}
The Orca Note plugin API provides rich functional interfaces. Here's an overview of the most commonly used models and APIs:
The global object orca
is the main entry point for the plugin system, providing access to all plugin functionality.
orca.state
contains the current state of the application, including current panels, block data, settings, etc.
// Example: Get current language
const currentLocale = orca.state.locale
// Example: Get loaded block data
const currentBlock = orca.state.blocks[blockId]
// Example: Get application settings
const themeMode = orca.state.themeMode // "light" or "dark"
Orca Note uses the valtio
library to manage application state (mounted to window.Valtio
). You can listen to state changes using the subscribe
function provided by valtio
or other supported mechanisms.
The command system is the most basic extension point for plugins, allowing registration of executable function units:
// Register normal command
orca.commands.registerCommand(
"myplugin.command", // Command ID
() => {
/* Command logic */
}, // Command function
"Command Display Name", // Command label
)
// Register editor command (supports undo)
orca.commands.registerEditorCommand(
"myplugin.editorCommand", // Command ID
doFn, // Execute function
undoFn, // Undo function
{ label: "Editor Command" }, // Configuration
)
// Execute command
await orca.commands.invokeCommand("core.toggleThemeMode")
// Execute editor command
await orca.commands.invokeEditorCommand("myplugin.editorCommand", cursor)
Allows registration of custom block types and inline content renderers:
// Register block renderer
orca.renderers.registerBlock(
"myplugin.customBlock", // Block type
true, // Is editable
CustomBlockRenderer, // React component
["src"], // Fields that use assets (optional)
)
// Register inline content renderer
orca.renderers.registerInline(
"myplugin.customInline", // Inline type
true, // Is editable
CustomInlineRenderer, // React component
)
Orca Note's UI is based on React 18 (mounted to window.React
). If you need to develop custom UI components, you can use the globally exposed React directly without importing the React library separately.
Responsible for converting block content between different formats:
// Register block converter
orca.converters.registerBlock(
"html", // Target format
"myplugin.customBlock", // Block type
(block, repr) => {
// Convert function
return `<div>${block.text}</div>`
},
)
// Register inline content converter
orca.converters.registerInline(
"plain", // Target format
"myplugin.customInline", // Inline type
(content) => {
// Convert function
return content.v.toString()
},
)
Allows adding custom UI elements:
// Add toolbar button
orca.toolbar.registerToolbarButton("myplugin.toolbarButton", {
icon: "ti ti-star",
tooltip: "My Tool Button",
command: "myplugin.helloWorld",
})
// Add header bar button
orca.headbar.registerHeadbarButton("myplugin.headbarButton", () => (
<MyHeadbarButtonComponent />
))
Plugins can persistently store data:
// Set plugin data
await orca.plugins.setData("myplugin", "key", "value")
// Get plugin data
const value = await orca.plugins.getData("myplugin", "key")
// Remove plugin data
await orca.plugins.removeData("myplugin", "key")
Display notification messages:
orca.notify(
"info", // Type: "info" | "success" | "warn" | "error"
"This is a notification message", // Message content
{
// Optional configuration
title: "Notification Title",
action: () => {
/* Execute when notification is clicked */
},
},
)
Blocks are the basic structural units of Orca Note:
interface Block {
id: DbId // Block ID
content?: ContentFragment[] // Block content
text?: string // Plain text content
created: Date // Creation time
modified: Date // Modification time
parent?: DbId // Parent block ID
left?: DbId // Left block ID
children: DbId[] // Child block ID list
aliases: string[] // Alias list
properties: BlockProperty[] // Property list
refs: BlockRef[] // Reference list
backRefs: BlockRef[] // Back reference list
}
Panels are the main organizational units of the UI:
interface ViewPanel {
id: string // Panel ID
view: PanelView // View type ("journal" | "block")
viewArgs: Record<string, any> // View parameters
viewState: Record<string, any> // View state
width?: number // Width
height?: number // Height
locked?: boolean // Is locked
wide?: boolean // Is wide screen
}
When developing plugins for Orca Note, please adhere to the following conventions to ensure compatibility and maintainability:
Avoid Reserved Names: Any name starting with an underscore (_
) is reserved for system use. Plugin developers should not use such names for commands, renderers, settings, or any other identifiers.
Use Unique Prefixes: To avoid conflicts with other plugins, always include a unique prefix related to your plugin in the names of commands, renderers, and other identifiers. For example, if your plugin is named myplugin
, use a prefix like myplugin.
for all identifiers (e.g., myplugin.commandName
, myplugin.rendererName
).
Follow Naming Standards: Use descriptive and consistent naming conventions for all identifiers. This improves readability and helps other developers understand your code.
Respect System Behavior: Do not override or interfere with system-level commands, renderers, or UI elements unless explicitly allowed by the API.
By following these conventions, you can ensure that your plugin integrates seamlessly with Orca Note and coexists harmoniously with other plugins.
To quickly start development, you can use the following project template:
Here are several common plugin development examples to help you quickly get started with Orca Note plugin development:
This example shows how to create a simple command that inserts a new block with the current time after the current block:
// src/main.ts
export async function load(pluginName: string) {
// Register command
orca.commands.registerEditorCommand(
"myplugin.insertTimeBlock",
async ([_panelId, _rootBlockId, cursor]) => {
if (!cursor || !cursor.anchor) return null
const currentBlock = orca.state.blocks[cursor.anchor.blockId]
if (!currentBlock) return null
// Get current time
const now = new Date()
const timeStr = now.toLocaleTimeString()
// Create new block content
const content = [{ t: "t", v: `Current time is: ${timeStr}` }]
// Call editor command to insert new block
await orca.commands.invokeEditorCommand(
"core.editor.insertBlock",
null,
currentBlock,
"after",
content,
)
return null
},
() => {},
{ label: "Insert Time Block" },
)
// Register slash command
orca.slashCommands.registerSlashCommand("myplugin.insertTimeBlock", {
icon: "ti ti-clock",
group: "Utilities",
title: "Insert Time Block",
command: "myplugin.insertTimeBlock",
})
}
export async function unload() {
// Unregister command
orca.commands.unregisterCommand("myplugin.insertTimeBlock")
// Remove slash command
orca.slashCommands.unregisterSlashCommand("myplugin.insertTimeBlock")
}
This example shows how to create a custom map block renderer:
// src/MapBlock.tsx
import type { Block, DbId } from "./orca.d.ts"
const { useRef, useMemo } = window.React
const { useSnapshot } = window.Valtio
const { BlockShell, BlockChildren } = orca.components
type Props = {
panelId: string
blockId: DbId
rndId: string
blockLevel: number
indentLevel: number
mirrorId?: DbId
withBreadcrumb?: boolean
initiallyCollapsed?: boolean
renderingMode?: "normal" | "simple" | "simple-children" | "readonly"
keyword: string // Prop to receive from _repr
}
export default function MapBlockRenderer({
panelId,
blockId,
rndId,
blockLevel,
indentLevel,
mirrorId,
withBreadcrumb,
initiallyCollapsed,
renderingMode,
keyword, // Received from _repr
}: Props) {
const { blocks } = useSnapshot(orca.state)
const block = blocks[mirrorId ?? blockId]
const childrenBlocks = useMemo(
() => (
<BlockChildren
block={block as Block}
panelId={panelId}
blockLevel={blockLevel}
indentLevel={indentLevel}
renderingMode={renderingMode}
/>
),
[block?.children],
)
return (
<BlockShell
panelId={panelId}
blockId={blockId}
rndId={rndId}
mirrorId={mirrorId}
blockLevel={blockLevel}
indentLevel={indentLevel}
withBreadcrumb={withBreadcrumb}
initiallyCollapsed={initiallyCollapsed}
renderingMode={renderingMode}
reprClassName="myplugin-repr-map" // Custom class for the block shell
contentClassName="myplugin-repr-map-content" // Custom class for the content area
contentAttrs={{ contentEditable: false }} // Prevent editing the iframe itself
contentJsx={
<iframe
src={`https://ditu.amap.com/search?query=${encodeURIComponent(
keyword,
)}`}
width="100%" // Example width
height="400" // Example height
style={{ border: 0 }} // Basic styling
allow="geolocation" // Permissions for the iframe
/>
}
childrenJsx={childrenBlocks}
/>
)
}
// src/main.ts
import MapBlockRenderer from "./MapBlock"
export async function load(pluginName: string) {
// Register block renderer
orca.renderers.registerBlock("myplugin.map", false, MapBlockRenderer)
// Register block converter
orca.converters.registerBlock("plain", "myplugin.map", (block, repr) => {
return `Map of: ${repr.keyword}`
})
// Register editor command to insert the map block
orca.commands.registerEditorCommand(
"myplugin.insertMapBlockCommand",
async ([_panelId, _rootBlockId, cursor]) => {
if (!cursor || !cursor.anchor) return null
const currentBlock = orca.state.blocks[cursor.anchor.blockId]
if (!currentBlock) return null
// Define the representation for the new map block
const repr = { type: "myplugin.map", keyword: "Beijing" }
// Insert the new map block after the current block using core.editor.insertBlock
const newBlockId = await orca.commands.invokeEditorCommand(
"core.editor.insertBlock",
null, // No initial content needed
currentBlock, // Reference block
"after", // Position
null, // No content fragments
repr, // Representation object
)
return null // Indicate success
},
() => {},
{ label: "Insert Map Block" },
)
// Register slash command to trigger the map block insertion
orca.slashCommands.registerSlashCommand("myplugin.insertMapBlock", {
icon: "ti ti-map-pin", // Icon for the slash command
group: "Insert", // Group in the slash command menu
title: "Insert Map Block", // Title displayed in the menu
command: "myplugin.insertMapBlockCommand", // The editor command to execute
})
}
export async function unload() {
// Unregister block renderer
orca.renderers.unregisterBlock("myplugin.map")
// Unregister block converter
orca.converters.unregisterBlock("plain", "myplugin.map")
// Unregister the editor command
orca.commands.unregisterCommand("myplugin.insertMapBlockCommand")
// Unregister the slash command
orca.slashCommands.unregisterSlashCommand("myplugin.insertMapBlock")
}
This example shows how to create a custom theme:
// public/sand-yellow.css
@media (prefers-color-scheme: dark) {
:root {
/* Sand Yellow Dark Theme */
--orca-color-bg-1: #3a3226; /* Dark sand/brown */
--orca-color-bg-2: #4f4639; /* Slightly lighter sand/brown */
--orca-color-text-1: #f0e6d6; /* Light sand/beige */
--orca-color-text-2: #bfae90; /* Muted sand/light brown */
--orca-color-primary-5: #d4ac0d; /* Golden yellow */
--orca-color-dangerous-5: #e74c3c; /* Standard danger red, or adjust if needed */
--orca-color-border: #6b5f4e; /* Mid-tone sand/brown */
--orca-color-selection: oklch(from var(--orca-color-primary-5) l c h / 50%); /* Selection based on primary */
}
}
// src/index.ts
export async function load(pluginName: string) {
// Register custom theme
orca.themes.register(
"myplugin", // Plugin name
"sand-yellow", // Theme name
"sand-yellow.css" // Theme CSS file path
)
}
export async function unload() {
// Unregister custom theme
orca.themes.unregister("sand-yellow")
}