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