Initial commit: TODO Everywhere Logseq plugin
- Синхронизация задач из PPDB - Синхронизация issues из Gitea - Автоматическая настройка config.edn - Slash-команды и кнопка в тулбаре - Конвертация приоритетов и статусов
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
102
README.md
Normal file
102
README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# TODO Everywhere
|
||||||
|
|
||||||
|
Logseq плагин для синхронизации задач из внешних источников.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- Синхронизация задач из PPDB (PortProton Database)
|
||||||
|
- Синхронизация issues из Gitea репозиториев
|
||||||
|
- Автоматическая настройка config.edn при установке
|
||||||
|
- Конвертация приоритетов и статусов в формат Logseq
|
||||||
|
- Периодическая синхронизация (опционально)
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
1. Склонируйте репозиторий или скачайте релиз
|
||||||
|
2. Установите зависимости и соберите плагин:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
3. В Logseq: `Settings → Plugins → Load unpacked plugin`
|
||||||
|
4. Выберите папку с плагином
|
||||||
|
|
||||||
|
## Настройка
|
||||||
|
|
||||||
|
### PPDB
|
||||||
|
|
||||||
|
1. Получите API ключ на сервере PPDB
|
||||||
|
2. В настройках плагина включите "PPDB - Включить синхронизацию"
|
||||||
|
3. Укажите URL сервера и API ключ
|
||||||
|
|
||||||
|
### Gitea
|
||||||
|
|
||||||
|
1. Создайте токен доступа в Gitea (`Settings → Applications → Generate Token`)
|
||||||
|
2. В настройках плагина включите "Gitea - Включить синхронизацию"
|
||||||
|
3. Добавьте источники в формате JSON:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "MyProject",
|
||||||
|
"url": "https://gitea.example.com",
|
||||||
|
"token": "your-token-here",
|
||||||
|
"owner": "username",
|
||||||
|
"repo": "repository",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Slash-команды
|
||||||
|
|
||||||
|
- `/sync-tasks` — синхронизировать все включенные источники
|
||||||
|
- `/sync-ppdb` — синхронизировать только PPDB
|
||||||
|
- `/sync-gitea` — синхронизировать только Gitea
|
||||||
|
|
||||||
|
### Кнопка в тулбаре
|
||||||
|
|
||||||
|
Нажмите на кнопку синхронизации в верхней панели Logseq.
|
||||||
|
|
||||||
|
## Структура задач
|
||||||
|
|
||||||
|
Плагин создаёт отдельные страницы для каждого источника:
|
||||||
|
|
||||||
|
- `PPDB - TODO` — задачи из PPDB
|
||||||
|
- `Gitea - {name} - TODO` — issues из Gitea
|
||||||
|
|
||||||
|
Задачи автоматически появляются в журнале в соответствующих секциях (PPDB, Gitea).
|
||||||
|
|
||||||
|
## Конвертация статусов
|
||||||
|
|
||||||
|
| Источник | Logseq |
|
||||||
|
|----------|--------|
|
||||||
|
| new | TODO |
|
||||||
|
| in_progress | DOING |
|
||||||
|
| completed | DONE |
|
||||||
|
| rejected | DONE |
|
||||||
|
| open (Gitea) | TODO |
|
||||||
|
|
||||||
|
## Конвертация приоритетов
|
||||||
|
|
||||||
|
| PPDB/Gitea | Logseq |
|
||||||
|
|------------|--------|
|
||||||
|
| critical | [#A] |
|
||||||
|
| high | [#A] |
|
||||||
|
| normal | [#B] |
|
||||||
|
| low | [#C] |
|
||||||
|
|
||||||
|
## Разработка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Режим разработки с автопересборкой
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Сборка для продакшена
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT
|
||||||
6
icon.svg
Normal file
6
icon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<circle cx="50" cy="50" r="45" fill="#4f46e5" />
|
||||||
|
<path d="M30 50 L45 65 L70 35" stroke="white" stroke-width="8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="75" cy="25" r="12" fill="#22c55e" />
|
||||||
|
<path d="M70 25 L75 30 L82 20" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 424 B |
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "logseq-todo-everywhere",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Sync tasks from external sources (PPDB, Gitea) to Logseq",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "npx vite build --watch",
|
||||||
|
"build": "npx vite build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"logseq",
|
||||||
|
"plugin",
|
||||||
|
"todo",
|
||||||
|
"tasks",
|
||||||
|
"gitea",
|
||||||
|
"sync"
|
||||||
|
],
|
||||||
|
"author": "xpamych",
|
||||||
|
"license": "MIT",
|
||||||
|
"logseq": {
|
||||||
|
"id": "logseq-todo-everywhere",
|
||||||
|
"title": "TODO Everywhere",
|
||||||
|
"icon": "./icon.svg"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@logseq/libs": "^0.0.17",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.10",
|
||||||
|
"vite-plugin-logseq": "^1.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
650
src/index.ts
Normal file
650
src/index.ts
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
import '@logseq/libs'
|
||||||
|
import { SettingSchemaDesc } from '@logseq/libs/dist/LSPlugin'
|
||||||
|
|
||||||
|
// Шаблоны для config.edn
|
||||||
|
const PPDB_QUERY = `{:title "PPDB"
|
||||||
|
:query [:find (pull ?h [:db/id :block/uuid :block/content :block/marker :block/priority :block/created-at :block/updated-at {:block/page [:block/journal-day]}])
|
||||||
|
:where
|
||||||
|
[?p :block/original-name "PPDB - TODO"]
|
||||||
|
[?h :block/marker ?marker]
|
||||||
|
[(contains? #{"NOW" "DOING" "TODO"} ?marker)]
|
||||||
|
[?h :block/page ?p]
|
||||||
|
[?p :block/journal? false]]
|
||||||
|
:result-transform (fn [result]
|
||||||
|
(sort-by (fn [h]
|
||||||
|
[(get h :block/priority "Z")
|
||||||
|
(or (get h :block/created-at) 999999999999999)]) result))
|
||||||
|
:collapsed? true}`
|
||||||
|
|
||||||
|
const GITEA_QUERY = `{:title "Gitea"
|
||||||
|
:query [:find (pull ?h [:db/id :block/uuid :block/content :block/marker :block/priority :block/created-at :block/updated-at {:block/page [:block/journal-day :block/original-name]}])
|
||||||
|
:where
|
||||||
|
[?p :block/original-name ?name]
|
||||||
|
[(clojure.string/starts-with? ?name "Gitea -")]
|
||||||
|
[?h :block/marker ?marker]
|
||||||
|
[(contains? #{"NOW" "DOING" "TODO"} ?marker)]
|
||||||
|
[?h :block/page ?p]
|
||||||
|
[?p :block/journal? false]]
|
||||||
|
:result-transform (fn [result]
|
||||||
|
(sort-by (fn [h]
|
||||||
|
[(get h :block/priority "Z")
|
||||||
|
(or (get h :block/created-at) 999999999999999)]) result))
|
||||||
|
:collapsed? true}`
|
||||||
|
|
||||||
|
// Интерфейсы
|
||||||
|
interface PPDBTask {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
description: string | null
|
||||||
|
url: string | null
|
||||||
|
category: string
|
||||||
|
priority: string
|
||||||
|
task_type: string | null
|
||||||
|
status: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string | null
|
||||||
|
completed_at: string | null
|
||||||
|
admin_comment: string | null
|
||||||
|
author_username: string
|
||||||
|
assigned_to_username: string | null
|
||||||
|
logseq_priority: string
|
||||||
|
logseq_status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GiteaIssue {
|
||||||
|
id: number
|
||||||
|
number: number
|
||||||
|
title: string
|
||||||
|
body: string
|
||||||
|
state: string
|
||||||
|
labels: { name: string; color: string }[]
|
||||||
|
milestone: { title: string } | null
|
||||||
|
assignee: { login: string } | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
html_url: string
|
||||||
|
repository: {
|
||||||
|
full_name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GiteaSource {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
token: string
|
||||||
|
owner: string
|
||||||
|
repo: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройки плагина
|
||||||
|
const settingsSchema: SettingSchemaDesc[] = [
|
||||||
|
{
|
||||||
|
key: 'ppdbEnabled',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
title: 'PPDB - Включить синхронизацию',
|
||||||
|
description: 'Синхронизировать задачи из PPDB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ppdbUrl',
|
||||||
|
type: 'string',
|
||||||
|
default: 'https://ppdb.linux-gaming.ru',
|
||||||
|
title: 'PPDB - URL сервера',
|
||||||
|
description: 'Базовый URL PPDB API'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ppdbApiKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
title: 'PPDB - API ключ',
|
||||||
|
description: 'Ключ для доступа к API'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ppdbPageName',
|
||||||
|
type: 'string',
|
||||||
|
default: 'PPDB - TODO',
|
||||||
|
title: 'PPDB - Имя страницы',
|
||||||
|
description: 'Название страницы для задач PPDB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ppdbCategories',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
title: 'PPDB - Категории (через запятую)',
|
||||||
|
description: 'Фильтр по категориям: ppdb, linux-gaming, portproton, portprotonqt. Пусто = все'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'giteaEnabled',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
title: 'Gitea - Включить синхронизацию',
|
||||||
|
description: 'Синхронизировать issues из Gitea'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'giteaSources',
|
||||||
|
type: 'string',
|
||||||
|
default: '[]',
|
||||||
|
title: 'Gitea - Источники (JSON)',
|
||||||
|
description: 'JSON массив источников: [{"name":"MyRepo","url":"https://gitea.example.com","token":"xxx","owner":"user","repo":"repo","enabled":true}]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'syncOnStartup',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
title: 'Синхронизация при запуске',
|
||||||
|
description: 'Автоматически синхронизировать при запуске Logseq'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'syncInterval',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
title: 'Интервал синхронизации (минуты)',
|
||||||
|
description: '0 = только вручную'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'configVersion',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
title: 'Версия конфигурации (не изменять)',
|
||||||
|
description: 'Внутренняя переменная для отслеживания настройки config.edn'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const CURRENT_CONFIG_VERSION = 1
|
||||||
|
|
||||||
|
// Настройка config.edn
|
||||||
|
async function setupConfig(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const graph = await logseq.App.getCurrentGraph()
|
||||||
|
if (!graph?.path) {
|
||||||
|
console.error('Cannot get graph path')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPath = `${graph.path}/logseq/config.edn`
|
||||||
|
|
||||||
|
// Читаем текущий конфиг
|
||||||
|
const configContent = await (window as any).logseq.FileStorage.getItem(configPath)
|
||||||
|
if (!configContent) {
|
||||||
|
console.error('Cannot read config.edn')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let modified = false
|
||||||
|
let newContent = configContent
|
||||||
|
|
||||||
|
// Проверяем и добавляем PPDB query
|
||||||
|
if (!configContent.includes(':title "PPDB"')) {
|
||||||
|
const insertPos = findQueryInsertPosition(newContent)
|
||||||
|
if (insertPos > 0) {
|
||||||
|
newContent = newContent.slice(0, insertPos) + '\n ' + PPDB_QUERY + newContent.slice(insertPos)
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем и добавляем Gitea query
|
||||||
|
if (!newContent.includes(':title "Gitea"')) {
|
||||||
|
const insertPos = findQueryInsertPosition(newContent)
|
||||||
|
if (insertPos > 0) {
|
||||||
|
newContent = newContent.slice(0, insertPos) + '\n ' + GITEA_QUERY + newContent.slice(insertPos)
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем исключения в существующих queries
|
||||||
|
// Добавляем PPDB - TODO в исключения
|
||||||
|
if (!newContent.includes('"PPDB - TODO"')) {
|
||||||
|
// Ищем паттерн (not [(contains? #{"...", "..."} ?name)]) и добавляем PPDB - TODO
|
||||||
|
newContent = newContent.replace(
|
||||||
|
/\(not \[\(contains\? #\{([^}]*)"РЕД СОФТ - TODO"([^}]*)\} \?name\)\]\)/g,
|
||||||
|
'(not [(contains? #{$1"РЕД СОФТ - TODO" "PPDB - TODO"$2} ?name)])'
|
||||||
|
)
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем исключение для Gitea страниц
|
||||||
|
if (!newContent.includes('clojure.string/starts-with? ?name "Gitea -"')) {
|
||||||
|
// После (not [(contains? ... добавляем проверку на Gitea
|
||||||
|
newContent = newContent.replace(
|
||||||
|
/(\(not \[\(contains\? #\{[^}]*"PPDB - TODO"[^}]*\} \?name\)\]\))(\s*)(\[?\?h :block\/marker)/g,
|
||||||
|
'$1\n (not [(clojure.string/starts-with? ?name "Gitea -")])$2$3'
|
||||||
|
)
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем PPDB - TODO в favorites
|
||||||
|
if (!newContent.includes('"PPDB - TODO"')) {
|
||||||
|
newContent = newContent.replace(
|
||||||
|
/:favorites \[([^\]]*)"РЕД СОФТ - TODO"([^\]]*)\]/,
|
||||||
|
':favorites [$1"РЕД СОФТ - TODO" "PPDB - TODO"$2]'
|
||||||
|
)
|
||||||
|
modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified) {
|
||||||
|
await (window as any).logseq.FileStorage.setItem(configPath, newContent)
|
||||||
|
console.log('config.edn updated successfully')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting up config:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находит позицию для вставки нового query (перед последним query в :journals)
|
||||||
|
function findQueryInsertPosition(content: string): number {
|
||||||
|
// Ищем паттерн {:title "Остальные дела" или последний query перед закрывающим ]}
|
||||||
|
const patterns = [
|
||||||
|
/:title "Остальные дела"/,
|
||||||
|
/:title "Остальные"/,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = content.match(pattern)
|
||||||
|
if (match && match.index) {
|
||||||
|
// Возвращаем позицию перед найденным query
|
||||||
|
// Ищем начало блока { перед :title
|
||||||
|
let pos = match.index
|
||||||
|
while (pos > 0 && content[pos] !== '{') {
|
||||||
|
pos--
|
||||||
|
}
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли, ищем конец journals array
|
||||||
|
const journalsEnd = content.search(/\]\s*\}\s*;;\s*Add custom commands/)
|
||||||
|
if (journalsEnd > 0) {
|
||||||
|
return journalsEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Главная функция
|
||||||
|
async function main() {
|
||||||
|
logseq.useSettingsSchema(settingsSchema)
|
||||||
|
|
||||||
|
// Проверяем нужно ли настроить config.edn
|
||||||
|
const configVersion = logseq.settings?.configVersion as number || 0
|
||||||
|
if (configVersion < CURRENT_CONFIG_VERSION) {
|
||||||
|
logseq.UI.showMsg('TODO Everywhere: настройка config.edn...', 'info')
|
||||||
|
const success = await setupConfig()
|
||||||
|
if (success) {
|
||||||
|
logseq.updateSettings({ configVersion: CURRENT_CONFIG_VERSION })
|
||||||
|
logseq.UI.showMsg('config.edn обновлён. Перезапустите Logseq для применения изменений.', 'success')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регистрируем команды
|
||||||
|
logseq.Editor.registerSlashCommand('sync-tasks', async () => {
|
||||||
|
await syncAllTasks()
|
||||||
|
})
|
||||||
|
|
||||||
|
logseq.Editor.registerSlashCommand('sync-ppdb', async () => {
|
||||||
|
await syncPPDBTasks()
|
||||||
|
})
|
||||||
|
|
||||||
|
logseq.Editor.registerSlashCommand('sync-gitea', async () => {
|
||||||
|
await syncGiteaTasks()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Кнопка в тулбаре
|
||||||
|
logseq.App.registerUIItem('toolbar', {
|
||||||
|
key: 'todo-everywhere-sync',
|
||||||
|
template: `
|
||||||
|
<a class="button" data-on-click="syncAll" title="Sync External Tasks">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
logseq.provideModel({
|
||||||
|
async syncAll() {
|
||||||
|
await syncAllTasks()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Синхронизация при запуске
|
||||||
|
if (logseq.settings?.syncOnStartup) {
|
||||||
|
setTimeout(() => syncAllTasks(), 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Периодическая синхронизация
|
||||||
|
const interval = logseq.settings?.syncInterval as number
|
||||||
|
if (interval > 0) {
|
||||||
|
setInterval(() => syncAllTasks(), interval * 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('TODO Everywhere plugin loaded')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Синхронизация всех источников
|
||||||
|
async function syncAllTasks() {
|
||||||
|
logseq.UI.showMsg('Синхронизация задач...', 'info')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results: string[] = []
|
||||||
|
|
||||||
|
if (logseq.settings?.ppdbEnabled) {
|
||||||
|
const ppdbResult = await syncPPDBTasks()
|
||||||
|
results.push(ppdbResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logseq.settings?.giteaEnabled) {
|
||||||
|
const giteaResult = await syncGiteaTasks()
|
||||||
|
results.push(giteaResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
logseq.UI.showMsg('Нет включенных источников', 'warning')
|
||||||
|
} else {
|
||||||
|
logseq.UI.showMsg(results.join('\n'), 'success')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Sync error:', error)
|
||||||
|
logseq.UI.showMsg(`Ошибка синхронизации: ${error}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Синхронизация PPDB
|
||||||
|
async function syncPPDBTasks(): Promise<string> {
|
||||||
|
const url = logseq.settings?.ppdbUrl as string
|
||||||
|
const apiKey = logseq.settings?.ppdbApiKey as string
|
||||||
|
const pageName = logseq.settings?.ppdbPageName as string || 'PPDB - TODO'
|
||||||
|
const categories = (logseq.settings?.ppdbCategories as string || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||||
|
|
||||||
|
if (!url || !apiKey) {
|
||||||
|
throw new Error('PPDB: не настроены URL или API ключ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем задачи
|
||||||
|
let apiUrl = `${url}/api/logseq/tasks?api_key=${apiKey}`
|
||||||
|
if (categories.length === 1) {
|
||||||
|
apiUrl += `&category=${categories[0]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`PPDB API error: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
let tasks: PPDBTask[] = data.tasks
|
||||||
|
|
||||||
|
// Фильтруем по категориям если указано несколько
|
||||||
|
if (categories.length > 1) {
|
||||||
|
tasks = tasks.filter(t => categories.includes(t.category))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем контент страницы
|
||||||
|
const content = generatePPDBPageContent(tasks)
|
||||||
|
|
||||||
|
// Записываем в страницу
|
||||||
|
await writeToPage(pageName, content)
|
||||||
|
|
||||||
|
return `PPDB: ${tasks.length} задач`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Синхронизация Gitea
|
||||||
|
async function syncGiteaTasks(): Promise<string> {
|
||||||
|
const sourcesJson = logseq.settings?.giteaSources as string || '[]'
|
||||||
|
let sources: GiteaSource[]
|
||||||
|
|
||||||
|
try {
|
||||||
|
sources = JSON.parse(sourcesJson)
|
||||||
|
} catch {
|
||||||
|
throw new Error('Gitea: неверный формат JSON источников')
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledSources = sources.filter(s => s.enabled)
|
||||||
|
if (enabledSources.length === 0) {
|
||||||
|
return 'Gitea: нет активных источников'
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: string[] = []
|
||||||
|
|
||||||
|
for (const source of enabledSources) {
|
||||||
|
try {
|
||||||
|
const issues = await fetchGiteaIssues(source)
|
||||||
|
const pageName = `Gitea - ${source.name} - TODO`
|
||||||
|
const content = generateGiteaPageContent(issues, source)
|
||||||
|
await writeToPage(pageName, content)
|
||||||
|
results.push(`${source.name}: ${issues.length}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Gitea ${source.name} error:`, error)
|
||||||
|
results.push(`${source.name}: ошибка`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Gitea: ${results.join(', ')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение issues из Gitea
|
||||||
|
async function fetchGiteaIssues(source: GiteaSource): Promise<GiteaIssue[]> {
|
||||||
|
const url = `${source.url}/api/v1/repos/${source.owner}/${source.repo}/issues?state=open&type=issues`
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${source.token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Gitea API error: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерация контента для PPDB страницы
|
||||||
|
function generatePPDBPageContent(tasks: PPDBTask[]): string {
|
||||||
|
const lines: string[] = []
|
||||||
|
lines.push(`source:: PPDB`)
|
||||||
|
lines.push(`synced:: ${new Date().toISOString()}`)
|
||||||
|
lines.push('')
|
||||||
|
|
||||||
|
// Группируем по статусам
|
||||||
|
const byStatus: Record<string, PPDBTask[]> = {
|
||||||
|
'TODO': [],
|
||||||
|
'DOING': [],
|
||||||
|
'DONE': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
const status = task.logseq_status
|
||||||
|
if (byStatus[status]) {
|
||||||
|
byStatus[status].push(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем по приоритету внутри каждого статуса
|
||||||
|
const priorityOrder: Record<string, number> = { 'A': 0, 'B': 1, 'C': 2 }
|
||||||
|
for (const status of Object.keys(byStatus)) {
|
||||||
|
byStatus[status].sort((a, b) =>
|
||||||
|
(priorityOrder[a.logseq_priority] || 1) - (priorityOrder[b.logseq_priority] || 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем TODO блоки
|
||||||
|
for (const task of [...byStatus['DOING'], ...byStatus['TODO'], ...byStatus['DONE']]) {
|
||||||
|
const priorityStr = task.logseq_priority ? `[#${task.logseq_priority}] ` : ''
|
||||||
|
const typeTag = task.task_type ? `#${task.task_type} ` : ''
|
||||||
|
const categoryTag = `#${task.category} `
|
||||||
|
|
||||||
|
let line = `- ${task.logseq_status} ${priorityStr}${typeTag}${categoryTag}${task.title}`
|
||||||
|
|
||||||
|
// Добавляем свойства
|
||||||
|
lines.push(line)
|
||||||
|
lines.push(` id:: ppdb-${task.id}`)
|
||||||
|
|
||||||
|
if (task.url) {
|
||||||
|
lines.push(` url:: ${task.url}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.description) {
|
||||||
|
// Описание как вложенный блок
|
||||||
|
const descLines = task.description.split('\n').filter(l => l.trim())
|
||||||
|
for (const descLine of descLines) {
|
||||||
|
lines.push(` - ${descLine}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.admin_comment) {
|
||||||
|
lines.push(` - **Комментарий:** ${task.admin_comment}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.assigned_to_username) {
|
||||||
|
lines.push(` assignee:: ${task.assigned_to_username}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` author:: ${task.author_username}`)
|
||||||
|
lines.push(` created:: ${task.created_at.split('T')[0]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерация контента для Gitea страницы
|
||||||
|
function generateGiteaPageContent(issues: GiteaIssue[], source: GiteaSource): string {
|
||||||
|
const lines: string[] = []
|
||||||
|
lines.push(`source:: Gitea`)
|
||||||
|
lines.push(`repo:: ${source.owner}/${source.repo}`)
|
||||||
|
lines.push(`synced:: ${new Date().toISOString()}`)
|
||||||
|
lines.push('')
|
||||||
|
|
||||||
|
// Определяем приоритет по меткам
|
||||||
|
const getPriority = (issue: GiteaIssue): string => {
|
||||||
|
const labels = issue.labels.map(l => l.name.toLowerCase())
|
||||||
|
if (labels.some(l => l.includes('critical') || l.includes('urgent') || l.includes('p0'))) return 'A'
|
||||||
|
if (labels.some(l => l.includes('high') || l.includes('important') || l.includes('p1'))) return 'A'
|
||||||
|
if (labels.some(l => l.includes('low') || l.includes('minor') || l.includes('p3'))) return 'C'
|
||||||
|
return 'B'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем тип по меткам
|
||||||
|
const getType = (issue: GiteaIssue): string => {
|
||||||
|
const labels = issue.labels.map(l => l.name.toLowerCase())
|
||||||
|
if (labels.some(l => l.includes('bug') || l.includes('fix'))) return 'bug'
|
||||||
|
if (labels.some(l => l.includes('feature') || l.includes('enhancement'))) return 'feature'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем по приоритету
|
||||||
|
const priorityOrder: Record<string, number> = { 'A': 0, 'B': 1, 'C': 2 }
|
||||||
|
issues.sort((a, b) =>
|
||||||
|
(priorityOrder[getPriority(a)] || 1) - (priorityOrder[getPriority(b)] || 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const issue of issues) {
|
||||||
|
const priority = getPriority(issue)
|
||||||
|
const type = getType(issue)
|
||||||
|
const typeTag = type ? `#${type} ` : ''
|
||||||
|
const labelTags = issue.labels.map(l => `#${l.name.replace(/\s+/g, '-')}`).join(' ')
|
||||||
|
|
||||||
|
let line = `- TODO [#${priority}] ${typeTag}#${issue.number} ${issue.title}`
|
||||||
|
if (labelTags) {
|
||||||
|
line += ` ${labelTags}`
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(line)
|
||||||
|
lines.push(` id:: gitea-${source.name}-${issue.number}`)
|
||||||
|
lines.push(` url:: ${issue.html_url}`)
|
||||||
|
|
||||||
|
if (issue.assignee) {
|
||||||
|
lines.push(` assignee:: ${issue.assignee.login}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issue.milestone) {
|
||||||
|
lines.push(` milestone:: ${issue.milestone.title}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` created:: ${issue.created_at.split('T')[0]}`)
|
||||||
|
|
||||||
|
// Тело issue как вложенный блок
|
||||||
|
if (issue.body) {
|
||||||
|
const bodyLines = issue.body.split('\n').filter(l => l.trim()).slice(0, 5)
|
||||||
|
for (const bodyLine of bodyLines) {
|
||||||
|
lines.push(` - ${bodyLine.substring(0, 200)}`)
|
||||||
|
}
|
||||||
|
if (issue.body.split('\n').length > 5) {
|
||||||
|
lines.push(` - ...`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запись контента в страницу
|
||||||
|
async function writeToPage(pageName: string, content: string) {
|
||||||
|
// Получаем или создаём страницу
|
||||||
|
let page = await logseq.Editor.getPage(pageName)
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
page = await logseq.Editor.createPage(pageName, {}, { redirect: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
throw new Error(`Не удалось создать страницу ${pageName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем блоки страницы
|
||||||
|
const blocks = await logseq.Editor.getPageBlocksTree(pageName)
|
||||||
|
|
||||||
|
// Удаляем все существующие блоки
|
||||||
|
for (const block of blocks) {
|
||||||
|
await logseq.Editor.removeBlock(block.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вставляем новый контент
|
||||||
|
const contentLines = content.split('\n')
|
||||||
|
|
||||||
|
// Первый блок
|
||||||
|
if (contentLines.length > 0) {
|
||||||
|
const firstBlock = await logseq.Editor.insertBlock(page.uuid, contentLines[0], { sibling: false })
|
||||||
|
|
||||||
|
if (firstBlock) {
|
||||||
|
// Остальные блоки добавляем как siblings
|
||||||
|
let prevBlockUuid = firstBlock.uuid
|
||||||
|
|
||||||
|
for (let i = 1; i < contentLines.length; i++) {
|
||||||
|
const line = contentLines[i]
|
||||||
|
|
||||||
|
// Пропускаем пустые строки
|
||||||
|
if (!line.trim()) continue
|
||||||
|
|
||||||
|
// Определяем уровень вложенности
|
||||||
|
const indent = line.search(/\S/)
|
||||||
|
|
||||||
|
if (indent <= 0 || line.startsWith('-')) {
|
||||||
|
// Верхний уровень
|
||||||
|
const newBlock = await logseq.Editor.insertBlock(prevBlockUuid, line.replace(/^-\s*/, ''), { sibling: true })
|
||||||
|
if (newBlock) {
|
||||||
|
prevBlockUuid = newBlock.uuid
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Вложенный блок (свойства или контент)
|
||||||
|
const cleanLine = line.trim()
|
||||||
|
if (cleanLine.includes('::')) {
|
||||||
|
// Это свойство - добавляем к предыдущему блоку
|
||||||
|
await logseq.Editor.upsertBlockProperty(prevBlockUuid, cleanLine.split('::')[0].trim(), cleanLine.split('::')[1].trim())
|
||||||
|
} else if (cleanLine.startsWith('-')) {
|
||||||
|
// Вложенный контент
|
||||||
|
await logseq.Editor.insertBlock(prevBlockUuid, cleanLine.replace(/^-\s*/, ''), { sibling: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск плагина
|
||||||
|
logseq.ready(main).catch(console.error)
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import logseqPlugin from 'vite-plugin-logseq'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [logseqPlugin()],
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
minify: 'esbuild'
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user