mirror of
https://github.com/signynt/virtual-content.git
synced 2026-07-22 05:46:44 +00:00
Finished initial development
This commit is contained in:
parent
3be323fdee
commit
dfb55e2154
6 changed files with 117 additions and 152 deletions
105
README.md
105
README.md
|
|
@ -1,94 +1,29 @@
|
|||
# Obsidian Sample Plugin
|
||||
# Virtual Footer
|
||||
|
||||
This is a sample plugin for Obsidian (https://obsidian.md).
|
||||
Set rules to add markdown text to the bottom of each file in a folder. This text get's rendered normally, including dataview blocks.
|
||||
Your notes don't get modified or changed, the given markdown text is simply rendered "virtually".
|
||||
|
||||
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 is especially useful if you have many files with the same dataview block. Instead of pasting the datview codeblock into every note, you can simply add it with this plugin.
|
||||
This prevents unecessary bloat, while also letting you easily change the code for all files at the same time.
|
||||
|
||||
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.
|
||||
## Example use cases
|
||||
|
||||
## First time developing plugins?
|
||||
I have a folder called "Authors" which contains a note on each author of media I've read/watched. I want to see what media the Author has made when I open the note, so I use the following dataview query to query that info from my media notes:
|
||||
|
||||
Quick starting guide for new plugin devs:
|
||||
|
||||
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
|
||||
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
|
||||
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
|
||||
- Install NodeJS, then run `npm i` in the command line under your repo folder.
|
||||
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
|
||||
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
|
||||
- Reload Obsidian to load the new version of your plugin.
|
||||
- Enable plugin in settings window.
|
||||
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
|
||||
|
||||
## Releasing new releases
|
||||
|
||||
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
|
||||
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
|
||||
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
|
||||
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
|
||||
- Publish the release.
|
||||
|
||||
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
|
||||
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
|
||||
|
||||
## Adding your plugin to the community plugin list
|
||||
|
||||
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
|
||||
- Publish an initial version.
|
||||
- Make sure you have a `README.md` file in the root of your repo.
|
||||
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
|
||||
|
||||
## How to use
|
||||
|
||||
- Clone this repo.
|
||||
- Make sure your NodeJS is at least v16 (`node --version`).
|
||||
- `npm i` or `yarn` to install dependencies.
|
||||
- `npm run dev` to start compilation in watch mode.
|
||||
|
||||
## Manually installing the plugin
|
||||
|
||||
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
|
||||
## Improve code quality with eslint (optional)
|
||||
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
|
||||
- To use eslint with this project, make sure to install eslint from terminal:
|
||||
- `npm install -g eslint`
|
||||
- To use eslint to analyze this project use this command:
|
||||
- `eslint main.ts`
|
||||
- eslint will then create a report with suggestions for code improvement by file and line number.
|
||||
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
|
||||
- `eslint .\src\`
|
||||
|
||||
## Funding URL
|
||||
|
||||
You can include funding URLs where people who use your plugin can financially support it.
|
||||
|
||||
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": "https://buymeacoffee.com"
|
||||
}
|
||||
```md
|
||||
#### Made
|
||||
\`\`\`dataview
|
||||
TABLE without ID
|
||||
file.link AS "Name"
|
||||
FROM "References/Media Thoughts"
|
||||
WHERE contains(creator, this.file.link)
|
||||
SORT file.link DESC
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
If you have multiple URLs, you can also do:
|
||||
Instead of having to add this to each file, I can simply add a rule to the folder "Authors" which contains the above text, and it will be automatically shown in each file.
|
||||
I can do this with as many folders as I like.
|
||||
|
||||
```json
|
||||
{
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://buymeacoffee.com",
|
||||
"GitHub Sponsor": "https://github.com/sponsors",
|
||||
"Patreon": "https://www.patreon.com/"
|
||||
}
|
||||
}
|
||||
```
|
||||
## Limitations
|
||||
|
||||
## API Documentation
|
||||
|
||||
See https://github.com/obsidianmd/obsidian-api
|
||||
Links in the markdown text work natively when in Reading mode, however they don't in Live Preview, so I've added a workaround that gets most functionality back. This means that `left click` works to open the link in the current tab, and `middle mouse` and `ctrl/cmd + left click` works to open the link in a new tab. Right click currently doesn't work.
|
||||
131
main.ts
131
main.ts
|
|
@ -1,53 +1,55 @@
|
|||
import { App, Plugin, PluginSettingTab, Setting, MarkdownView, MarkdownRenderer } from 'obsidian';
|
||||
import { App, Plugin, PluginSettingTab, Setting, MarkdownView, MarkdownRenderer, MarkdownPreviewView } from 'obsidian';
|
||||
|
||||
interface DynamicFooterSettings {
|
||||
interface VirtualFooterSettings {
|
||||
rules: { folderPath: string; footerText: string }[];
|
||||
}
|
||||
const DEFAULT_SETTINGS: DynamicFooterSettings = {
|
||||
const DEFAULT_SETTINGS: VirtualFooterSettings = {
|
||||
rules: [{ folderPath: '', footerText: '' }]
|
||||
}
|
||||
|
||||
export default class DynamicFooterPlugin extends Plugin {
|
||||
settings: DynamicFooterSettings;
|
||||
export default class VirtualFooterPlugin extends Plugin {
|
||||
settings: VirtualFooterSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new DynamicFooterSettingTab(this.app, this));
|
||||
|
||||
// Register event to handle file open
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('file-open', async (file) => {
|
||||
if (!file) return;
|
||||
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!view) return;
|
||||
|
||||
// Handle both reading and editing modes
|
||||
if (view.getMode() === 'preview') {
|
||||
this.injectFooterToPreview(view);
|
||||
} else {
|
||||
this.injectFooterToEditor(view);
|
||||
this.addSettingTab(new VirtualFooterSettingTab(this.app, this));
|
||||
|
||||
//Take care of the footer when the view changes
|
||||
const handleView = async (view: MarkdownView | null) => {
|
||||
if (!view?.file) return;
|
||||
const state = view.getState();
|
||||
|
||||
if (state.mode === 'preview') {
|
||||
await this.injectFooterToPreview(view);
|
||||
} else if (state.mode === 'source' && !state.source) {
|
||||
// Remove the padding from the content area to append the footer
|
||||
const cmContent = view.containerEl.querySelector('.cm-editor .cm-content') as HTMLDivElement;
|
||||
if (cmContent) {
|
||||
cmContent.classList.add('virtual-footer-cm-padding');
|
||||
}
|
||||
})
|
||||
await this.injectFooterToEditor(view);
|
||||
} else {
|
||||
// Remove the custom styling from the content area
|
||||
const cmContent = view.containerEl.querySelector('.cm-editor .cm-content') as HTMLDivElement;
|
||||
if (cmContent) {
|
||||
cmContent.classList.remove('virtual-footer-cm-padding');
|
||||
}
|
||||
await this.removeFooter(view);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the view when a file is opened
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('file-open', () =>
|
||||
handleView(this.app.workspace.getActiveViewOfType(MarkdownView))
|
||||
)
|
||||
);
|
||||
|
||||
// Register event for layout change (switching between preview/edit modes)
|
||||
|
||||
// Handle the view when a file layout changes
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('layout-change', () => {
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!view) return;
|
||||
|
||||
const file = view.file;
|
||||
if (!file) return;
|
||||
|
||||
if (view.getMode() === 'preview') {
|
||||
this.injectFooterToPreview(view);
|
||||
} else {
|
||||
this.injectFooterToEditor(view);
|
||||
}
|
||||
})
|
||||
this.app.workspace.on('layout-change', () =>
|
||||
handleView(this.app.workspace.getActiveViewOfType(MarkdownView))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +58,7 @@ export default class DynamicFooterPlugin extends Plugin {
|
|||
if (!container) return;
|
||||
|
||||
// Remove existing footer if any
|
||||
const existingFooter = container.querySelector('.dynamic-footer');
|
||||
const existingFooter = container.querySelector('.virtual-footer');
|
||||
if (existingFooter) existingFooter.remove();
|
||||
|
||||
// Determine the appropriate footer text based on the file path
|
||||
|
|
@ -64,7 +66,7 @@ export default class DynamicFooterPlugin extends Plugin {
|
|||
|
||||
// Create and inject new footer as a widget below the editor
|
||||
const footerDiv = document.createElement('div');
|
||||
footerDiv.className = 'dynamic-footer';
|
||||
footerDiv.className = 'virtual-footer';
|
||||
|
||||
await MarkdownRenderer.render(
|
||||
this.app,
|
||||
|
|
@ -78,11 +80,11 @@ export default class DynamicFooterPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private async injectFooterToEditor(view: MarkdownView) {
|
||||
const cmEditor = view.containerEl.querySelector('.cm-editor');
|
||||
const cmEditor = view.containerEl.querySelector('.cm-sizer');
|
||||
if (!cmEditor) return;
|
||||
|
||||
// Remove existing footer if any
|
||||
const existingFooter = cmEditor.querySelector('.dynamic-footer');
|
||||
const existingFooter = cmEditor.querySelector('.virtual-footer');
|
||||
if (existingFooter) existingFooter.remove();
|
||||
|
||||
// Determine the appropriate footer text based on the file path
|
||||
|
|
@ -90,8 +92,8 @@ export default class DynamicFooterPlugin extends Plugin {
|
|||
|
||||
// Create and inject new footer as a widget below the editor
|
||||
const footerDiv = document.createElement('div');
|
||||
footerDiv.className = 'dynamic-footer';
|
||||
//footerDiv.style.marginInline = 'var(--content-margin)';
|
||||
footerDiv.className = 'virtual-footer';
|
||||
footerDiv.style.minHeight = '528px';
|
||||
|
||||
await MarkdownRenderer.render(
|
||||
this.app,
|
||||
|
|
@ -102,12 +104,39 @@ export default class DynamicFooterPlugin extends Plugin {
|
|||
);
|
||||
|
||||
// Get the content container and append the footer at the bottom
|
||||
const content = cmEditor.querySelector('.cm-sizer');
|
||||
if (content) {
|
||||
content.appendChild(footerDiv);
|
||||
}
|
||||
cmEditor.appendChild(footerDiv);
|
||||
|
||||
// Re-register all internal link click behaviors manually
|
||||
this.attachInternalLinkHandlers(footerDiv, view.file?.path || '');
|
||||
}
|
||||
|
||||
// Manually attach internal link handlers to the footer since they don't work natively, this is a workaround for now
|
||||
private attachInternalLinkHandlers(container: HTMLElement, sourcePath: string) {
|
||||
container.querySelectorAll('a.internal-link').forEach(link => {
|
||||
const handleClick = (event: MouseEvent, forceNewLeaf = false) => {
|
||||
event.preventDefault();
|
||||
const href = link.getAttribute('href');
|
||||
const target = href && this.app.metadataCache.getFirstLinkpathDest(href, sourcePath);
|
||||
if (target) {
|
||||
this.app.workspace.getLeaf(forceNewLeaf || event.ctrlKey || event.metaKey)
|
||||
.openFile(target);
|
||||
}
|
||||
};
|
||||
|
||||
link.addEventListener('click', handleClick);
|
||||
link.addEventListener('auxclick', (e: MouseEvent) => e.button === 1 && handleClick(e, true));
|
||||
});
|
||||
}
|
||||
|
||||
private async removeFooter(view: MarkdownView) {
|
||||
const cmEditor = view.containerEl.querySelector('.cm-sizer');
|
||||
if (!cmEditor) return;
|
||||
|
||||
const selectors = ['.cm-sizer', '.mod-footer'].map(s => view.containerEl.querySelector(s));
|
||||
selectors.forEach(el => el?.querySelector('.virtual-footer')?.remove());
|
||||
}
|
||||
|
||||
// Get the footer text for a given file path based on the rules
|
||||
private getFooterTextForFile(filePath: string): string {
|
||||
for (const rule of this.settings.rules) {
|
||||
if (filePath.startsWith(rule.folderPath)) {
|
||||
|
|
@ -126,10 +155,10 @@ export default class DynamicFooterPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
class DynamicFooterSettingTab extends PluginSettingTab {
|
||||
plugin: DynamicFooterPlugin;
|
||||
class VirtualFooterSettingTab extends PluginSettingTab {
|
||||
plugin: VirtualFooterPlugin;
|
||||
|
||||
constructor(app: App, plugin: DynamicFooterPlugin) {
|
||||
constructor(app: App, plugin: VirtualFooterPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "dynamic-footer",
|
||||
"name": "Dynamic Footer",
|
||||
"id": "virtual-footer",
|
||||
"name": "Virtual Footer",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Add a dynamic footer to your notes",
|
||||
"description": "Display markdown text (including dataview code) to the bottom of all notes in a folder, without modifying the notes.",
|
||||
"author": "Signynt",
|
||||
"authorUrl": "https://obsidian.md",
|
||||
"fundingUrl": "https://obsidian.md/pricing",
|
||||
"authorUrl": "https://github.com/Signynt",
|
||||
"fundingUrl": "",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"name": "virtual-footer",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-sample-plugin",
|
||||
"name": "virtual-footer",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"name": "virtual-footer",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"description": "Display markdown text (including dataview code) to the bottom of all notes in a folder, without modifying the notes.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
|||
15
styles.css
15
styles.css
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
.markdown-preview-view.is-readable-line-width .markdown-preview-sizer>div, .markdown-source-view.mod-cm6.is-readable-line-width .cm-contentContainer.cm-contentContainer>.cm-content>div, .markdown-source-view.mod-cm6.is-readable-line-width .cm-sizer>.virtual-footer, .markdown-source-view.mod-cm6.is-readable-line-width .cm-sizer>.inline-title, .markdown-source-view.mod-cm6.is-readable-line-width .cm-sizer>.metadata-container {
|
||||
max-width: var(--max-width);
|
||||
width: var(--line-width);
|
||||
margin-inline: var(--content-margin) !important;
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
.virtual-footer-cm-padding {
|
||||
padding-bottom: var(--p-spacing) !important;
|
||||
}
|
||||
Loading…
Reference in a new issue