Add initial version

This commit is contained in:
wz 2025-03-09 00:09:41 +01:00
parent 05e5fae703
commit 7012cdd492
9 changed files with 6310 additions and 658 deletions

View file

@ -1,55 +1,64 @@
# Plugin Template
# Canvas Format Brush for Obsidian
This is a template for creating plugins for [Obsidian](https://obsidian.md), maintained by [wenlzhang](https://github.com/wenlzhang).
This plugin allows you to copy formatting attributes from one canvas element and apply them to other canvas elements in [Obsidian](https://obsidian.md).
## Getting started
## Features
1. Clone this repository to your local machine
2. Update the following files with your plugin information:
- `manifest.json`:
- `id`: Your plugin ID (in kebab-case)
- `name`: Your plugin name
- `author`: Your name
- `authorUrl`: Your website or GitHub profile URL
- `fundingUrl`: Optional funding information
- `package.json`:
- `name`: Your plugin name (should match manifest.json)
- `description`: Your plugin description
- `author`: Your name
- `keywords`: Relevant keywords for your plugin
- Copy and paste formatting between canvas elements
- Supports copying:
- Card color
- Card size
- Border color
- Background color
- Context menu integration for easy access
- Status bar indicator showing when format is copied
- Commands for keyboard shortcuts
## Development
## How to Use
1. Install dependencies:
```bash
npm install
```
1. Select a canvas element you want to copy formatting from
2. Right-click and select "Copy format" from the context menu (or use the command)
3. Select one or more canvas elements you want to apply the formatting to
4. Right-click and select "Paste format" from the context menu (or use the command)
2. Start development server:
```bash
npm run dev
```
## Commands
3. Build the plugin:
```bash
npm run build
```
- **Copy format from selected canvas element**: Copies formatting attributes from the currently selected canvas element
- **Paste format to selected canvas elements**: Applies the copied formatting to all currently selected canvas elements
## Testing your plugin
## Settings
1. Create a test vault in Obsidian
2. Create a `.obsidian/plugins` folder in your test vault
3. Copy your plugin folder into the plugins folder
4. Reload Obsidian to load the plugin (Ctrl/Cmd + R)
5. Enable the plugin in Obsidian's settings
You can customize which formatting attributes are copied:
## Publishing your plugin
- **Copy color**: Enable/disable copying of the card color
- **Copy size**: Enable/disable copying of the card size
- **Copy border color**: Enable/disable copying of the border color
- **Copy background color**: Enable/disable copying of the background color
1. Update `versions.json` with your plugin's version history
2. Test your plugin thoroughly
3. Create a GitHub release
4. Submit your plugin to the Obsidian Plugin Gallery
Additional settings:
## Support me
- **Show status bar item**: Show/hide the format brush status in the status bar
- **Enable hotkeys**: Enable/disable hotkeys for copy and paste format (configure in Obsidian hotkeys settings)
<a href='https://ko-fi.com/C0C66C1TB' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi1.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
## Installation
### From Obsidian
1. Open Settings in Obsidian
2. Go to Community Plugins and turn off Safe Mode
3. Click Browse and search for "Canvas Format Brush"
4. Install the plugin and enable it
### Manual Installation
1. Download the latest release from the releases page
2. Extract the zip file into your Obsidian vault's `.obsidian/plugins` folder
3. Enable the plugin in Obsidian's Community Plugins settings
## Support
If you encounter any issues or have suggestions for improvements, please create an issue on the GitHub repository.
## License
This project is licensed under the MIT License.

View file

@ -1,6 +1,6 @@
{
"id": "plugin-template",
"name": "Plugin Template",
"id": "canvas-format-brush",
"name": "Canvas Format Brush",
"version": "0.0.1",
"minAppVersion": "1.0.0",
"author": "wenlzhang",
@ -8,5 +8,6 @@
"fundingUrl": {
"Buy Me a Coffee": "https://ko-fi.com/f84556"
},
"description": "Copy and paste formatting between canvas elements, such as card size, color, and other attributes.",
"isDesktopOnly": false
}

6296
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "plugin-template",
"name": "canvas-format-brush",
"version": "0.0.1",
"description": "Plugin Template.",
"description": "Copy and paste formatting between canvas elements, such as card size, color, and other attributes.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -15,7 +15,10 @@
},
"version-tag-prefix": "",
"keywords": [
"obsidian"
"obsidian",
"canvas",
"format",
"brush"
],
"author": "wenlzhang",
"devDependencies": {

View file

@ -1,9 +1,41 @@
import { Plugin } from 'obsidian';
import { SettingsTab } from './settingsTab';
import { PluginSettings, DEFAULT_SETTINGS } from './settings';
import { Plugin, WorkspaceLeaf, Menu, Notice, setIcon } from "obsidian";
import { SettingsTab } from "./settingsTab";
import { CanvasFormatBrushSettings, DEFAULT_SETTINGS } from "./settings";
export default class MyPlugin extends Plugin {
settings: PluginSettings;
// Define interfaces for Canvas API
interface CanvasNode {
id: string;
x: number;
y: number;
width: number;
height: number;
color?: string;
borderColor?: string;
backgroundColor?: string;
}
interface Canvas {
nodes: Map<string, CanvasNode>;
selection: Set<string>;
requestSave: () => void;
}
interface CanvasView {
canvas: Canvas;
menu?: Menu;
getViewType: () => string;
}
export default class CanvasFormatBrushPlugin extends Plugin {
settings: CanvasFormatBrushSettings;
statusBarItem: HTMLElement | null = null;
copiedFormat: {
color?: string;
width?: number;
height?: number;
borderColor?: string;
backgroundColor?: string;
} | null = null;
async onload() {
await this.loadSettings();
@ -11,26 +43,280 @@ export default class MyPlugin extends Plugin {
// Add settings tab
this.addSettingTab(new SettingsTab(this.app, this));
// Add your plugin's functionality here
// For example:
// this.addCommand({
// id: 'example-command',
// name: 'Example Command',
// callback: () => {
// // Command logic
// }
// });
// Register commands
this.addCommand({
id: "copy-canvas-format",
name: "Copy format from selected canvas element",
checkCallback: (checking: boolean) => {
const canvasView = this.getActiveCanvasView();
if (canvasView && canvasView.canvas.selection.size === 1) {
if (!checking) {
this.copyFormat(canvasView);
}
return true;
}
return false;
},
});
this.addCommand({
id: "paste-canvas-format",
name: "Paste format to selected canvas elements",
checkCallback: (checking: boolean) => {
const canvasView = this.getActiveCanvasView();
if (
canvasView &&
canvasView.canvas.selection.size > 0 &&
this.copiedFormat
) {
if (!checking) {
this.pasteFormat(canvasView);
}
return true;
}
return false;
},
});
// Register context menu event
this.registerEvent(
this.app.workspace.on(
"file-menu",
(menu: Menu, file: any, source: string) => {
// Only add menu items if we're in a canvas view
const canvasView = this.getActiveCanvasView();
if (canvasView && source === "canvas-menu") {
// Only add menu items if there is a selection
if (canvasView.canvas.selection.size > 0) {
menu.addItem((item) => {
item.setTitle("Copy format")
.setIcon("clipboard-copy")
.onClick(() => this.copyFormat(canvasView))
.setSection("canvas-format-brush");
});
// Only enable paste if we have a copied format
if (this.copiedFormat) {
menu.addItem((item) => {
item.setTitle("Paste format")
.setIcon("clipboard-paste")
.onClick(() =>
this.pasteFormat(canvasView),
)
.setSection("canvas-format-brush");
});
}
}
}
},
),
);
// Register status bar
this.registerEvent(
this.app.workspace.on(
"active-leaf-change",
(leaf: WorkspaceLeaf | null) => {
// Check if the active leaf is a canvas view
if (
leaf &&
leaf.view &&
leaf.view.getViewType() === "canvas"
) {
this.updateStatusBar();
} else {
// Hide status bar if not in canvas view
if (this.statusBarItem) {
this.statusBarItem.style.display = "none";
}
}
},
),
);
// Initialize status bar
this.initStatusBar();
}
onunload() {
// Cleanup when the plugin is disabled
// Clean up status bar
if (this.statusBarItem) {
this.statusBarItem.remove();
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData(),
);
}
async saveSettings() {
await this.saveData(this.settings);
}
getActiveCanvasView(): CanvasView | null {
const activeLeaf = this.app.workspace.activeLeaf;
if (
activeLeaf &&
activeLeaf.view &&
activeLeaf.view.getViewType() === "canvas"
) {
return activeLeaf.view as unknown as CanvasView;
}
return null;
}
copyFormat(canvasView: CanvasView) {
// Get the selected node
const selectedNodeId = Array.from(canvasView.canvas.selection)[0];
const selectedNode = canvasView.canvas.nodes.get(selectedNodeId);
if (!selectedNode) {
new Notice("No canvas element selected");
return;
}
// Create a new format object
this.copiedFormat = {};
// Copy only the attributes that are enabled in settings
if (this.settings.copyColor && selectedNode.color) {
this.copiedFormat.color = selectedNode.color;
}
if (this.settings.copySize) {
this.copiedFormat.width = selectedNode.width;
this.copiedFormat.height = selectedNode.height;
}
if (this.settings.copyBorderColor && selectedNode.borderColor) {
this.copiedFormat.borderColor = selectedNode.borderColor;
}
if (this.settings.copyBackgroundColor && selectedNode.backgroundColor) {
this.copiedFormat.backgroundColor = selectedNode.backgroundColor;
}
// Show a notice
new Notice("Format copied from canvas element");
// Update status bar
this.updateStatusBar();
}
pasteFormat(canvasView: CanvasView) {
if (!this.copiedFormat) {
new Notice("No format copied");
return;
}
// Get all selected nodes
const selectedNodeIds = Array.from(canvasView.canvas.selection);
if (selectedNodeIds.length === 0) {
new Notice("No canvas elements selected");
return;
}
// Apply format to all selected nodes
let modifiedCount = 0;
for (const nodeId of selectedNodeIds) {
const node = canvasView.canvas.nodes.get(nodeId);
if (node) {
modifiedCount++;
// Apply only the attributes that were copied
if (this.copiedFormat.color !== undefined) {
node.color = this.copiedFormat.color;
}
if (
this.copiedFormat.width !== undefined &&
this.copiedFormat.height !== undefined
) {
node.width = this.copiedFormat.width;
node.height = this.copiedFormat.height;
}
if (this.copiedFormat.borderColor !== undefined) {
node.borderColor = this.copiedFormat.borderColor;
}
if (this.copiedFormat.backgroundColor !== undefined) {
node.backgroundColor = this.copiedFormat.backgroundColor;
}
}
}
// Show a notice
new Notice(
`Format applied to ${modifiedCount} canvas element${modifiedCount > 1 ? "s" : ""}`,
);
// Force canvas to redraw
// This is a hack, but it works to refresh the canvas view
const event = new MouseEvent("mousemove", {
view: window,
bubbles: true,
cancelable: true,
});
document.dispatchEvent(event);
// Save changes
canvasView.canvas.requestSave();
}
initStatusBar() {
// Add status bar item
this.statusBarItem = this.addStatusBarItem();
this.statusBarItem.addClass("canvas-format-brush-status");
// Initialize with empty state
this.updateStatusBar();
}
updateStatusBar() {
if (!this.statusBarItem) return;
// Only show if enabled in settings and we're in a canvas view
const canvasView = this.getActiveCanvasView();
if (!this.settings.showStatusBarItem || !canvasView) {
this.statusBarItem.style.display = "none";
return;
}
this.statusBarItem.style.display = "block";
this.statusBarItem.empty();
const container = this.statusBarItem.createEl("div", {
cls: "canvas-format-brush-container",
});
// Icon
const iconEl = container.createEl("div", {
cls: "canvas-format-brush-icon",
});
setIcon(iconEl, "brush");
// Text
const textEl = container.createEl("span", {
cls: "canvas-format-brush-text",
});
if (this.copiedFormat) {
textEl.setText("Format copied");
// Add color preview if color was copied
if (this.copiedFormat.color) {
const colorPreview = container.createEl("div", {
cls: "canvas-format-brush-color-preview",
});
colorPreview.style.backgroundColor = this.copiedFormat.color;
}
} else {
textEl.setText("No format copied");
}
}
}

View file

@ -1,10 +1,23 @@
import { App } from 'obsidian';
import { Plugin } from "obsidian";
export interface PluginSettings {
// Add your settings properties here
exampleSetting: string;
export interface CanvasFormatBrushSettings {
copyColor: boolean;
copySize: boolean;
copyBorderColor: boolean;
copyBackgroundColor: boolean;
enableHotkeys: boolean;
copyFormatHotkey: string;
pasteFormatHotkey: string;
showStatusBarItem: boolean;
}
export const DEFAULT_SETTINGS: PluginSettings = {
exampleSetting: 'default'
export const DEFAULT_SETTINGS: CanvasFormatBrushSettings = {
copyColor: true,
copySize: true,
copyBorderColor: true,
copyBackgroundColor: true,
enableHotkeys: true,
copyFormatHotkey: "Ctrl+Shift+C",
pasteFormatHotkey: "Ctrl+Shift+V",
showStatusBarItem: true,
};

View file

@ -1,10 +1,16 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import type MyPlugin from './main';
import {
App,
PluginSettingTab,
Setting,
ToggleComponent,
TextComponent,
} from "obsidian";
import type CanvasFormatBrushPlugin from "./main";
export class SettingsTab extends PluginSettingTab {
plugin: MyPlugin;
plugin: CanvasFormatBrushPlugin;
constructor(app: App, plugin: MyPlugin) {
constructor(app: App, plugin: CanvasFormatBrushPlugin) {
super(app, plugin);
this.plugin = plugin;
}
@ -13,17 +19,113 @@ export class SettingsTab extends PluginSettingTab {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Plugin Settings' });
containerEl.createEl("h2", { text: "Canvas Format Brush settings" });
// Format Attributes Section
containerEl.createEl("h3", { text: "Format attributes" });
new Setting(containerEl)
.setName('Example Setting')
.setDesc('This is an example setting')
.addText(text => text
.setPlaceholder('Enter your setting')
.setValue(this.plugin.settings.exampleSetting)
.onChange(async (value) => {
this.plugin.settings.exampleSetting = value;
.setName("Copy color")
.setDesc("Copy the color of the canvas element")
.addToggle((toggle: ToggleComponent) =>
toggle
.setValue(this.plugin.settings.copyColor)
.onChange(async (value: boolean) => {
this.plugin.settings.copyColor = value;
await this.plugin.saveSettings();
}));
}),
);
new Setting(containerEl)
.setName("Copy size")
.setDesc("Copy the size of the canvas element")
.addToggle((toggle: ToggleComponent) =>
toggle
.setValue(this.plugin.settings.copySize)
.onChange(async (value: boolean) => {
this.plugin.settings.copySize = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName("Copy border color")
.setDesc("Copy the border color of the canvas element")
.addToggle((toggle: ToggleComponent) =>
toggle
.setValue(this.plugin.settings.copyBorderColor)
.onChange(async (value: boolean) => {
this.plugin.settings.copyBorderColor = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName("Copy background color")
.setDesc("Copy the background color of the canvas element")
.addToggle((toggle: ToggleComponent) =>
toggle
.setValue(this.plugin.settings.copyBackgroundColor)
.onChange(async (value: boolean) => {
this.plugin.settings.copyBackgroundColor = value;
await this.plugin.saveSettings();
}),
);
// Hotkeys Section
containerEl.createEl("h3", { text: "Hotkeys" });
new Setting(containerEl)
.setName("Enable hotkeys")
.setDesc("Enable hotkeys for copy and paste format")
.addToggle((toggle: ToggleComponent) =>
toggle
.setValue(this.plugin.settings.enableHotkeys)
.onChange(async (value: boolean) => {
this.plugin.settings.enableHotkeys = value;
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName("Copy format hotkey")
.setDesc("Hotkey to copy format (set in Obsidian hotkeys settings)")
.setDisabled(!this.plugin.settings.enableHotkeys)
.addText((text: TextComponent) =>
text
.setPlaceholder("Ctrl+Shift+C")
.setValue(this.plugin.settings.copyFormatHotkey)
.setDisabled(true),
);
new Setting(containerEl)
.setName("Paste format hotkey")
.setDesc(
"Hotkey to paste format (set in Obsidian hotkeys settings)",
)
.setDisabled(!this.plugin.settings.enableHotkeys)
.addText((text: TextComponent) =>
text
.setPlaceholder("Ctrl+Shift+V")
.setValue(this.plugin.settings.pasteFormatHotkey)
.setDisabled(true),
);
// UI Settings Section
containerEl.createEl("h3", { text: "UI settings" });
new Setting(containerEl)
.setName("Show status bar item")
.setDesc("Show format brush status in the status bar")
.addToggle((toggle: ToggleComponent) =>
toggle
.setValue(this.plugin.settings.showStatusBarItem)
.onChange(async (value: boolean) => {
this.plugin.settings.showStatusBarItem = value;
await this.plugin.saveSettings();
// Update status bar visibility
this.plugin.updateStatusBar();
}),
);
}
}

29
styles.css Normal file
View file

@ -0,0 +1,29 @@
/* Canvas Format Brush Plugin Styles */
.canvas-format-brush-status {
display: flex;
align-items: center;
}
.canvas-format-brush-container {
display: flex;
align-items: center;
gap: 6px;
}
.canvas-format-brush-icon {
display: flex;
align-items: center;
justify-content: center;
}
.canvas-format-brush-text {
font-size: 12px;
}
.canvas-format-brush-color-preview {
width: 12px;
height: 12px;
border-radius: 3px;
border: 1px solid var(--background-modifier-border);
}

View file

@ -15,8 +15,7 @@
"DOM",
"ES5",
"ES6",
"ES7",
"ES2021"
"ES7"
]
},
"include": [