Используя Obsidian более двух лет, я привык организовывать в нём все свои заметки, в том числе и по проектам. Хоть Obsidian и предлагает широкий набор сторонних плагинов для расширения своего функционала, но мне так и не удалось найти идеальный для управления проектами и задачами. Это подтолкнуло меня к созданию нескольких автоматизаций, о которых и пойдет речь дальше.

Алгоритм работы с проектом

  1. Создаем заметку с типом проект.

    • Проект автоматически добавляется в заметку Homepage.

    • В проекте отображаются все его задачи с датами, статусами и ссылками.

  2. Создаем заметку с типом задача.

    • Автоматически создается ежедневная заметка.

    • В задаче отображается все содержание из ежедневных заметок, относящееся к этой задаче.

  3. Вносим записи в ежедневную заметку.

Требования к окружению Obsidian

Заметка Homepage

Это центральная заметка, где лежат ссылки на все существующие проекты. Заметка должна содержать заголовок третьего уровня (###), а проекты должны быть в виде списка:

заметка Homepage
заметка Homepage

В Homepage удобно хранить ссылки на центральные заметки хранилища Obsidian, которые агрегируют под собой заметки (ссылки на приложения, обучение, языки и прочее).

Шаблоны

  1. Создаем папку templates. Все шаблоны будут находиться в ней.

  2. Создаем четыре шаблона: main, daily, project и task.

Плагины для Obsidian

Чтобы установить плагины, нужно зайти в настройки Obsidian и в меню слева перейти в раздел Community plugin.

Templater

Шаблон main должен применяться ко всем вновь созданным заметкам. Это можно реализовать в настройках плагина templater. В разделе Folder templates в качестве директории нужно выбрать / (корень), а в качестве шаблона - main.md:

установка шаблона по умолчанию для заметок
установка шаблона по умолчанию для заметок

Calendar и Periodic Notes

В Periodic Notes я использую только ежедневные заметки. Тут надо задать формат даты и папку для хранения ежедневных заметок:

А для удобства создания и управления ежедневными заметками уже используется плагин Calendar.

Kanban

Доску я назвал Рабочие задачи. Я использовал следующие колонки: Backlog, To do, В работе, Тестирование, Done, Canceled и Повторяющиеся. Это важно, так как имя доски и статусы будут далее использоваться в коде автоматизаций.

Dataview

Тут надо включить поддержку JS:

CSS

Ограничимся двумя файлами стилей: wide-page.css и table-styling.css. Оба нужны, чтобы сделать заметки более читаемыми и удобными в работе.

wide-page

Растягивает страницу на всю ширину.

body .wide-page { --file-line-width: 100%; }

table-styling

Добавляет в таблицу разделители. Помогает лучше воспринимать таблицу в заметках с проектами.

.table-divider table tr {
  border-bottom: 1px solid #444;
}

Новые CSS добавляются в Obsidian в папку .obsidian\snippets (если папки нет, ее надо создать). Активировать стили надо в настройках:

Общий синтаксис

Templater

Шаблоны для плагина Templater пишутся внутри двойных фигурных скобок с процентами: <<% шаблон %>>. Но если мы хотим использовать в шаблоне JS код, то надо добавить звездочку: <<%* шаблон с JS кодом %>> .

Obsidian

Для хранения метаданных в Obsidian заметках используется YAML заголовок (front matter), который находится в блоке из тройных дефисов до и после него:

---
метаданные
---

Dataview

Для работы с JS кодом в плагине Dataview используется расширенная функция DataviewJS:

```dataviewjs
JS код
```

Обертка dataviewjs обязательна для корректной интерпретации кода, но в статье я не буду ее далее использовать, так она ломает подсветку в блоке кода. Понять, когда используется dataviewjs можно по заголовку.

Содержание шаблонов

Использование чужих шаблонов в Obsidian может быть опасным из-за рисков вредоносного кода, утечки данных. Проверяйте источник и код перед использованием.

При написании шаблонов я использовал:

Шаблон main

Это центральный шаблон.

main
<%*
try {
    // Проверяем, содержит ли каталог заметки имя periodic/daily
    const isDaily = tp.file.folder(true).includes("periodic/daily");
    // Создаем массив с возможными типами заметок
    const options = ["задача", "проект"];
    
    // Проверяем, является ли заметка ежедневной
    if (isDaily) {
        // Если это ежедневная заметка, то применяем на нее шаблон daily
        tR += await tp.file.include("[[templates/daily]]");
    } else {
        // Предлагаем выбрать тип заметки
        const chosenOption = await tp.system.suggester(options, options);

        let noteName;
        let fileExists;
        
        // Цикл для проверки имени заметки на уникальность
        do {
            // Предлагаем ввести новое имя для заметки
            noteName = await tp.system.prompt("Введите новое имя для файла:");
            
            if (noteName) {
                // Проверяем, существует ли заметка с таким именем
                fileExists = await tp.file.exists(noteName + ".md");
                
                if (fileExists) {
                    // Выводим уведомление, если заметка существует
                    new Notice("Заметка с таким именем уже существует. Пожалуйста, выберите другое имя.");
                }
            } else {
                // Выводим уведомление, если пользователь отменил ввод имени заметки
                new Notice("Переименование отменено.");
                break;
            }
        } while (fileExists);
        
        if (noteName && !fileExists) {
            // Переименовываем заметку
            await tp.file.rename(noteName);
        }
        
        if (chosenOption === "задача") {
            // Если тип заметки "задача", применяем к ней шаблон task
            tR += await tp.file.include("[[templates/task]]");
        } else if (chosenOption === "проект") {
            // Если тип заметки "проект", применяем к ней шаблон project
            tR += await tp.file.include("[[templates/project]]");
        }
    }
} catch (error) {
    console.error("Templater Error:", error);
}
%>

Пояснения к работе кода:

  • При создании ежедневной заметки, к ней будет автоматически применен шаблон daily. Во всех остальных случаях будет предложено выбрать тип создаваемой заметки.

  • Предлагается два типа заметок, проект и задача. Можно создать пустую заметку, нажав Esc. Как только выбор сделан, будет предложено поменять имя заметки (можно оставить текущее имя, нажав Esc).

  • В зависимости от выбранного типа заметки, к ней будут применен соответствующий шаблон. Дальнейшие действия зависят от шаблона.

Шаблон daily

Шаблон для ежедневных заметок.

daily
<%*
try {
    // Получаем имя текущей ежедневной заметки
    const noteName = tp.file.title;
    
    // Разбиваем полученное имя на компоненты даты
    const [day, month, year] = noteName.split('-').map(Number);

    // Создаём объект Date на основе поученных компонентов
    const currentNoteDate = new Date(year, month - 1, day);

    // Вычисляем предыдущий и следующий день
    let previousDayDate = new Date(currentNoteDate.setDate(currentNoteDate.getDate() - 1));
    let nextDayDate = new Date(currentNoteDate.setDate(currentNoteDate.getDate() + 2));

    // Форматируем дату обратно в "DD-MM-YYYY"
    const formatDate = (date) => {
        const dd = String(date.getDate()).padStart(2, '0');
        const mm = String(date.getMonth() + 1).padStart(2, '0');
        const yyyy = date.getFullYear();
        return `${dd}-${mm}-${yyyy}`;
    };

    const previousDay = formatDate(previousDayDate);
    const nextDay = formatDate(nextDayDate);

    // Формируем ссылки
    const baseFolder = tp.file.folder(true);
    const previousNotePath = `${baseFolder}/${previousDay}.md`;
    const nextNotePath = `${baseFolder}/${nextDay}.md`;

    // Выводим даты в виде ссылок
    tR += `← [[${previousNotePath}|${previousDay}]]  |  [[${nextNotePath}|${nextDay}]] →`;
} catch (error) {
    console.error("Templater Error:", error);
}
%>

Пояснения к работе кода:

  • В ежедневной заметке создается навигация: ← вчерашний день | завтрашний день →.

  • При переходе по этой навигации, в случае если ежедневнfя заметка существует, она будет открыта. Если ежедневная заметка не существует, она будет создана в директории для ежедневных заметок (periodic/daily).

Шаблон project

Шаблон для заметок с типом проект.

Шаблон состоит из двух блоков: properties и dataviewjs.

Блок properties шаблона project

project properties
---
project: <%*
try {
    // Получаем путь до заметки Homepage
    const homepageFile = await app.vault.getAbstractFileByPath('Homepage.md');
    // Читаем содержимое заметки Homepage
    const content = await app.vault.cachedRead(homepageFile);
    // Определяем название секции с проектами
    const sectionTitle = 'Проекты'; 
    // Создаём динамическое регулярное выражение для извлечения нужной секции
    const sectionRegex = new RegExp(`### ${sectionTitle}:\n([\\s\\S]*?)(?=\\n###|$)`);
    // Извлекаем содержимое секции
    const sectionMatch = sectionRegex.exec(content);
    const sectionContent = sectionMatch?.[1] || '';
    // Ищем все ссылки на проекты в квадратных скобках
    const matchesIterator = sectionContent.matchAll(/- \[\[(.*?)\]\]/g);
    // Преобразуем итератор в массив названий проектов
    const projects = Array.from(matchesIterator, m => m[1]);
    // Получаем имя текущей заметки
    const currentNoteName = app.workspace.getActiveFile()?.basename;
    
    // Проверяем, есть ли создаваемый проект в общем списке проектов
    if (projects.includes(currentNoteName)) {
        new Notice(`Проект "${currentNoteName}" уже существует. Добавление отменено.`);
    } else {
        // Добавляем новый проект в список проектов
        const newSectionContent = sectionContent.trim() + `\n- [[${currentNoteName}]]\n`;
        // Обновляем содержимое списка проектов, добавляя новый проект
        const updatedContent = content.replace(sectionRegex, `### ${sectionTitle}:\n${newSectionContent}`);
        await app.vault.modify(homepageFile, updatedContent);
        new Notice(`Проект "${currentNoteName}" добавлен в секцию "${sectionTitle}".`);
    }
    tR += currentNoteName;
} catch (error) {
    console.error("Templater Error:", error);
}
%>
cssclasses:
  - wide-page
---

В properties задаются параметры project и cssclasses. Project будет использоваться в dataviewjs блоке, для поиска нужны заметок.

Пояснения к работе кода:

  • Вычитывается содержимое заметки Homepage, для получения списка проектов по ключевому слову "Проекты".

  • Если проекта с именем заметки еще нет, то он добавляется в список проектов в Homepage.

  • Project подставляется автоматически и равен имени заметки.

Блок dataviewjs шаблона project

project dataviewjs
try {
  // Получаем имя заметки
  const filterProject = app.workspace.getActiveFile()?.basename.toLowerCase();
  const currentPath = dv.current().file.path;

  // Функция для преобразования строки в дату
  function parseDate(dateStr) {
    return moment(dateStr, 'DD-MM-YYYY').toDate();
  }

  // Функция для преобразования даты в строку
  function formatDate(date) {
    return moment(date).format('DD-MM-YYYY');
  }

  // Функция для получения иконки по статусу задачи
  function getStatusIcon(status) {
    const icons = {
      'backlog': '?️',
      'to do': '?',
      'canceled': '?',
      'в работе': '⚙️',
      'тестирование': '?',
      'done': '☑️'
    };
    return icons[status.toLowerCase()] || '❓';
  }

  // Функция для получения даты из имени ежедневной заметки
  async function getEventDatesFromDailyNotes(taskName) {
    const dailyNotes = dv.pages('"periodic/daily"').values;
    const eventDates = [];

    for (const page of dailyNotes) {
      const file = app.vault.getAbstractFileByPath(page.file.path);

      if (file?.extension === 'md') {
        const fileContent = await app.vault.cachedRead(file);
        const taskHeaderPattern = new RegExp(`###\\s*[^\\n]*\\[\\[${taskName}(#[^\\]]+)?\\]\\]`, 'i');

        if (taskHeaderPattern.test(fileContent)) {
          const dateStr = page.file.name;
          const date = parseDate(dateStr);
          if (date) {
            eventDates.push(date);
          }
        }
      }
    }
    return eventDates;
  }

  // Проверяем наличие Kanban доски
  const kanbanFile = app.vault.getAbstractFileByPath("Рабочие задачи.md");
  if (!kanbanFile) {
    dv.paragraph("Kanban доска не найдена.");
    return;
  }

  // Получаем содержимое Kanban доски
  const kanbanContent = await app.vault.cachedRead(kanbanFile);
  const taskStatusMap = {};
  let currentStatus = null;

  // Разбираем содержимого Kanban доски по строкам
  kanbanContent.split('\n').forEach(line => {
    // Ищем заголовки статусов
    const headingMatch = line.match(/^##\s+(.*)/);
    if (headingMatch) {
      // Устанавливаем текущий статус
      currentStatus = headingMatch[1].trim();
    } else if (currentStatus) {
      // Ищем ссылки на задачи
      const linkMatch = line.match(/\[\[([^\]]+)\]\]/);
      // Сопоставляем задачу со статусом
      if (linkMatch) taskStatusMap[linkMatch[1].trim()] = currentStatus;
    }
  });

  // Фильтруем страницы по проекту
  const pages = dv.pages().filter(p => p.project && p.project.toLowerCase() === filterProject && p.file.path !== currentPath);
  let data = [];

  for (let page of pages) {
    // Получаем даты событий из ежедневных заметок
    let eventDates = await getEventDatesFromDailyNotes(page.file.name);
    // Если даты нет, используем дату страницы
    if (!eventDates.length && page.date) eventDates.push(parseDate(page.date));

    // Определяем начальную дату
    let startDate = eventDates.length ? new Date(Math.min(...eventDates)) : null;
    // Определяем конечную дату
    let endDate = eventDates.length ? new Date(Math.max(...eventDates)) : null;

    const taskName = page.file.name;
    // Получаем текущий статус задачи
    const status = taskStatusMap[taskName] || "Не указано";
    // Получаем иконку статуса
    const statusIcon = getStatusIcon(status);

    // Определяем формат времени выполнения
    let executionTime;
    if (startDate && endDate && startDate.getTime() !== endDate.getTime()) {
      // Если диапазон дат
      executionTime = `${formatDate(startDate)} — ${formatDate(endDate)}`;
    } else if (startDate) {
      // Если одна дата
      executionTime = formatDate(startDate);
    } else {
      // Если даты нет
      executionTime = "Нет даты";
    }

    // Заполняем массив данными для таблицы
    data.push({
      note: page.file.link,
      instance: page.instance || "Не указано",
      status: `${status} ${statusIcon}`,
      executionTime,
      startDate
    });
  }

  // Сортируем данные по дате начала задачи
  data.sort((a, b) => (a.startDate || Infinity) - (b.startDate || Infinity));

  if (data.length) {
    // Отображаем таблицу с данными
    dv.table(
      ["Заметка", "Инстанс", "Статус", "Время выполнения"],
      data.map(d => [d.note, d.instance, d.status, d.executionTime])
    );
  } else {
    // Выводим сообщение, если данных нет
    dv.paragraph("Нет данных для отображения.");
  }
} catch (error) {
  console.error("Templater Error:", error);
}

В результате получаем таблицу со всеми задачами проекта.

Пояснения к работе кода:

  • Добавляются все заметки, содержащие мету project со значением текущего проекта.

  • Статус задачи берется из Kanban доски "Рабочие задачи".

  • Даты времени выполнения берутся из ежедневных заметок, в которых есть заголовок третьего уровня (###) и ссылка ([[ ]]) на текущую заметку (задачу).

Шаблон task

Шаблон для заметок с типом задача.

Шаблон состоит из трех блоков: properties, дополнительных шаблон и dataviewjs.

Блок properties шаблона task

task properties
---
project: <%* 
try {
    // Подключаем содержимое заметки Homepage
    const content = await tp.file.include("[[Homepage]]");
    // Определяем название секции с проектами
    const sectionTitle = 'Проекты'; 
    // Создаём динамическое регулярное выражение для извлечения нужной секции
    const sectionRegex = new RegExp(`### ${sectionTitle}:\n([\\s\\S]*?)(?=\n###|$)`);
    // Извлекаем содержимое секции
    const section = sectionRegex.exec(content)?.[1];

    if (section) {
        // Ищем все строки с квадратными скобками
        const matchesIterator = section.matchAll(/- \[\[(.*?)\]\]/g);
        // Преобразуем итератор в массив названий проектов
        const projects = Array.from(matchesIterator, m => m[1]);
        // Предлагаем выбрать проект из списка
        const selectedProject = await tp.system.suggester(projects, projects);
        // Выводим выбранный проект в заметку
        tR += `${selectedProject}`;
    } else {
        console.log("Секция не найдена.");
    }
} catch (error) {
    console.error("Templater Error:", error);
}
%>
instance: <%* 
try {
    const instanceValue = await tp.system.prompt("Введите значение для instance:");
    if (instanceValue !== null) {
        tR += instanceValue + " ";
    }
} catch (error) {
    console.error("Templater Error:", error);
}
%>
date: <% tp.date.now("YYYY-MM-DD") %>
cssclasses:
  - wide-page
---

Тут в properties, помимо project и cssclasses, задается еще instance и date. Project будет использоваться для связи заметки с проектом. Instance не влияет на прямую на работу кода. Значение будет просто выводиться в таблице проекта напротив задачи. date нужен как вспомогательный источник даты, на случай отсутствия ежедневной заметки.

Пояснения к работе кода:

  • Вычитывается содержимое заметки Homepage, для получения списка проектов по ключевому слову "Проекты".

  • Из полученного списка предлагается выбрать проект. Выбранный проект станет значением опции project в front matter.

  • Появляется предложение ввести имя инстанса (это опционально).

Блок с дополнительным шаблоном task

task дополнительный шаблон
<%*
try {
    // Формируем полный путь к сегодняшней ежедневной заметке
    const dailyNoteCatalog = 'periodic/daily';
    const currentDate = tp.date.now("DD-MM-YYYY");
    const dailyNotePath = `${dailyNoteCatalog}/${currentDate}`;
    const dailyNotePathMd = `${dailyNotePath}.md`;
    
    let dailyNoteFile;

    // Проверяем, существует ли ежедневная заметка
    const dailyNoteExists = await tp.file.exists(dailyNotePathMd);

    if (dailyNoteExists) {
        // Если существует, получаем ее полный адрес
        dailyNoteFile = app.vault.getAbstractFileByPath(dailyNotePathMd);
    } else {
        // Если не существует, создаем ее с применением шаблона daily
        dailyNoteFile = await tp.file.create_new(tp.file.find_tfile("daily"), dailyNotePath);
    }

    // Получаем имя текущей заметки
    const currentNoteName = app.workspace.getActiveFile()?.basename;
    
    // Читаем содержимое ежедневной заметки
    const dailyNoteContent = await app.vault.read(dailyNoteFile);

    // Подготавливаем заголовок для добавления в ежедневную заметку
    const headingToAdd = `### [[${currentNoteName}]]`;

    // Проверяем, есть ли уже заголовок с именем текущей заметки
    if (!dailyNoteContent.includes(headingToAdd)) {
        // Если нет, то добавляем заголовок в конец файла
        await app.vault.append(dailyNoteFile, `\n${headingToAdd}\n`);
    }

    // Проверяем, открыта ли ежедневная заметка
    let leaf = app.workspace.getLeavesOfType('markdown').find(
        (leaf) => leaf.view.file && leaf.view.file.path === dailyNoteFile.path
    );

    if (leaf) {
        // Если заметка уже открыта, переходим в нее
        app.workspace.setActiveLeaf(leaf);
    } else {
        // Если заметка не открыта, открываем ее в новой вкладке
        await app.workspace.getLeaf('tab').openFile(dailyNoteFile);
    }
} catch (error) {
    console.error("Templater Error:", error);
}
%>

Пояснения к работе кода:

  • Создается ежедневная заметка с текущей датой.

  • В ежедневную заметку добавляется ссылку на текущую заметку (задачу). Это нужно для связи с задачей.

Этот блок опциональный. Его можно не добавлять, если функционал не нужен.

Блок dataviewjs шаблона task

task dataviewjs
try {
    // Получаем имя текущей заметки
    const currentNoteName = dv.current().file.name;

    // Получаем все ежедневные заметки в виде массива
    let pages = dv.pages('"periodic/daily"').array();

    // Функция для извлечения даты из имени ежедневной заметки
    function datesFromDailyNotes(filename) {
        // Конвертируем строку формата "DD-MM-YYYY" в объект Date
        return moment(filename, 'DD-MM-YYYY').toDate();
    }

    // Сортируем ежедневные заметки по дате
    pages.sort((a, b) => datesFromDailyNotes(a.file.name) - datesFromDailyNotes(b.file.name));

    // Создаем массивы для оглавления и основного контента
    let tableOfContents = [];
    let mainContent = [];

    // Функция для подготовки заголовка в виде ссылки
    function escapeHeadingForLink(heading) {
        // Убираем из заголовка двойные квадратные скобки
        return heading.slice(2, -2);
    }

    // Проверяем, содержит ли заголовок имя текущей заметки
    function headingLinksToCurrentNote(heading, currentNoteName) {
        return heading.includes(currentNoteName);
    }

    // Проходим по каждой ежедневной заметке
    for (const page of pages) {
        // Получаем значение file.path заметки
        const file = app.vault.getAbstractFileByPath(page.file.path);

        // Получаем кэшированные метаданные файла
        const fileCache = app.metadataCache.getFileCache(file);

        // Проверяем, есть ли в полученном кэше заголовки
        if (fileCache?.headings) {
            // Если заголовки есть, то получаем их
            const headings = fileCache.headings;

            // Получаем содержимое ежедневной заметки
            const fileContent = await app.vault.cachedRead(file);

            // Проходим по каждому заголовку в ежедневной заметке
            for (let i = 0; i < headings.length; i++) {
                const heading = headings[i];

                // Если заголовок в ежедененой заметке совпадает с именем текущуей заметки
                if (headingLinksToCurrentNote(heading.heading, currentNoteName)) {
                    // Определяем начало секции с заголовком
                    const startOffset = heading.position.start.offset;
                    // По умолчанию конец секции - конец заметки
                    let endOffset = fileContent.length;

                    // Ищем конец текущей секции
                    for (let j = i + 1; j < headings.length; j++) {
                        // Если нашли заголовок третьего, второго или первого уровня, то считаем его началом следующей секции
                        if (headings[j].level <= heading.level) {
                            endOffset = headings[j].position.start.offset;
                            break;
                        }
                    }

                    // Извлекаем содержимое секции
                    const sectionContent = fileContent.substring(startOffset, endOffset).trim();
                    // Удаляем первую строку (сам заголовок) из содержимого
                    const contentWithoutHeading = sectionContent.split('\n').slice(1).join('\n').trim();
    
                    // Получаем дату из имени заметки
                    const formattedDate = page.file.name;
                    // Подготавливаем заголовок для вставки в ссылку
                    const encodedHeading = escapeHeadingForLink(heading.heading);
                    // Создаем ссылку, указывающую на секцию ежедневной заметки
                    const dateLink = `[[${page.file.path}#${encodedHeading}|${formattedDate}]]`;
    
                    // Добавляем содержимое секции в основной контент
                    mainContent.push(`**${dateLink}**\n${contentWithoutHeading}`);
                    // Добавляем ссылку на данную секцию в оглавление
                    tableOfContents.push(dateLink);
                }
            }
        }
    }

    // Если список оглавления не пустой, выводим его
    if (tableOfContents.length > 0) {
        dv.header(3, "Оглавление");
        dv.paragraph(tableOfContents.join(' -> '));
    }

    // Если основной контент не пустой, выводим его
    if (mainContent.length > 0) {
        dv.header(3, "Заметки");
        dv.paragraph(mainContent.join('\n\n'));
    }
} catch (error) {
    console.error("Templater Error:", error);
}

Тут мы получим текст всех ежедневных заметок, которые относятся к задаче.

Пояснения к работе кода:

  • Создается оглавление из ссылок на ежедневные заметки.

  • Добавляется заголовок в виде ссылки на ежедневную задачу и текст, относящийся к задаче.

Итоги

Эти шаблоны позволяют автоматизировать создание и управление проектами в Obsidian. Благодаря им, создание и организация проектов становятся более структурированными и эффективными. Благодарю всех за внимание.

Комментарии (12)


  1. AuToMaton
    21.10.2024 19:52

    Я был бы доволен и того больше если бы статья была не столько про конкретный способ управления проектами, у каждого своё мнение как и чем управлять, а про полученный опыт. Например, почему именно эти плагины - они лучшие, или безальтернативные, или что?

    Потом - про то, как это работает. Коротко. Чтобы не обращаться только к прочитавшим документацию и по Obsidian плагинам, и DataView, и Tasker. Потому что про шаблоны Tasker ещё можно строить предположения, а про «Блок dataviewjs шаблона task» совершенно не понятно и где лежит, и почему и когда выполняется, и откуда в нём берётся dv.

    И только потом - про конкретный код который неплохо было бы убрать под спойлер.

    И как бонус, если Вы в этом разбирались - на сколько страшно брать незнамо что из community plugins. Если пользовать планшет, то вроде под iPadOS не побалуешь (да?), а если десктоп?


    1. ashkaar Автор
      21.10.2024 19:52

      К варианту, который описан в статье я пришел постепенно, исходя из своих требований к удобству ведения проектов. Эти требования я описал в блоке "Алгоритм работы с проектом". Первоначально я формировал заметки с проектами и задачами вручную, используя вставки через ![[]]. Это не очень удобно и я сделал автоматизацию, которой и решил поделиться. Я понимаю что это подойдет не всем, но возможно кому-то поможет.

      Templater и Dataview это одни из самых популярных плагинов для Obsidian с миллионами скачиваний и открытым кодом (что сильно снижает риски, но полной безопасности никто не гарантирует). Возможно есть и более подходящие плагины, но эти активно используются в комьюнити Obsidian и имеют документацию. Calendar, Periodic Notes и Kanban тоже популярны. Их я использовал и ранее, поэтому использую и в автоматизациях. Про Tasker ничего сказать не могу, видимо Вы имели ввиду Templater.

      Всего шаблонов четыре: main, daily, project и task. Каждый шаблон это отдельный файл. Шаблоны main и daily описаны одним блоком кода. Шаблоны project и task я разделил на несколько блоков, чтобы не делать сплошную портянку кода. Для project это блок с properties и dataviewjs. Для task это properties, дополнительный шаблон и dataviewjs.

      Код действительно стоит убрать под спойлер, спасибо за совет.

      Я использую Obsidian только на десктопе. Заметки синхронизируется с личным облаком Nextcloud. При желании это позволяет посмотреть заметку и с телефона, так как они хранятся в обычном текстовом формате с md разметкой.


      1. AuToMaton
        21.10.2024 19:52

        Зря Вы не описали верхний уровень - что как работает. Я сам посмотрел что спрашивал - блок Templater просто вписывается в страницу и заменяет себя на создаваемый скриптом текст. При этом некоторые вещи типа интерполяции строк не работают. Сколько раз и когда он выполнится - как повезёт. Кроме того, невозможно создать в странице интерактивные элементы и получить неиндексируемые данные.

        Иными словами, такое «управление проектами» - на пределе возможностей этих двух плагинов. Получается как всегда. Вот смотрите какие технологии, вот как можно. Приглядишься - да, так можно, но технологий нет, дальше стена. Для полного удобства, писать полновесный плагин - единственное решение, а это, по крайней мере если делать нормально, комп и Node.js, хотя и с Bun у меня работает.

        А вот было бы описание верхнего уровня - не было бы разочарований. Но для нехитрого случая отслеживания японских онгоингов - можно и без полного удобства обойтись, даже Templater не понадобился, и не покидая iPad.


        1. ashkaar Автор
          21.10.2024 19:52

          Вы имеете ввиду описание работы самих плагинов? Это немного выходит за рамки статьи. Я думаю что тут лучше будет почитать документацию конкретного плагина.

          Пример интерполяции строк из кода:

          const currentNoteName = app.workspace.getActiveFile()?.basename;
          const headingToAdd = `### [[${currentNoteName}]]`;

          За время использования я не сталкивался с проблемой, что текущий код не выполнился.

          По поводу предела возможностей сказать сложно. Я планирую сделать еще пачку автоматизации, там будет видно.


          1. AuToMaton
            21.10.2024 19:52

            Ну, точно такая же интерполяция в кодовом блоке dataview у меня не работала, ну и ладно.

            Поставьте себя на место читателя. Он знает что есть такой Obsidian - о нём на Хабре писали. Что к нему есть плагины - они ко всему есть. Видит пример - просто работает, делает полезные вещи. Вопрос - как?

            Откуда брать плагины? Какие брать плагины? Они все в Интернете в одну кучу свалены… А что Obsidian при попытке скачать сам предлагает именно эти в числе первых с отрывом по числу скачиваний на порядки - это он не знает, я сам сегодня узнал.

            Что все плагины и внутри Obsidian тоже свалены в одну кучу и захламляют пространство имён - тоже. Что каждый плагин реагирует лишь на ограниченный набор событий - тоже. Зачем Templater когда в Obsidian уже есть темплеты - тоже.

            Самое моё, и я гарантирую - не только моё, недоумение - куда совать код для dataview - тоже не знает. Не знает общий принцип - dataview читает только индексируемые данные и ничего не может менять, templater может только создать ноут и дальше тоже не может менять. Что HTML в ноуте писать можно, но JavaScript работать не будет и поэтому для dataview нужен кодовый блок - тоже. И отсюда море ограничений убивающее океан фантазий.

            Про предел возможностей. Я хочу кнопку которая бы переносила файл из одной папки в другую и меняла в нём текст. Я хочу кнопку которая бы меняла дату по которой идёт поиск в dataview. В Интернете видел, что Obsidian на технологиях Интернета работает - знаю, вот и хочу. Ответ на это один - свой плагин пиши, скорее всего. Или сразу пиши, или после изучения пары тысяч уже написанных…

            Про комментарии к коду. Это последняя очередь. Кто JavaScript читает (а кто его не читает?) - тому комментарии не нудны. Кто не читает - тому не особо помогут.

            Вот это и есть верхний уровень без которого что-то рассказывать можно только специалистам по Obsidian. Preaching to the chorus называется. Интересующихся управлением проектами - мало, конкретными примерами использования плагинов - больше, возможностями Obsidian - много.


  1. odilovoybek
    21.10.2024 19:52

    А можете поделиться конфигурацией?)


    1. ashkaar Автор
      21.10.2024 19:52

      Уточните, какая конфигурация Вас интересует?

      Основные плагины, которыми пользуюсь, я перечислил. У меня еще установлены Note Refactor, Advanced Tables и Tasks. Они в автоматизациях не участвуют, поэтому их указывать не стал. Тему использую Things.

      В заметке Homepage использую CSS MCL Multi Column. Его брал отсюда: https://github.com/efemkay/obsidian-modular-css-layout.


  1. HMNIJP
    21.10.2024 19:52

    Раз статья больше про конкретику, то немного покритикую техническую часть:

    Судя по тому что автор использует имена периодических заметок [dd-mm-yyyy] - он ведет их менее месяца, иначе заметил бы что в списке будут идти сначала все заметки первого дня каждого месяца за все года, затем второго и т.д. а не по порядку даты... что немного не удобно, если искать конкретный день вне плашки календаря.

    Автор приводит ссылки на документацию по плагинам, но возможно сам туда не заглядывал... иначе как объяснить целую страницу кода для ручного парсинга даты и формирования ссылок вместо встроенных методов...
    ← [[<% tp.date.now('YYYY-MM-DD', -1, tp.file.path(true), 'YYYY-MM-DD') %>]] | [[<% tp.date.now('YYYY-MM-DD', 1, tp.file.path(true), 'YYYY-MM-DD') %>]] →

    В следующих шаблонах делается примерно аналогичное с добавлением ссылок на проект... хотя список их можно было просто вести динамическим запросом dataview...

    но возможно просто у него такой стиль, и в конечном счете будет тоже самое)


    1. ashkaar Автор
      21.10.2024 19:52

      Ежедневные заметки веду с ноября 2023 (до этого обходился без них). Формат даты в таком виде мне кажется более удобным.

      Сами шаблоны были переписаны уже пару раз. Но грубо говоря я ими пользуюсь около месяца. Даты фильтруются в шаблоне.

      function datesFromDailyNotes(filename) {
          return moment(filename, 'DD-MM-YYYY').toDate();
      }
      
      pages.sort((a, b) => datesFromDailyNotes(a.file.name) - datesFromDailyNotes(b.file.name));

      В итоге получается так:

      В документацию заглядывал. В коде же используются методы из нее (возможно не везде, где это было бы уместно).

      По поводу tp.date.now Вы правы, так действительно удобнее. Я не подумал что тут можно использовать tp.file.path(true), чтобы привязаться к дате заметки, а не текущей.


  1. flowing_abyss
    21.10.2024 19:52

    Было бы совсем нелишним, если бы вы добавили примеры заполненных шаблонов и выводов Dataview.

    И ещё кажется, что оборачивать всё в try/catch это излишне. Templater/Dataview и так ругнутся, если будет ошибка.


    1. ashkaar Автор
      21.10.2024 19:52

      Пример заметки выложил комментарием выше.

      Пример проекта:

      Про try/catch учту, спасибо.


  1. TheDiVaZo
    21.10.2024 19:52

    Спасибо за статью