Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
801fdec71e | ||
|
|
e5f1061db7 | ||
|
|
23cf742569 |
@@ -0,0 +1,42 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Verify version
|
||||
run: |
|
||||
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
||||
MANIFEST_VERSION=$(node -p "require('./manifest.json').version")
|
||||
if [ "$TAG_VERSION" != "$MANIFEST_VERSION" ]; then
|
||||
echo "❌ Tag ($TAG_VERSION) != manifest ($MANIFEST_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ github.ref_name }}
|
||||
files: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -2,35 +2,71 @@
|
||||
|
||||
Модульный плагин для **Obsidian** с набором QoL-функций, которые можно включать и отключать в настройках.
|
||||
|
||||
## Установка
|
||||
Modular **Obsidian** plugin with toggleable QoL modules in settings.
|
||||
|
||||
## Установка / Installation
|
||||
|
||||
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**
|
||||
|
||||
## Модули
|
||||
## Язык / Language
|
||||
|
||||
| Модуль | Описание |
|
||||
|--------|----------|
|
||||
| **Attachment Clean Paste** | Заменяет `![[вложение]]` на `[[вложение|имя]]` при вставке/перетаскивании файлов с указанными расширениями |
|
||||
Язык плагина синхронизируется с **языком интерфейса Obsidian** (Settings → About → Language).
|
||||
|
||||
- Obsidian на русском → плагин на русском
|
||||
- Obsidian на другом языке → плагин на английском
|
||||
|
||||
The plugin language syncs with **Obsidian's UI language** setting.
|
||||
|
||||
- Obsidian in English → plugin in English
|
||||
- Obsidian in any other language → falls back to English
|
||||
|
||||
## Модули / Modules
|
||||
|
||||
- **Attachment Clean Paste** — Заменяет `![[embed]]` на `[[path/file|name]]` для файлов с указанными расширениями при вставке/перетаскивании
|
||||
|
||||
Каждый модуль включается/отключается в настройках плагина.
|
||||
|
||||
## Разработка
|
||||
## Разработка / Development
|
||||
|
||||
### Добавление нового модуля
|
||||
### Архитектура / Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.ts # точка входа, Plugin class
|
||||
├── settings.ts # общая панель настроек
|
||||
├── types.ts # базовые типы
|
||||
├── locales/
|
||||
│ ├── en.ts # тип Locale + EN-словарь
|
||||
│ ├── ru.ts # RU-словарь
|
||||
│ └── index.ts # авто-определение языка
|
||||
└── modules/
|
||||
├── index.ts # реестр модулей (статический импорт)
|
||||
├── base-module.ts # абстрактный класс модуля
|
||||
└── attachment-clean-paste/
|
||||
└── index.ts # реализация модуля
|
||||
```
|
||||
|
||||
**Принцип работы:**
|
||||
- `main.ts` загружает настройки, итерирует `MODULES` и вызывает `onload()` для включённых
|
||||
- Каждый модуль через `registerCleanup()` регистрирует функции очистки, которые вызываются при выключении модуля или выгрузке плагина
|
||||
- В настройках отображаются все модули с чекбоксами вкл/выкл
|
||||
|
||||
### Добавление нового модуля / Adding a module
|
||||
|
||||
1. Создайте папку `src/modules/my-module/`
|
||||
2. Создайте класс, унаследованный от `BaseModule`:
|
||||
|
||||
```typescript
|
||||
import { BaseModule } from '../base-module';
|
||||
import { locale } from '../../locales';
|
||||
|
||||
export class MyModule extends BaseModule {
|
||||
id = 'my-module';
|
||||
name = 'My Module';
|
||||
description = 'Описание модуля';
|
||||
name = locale.modules['my-module'].name;
|
||||
description = locale.modules['my-module'].description;
|
||||
|
||||
get defaultSettings() {
|
||||
return { /* настройки по умолчанию */ };
|
||||
@@ -46,7 +82,8 @@ export class MyModule extends BaseModule {
|
||||
}
|
||||
```
|
||||
|
||||
3. Зарегистрируйте модуль в `src/modules/index.ts`:
|
||||
3. Добавьте переводы в `src/locales/en.ts` и `src/locales/ru.ts`
|
||||
4. Зарегистрируйте модуль в `src/modules/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { MyModule } from './my-module';
|
||||
@@ -57,34 +94,22 @@ export const MODULES: QoLModule[] = [
|
||||
];
|
||||
```
|
||||
|
||||
4. Если модулю нужна своя вкладка настроек — создайте `PluginSettingTab` и добавьте её через `plugin.addSettingTab()` в `onload`
|
||||
### Добавление нового языка / Adding a language
|
||||
|
||||
### Сборка
|
||||
1. Создайте `src/locales/de.ts` со структурой `Locale`
|
||||
2. Импортируйте и добавьте в словарь в `src/locales/index.ts`:
|
||||
```typescript
|
||||
import { de } from './de';
|
||||
const locales: Record<string, Locale> = { en, ru, de };
|
||||
```
|
||||
|
||||
### Сборка / Build
|
||||
|
||||
```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()` регистрирует функции отчистки, которые вызываются при выключении модуля или выгрузке плагина
|
||||
- В настройках отображаются все модули с чекбоксами вкл/выкл
|
||||
|
||||
## Лицензия
|
||||
## Лицензия / License
|
||||
|
||||
MIT
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"id": "zeroq-qol-modules",
|
||||
"name": "ZeroQ QoL Modules",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Набор QoL-модулей для Obsidian: clean paste, авто-линковка и другое",
|
||||
"author": "Custom",
|
||||
"description": "QoL Modules",
|
||||
"author": "oqyude",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ru } from './ru';
|
||||
const locales: Record<string, Locale> = { en, ru };
|
||||
|
||||
function detectLocale(): Locale {
|
||||
const lang = (navigator.language || 'en').slice(0, 2);
|
||||
const lang = (document.documentElement.lang || 'en').slice(0, 2);
|
||||
return locales[lang] || en;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
MarkdownFileInfo,
|
||||
MarkdownView,
|
||||
Notice,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
normalizePath,
|
||||
} from 'obsidian';
|
||||
@@ -33,25 +32,45 @@ export class AttachmentCleanPasteModule extends BaseModule {
|
||||
|
||||
onload(plugin: ZeroQoLModulesPlugin, moduleSettings: Record<string, unknown>): void {
|
||||
this.app = plugin.app;
|
||||
const settings = moduleSettings as unknown as AttachmentCleanPasteSettings;
|
||||
|
||||
plugin.addSettingTab(
|
||||
new AttachmentCleanPasteSettingTab(plugin.app, plugin, MODULE_ID),
|
||||
);
|
||||
const rawSettings = moduleSettings as AttachmentCleanPasteSettings;
|
||||
|
||||
const pasteRef = plugin.app.workspace.on(
|
||||
'editor-paste',
|
||||
this.createPasteHandler(settings),
|
||||
this.createPasteHandler(rawSettings),
|
||||
);
|
||||
this.registerCleanup(() => plugin.app.workspace.offref(pasteRef));
|
||||
|
||||
const dropRef = plugin.app.workspace.on(
|
||||
'editor-drop',
|
||||
this.createDropHandler(settings),
|
||||
this.createDropHandler(rawSettings),
|
||||
);
|
||||
this.registerCleanup(() => plugin.app.workspace.offref(dropRef));
|
||||
}
|
||||
|
||||
renderInlineSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: Record<string, unknown>,
|
||||
saveSettings: () => Promise<void>,
|
||||
): void {
|
||||
const loc = locale.modules['attachment-clean-paste'];
|
||||
const extSettings = settings as unknown as AttachmentCleanPasteSettings;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(loc.extensionsLabel)
|
||||
.setDesc(loc.extensionsDesc)
|
||||
.addTextArea((textarea) => {
|
||||
textarea
|
||||
.setPlaceholder(loc.extensionsPlaceholder)
|
||||
.setValue(extSettings?.extensions ?? '')
|
||||
.onChange(async (value) => {
|
||||
settings.extensions = value;
|
||||
await saveSettings();
|
||||
});
|
||||
textarea.inputEl.rows = 4;
|
||||
textarea.inputEl.style.width = '100%';
|
||||
});
|
||||
}
|
||||
|
||||
private createPasteHandler(settings: AttachmentCleanPasteSettings) {
|
||||
return async (
|
||||
evt: ClipboardEvent,
|
||||
@@ -155,50 +174,3 @@ export class AttachmentCleanPasteModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
const loc = locale.modules['attachment-clean-paste'];
|
||||
|
||||
containerEl.createEl('h2', { text: loc.name });
|
||||
containerEl.createEl('p', {
|
||||
text: loc.descriptionShort,
|
||||
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(loc.extensionsLabel)
|
||||
.setDesc(loc.extensionsDesc)
|
||||
.addTextArea((textarea) => {
|
||||
textarea
|
||||
.setPlaceholder(loc.extensionsPlaceholder)
|
||||
.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%';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ export abstract class BaseModule implements QoLModule {
|
||||
this.cleanupFns = [];
|
||||
}
|
||||
|
||||
renderInlineSettings(
|
||||
_containerEl: HTMLElement,
|
||||
_settings: Record<string, unknown>,
|
||||
_saveSettings: () => Promise<void>,
|
||||
): void {}
|
||||
|
||||
protected registerCleanup(fn: () => void): void {
|
||||
this.cleanupFns.push(fn);
|
||||
}
|
||||
|
||||
+10
-2
@@ -45,14 +45,14 @@ export class ZeroQSettingTab extends PluginSettingTab {
|
||||
});
|
||||
|
||||
for (const module of MODULES) {
|
||||
const moduleSettings = this.plugin.settings.modules[module.id];
|
||||
const moduleCfg = this.plugin.settings.modules[module.id];
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(module.name)
|
||||
.setDesc(module.description)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(moduleSettings?.enabled ?? true)
|
||||
.setValue(moduleCfg?.enabled ?? true)
|
||||
.onChange(async (value) => {
|
||||
if (!this.plugin.settings.modules[module.id]) {
|
||||
this.plugin.settings.modules[module.id] = {
|
||||
@@ -70,6 +70,14 @@ export class ZeroQSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (moduleCfg) {
|
||||
module.renderInlineSettings(
|
||||
containerEl,
|
||||
moduleCfg.settings,
|
||||
() => this.plugin.saveSettings(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,9 @@ export interface QoLModule {
|
||||
defaultSettings: Record<string, unknown>;
|
||||
onload(plugin: Plugin, moduleSettings: Record<string, unknown>): void;
|
||||
onunload(plugin: Plugin): void;
|
||||
renderInlineSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: Record<string, unknown>,
|
||||
saveSettings: () => Promise<void>,
|
||||
): void;
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
Reference in New Issue
Block a user