diff --git a/README.md b/README.md
index c773152..a107f46 100644
--- a/README.md
+++ b/README.md
@@ -1,94 +1,125 @@
-# Obsidian Sample Plugin
+# Obsidian Clipper Catalog
-This is a sample plugin for Obsidian (https://obsidian.md).
+An Obsidian plugin that provides a powerful catalog interface for all your clipped web articles and content. Easily organize, search, and manage your web clippings within your Obsidian vault.
-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.
+## Features
-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.
+🔍 Catalog all your clipped articles in one view
+📱 Fully responsive design that works on desktop and mobile
+🏷️ Tag-based organization and filtering
+🔄 Real-time search and sorting capabilities
+⚡ Command palette integration
+🎨 Clean, modern UI with Obsidian theme integration
+📂 Advanced directory filtering options
-## First time developing plugins?
+## Installation
-Quick starting guide for new plugin devs:
+### From Obsidian Community Plugins
+1. Open Obsidian Settings
+2. Go to Community Plugins and turn off Safe Mode
+3. Click Browse and search for "Clipper Catalog"
+4. Install the plugin and enable it
-- 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.
+### Manual Installation
+1. Download the latest release from GitHub
+2. Extract the zip archive into your vault's `.obsidian/plugins` directory
+3. Enable the plugin in Obsidian's settings
-## Releasing new releases
+## Configuration
-- 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.
+1. Open the plugin settings in Obsidian
+2. Configure the property name that identifies clipped content (default: 'source')
+3. The catalog will display all notes that contain the specified property with a URL value
-> 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`
+## Usage
-## Adding your plugin to the community plugin list
+1. Open the Clipper Catalog view using:
+ - Command palette: Search for "Open Clipper Catalog"
+ - Or click the Clipper Catalog icon in the sidebar
+2. Browse, search, and filter your clipped articles
+3. Use advanced search options to exclude specific directories
+4. Click on article titles to open them in your vault
+5. Click on tags to filter by specific tags
+6. Sort by date, title, or path
-- 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.
+## Features
-## How to use
+### Search and Filter
+- Full-text search across titles and tags
+- Tag-based filtering
+- Directory exclusion options
+- Real-time updates
-- 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.
+### Organization
+- Sort by multiple columns
+- Advanced directory filtering
+- Tag management
+- Path-based organization
-## Manually installing the plugin
+### Interface
+- Clean, modern design
+- Responsive layout
+- Theme-aware styling
+- Keyboard navigation
-- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
+## Support
-## 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\`
+- For bugs or feature requests, please [open an issue](https://github.com/soundslikeinfo/obsidian-clipper-catalog/issues)
-## Funding URL
+## Privacy
-You can include funding URLs where people who use your plugin can financially support it.
+This plugin only reads and organizes notes within your local Obsidian vault. No data is sent to external servers.
-The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
+## Contributing
-```json
-{
- "fundingUrl": "https://buymeacoffee.com"
-}
+1. Fork the repository
+2. Create a feature branch
+3. Commit your changes
+4. Push to the branch
+5. Create a Pull Request
+
+## License
+This project is licensed under the MIT License - see the LICENSE file for details.
+
+## Frequently Asked Questions
+
+### How does the plugin find my clipped articles?
+The plugin looks for notes in your vault that contain a specified property (default: 'source') with a URL value in the frontmatter. This can be changed in the plugin's settings.
+
+### Does it work on mobile?
+Yes! The plugin is fully compatible with both desktop and mobile versions of Obsidian.
+
+### Can I exclude certain folders from the catalog?
+Yes! Use the advanced search options to exclude specific directories from your catalog view.
+
+## Support
+
+- For bugs or feature requests, please [open an issue](https://github.com/soundslikeinfo/obsidian-clipper-catalog/issues)
+
+## Privacy
+
+This plugin sends article content to OpenAI's API for processing. Please review OpenAI's privacy policy and ensure you're comfortable with their data handling practices.
+
+## Contributing
+
+1. Fork the repository
+2. Create a feature branch
+3. Commit your changes
+4. Push to the branch
+5. Create a Pull Request
+
+### 🧠 Crafted with AI & Human Creativity
+```
+🎨 Design & Development
+Greg K. (@soundslikeinfo)
+
+🤖 AI Pair Programming
+Claude 3.5 Sonnet by Anthropic
```
-If you have multiple URLs, you can also do:
+### 💝 Support the Project
-```json
-{
- "fundingUrl": {
- "Buy Me a Coffee": "https://buymeacoffee.com",
- "GitHub Sponsor": "https://github.com/sponsors",
- "Patreon": "https://www.patreon.com/"
- }
-}
-```
+[](https://github.com/soundslikeinfo/obsidian-clipper-catalog)
+[](https://www.buymeacoffee.com/soundslikeinfo)
-## API Documentation
-
-See https://github.com/obsidianmd/obsidian-api
+Made with ❤️ for the Obsidian Community
diff --git a/esbuild.config.mjs b/esbuild.config.mjs
index a5de8b8..7c0e9f1 100644
--- a/esbuild.config.mjs
+++ b/esbuild.config.mjs
@@ -1,6 +1,8 @@
import esbuild from "esbuild";
import process from "process";
-import builtins from "builtin-modules";
+import { builtinModules } from 'module';
+import fs from 'fs';
+import path from 'path';
const banner =
`/*
@@ -11,39 +13,64 @@ if you want to view the source, please visit the github repository of this plugi
const prod = (process.argv[2] === "production");
+// Ensure the src/styles.css exists with Tailwind imports
+const initStylesFile = () => {
+ const stylesDir = path.join(process.cwd(), 'src');
+ const stylesFile = path.join(stylesDir, 'styles.css');
+
+ if (!fs.existsSync(stylesDir)) {
+ fs.mkdirSync(stylesDir, { recursive: true });
+ }
+
+ if (!fs.existsSync(stylesFile)) {
+ const tailwindImports = `
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Your custom styles here */
+ `.trim();
+
+ fs.writeFileSync(stylesFile, tailwindImports);
+ }
+};
+
+initStylesFile();
+
const context = await esbuild.context({
- banner: {
- js: banner,
- },
- entryPoints: ["main.ts"],
- bundle: true,
- external: [
- "obsidian",
- "electron",
- "@codemirror/autocomplete",
- "@codemirror/collab",
- "@codemirror/commands",
- "@codemirror/language",
- "@codemirror/lint",
- "@codemirror/search",
- "@codemirror/state",
- "@codemirror/view",
- "@lezer/common",
- "@lezer/highlight",
- "@lezer/lr",
- ...builtins],
- format: "cjs",
- target: "es2018",
- logLevel: "info",
- sourcemap: prod ? false : "inline",
- treeShaking: true,
- outfile: "main.js",
- minify: prod,
+ banner: {
+ js: banner,
+ },
+ entryPoints: ["src/main.tsx"],
+ bundle: true,
+ external: [
+ "obsidian",
+ "electron",
+ "@codemirror/autocomplete",
+ "@codemirror/collab",
+ "@codemirror/commands",
+ "@codemirror/language",
+ "@codemirror/lint",
+ "@codemirror/search",
+ "@codemirror/state",
+ "@codemirror/view",
+ "@lezer/common",
+ "@lezer/highlight",
+ "@lezer/lr",
+ ...builtinModules,
+ ],
+ format: "cjs",
+ target: "es2018",
+ logLevel: "info",
+ sourcemap: prod ? false : "inline",
+ treeShaking: true,
+ outfile: "main.js",
+ jsx: "automatic",
});
if (prod) {
- await context.rebuild();
- process.exit(0);
+ await context.rebuild();
+ context.dispose();
} else {
- await context.watch();
-}
+ await context.watch();
+}
\ No newline at end of file
diff --git a/main.ts b/main.ts
deleted file mode 100644
index 2d07212..0000000
--- a/main.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
-
-// Remember to rename these classes and interfaces!
-
-interface MyPluginSettings {
- mySetting: string;
-}
-
-const DEFAULT_SETTINGS: MyPluginSettings = {
- mySetting: 'default'
-}
-
-export default class MyPlugin extends Plugin {
- settings: MyPluginSettings;
-
- async onload() {
- await this.loadSettings();
-
- // This creates an icon in the left ribbon.
- const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
- // Called when the user clicks the icon.
- new Notice('This is a notice!');
- });
- // Perform additional things with the ribbon
- ribbonIconEl.addClass('my-plugin-ribbon-class');
-
- // This adds a status bar item to the bottom of the app. Does not work on mobile apps.
- const statusBarItemEl = this.addStatusBarItem();
- statusBarItemEl.setText('Status Bar Text');
-
- // This adds a simple command that can be triggered anywhere
- this.addCommand({
- id: 'open-sample-modal-simple',
- name: 'Open sample modal (simple)',
- callback: () => {
- new SampleModal(this.app).open();
- }
- });
- // This adds an editor command that can perform some operation on the current editor instance
- this.addCommand({
- id: 'sample-editor-command',
- name: 'Sample editor command',
- editorCallback: (editor: Editor, view: MarkdownView) => {
- console.log(editor.getSelection());
- editor.replaceSelection('Sample Editor Command');
- }
- });
- // This adds a complex command that can check whether the current state of the app allows execution of the command
- this.addCommand({
- id: 'open-sample-modal-complex',
- name: 'Open sample modal (complex)',
- checkCallback: (checking: boolean) => {
- // Conditions to check
- const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
- if (markdownView) {
- // If checking is true, we're simply "checking" if the command can be run.
- // If checking is false, then we want to actually perform the operation.
- if (!checking) {
- new SampleModal(this.app).open();
- }
-
- // This command will only show up in Command Palette when the check function returns true
- return true;
- }
- }
- });
-
- // This adds a settings tab so the user can configure various aspects of the plugin
- this.addSettingTab(new SampleSettingTab(this.app, this));
-
- // If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
- // Using this function will automatically remove the event listener when this plugin is disabled.
- this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
- console.log('click', evt);
- });
-
- // When registering intervals, this function will automatically clear the interval when the plugin is disabled.
- this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
- }
-
- onunload() {
-
- }
-
- async loadSettings() {
- this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
- }
-
- async saveSettings() {
- await this.saveData(this.settings);
- }
-}
-
-class SampleModal extends Modal {
- constructor(app: App) {
- super(app);
- }
-
- onOpen() {
- const {contentEl} = this;
- contentEl.setText('Woah!');
- }
-
- onClose() {
- const {contentEl} = this;
- contentEl.empty();
- }
-}
-
-class SampleSettingTab extends PluginSettingTab {
- plugin: MyPlugin;
-
- constructor(app: App, plugin: MyPlugin) {
- super(app, plugin);
- this.plugin = plugin;
- }
-
- display(): void {
- const {containerEl} = this;
-
- containerEl.empty();
-
- new Setting(containerEl)
- .setName('Setting #1')
- .setDesc('It\'s a secret')
- .addText(text => text
- .setPlaceholder('Enter your secret')
- .setValue(this.plugin.settings.mySetting)
- .onChange(async (value) => {
- this.plugin.settings.mySetting = value;
- await this.plugin.saveSettings();
- }));
- }
-}
diff --git a/manifest.json b/manifest.json
index dfa940e..fd275a6 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,11 +1,13 @@
{
- "id": "sample-plugin",
- "name": "Sample Plugin",
- "version": "1.0.0",
+ "id": "Clipper Catalog",
+ "name": "Clipper Catalog",
+ "version": "0.10.1",
"minAppVersion": "0.15.0",
- "description": "Demonstrates some of the capabilities of the Obsidian API.",
+ "description": "This provides any Obsidian vault with a catalog of all the clippings gathered with a common source property.",
"author": "Obsidian",
- "authorUrl": "https://obsidian.md",
- "fundingUrl": "https://obsidian.md/pricing",
+ "authorUrl": "https://github.com/soundslikeinfo",
+ "fundingUrl": {
+ "Buy Me a Coffee": "https://www.buymeacoffee.com/soundslikeinfo"
+ },
"isDesktopOnly": false
}
diff --git a/package.json b/package.json
index 6a00766..b52e405 100644
--- a/package.json
+++ b/package.json
@@ -1,24 +1,37 @@
{
- "name": "obsidian-sample-plugin",
+ "name": "obsidian-clipper-catalog",
"version": "1.0.0",
- "description": "This is a sample plugin for Obsidian (https://obsidian.md)",
+ "description": "This provides any Obsidian vault with a catalog of all the clippings gathered with a common source property.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
- "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
+ "build": "tailwindcss -i ./src/styles.css -o ./styles.css --minify && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
+ "css:dev": "tailwindcss -i ./src/styles.css -o ./styles.css --watch",
"version": "node version-bump.mjs && git add manifest.json versions.json"
- },
+ },
"keywords": [],
"author": "",
"license": "MIT",
+ "bugs": {
+ "url": "https://github.com/soundslikeinfo/obsidian-clipper-catalog/issues"
+ },
+ "homepage": "https://github.com/soundslikeinfo/obsidian-clipper-catalog#readme",
"devDependencies": {
"@types/node": "^16.11.6",
- "@typescript-eslint/eslint-plugin": "5.29.0",
- "@typescript-eslint/parser": "5.29.0",
- "builtin-modules": "3.3.0",
+ "@types/react": "^18.0.27",
+ "@types/react-dom": "^18.0.10",
+ "autoprefixer": "^10.4.20",
"esbuild": "0.17.3",
+ "lucide-react": "^0.454.0",
"obsidian": "latest",
+ "postcss": "^8.4.47",
+ "tailwindcss": "^3.4.14",
"tslib": "2.4.0",
"typescript": "4.7.4"
- }
+ },
+ "dependencies": {
+ "openai": "^3.2.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..56dcb48
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+ }
+
\ No newline at end of file
diff --git a/src/ClipperCatalog.tsx b/src/ClipperCatalog.tsx
new file mode 100644
index 0000000..20d2b41
--- /dev/null
+++ b/src/ClipperCatalog.tsx
@@ -0,0 +1,660 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { Link, Search, RefreshCw, ChevronDown, ChevronRight, X } from 'lucide-react';
+import { TFile, App } from 'obsidian';
+import type ObsidianClipperCatalog from './main';
+
+interface ClipperCatalogProps {
+ app: App;
+ plugin: ObsidianClipperCatalog;
+}
+
+interface Article {
+ title: string;
+ url: string;
+ path: string;
+ date: number;
+ tags: string[];
+ basename: string;
+ content: string;
+}
+
+interface SortConfig {
+ key: keyof Article;
+ direction: 'asc' | 'desc';
+}
+
+interface AdvancedSettings {
+ ignoredDirectories: string[];
+ isExpanded: boolean;
+}
+
+const ArticleTitle = ({ file, content, title }: { file: TFile, content: string, title: string }) => {
+ const isUntitled = /^Untitled( \d+)?$/.test(file.basename);
+ const headerMatch = content.match(/^#+ (.+)$/m);
+
+ if (isUntitled && headerMatch) {
+ return (
+
+ Note: This catalog shows any markdown files containing a URL in their frontmatter under the property: "{plugin.settings.sourcePropertyName}".
+ You can change this property name in plugin settings to match your preferred clipping workflow.
+