diff --git a/.gitignore b/.gitignore index 2309cc8..52b52f3 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,9 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +# Obsidian plugin build output +main.js + # yarn v2 .yarn/cache .yarn/unplugged diff --git a/README.md b/README.md index 45131d7..1bb6697 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,90 @@ -# zeroq-qol-modules +# ZeroQ QoL Modules +Модульный плагин для **Obsidian** с набором QoL-функций, которые можно включать и отключать в настройках. + +## Установка + +1. Скачайте `main.js`, `manifest.json` и `styles.css` из [релизов](https://github.com/your/repo/releases) +2. Поместите их в папку `.obsidian/plugins/zeroq-qol-modules/` вашего хранилища +3. В Obsidian: **Настройки** → **Community plugins** → **Включить** +4. Активируйте плагин **ZeroQ QoL Modules** + +## Модули + +| Модуль | Описание | +|--------|----------| +| **Attachment Clean Paste** | Заменяет `![[вложение]]` на `[[вложение|имя]]` при вставке/перетаскивании файлов с указанными расширениями | + +Каждый модуль включается/отключается в настройках плагина. + +## Разработка + +### Добавление нового модуля + +1. Создайте папку `src/modules/my-module/` +2. Создайте класс, унаследованный от `BaseModule`: + +```typescript +import { BaseModule } from '../base-module'; + +export class MyModule extends BaseModule { + id = 'my-module'; + name = 'My Module'; + description = 'Описание модуля'; + + get defaultSettings() { + return { /* настройки по умолчанию */ }; + } + + onload(plugin, moduleSettings) { + // инициализация: регистрация событий, команд, настроек + } + + onunload(plugin) { + // очистка (cleanup происходит автоматически через registerCleanup) + } +} +``` + +3. Зарегистрируйте модуль в `src/modules/index.ts`: + +```typescript +import { MyModule } from './my-module'; + +export const MODULES: QoLModule[] = [ + new AttachmentCleanPasteModule(), + new MyModule(), +]; +``` + +4. Если модулю нужна своя вкладка настроек — создайте `PluginSettingTab` и добавьте её через `plugin.addSettingTab()` в `onload` + +### Сборка + +```bash +npm run build # production-сборка +npm run dev # dev-сборка с sourcemap +``` + +### Архитектура + +``` +src/ +├── main.ts # точка входа, Plugin class +├── settings.ts # общая панель настроек +├── types.ts # базовые типы +└── modules/ + ├── index.ts # реестр модулей (статический импорт) + ├── base-module.ts # абстрактный класс модуля + └── attachment-clean-paste/ + └── index.ts # реализация модуля +``` + +**Принцип работы:** +- `main.ts` загружает настройки, итерирует `MODULES` и вызывает `onload()` для включённых +- Каждый модуль через `registerCleanup()` регистрирует функции отчистки, которые вызываются при выключении модуля или выгрузке плагина +- В настройках отображаются все модули с чекбоксами вкл/выкл + +## Лицензия + +MIT diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..0ca0338 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,40 @@ +import esbuild from 'esbuild'; +import process from 'process'; +import builtins from 'builtin-modules'; + +const banner = `/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +*/ +`; + +const prod = process.argv[2] === 'production'; + +esbuild + .build({ + banner: { js: banner }, + entryPoints: ['src/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', + }) + .catch(() => process.exit(1)); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..4fb083f --- /dev/null +++ b/manifest.json @@ -0,0 +1,9 @@ +{ + "id": "zeroq-qol-modules", + "name": "ZeroQ QoL Modules", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "Набор QoL-модулей для Obsidian: clean paste, авто-линковка и другое", + "author": "Custom", + "isDesktopOnly": false +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..12ca266 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,575 @@ +{ + "name": "zeroq-qol-modules", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zeroq-qol-modules", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "builtin-modules": "^3.3.0", + "esbuild": "0.17.19", + "obsidian": "latest", + "tslib": "^2.6.2", + "typescript": "^5.3.3" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/obsidian": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.1.tgz", + "integrity": "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a86aaf2 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "zeroq-qol-modules", + "version": "1.0.0", + "description": "Modular Obsidian plugin with toggleable QoL modules", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "node esbuild.config.mjs production" + }, + "keywords": ["obsidian", "plugin"], + "author": "", + "license": "MIT", + "devDependencies": { + "builtin-modules": "^3.3.0", + "esbuild": "0.17.19", + "obsidian": "latest", + "tslib": "^2.6.2", + "typescript": "^5.3.3" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e4f1b2b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,64 @@ +import { Plugin } from 'obsidian'; +import { ZeroQSettings, QoLModule } from './types'; +import { MODULES } from './modules'; +import { DEFAULT_SETTINGS, ZeroQSettingTab, getModuleSettings } from './settings'; + +export default class ZeroQoLModulesPlugin extends Plugin { + settings: ZeroQSettings; + private loadedModules: Set = new Set(); + + async onload() { + await this.loadSettings(); + + this.addSettingTab(new ZeroQSettingTab(this.app, this)); + + for (const module of MODULES) { + const moduleCfg = this.settings.modules[module.id]; + if (moduleCfg?.enabled) { + this.loadModule(module); + } + } + } + + onunload() { + for (const module of MODULES) { + this.unloadModule(module); + } + } + + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData(), + ) as ZeroQSettings; + + for (const module of MODULES) { + if (!this.settings.modules[module.id]) { + this.settings.modules[module.id] = { + enabled: true, + settings: { ...module.defaultSettings }, + }; + } + } + } + + async saveSettings() { + await this.saveData(this.settings); + } + + loadModule(module: QoLModule) { + if (this.loadedModules.has(module.id)) return; + + const moduleSettings = getModuleSettings(this.settings, module.id); + module.onload(this, moduleSettings); + this.loadedModules.add(module.id); + } + + unloadModule(module: QoLModule) { + if (!this.loadedModules.has(module.id)) return; + + module.onunload(this); + this.loadedModules.delete(module.id); + } +} diff --git a/src/modules/attachment-clean-paste/index.ts b/src/modules/attachment-clean-paste/index.ts new file mode 100644 index 0000000..dc0ce0e --- /dev/null +++ b/src/modules/attachment-clean-paste/index.ts @@ -0,0 +1,204 @@ +import { + App, + Editor, + MarkdownFileInfo, + MarkdownView, + Notice, + PluginSettingTab, + Setting, + normalizePath, +} from 'obsidian'; +import type ZeroQoLModulesPlugin from '../../main'; +import { BaseModule } from '../base-module'; + +const MODULE_ID = 'attachment-clean-paste'; + +interface AttachmentCleanPasteSettings { + extensions: string; +} + +export class AttachmentCleanPasteModule extends BaseModule { + id = MODULE_ID; + name = 'Attachment Clean Paste'; + description = + 'Заменяет ![[вложение]] на [[вложение|имя]] для указанных расширений при вставке/перетаскивании'; + + private app: App; + + get defaultSettings(): Record { + return { + extensions: 'png, jpg, jpeg, gif, svg, webp, bmp, pdf', + }; + } + + onload(plugin: ZeroQoLModulesPlugin, moduleSettings: Record): void { + this.app = plugin.app; + const settings = moduleSettings as unknown as AttachmentCleanPasteSettings; + + plugin.addSettingTab( + new AttachmentCleanPasteSettingTab(plugin.app, plugin, MODULE_ID), + ); + + const pasteRef = plugin.app.workspace.on( + 'editor-paste', + this.createPasteHandler(settings), + ); + this.registerCleanup(() => plugin.app.workspace.offref(pasteRef)); + + const dropRef = plugin.app.workspace.on( + 'editor-drop', + this.createDropHandler(settings), + ); + this.registerCleanup(() => plugin.app.workspace.offref(dropRef)); + } + + private createPasteHandler(settings: AttachmentCleanPasteSettings) { + return async ( + evt: ClipboardEvent, + editor: Editor, + _info: MarkdownView | MarkdownFileInfo, + ): Promise => { + const files = evt.clipboardData?.files; + if (!files || files.length === 0) return; + + const matchingFiles = Array.from(files).filter((f) => + this.shouldProcess(f.name, settings), + ); + if (matchingFiles.length === 0) return; + + evt.preventDefault(); + evt.stopPropagation(); + + for (const file of matchingFiles) { + await this.processFile(file, editor, settings); + } + }; + } + + private createDropHandler(settings: AttachmentCleanPasteSettings) { + return async ( + evt: DragEvent, + editor: Editor, + _info: MarkdownView | MarkdownFileInfo, + ): Promise => { + const files = evt.dataTransfer?.files; + if (!files || files.length === 0) return; + + const matchingFiles = Array.from(files).filter((f) => + this.shouldProcess(f.name, settings), + ); + if (matchingFiles.length === 0) return; + + evt.preventDefault(); + evt.stopPropagation(); + + for (const file of matchingFiles) { + await this.processFile(file, editor, settings); + } + }; + } + + private shouldProcess( + filename: string, + settings: AttachmentCleanPasteSettings, + ): boolean { + const dotIndex = filename.lastIndexOf('.'); + if (dotIndex === -1) return false; + const ext = filename.slice(dotIndex + 1).toLowerCase(); + + const exts = settings.extensions + .split(',') + .map((e) => e.trim().toLowerCase()) + .filter((e) => e.length > 0); + + return exts.some((e) => { + const cleanExt = e.startsWith('.') ? e.slice(1) : e; + return cleanExt === ext; + }); + } + + private async processFile( + file: File, + editor: Editor, + _settings: AttachmentCleanPasteSettings, + ): Promise { + try { + const arrayBuffer = await file.arrayBuffer(); + const filename = file.name; + const nameWithoutExt = filename.replace(/\.[^/.]+$/, ''); + + let attachmentFolder = this.app.vault.getConfig( + 'attachmentFolderPath', + ) as string; + if (!attachmentFolder) attachmentFolder = 'attachments'; + + const folderPath = normalizePath(attachmentFolder); + if (!(await this.app.vault.adapter.exists(folderPath))) { + await this.app.vault.createFolder(folderPath); + } + + const ext = filename.slice(filename.lastIndexOf('.') + 1); + const basePath = normalizePath(`${folderPath}/${nameWithoutExt}`); + + const availablePath = await this.app.vault.getAvailablePath( + basePath, + ext, + ); + + await this.app.vault.createBinary(availablePath, arrayBuffer); + + const link = `[[${availablePath}|${nameWithoutExt}]]`; + editor.replaceSelection(link); + } catch (e) { + new Notice(`Clean Paste error: ${e.message}`); + console.error('Attachment Clean Paste:', e); + } + } +} + +class AttachmentCleanPasteSettingTab extends PluginSettingTab { + private plugin: ZeroQoLModulesPlugin; + private moduleId: string; + + constructor(app: App, plugin: ZeroQoLModulesPlugin, moduleId: string) { + super(app, plugin); + this.plugin = plugin; + this.moduleId = moduleId; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + containerEl.createEl('h2', { text: 'Attachment Clean Paste' }); + containerEl.createEl('p', { + text: 'Заменяет ![[вложение]] на [[вложение|имя]] для указанных расширений.', + attr: { style: 'color: var(--text-muted); margin-bottom: 20px;' }, + }); + + const rawSettings = this.plugin.settings.modules[this.moduleId]?.settings as + | AttachmentCleanPasteSettings + | undefined; + + new Setting(containerEl) + .setName('Расширения файлов') + .setDesc( + 'Расширения через запятую (например: png, jpg, pdf, mp3). Файлы с этими расширениями будут вставляться как [[path/file|name]] вместо ![[path/file]].', + ) + .addTextArea((textarea) => { + textarea + .setPlaceholder('png, jpg, jpeg, gif, svg, webp, pdf') + .setValue(rawSettings?.extensions ?? '') + .onChange(async (value) => { + if (this.plugin.settings.modules[this.moduleId]) { + this.plugin.settings.modules[this.moduleId].settings = { + extensions: value, + }; + } + await this.plugin.saveSettings(); + }); + textarea.inputEl.rows = 4; + textarea.inputEl.style.width = '100%'; + }); + } +} diff --git a/src/modules/base-module.ts b/src/modules/base-module.ts new file mode 100644 index 0000000..3b2ce75 --- /dev/null +++ b/src/modules/base-module.ts @@ -0,0 +1,27 @@ +import { Plugin } from 'obsidian'; +import { QoLModule } from '../types'; + +export abstract class BaseModule implements QoLModule { + abstract id: string; + abstract name: string; + abstract description: string; + + private cleanupFns: (() => void)[] = []; + + get defaultSettings(): Record { + return {}; + } + + abstract onload(plugin: Plugin, moduleSettings: Record): void; + + onunload(_plugin: Plugin): void { + for (const fn of this.cleanupFns) { + fn(); + } + this.cleanupFns = []; + } + + protected registerCleanup(fn: () => void): void { + this.cleanupFns.push(fn); + } +} diff --git a/src/modules/index.ts b/src/modules/index.ts new file mode 100644 index 0000000..9bda70c --- /dev/null +++ b/src/modules/index.ts @@ -0,0 +1,4 @@ +import { AttachmentCleanPasteModule } from './attachment-clean-paste'; +import { QoLModule } from '../types'; + +export const MODULES: QoLModule[] = [new AttachmentCleanPasteModule()]; diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..db7092d --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,74 @@ +import { App, PluginSettingTab, Setting } from 'obsidian'; +import type ZeroQoLModulesPlugin from './main'; +import { MODULES } from './modules'; +import { ZeroQSettings } from './types'; + +const MODULE_DEFAULTS = MODULES.reduce( + (acc, mod) => { + acc[mod.id] = { + enabled: true, + settings: { ...mod.defaultSettings }, + }; + return acc; + }, + {} as ZeroQSettings['modules'], +); + +export const DEFAULT_SETTINGS: ZeroQSettings = { + modules: MODULE_DEFAULTS, +}; + +export function getModuleSettings( + settings: ZeroQSettings, + moduleId: string, +): Record { + return settings.modules[moduleId]?.settings ?? {}; +} + +export class ZeroQSettingTab extends PluginSettingTab { + plugin: ZeroQoLModulesPlugin; + + constructor(app: App, plugin: ZeroQoLModulesPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + containerEl.createEl('h1', { text: 'ZeroQ QoL Modules' }); + containerEl.createEl('p', { + text: 'Включайте и отключайте модули по своему усмотрению.', + attr: { style: 'color: var(--text-muted); margin-bottom: 20px;' }, + }); + + for (const module of MODULES) { + const moduleSettings = this.plugin.settings.modules[module.id]; + + new Setting(containerEl) + .setName(module.name) + .setDesc(module.description) + .addToggle((toggle) => + toggle + .setValue(moduleSettings?.enabled ?? true) + .onChange(async (value) => { + if (!this.plugin.settings.modules[module.id]) { + this.plugin.settings.modules[module.id] = { + enabled: value, + settings: { ...module.defaultSettings }, + }; + } else { + this.plugin.settings.modules[module.id].enabled = value; + } + await this.plugin.saveSettings(); + if (value) { + this.plugin.loadModule(module); + } else { + this.plugin.unloadModule(module); + } + }), + ); + } + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..33f2470 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,19 @@ +import { Plugin } from 'obsidian'; + +export interface ModuleSettings { + enabled: boolean; + settings: Record; +} + +export interface ZeroQSettings { + modules: Record; +} + +export interface QoLModule { + id: string; + name: string; + description: string; + defaultSettings: Record; + onload(plugin: Plugin, moduleSettings: Record): void; + onunload(plugin: Plugin): void; +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..7916e2b --- /dev/null +++ b/styles.css @@ -0,0 +1 @@ +/* ZeroQ QoL Modules - no custom styles needed */ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..363092e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "lib": ["DOM", "ES5", "ES6", "ES7"], + "allowSyntheticDefaultImports": true, + "strictNullChecks": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts"] +}