# Как я веду ежедневные заметки в Obsidian: дневник, задачи, привычки и синхронизация с Google Calendar
![[main.00_00_56_02.Still001.jpg]]
> Раньше каждое утро я тонул в задачах и мыслях: что сделать прямо сейчас, кому ответить, какой приоритет у дел и о чём я вообще думал пару дней назад? Сейчас я открываю одну заметку — и сразу вижу все свои проекты, задачи, привычки и мысли. В этой статье я подробно, по шагам и максимально простым языком разберу всю свою систему ежедневных заметок — так, чтобы вы смогли собрать её у себя буквально за вечер.
---
## Зачем вообще вести дневник в Obsidian
> [!quote]- 🤔 Что такое Obsidian (если вы впервые слышите)
> **Obsidian** — это бесплатная программа для заметок, которая хранит всё у вас на компьютере в виде обычных текстовых файлов.
>
> Главное её отличие от блокнота или Word — заметки можно **связывать между собой** ссылками, как страницы в интернете. Получается личная «база знаний», которая растёт вместе с вами.
>
> А ещё Obsidian можно расширять **плагинами** — небольшими дополнениями, которые добавляют новые возможности. Именно на них и построена вся система из этой статьи.
> [!quote]- ⬇️ Как скачать и установить Obsidian (с нуля)
> Это бесплатно и занимает пару минут.
>
> **1. Скачайте программу.** Зайдите на официальный сайт [obsidian.md](https://obsidian.md) и нажмите большую кнопку **Download**. Сайт сам определит вашу систему (Windows, macOS или Linux). Для телефона ищите «Obsidian» в App Store или Google Play.
>
> **2. Установите.** Запустите скачанный файл и пройдите обычную установку, как с любой программой.
>
> **3. Создайте хранилище (Vault).** При первом запуске Obsidian попросит создать **Vault** — это просто папка на вашем компьютере, где будут лежать все заметки. Нажмите «Create new vault», придумайте название и выберите место на диске.
>
> **4. Готово.** Можно создавать первую заметку. А все плагины из этой статьи устанавливаются уже внутри программы — мы дойдём до этого в шаге 1.
>
> ⚠️ Качайте Obsidian только с официального сайта **obsidian.md** — так вы получите свежую и безопасную версию.
> [!quote]- 💚 Почему именно Obsidian, а не заметки в телефоне
> Коротко — три причины:
>
> **Всё ваше и навсегда.** Заметки хранятся у вас на устройстве обычными текстовыми файлами. Не закроется сервис — не пропадут данные. Их можно открыть даже простым «Блокнотом».
>
> **Связи между заметками.** Идеи не лежат отдельными листочками, а соединяются ссылками в единую сеть знаний — со временем это превращается в ваш «второй мозг».
>
> **Гибкость.** Плагины позволяют подстроить программу под себя: от простого дневника до сложной системы задач, как в этой статье.
Бумажный блокнот — это прекрасно. Но у него есть один минус: то, что вы вчера записали, остаётся «вчера».
Задачи не переносятся сами, привычки не складываются в график, а мысли теряются между страницами.
В Obsidian всё иначе. Ежедневная заметка становится центром вашего дня:
<div style="display:flex; flex-wrap:wrap; gap:12px; margin:20px 0; justify-content:center;">
<div style="flex:1 1 150px; min-width:140px; max-width:200px; background:#f4f6fb; border:1px solid #e0e4ee; border-radius:10px; padding:16px; text-align:center;">
<div style="font-size:1.8em;">🔁</div>
<b>Задачи</b><br>
<span style="font-size:0.85em; color:#666;">переносятся сами и приходят из проектов</span>
</div>
<div style="flex:1 1 150px; min-width:140px; max-width:200px; background:#f4f6fb; border:1px solid #e0e4ee; border-radius:10px; padding:16px; text-align:center;">
<div style="font-size:1.8em;">📅</div>
<b>Календарь</b><br>
<span style="font-size:0.85em; color:#666;">синхронизация с Google Calendar</span>
</div>
<div style="flex:1 1 150px; min-width:140px; max-width:200px; background:#f4f6fb; border:1px solid #e0e4ee; border-radius:10px; padding:16px; text-align:center;">
<div style="font-size:1.8em;">✅</div>
<b>Привычки</b><br>
<span style="font-size:0.85em; color:#666;">кнопки и графики прогресса</span>
</div>
<div style="flex:1 1 150px; min-width:140px; max-width:200px; background:#f4f6fb; border:1px solid #e0e4ee; border-radius:10px; padding:16px; text-align:center;">
<div style="font-size:1.8em;">💭</div>
<b>Мысли</b><br>
<span style="font-size:0.85em; color:#666;">свободное поле для идей и рассуждений</span>
</div>
</div>
Просыпаясь утром, я сразу вспоминаю, чем был занят вчера. Невыполненные задачи автоматически переносятся на сегодня. Простые рутины — привычки, медитация — отмечаю одной кнопкой, и они складываются в наглядную динамику. А новые задачи я беру прямо из своих проектов, которые связаны с моими целями.
Эта простая система держит меня в концентрации: я делаю то, что действительно нужно, а не прокрастинирую. Давайте соберём её вместе.
> [!tip] Полное описание моей системы и структуры PARA — в предыдущих материалах на сайте.
> https://eltonlabs.org/obsidian
---
## Шаг 1. Что нужно установить
Чтобы вести задачи, мысли и привычки в Obsidian, нам понадобится несколько плагинов и одна встроенная настройка.
> [!quote]- 🧩 Что такое плагин и где его взять
> **Плагин** — это дополнение, которое добавляет в Obsidian новую функцию. Что-то вроде расширения для браузера.
>
> Сам Obsidian умеет немного: писать и связывать заметки. А вот кнопки, графики, синхронизация с календарём — всё это появляется именно благодаря плагинам.
>
> Все плагины бесплатны и устанавливаются в пару кликов прямо внутри программы: **Настройки → Сторонние плагины → Обзор → поиск по названию → Установить → Включить.**
### Сторонние плагины
Открываем **Настройки → Сторонние плагины → Обзор** и устанавливаем по очереди:
| Плагин | Зачем нужен |
|---|---|
| **Dataview** | Собирает данные из заметок (например, мысли по дням, графики) |
| **Meta Bind** | Превращает свойства заметки в кнопки и поля прямо в тексте |
| **Homepage** | Делает домашнюю страницу, которая открывается при запуске |
| **Templater** | Запускает скрипты при создании заметки (перенос задач) |
| **Buttons** | Создаёт кнопки, запускающие команды в один клик |
| **QuickAdd** | «Мозг» кнопок: запускает макросы и скрипты |
> [!warning] После установки Dataview зайдите в его настройки и включите все галочки (особенно **Enable JavaScript Queries** и **Enable Inline JavaScript Queries**) — без этого виджет календаря не заработает.
![[main.00_01_27_18.Still002.jpg]]
### Включаем встроенные «Ежедневные заметки»
Obsidian умеет вести дневник прямо «из коробки». Заходим в **Настройки → Основные плагины** и включаем **Ежедневные заметки**. Затем настраиваем их под себя.
> [!quote]- 📅 Что такое «Ежедневные заметки»
> Это встроенная функция Obsidian: одна заметка на каждый календарный день.
>
> Нажимаете кнопку «сегодня» — и программа сама создаёт (или открывает) заметку с сегодняшней датой в названии, например `20-06-2026`.
>
> Удобство в том, что каждый день получает своё постоянное «место». Вы всегда знаете, где искать записи за конкретную дату, а заметки автоматически выстраиваются в дневник-хронологию.
![[main.00_01_53_24.Still003.jpg]]
Три ключевые настройки:
1. **Формат названия заметки.** У меня это просто день, месяц и год:
```
DD-MM-YYYY
```
Это важно: весь мой код (виджет, перенос задач) ищет заметки именно в формате `ДД-ММ-ГГГГ`. Если хотите добавить в название день недели — допишите буквы (например, `dddd`), но тогда поправьте формат и в коде. Какая буква за что отвечает, удобно смотреть на сайте [format.cm](https://momentjs.com/docs/#/displaying/format/) (документация Moment.js).
2. **Папка новых заметок.** У меня ежедневные заметки лежат в `2. Areas/Дневники/Ежедневные заметки`. Я храню всё по структуре **PARA**, а ведение дневника — это «сфера жизни» (Area), поэтому ей место в папке `Areas`.
> [!quote]- 🗂️ Что такое PARA (система папок)
> **PARA** — это простой способ разложить все заметки по четырём папкам, чтобы ничего не терялось:
>
> • **P**rojects (Проекты) — то, над чем вы работаете прямо сейчас и что имеет конкретный результат.
>
> • **A**reas (Сферы) — то, что вы поддерживаете постоянно: здоровье, финансы, и в том числе ведение дневника.
>
> • **R**esources (Ресурсы) — материалы и знания по интересным темам «на будущее».
>
> • **A**rchive (Архив) — то, что уже неактуально, но жалко удалять.
>
> Дневник — это то, что вы ведёте постоянно, поэтому он живёт в папке **Areas**.
3. **Шаблон.** Это структура, по которой будет создаваться каждая новая заметка. Мой шаблон лежит в `0. Files/4. Templates/Шаблон ежедневных заметок`. «Templates» переводится как «шаблоны».
> [!info] Вот так выглядит структура моей ежедневной заметки:
> **Свойства → виджет недели/календаря → планы на день → перенос задач → привычки → питание → мысли.**
> Не всегда получается заполнить всё. Но само стремление делает меня дисциплинированнее.
<div class="cta-section" style="padding: 15px; text-align: center; background-color: #f0f0f0; border-radius: 6px; margin: 20px 0;">
<h3>Для тех, кто не хочет долго разбираться</h3>
<p style="font-size: 0.95em; margin: 10px 0;">Попробуйте мой готовый шаблон Obsidian и начните систематизировать информацию уже сегодня</p>
<a href="https://eltonlabs.org/template" style="display: inline-block; padding: 8px 16px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; cursor: pointer; font-size: 0.95em; margin-top: 10px;">Узнать про шаблон</a>
</div>
---
## Шаг 2. Свойства заметки (YAML)
Откроем шаблон и разберём его сверху вниз. Первое, что мы видим, — **свойства заметки** (их ещё называют YAML или frontmatter).
В чём их смысл? Свойства — это «официальные» данные заметки, которые **легко считывают другие плагины**. Например, график привычек берёт информацию именно из свойств, а не из обычного текста. Это правило стоит запомнить: *что важно для графиков и автоматизации — храните в свойствах.*
Свойства пишутся в самом верху заметки между двумя строчками из трёх дефисов:
```yaml
---
Спорт: false
Чтение: false
Прогулка: false
Завтрак:
Перекусы:
Обед:
Ужин:
Итого_ккал:
---
```
Здесь две группы:
- **Привычки** (`Спорт`, `Чтение`, `Прогулка`) — тип «флажок» (галочка). `false` значит «ещё не выполнено».
- **Питание** (`Завтрак`, `Обед`, `Ужин`, `Перекусы`, `Итого_ккал`) — тип «текст», сюда записываем, что ели, и счётчик калорий.
> [!tip] Добавить своё свойство просто: в режиме свойств нажмите **«+ Добавить свойство»**, впишите название и выберите тип (текст, число, флажок, дата). Через пару дней заполнения вы уже увидите динамику на графике.
![[main.00_03_23_26.Still004.jpg]]
---
## Шаг 3. Синхронизация с Google Calendar и задачами
Сразу под свойствами в шаблоне идёт большой блок кода. **Не пугайтесь** — его написал ИИ, а я просто вставил. Этот код рисует красивый виджет: он показывает день недели, прогресс по неделе, а в режиме «Задачи» — синхронизируется с вашим **Google Calendar** и **Google Tasks**. Прямо из Obsidian вы видите события и задачи на день, можете отмечать их выполненными и добавлять новые.
![[main.00_04_22_24.Still005.jpg]]
Чтобы это заработало, одного кода в шаблоне мало. Нужно **открыть Obsidian доступ к вашему Google-аккаунту**. Делается это через бесплатный сервис Google Apps Script. Разберём по шагам — медленно и подробно.
> [!quote]- ☁️ Что такое Google Apps Script и зачем он нужен
> **Google Apps Script** — это бесплатный сервис от Google, где можно запускать небольшие программы, работающие с вашими же Google-сервисами (Календарь, Задачи, Почта).
>
> Зачем он здесь? Obsidian не умеет напрямую заглянуть в ваш Google Calendar. Поэтому мы создаём маленький «посредник»: программу, которая живёт на серверах Google, видит ваш календарь и отдаёт данные по специальной ссылке.
>
> Obsidian обращается к этой ссылке — и получает список событий и задач. Никакого программирования от вас не требуется: нужно просто вставить готовый код и пройти настройку по шагам ниже.
### 3.1. Готовим сервер на scripts.google.com
<div style="margin:20px 0;">
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#007bff; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">1</div>
<div>Откройте <b><a href="https://script.google.com">script.google.com</a></b> и нажмите <b>«Новый проект»</b>.</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#007bff; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">2</div>
<div>Удалите весь текст в редакторе и вставьте код, который я привожу ниже.</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#007bff; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">3</div>
<div>Подключите сервис <b>Google Tasks API</b>: слева в меню <b>«Службы»</b> (значок «+») найдите <b>Tasks API</b> и нажмите <b>«Добавить»</b>. Без этого шага код не увидит ваши задачи.</div>
</div>
</div>
> [!warning] Шаг 3 пропускают чаще всего. Код использует `Tasks.Tasklists.list()`, а это «расширенная служба». Если не добавить **Tasks API**, скрипт будет падать с ошибкой.
Вот код, который нужно вставить в редактор (он отвечает за чтение событий/задач и за создание/закрытие задач):
```javascript
// ======================================
// GOOGLE APPS SCRIPT — TASKS + CALENDAR
// ======================================
// Проверка авторизации (запусти один раз вручную)
function testAuth() {
const taskLists = Tasks.Tasklists.list();
Logger.log(taskLists);
const events = CalendarApp.getDefaultCalendar().getEvents(new Date(), new Date());
Logger.log(events.length + " событий сегодня");
}
// ====== ЧТЕНИЕ: события календаря + задачи на день ======
function doGet(e) {
try {
const tz = Session.getScriptTimeZone();
// DEBUG-режим: открой GAS_URL?debug=1 в браузере — покажет ВСЕ задачи
if (e && e.parameter && e.parameter.debug) {
const debugLists = (Tasks.Tasklists.list().items) || [];
const all = [];
debugLists.forEach(list => {
const items = (Tasks.Tasks.list(list.id, {
showCompleted: true, showHidden: true, maxResults: 100
}).items) || [];
items.forEach(t => {
all.push({ list: list.title, listId: list.id, title: t.title, due: t.due || null, status: t.status });
});
});
return ContentService
.createTextOutput(JSON.stringify({
success: true, scriptTimeZone: tz,
listsFound: debugLists.map(l => l.title),
totalTasks: all.length, all: all
}))
.setMimeType(ContentService.MimeType.JSON);
}
const param = (e && e.parameter && e.parameter.date) ? e.parameter.date : null;
const base = param
? new Date(+param.split("-")[0], +param.split("-")[1] - 1, +param.split("-")[2])
: new Date();
const start = new Date(base.getFullYear(), base.getMonth(), base.getDate(), 0, 0, 0);
const end = new Date(base.getFullYear(), base.getMonth(), base.getDate(), 23, 59, 59);
const dayStr = Utilities.formatDate(base, tz, "yyyy-MM-dd");
const events = CalendarApp.getDefaultCalendar().getEvents(start, end).map(ev => ({
title: ev.getTitle(),
allDay: ev.isAllDayEvent(),
time: ev.isAllDayEvent() ? null : Utilities.formatDate(ev.getStartTime(), tz, "HH:mm")
}));
const tasks = [];
const lists = (Tasks.Tasklists.list().items) || [];
const today = new Date();
const todayStr = Utilities.formatDate(today, tz, "yyyy-MM-dd");
const isToday = dayStr === todayStr;
lists.forEach(list => {
const items = (Tasks.Tasks.list(list.id, { showCompleted: true, showHidden: true }).items) || [];
items.forEach(t => {
if (!t.due) return;
const taskDay = t.due.substring(0, 10);
const isExactDay = taskDay === dayStr;
const isOverdue = isToday && taskDay < dayStr && t.status === "needsAction";
if (isExactDay || isOverdue) {
tasks.push({
id: t.id, listId: list.id, title: t.title,
status: t.status, completed: t.completed || null, overdue: isOverdue
});
}
});
});
tasks.sort((a, b) => (a.overdue === b.overdue) ? 0 : (a.overdue ? -1 : 1));
return ContentService
.createTextOutput(JSON.stringify({ success: true, date: dayStr, events, tasks }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({ success: false, error: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
// ====== ЗАПИСЬ: создать ИЛИ закрыть задачу ======
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
if (data.action === "complete") {
const listId = data.listId || Tasks.Tasklists.list().items[0].id;
Tasks.Tasks.patch({ status: "completed" }, listId, data.taskId);
return ContentService
.createTextOutput(JSON.stringify({ success: true, message: 'Задача выполнена!' }))
.setMimeType(ContentService.MimeType.JSON);
}
const title = data.title;
const dueDate = data.dueDate;
const taskLists = Tasks.Tasklists.list();
const defaultTaskList = taskLists.items[0].id;
const task = Tasks.Tasks.insert({ title: title, due: dueDate, status: 'needsAction' }, defaultTaskList);
return ContentService
.createTextOutput(JSON.stringify({ success: true, taskId: task.id, message: 'Задача добавлена!' }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService
.createTextOutput(JSON.stringify({ success: false, error: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
```
### 3.2. Даём разрешение в первый раз
Google не позволит коду трогать ваш календарь, пока вы лично это не разрешите. Сделаем это один раз вручную:
<div style="margin:20px 0;">
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#28a745; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">1</div>
<div>В выпадающем списке функций сверху выберите <b>testAuth</b> и нажмите <b>«Выполнить»</b> (▶).</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#28a745; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">2</div>
<div>Появится окно <b>«Требуется авторизация»</b> → нажмите <b>«Проверить разрешения»</b> и выберите свой Google-аккаунт.</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#28a745; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">3</div>
<div>Появится пугающий экран <b>«Google не проверил это приложение»</b>. Это нормально — приложение ваше собственное. Нажмите внизу <b>«Дополнительные настройки» → «Перейти на страницу … (небезопасно)»</b>.</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#28a745; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">4</div>
<div>Нажмите <b>«Разрешить»</b>. Готово — доступ выдан. В журнале (Logs) вы увидите свои списки задач.</div>
</div>
</div>
> [!tip] Экран «Google не проверил приложение» появляется у всех личных скриптов. Вы не передаёте данные никому постороннему — код работает только в вашем аккаунте.
![[main.00_04_51_19.Still006.jpg]]
### 3.3. Развёртывание и какую ссылку копировать
Теперь сделаем из кода настоящий веб-адрес, к которому будет обращаться Obsidian:
<div style="margin:20px 0;">
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#6f42c1; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">1</div>
<div>Справа вверху нажмите <b>«Начать развёртывание» → «Новое развёртывание»</b>.</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#6f42c1; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">2</div>
<div>Нажмите на шестерёнку выбора типа и выберите <b>«Веб-приложение»</b> (Web app).</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#6f42c1; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">3</div>
<div>Заполните:<br>• <b>Запуск от имени:</b> «Я» (ваш аккаунт)<br>• <b>У кого есть доступ:</b> «Все» (Anyone)</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:14px;">
<div style="flex-shrink:0; width:30px; height:30px; border-radius:50%; background:#6f42c1; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">4</div>
<div>Нажмите <b>«Развернуть»</b>. Скопируйте <b>«URL веб-приложения»</b> — это та самая ссылка, которая заканчивается на <code>/exec</code>.</div>
</div>
</div>
> [!warning] Копируйте именно ссылку, которая **оканчивается на `/exec`**. Ссылка с `/dev` работать в Obsidian не будет.
>
> ```
> https://script.google.com/macros/s/AKfycbx…………/exec
> ```
![[main.00_04_58_04.Still007.jpg]]
### 3.4. Вставляем код виджета в шаблон
Теперь в шаблон ежедневной заметки вставляем код виджета. Это блок ` ```dataviewjs `, который рисует карточку с неделей и календарём. **Самое важное** — в первой строке заменить ссылку в кавычках на вашу из шага 3.3:
```javascript
const GAS_URL = "СЮДА_ВСТАВЬТЕ_СВОЮ_ССЫЛКУ_ЗАКАНЧИВАЮЩУЮСЯ_НА_exec";
```
Вот так это выглядит в начале кода виджета (заменяете только строку с `GAS_URL`, остальное не трогаете):
````
```dataviewjs
const GAS_URL = "https://script.google.com/macros/s/AKfyc…………/exec";
const obsidian = require("obsidian");
const fileName = dv.current().file.name;
const m = fileName.match(/^(\d{2})-(\d{2})-(\d{4})$/);
if (!m) {
dv.paragraph("⚠️ Название заметки должно быть в формате ДД-ММ-ГГГГ");
} else {
// … дальше идёт длинный код виджета: неделя, прогресс,
// кнопки «Задачи», загрузка событий и задач из календаря …
}
```
````
> [!note]- Полный код виджета (нажмите, чтобы раскрыть)
> Это тот же блок, что лежит в моём шаблоне. Скопируйте его целиком в шаблон ежедневной заметки и замените только `GAS_URL` в первой строке.
>
> ```javascript
> const GAS_URL = "СЮДА_СВОЮ_ССЫЛКУ/exec";
>
> const obsidian = require("obsidian");
> const fileName = dv.current().file.name;
> const m = fileName.match(/^(\d{2})-(\d{2})-(\d{4})$/);
>
> if (!m) {
> dv.paragraph("⚠️ Название заметки должно быть в формате ДД-ММ-ГГГГ");
> } else {
> const [, dd, mm, yyyy] = m;
> const date = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
> const dateISO = `${yyyy}-${mm}-${dd}`;
>
> const yesterday = new Date(date); yesterday.setDate(date.getDate() - 1);
> const tomorrow = new Date(date); tomorrow.setDate(date.getDate() + 1);
> const fmt = d => `${String(d.getDate()).padStart(2,"0")}-${String(d.getMonth()+1).padStart(2,"0")}-${d.getFullYear()}`;
> const prevNote = fmt(yesterday);
> const nextNote = fmt(tomorrow);
>
> const dayNames = ["ПН","ВТ","СР","ЧТ","ПТ","СБ","ВС"];
> const dow = date.getDay();
> const todayIdx = dow === 0 ? 6 : dow - 1;
> const progress = (((todayIdx + 1) / 7) * 100).toFixed(0);
>
> const weekHtml = dayNames.map((label, i) => {
> let cls = "el-day";
> if (i === todayIdx) cls += " is-today";
> else if (i < todayIdx) cls += " is-past";
> else cls += " is-future";
> return `<span class="${cls}">${label}</span>`;
> }).join(`<span class="el-dot-sep">•</span>`);
>
> const CACHE_KEY = `eltonlabs-gcal-${dateISO}`;
> function readCache() {
> try { const raw = localStorage.getItem(CACHE_KEY); return raw ? JSON.parse(raw) : null; }
> catch { return null; }
> }
> function writeCache(data) {
> const payload = { ...data, _ts: Date.now() };
> try { localStorage.setItem(CACHE_KEY, JSON.stringify(payload)); } catch {}
> return payload;
> }
>
> const root = dv.el("div", "", { cls: "eltonlabs-wrapper" });
> root.innerHTML = `
> <style>
> .el-card { background: var(--background-secondary); border: 1px solid var(--background-modifier-border); border-radius: 12px; overflow: hidden; margin: 8px 0; }
> .el-accent { height: 3px; background: linear-gradient(90deg, var(--interactive-accent) 0%, var(--color-purple, #8b5cf6) 100%); }
> .el-body { padding: 12px 16px 14px; }
> .el-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; }
> .el-title { display:flex; align-items:center; gap:7px; margin:0; font-size:0.92em; font-weight:600; color:var(--text-normal); }
> .el-dot { width:8px; height:8px; border-radius:50%; background: var(--interactive-accent); box-shadow: 0 0 6px rgba(99,102,241,.5); flex-shrink:0; }
> .el-actions { display:flex; align-items:center; gap:6px; }
> .el-icon-btn { width:26px; height:26px; display:flex; align-items:center; justify-content:center; background:transparent; border:none; border-radius:6px; cursor:pointer; color:var(--text-muted); font-size:0.88em; transition: background .15s, color .15s; padding:0; }
> .el-icon-btn:hover { background: var(--interactive-hover); color: var(--text-normal); }
> .el-icon-btn.spin { animation: el-spin .55s linear; }
> .el-toggle-btn { display:flex; align-items:center; gap:5px; padding:4px 10px; border-radius:7px; border:1px solid var(--background-modifier-border); background: var(--interactive-normal); color: var(--text-normal); font-size:0.78em; font-weight:500; cursor:pointer; transition: background .15s, border-color .15s; }
> .el-toggle-btn:hover { background: var(--interactive-hover); border-color: var(--interactive-accent); }
> @keyframes el-spin { to { transform: rotate(360deg); } }
> @keyframes el-shimmer { to { background-position: -200% 0; } }
> .el-weekrow { text-align:center; font-size:0.95em; margin-bottom:12px; }
> .el-day { padding: 4px 9px; border-radius:6px; }
> .el-day.is-today { font-weight:bold; color:#fff; background: linear-gradient(135deg, var(--interactive-accent) 0%, var(--color-purple, #8b5cf6) 100%); box-shadow: 0 2px 8px rgba(99,102,241,.3); }
> .el-day.is-past { color: var(--color-green, #10b981); opacity:.6; text-decoration: line-through; }
> .el-day.is-future { color: var(--text-muted); opacity:.55; }
> .el-dot-sep { color: var(--text-faint, var(--text-muted)); margin:0 8px; }
> .el-progress { background: var(--background-modifier-border); height:4px; border-radius:2px; overflow:hidden; }
> .el-progress-fill { background: linear-gradient(90deg, var(--interactive-accent) 0%, var(--color-purple, #8b5cf6) 100%); height:100%; transition: width .3s; }
> .el-progress-label { text-align:center; color: var(--text-muted); font-size:0.85em; margin-top:8px; }
> .el-navlinks { display:flex; justify-content:space-between; margin-top:12px; }
> .el-navlinks a { display:inline-flex; align-items:center; gap:5px; padding:5px 14px; border-radius:8px; background: var(--interactive-normal); border:1px solid var(--background-modifier-border); color: var(--text-normal) !important; text-decoration:none !important; font-size:13px; cursor:pointer; transition: background .15s; }
> .el-navlinks a:hover { background: var(--interactive-hover); }
> .el-meta { font-size:0.71em; color: var(--text-faint, var(--text-muted)); margin-bottom:12px; }
> .el-section { font-size:0.69em; color: var(--text-muted); text-transform:uppercase; letter-spacing:.07em; margin:10px 0 3px; padding-left:2px; }
> .el-divider { border:none; border-top:1px solid var(--background-modifier-border); margin:8px 0; }
> .el-item { display:flex; gap:8px; align-items:center; padding:4px 6px; border-radius:6px; font-size:0.875em; transition: background .12s; }
> .el-item.clickable { cursor:pointer; }
> .el-item.clickable:hover { background: var(--interactive-hover); }
> .el-item.completed { opacity:.38; }
> .el-item.completed .el-item-title { text-decoration: line-through; }
> .el-item.pending { opacity:.5; pointer-events:none; }
> .el-time { color: var(--text-accent, var(--interactive-accent)); font-variant-numeric: tabular-nums; min-width:60px; font-size:.85em; flex-shrink:0; }
> .el-checkbox { min-width:20px; text-align:center; flex-shrink:0; }
> .el-item-title { flex:1; }
> .el-spinner { display:inline-block; width:13px; height:13px; border:2px solid var(--background-modifier-border); border-top-color: var(--interactive-accent); border-radius:50%; animation: el-spin .65s linear infinite; vertical-align:middle; }
> .el-empty { color: var(--text-muted); font-size:.85em; padding:2px 2px 8px; font-style:italic; }
> .el-skeleton { height:16px; border-radius:6px; background: linear-gradient(90deg, var(--background-modifier-border) 25%, var(--background-primary) 50%, var(--background-modifier-border) 75%); background-size:200% 100%; animation: el-shimmer 1.2s ease infinite; margin:8px 0; }
> .el-skeleton.w80 { width:80%; } .el-skeleton.w55 { width:55%; }
> .el-load-btn { display:flex; align-items:center; justify-content:center; gap:7px; width:100%; padding:7px 14px; border-radius:8px; border:1px dashed var(--background-modifier-border); cursor:pointer; background:transparent; color: var(--text-muted); font-size:.85em; transition: background .15s, border-color .15s, color .15s; margin-top:2px; }
> .el-load-btn:hover { background: var(--interactive-hover); border-color: var(--interactive-accent); color: var(--text-normal); }
> .el-cmdbar-wrap { margin-top:14px; }
> .el-cmdbar { display:flex; align-items:stretch; height:38px; border:1px solid var(--background-modifier-border); border-radius:10px; overflow:hidden; background: var(--background-primary); box-sizing:border-box; transition: border-color .2s; }
> .el-cmdbar:focus-within { border-color: var(--interactive-accent); box-shadow: 0 0 0 2px rgba(99,102,241,.18); }
> .el-cmdbar-icon { display:flex; align-items:center; padding:0 2px 0 10px; color: var(--text-faint, var(--text-muted)); font-size:.78em; flex-shrink:0; line-height:1; user-select:none; pointer-events:none; }
> .el-cmdbar input[type=text] { flex:1; min-width:0; width:0; height:100%; background:transparent; border:none; outline:none !important; box-shadow:none !important; padding:0 8px 0 4px; margin:0; color: var(--text-normal); font-size:.86em; box-sizing:border-box; appearance:none; -webkit-appearance:none; }
> .el-cmdbar input[type=text]::placeholder { color: var(--text-faint, var(--text-muted)); }
> .el-vbar { width:1px; margin:7px 0; flex-shrink:0; background: var(--background-modifier-border); align-self:stretch; }
> .el-cmdbar input[type=date] { height:100%; width:120px; flex-shrink:0; background:transparent; border:none; outline:none !important; box-shadow:none !important; padding:0 8px; margin:0; color: var(--text-muted); font-size:.8em; color-scheme: dark light; cursor:pointer; box-sizing:border-box; appearance:none; -webkit-appearance:none; }
> .el-cta-btn { flex-shrink:0; height:100% !important; border-radius:0 !important; border-left:1px solid rgba(99,102,241,.3) !important; border-top:none !important; border-right:none !important; border-bottom:none !important; padding:0 16px !important; margin:0 !important; font-size:.84em !important; font-weight:600 !important; white-space:nowrap !important; cursor:pointer !important; display:flex !important; align-items:center !important; gap:5px !important; box-sizing:border-box !important; outline:none !important; transition: filter .18s !important; }
> .el-cta-btn:hover:not(:disabled) { filter: brightness(1.1) !important; }
> .el-cta-btn:disabled { opacity:.45 !important; cursor:default !important; filter:none !important; }
> .el-btn-spinner { display:inline-block; width:12px; height:12px; border:2px solid rgba(255,255,255,.35); border-top-color: currentColor; border-radius:50%; animation: el-spin .6s linear infinite; }
> </style>
>
> <div class="el-card" id="el-card">
> <div class="el-accent"></div>
> <div class="el-body">
> <div class="el-head">
> <h4 class="el-title"><span class="el-dot"></span><span id="el-title-text">Неделя</span></h4>
> <div class="el-actions">
> <button class="el-icon-btn" id="el-refresh" title="Обновить" style="display:none">🔄</button>
> <button class="el-toggle-btn" id="el-toggle">🗓️ Задачи</button>
> </div>
> </div>
> <div id="el-view-nav">
> <div class="el-weekrow">${weekHtml}</div>
> <div class="el-progress"><div class="el-progress-fill" style="width:${progress}%"></div></div>
> <div class="el-progress-label">Неделя пройдена на ${progress}%</div>
> <div class="el-navlinks">
> <a data-link="${prevNote}">← Вчера</a>
> <a data-link="${nextNote}">Завтра →</a>
> </div>
> </div>
> <div id="el-view-cal" style="display:none">
> <div id="el-cal-body">
> <div class="el-skeleton"></div>
> <div class="el-skeleton w80"></div>
> <div class="el-skeleton w55"></div>
> </div>
> </div>
> </div>
> </div>`;
>
> const navView = root.querySelector("#el-view-nav");
> const calView = root.querySelector("#el-view-cal");
> const calBody = root.querySelector("#el-cal-body");
> const toggleBtn = root.querySelector("#el-toggle");
> const refreshBtn = root.querySelector("#el-refresh");
> const titleText = root.querySelector("#el-title-text");
>
> let view = "nav";
> toggleBtn.addEventListener("click", () => {
> if (view === "nav") {
> navView.style.display = "none"; calView.style.display = "";
> titleText.textContent = "Google Calendar"; toggleBtn.innerHTML = "📅 Неделя";
> refreshBtn.style.display = ""; view = "cal";
> } else {
> calView.style.display = "none"; navView.style.display = "";
> titleText.textContent = "Неделя"; toggleBtn.innerHTML = "🗓️ Задачи";
> refreshBtn.style.display = "none"; view = "nav";
> }
> });
>
> root.querySelectorAll("a[data-link]").forEach(btn => {
> btn.addEventListener("click", (e) => {
> e.preventDefault();
> app.workspace.openLinkText(btn.dataset.link, "", false);
> });
> });
>
> let isLoading = false;
> function fmtTime(ts) {
> const n = ts ? new Date(ts) : new Date();
> return `${String(n.getHours()).padStart(2,"0")}:${String(n.getMinutes()).padStart(2,"0")}`;
> }
> function cmdBarHTML() {
> return `
> <div class="el-cmdbar-wrap">
> <div class="el-cmdbar">
> <span class="el-cmdbar-icon">✏️</span>
> <input type="text" id="el-input" placeholder="Новая задача…" />
> <div class="el-vbar"></div>
> <input type="date" id="el-date" value="${dateISO}" />
> <button class="mod-cta el-cta-btn" id="el-add-btn"><span>+</span> Добавить</button>
> </div>
> </div>`;
> }
> function renderCalSkeleton() {
> calBody.innerHTML = `<div class="el-skeleton"></div><div class="el-skeleton w80"></div><div class="el-skeleton w55"></div>`;
> }
> function renderCal(data) {
> const ev = data.events || [];
> const tk = data.tasks || [];
> let html = `<div class="el-meta">обновлено в ${fmtTime(data._ts)}</div>`;
> if (!ev.length && !tk.length) {
> html += `<div class="el-empty">Ничего не запланировано на этот день</div>`;
> } else {
> if (ev.length) {
> html += `<div class="el-section">События</div>`;
> ev.forEach(e => {
> html += `<div class="el-item"><span class="el-time">${e.allDay ? "весь день" : e.time}</span><span class="el-item-title">📌 ${e.title}</span></div>`;
> });
> }
> if (tk.length) {
> const overdueTasks = tk.filter(t => t.overdue);
> const normalTasks = tk.filter(t => !t.overdue);
> if (overdueTasks.length) {
> if (ev.length) html += `<hr class="el-divider">`;
> html += `<div class="el-section" style="color:#ef4444">Просрочено</div>`;
> overdueTasks.forEach(t => {
> html += `<div class="el-item clickable" data-id="${t.id}" data-list="${t.listId}" data-status="${t.status}"><span class="el-checkbox">⬜</span><span class="el-item-title" style="color:#ef4444">${t.title}</span></div>`;
> });
> }
> if (normalTasks.length) {
> if (ev.length || overdueTasks.length) html += `<hr class="el-divider">`;
> html += `<div class="el-section">Задачи</div>`;
> normalTasks.forEach(t => {
> const done = t.status === "completed";
> html += `<div class="el-item ${done ? "completed" : "clickable"}" data-id="${t.id}" data-list="${t.listId}" data-status="${t.status}"><span class="el-checkbox">${done ? "✅" : "⬜"}</span><span class="el-item-title">${t.title}</span></div>`;
> });
> }
> }
> }
> html += cmdBarHTML();
> calBody.innerHTML = html;
> bindCalEvents();
> }
> function renderCalError(msg) {
> calBody.innerHTML = `<div class="el-empty">⚠️ ${msg}</div><button class="el-load-btn" id="el-cal-retry">🔄 Повторить</button>`;
> calBody.querySelector("#el-cal-retry")?.addEventListener("click", loadCal);
> }
> function bindCalEvents() {
> calBody.querySelectorAll(".el-item.clickable[data-id]").forEach(item => {
> item.addEventListener("click", async () => {
> const taskId = item.getAttribute("data-id");
> const listId = item.getAttribute("data-list");
> item.classList.add("pending");
> item.querySelector(".el-checkbox").innerHTML = '<span class="el-spinner"></span>';
> try {
> await obsidian.requestUrl({ url: GAS_URL, method: "POST", contentType: "text/plain",
> body: JSON.stringify({ action: "complete", taskId, listId }) });
> new obsidian.Notice("✅ Задача выполнена!"); loadCal();
> } catch (err) {
> item.classList.remove("pending");
> item.querySelector(".el-checkbox").innerHTML = "⬜";
> new obsidian.Notice("⚠️ " + err.message);
> }
> });
> });
> const input = calBody.querySelector("#el-input");
> const dateIn = calBody.querySelector("#el-date");
> const btn = calBody.querySelector("#el-add-btn");
> if (!input || !btn) return;
> const add = async () => {
> const title = input.value.trim();
> const due = dateIn.value || dateISO;
> if (!title) { input.focus(); return; }
> if (!due) { dateIn.focus(); return; }
> btn.disabled = true; btn.innerHTML = `<span class="el-btn-spinner"></span>`;
> try {
> await obsidian.requestUrl({ url: GAS_URL, method: "POST", contentType: "text/plain",
> body: JSON.stringify({ title, dueDate: `${due}T00:00:00.000Z` }) });
> new obsidian.Notice(`✅ «${title}» добавлена`); input.value = "";
> if (due === dateISO) { loadCal(); }
> else { btn.disabled = false; btn.innerHTML = `<span>+</span> Добавить`; }
> } catch (err) {
> btn.disabled = false; btn.innerHTML = `<span>+</span> Добавить`;
> input.focus(); new obsidian.Notice("⚠️ " + err.message);
> }
> };
> btn.addEventListener("click", add);
> input.addEventListener("keydown", e => { if (e.key === "Enter") add(); });
> }
> async function loadCal() {
> if (isLoading) return;
> isLoading = true; renderCalSkeleton();
> try {
> const res = await obsidian.requestUrl({ url: `${GAS_URL}?date=${dateISO}`, method: "GET" });
> renderCal(writeCache(res.json));
> } catch (err) { renderCalError(err.message); }
> finally { isLoading = false; }
> }
> refreshBtn.addEventListener("click", () => {
> if (isLoading) return;
> refreshBtn.classList.add("spin");
> setTimeout(() => refreshBtn.classList.remove("spin"), 600);
> loadCal();
> });
> const cached = readCache();
> if (cached) { renderCal(cached); } else { loadCal(); }
> }
> ```
Готово! Теперь в заметке вы видите задачи на сегодня, просроченные задачи подсвечиваются красным, задачи можно отмечать выполненными и добавлять новые. Особенно удобно это на телефоне: открыл виджет — и быстро накидал дела.
> [!tip] Проверка работы. Откройте вашу ссылку в браузере, дописав в конце `?debug=1`. Если увидите JSON со списком ваших задач — сервер работает правильно.
<div class="cta-section" style="padding: 15px; text-align: center; background-color: #f0f0f0; border-radius: 6px; margin: 20px 0;">
<h3>Для тех, кто не хочет долго разбираться</h3>
<p style="font-size: 0.95em; margin: 10px 0;">Попробуйте мой готовый шаблон Obsidian и начните систематизировать информацию уже сегодня</p>
<a href="https://eltonlabs.org/template" style="display: inline-block; padding: 8px 16px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; cursor: pointer; font-size: 0.95em; margin-top: 10px;">Узнать про шаблон</a>
</div>
---
## Шаг 4. Планы на день и кнопки
Следующий раздел шаблона — **планы на день**. Здесь у меня стоят ярлыки кнопок от плагина **Buttons**:
```
`button-spaced-repetition` `button-sport` `button-morning-routine` `button-work-on-project` `button-litso`
```
Каждый такой ярлык — это одна кнопка, которую я нажимаю в течение дня:
| Кнопка | Что запускает |
|---|---|
| 🃏 Интервальное повторение | Вспомнить материал, который учу |
| 💪 Тренировки | Упражнения на сегодня (меняются автоматически по группам мышц) |
| 🙏 Утро | Мой утренний ритуал — медитация и концентрация |
| 📋 Задача | Открыть проекты и взять оттуда задачу (главная кнопка!) |
| 😌 Лицо | Личная рутина по уходу |
> [!quote]- ⚡ Что такое QuickAdd (что прячется за кнопками)
> **QuickAdd** — плагин, который запускает заранее настроенные действия одной командой.
>
> В коде кнопки строка `action QuickAdd: …` означает: «нажми кнопку — выполни вот это действие из QuickAdd». А действием может быть что угодно — открыть нужную заметку, создать новую по шаблону или запустить целый скрипт.
>
> Проще говоря: **кнопка — это «лицо», а QuickAdd — «мозг»**, который решает, что произойдёт при нажатии.
> [!info] Эти функции добавлялись **постепенно**. Сначала в заметке был только утренний ритуал. Потом я добавил интервальное повторение, тренировки и остальное — по мере необходимости, чтобы не перегружать мозг. Советую и вам начинать с одной-двух кнопок.
---
## Шаг 5. Как устроены кнопки (Buttons) — простыми словами
Самое частое непонимание новичков: «почему в заметке написано `` `button-sport` ``, а появляется красивая кнопка?» Разберёмся.
Логика делится на **две части**: где кнопка *описана* и где она *показана*.
<div style="margin:20px 0; background:#f4f6fb; border:1px solid #e0e4ee; border-radius:10px; padding:18px;">
<div style="display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:14px;">
<div style="flex:1 1 220px; min-width:200px; background:#fff; border:1px solid #d8deea; border-radius:8px; padding:14px;">
<b>1. Заметка «MOC - Buttons»</b><br>
<span style="font-size:0.88em; color:#555;">Здесь лежат полные описания всех кнопок (код + ярлык <code>^button-…</code>). Это «склад» кнопок.</span>
</div>
<div style="font-size:1.6em; color:#007bff;">→</div>
<div style="flex:1 1 220px; min-width:200px; background:#fff; border:1px solid #d8deea; border-radius:8px; padding:14px;">
<b>2. Ежедневная заметка</b><br>
<span style="font-size:0.88em; color:#555;">Здесь стоит только короткий ярлык <code>`button-sport`</code> — и плагин подставляет кнопку со «склада».</span>
</div>
</div>
</div>
Зачем так сложно? Чтобы **не копировать длинный код кнопки в каждую заметку**. Один раз описали — вставляем коротким ярлыком сколько угодно раз.
### Как выглядит описание кнопки
Все мои кнопки хранятся в одной заметке `MOC - Buttons` (или просто «Кнопки»). Вот пример описания одной кнопки:
````
```button
name 💪 Тренировки
type command
action QuickAdd: Открыть тренировки
color black
class btn-inline
```
^button-sport
````
Разберём построчно:
<div style="margin:16px 0;">
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><code style="flex-shrink:0; width:130px;">name</code><span>Что написано на кнопке (можно с эмодзи).</span></div>
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><code style="flex-shrink:0; width:130px;">type command</code><span>Тип «команда» — кнопка запускает команду Obsidian.</span></div>
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><code style="flex-shrink:0; width:130px;">action</code><span>Какую именно команду запустить (здесь — выбор из QuickAdd).</span></div>
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><code style="flex-shrink:0; width:130px;">color / class</code><span>Внешний вид. <code>btn-inline</code> — компактная кнопка в строку.</span></div>
<div style="display:flex; gap:10px; padding:8px 0;"><code style="flex-shrink:0; width:130px;">^button-sport</code><span>⭐ Главное: <b>ярлык</b> (block id). Это «адрес» кнопки.</span></div>
</div>
### Что такое `^button-...` и ярлык в тексте
Строчка `^button-sport` сразу под блоком кода — это **block id** (якорь блока) в Obsidian. Она даёт кнопке уникальный адрес.
После этого в **любой** заметке достаточно написать ярлык в виде *инлайн-кода* (в одинарных обратных кавычках):
```
`button-sport`
```
Плагин Buttons видит этот ярлык, находит по адресу `^button-sport` нужное описание и рисует на его месте рабочую кнопку. Можно ставить несколько ярлыков подряд в одну строку — получится ряд кнопок.
> [!tip] Правило простое:
> **`^button-имя`** (с «домиком») — это где кнопка *описана*.
> **`` `button-имя` ``** (в кавычках) — это где кнопка *показывается*.
> Имя после `button-` должно совпадать.
![[main.00_05_28_28.Still008.jpg]]
### Как создать свою кнопку с нуля
<div style="margin:20px 0;">
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:12px;">
<div style="flex-shrink:0; width:28px; height:28px; border-radius:50%; background:#007bff; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">1</div>
<div>В заметке «MOC - Buttons» скопируйте любой готовый блок <code>```button … ```</code>.</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:12px;">
<div style="flex-shrink:0; width:28px; height:28px; border-radius:50%; background:#007bff; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">2</div>
<div>Поменяйте <code>name</code> (надпись) и <code>action</code> (какую команду запускать).</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px; margin-bottom:12px;">
<div style="flex-shrink:0; width:28px; height:28px; border-radius:50%; background:#007bff; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">3</div>
<div>Снизу задайте уникальный ярлык, например <code>^button-water</code>.</div>
</div>
<div style="display:flex; align-items:flex-start; gap:12px;">
<div style="flex-shrink:0; width:28px; height:28px; border-radius:50%; background:#007bff; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold;">4</div>
<div>В нужной заметке напишите <code>`button-water`</code> — и кнопка появится.</div>
</div>
</div>
---
## Шаг 6. Главная кнопка «Задача»: связь дневника и проектов
Кнопка **«📋 Задача»** — одна из главных. Её ярлык — `button-work-on-project`, а запускает она через QuickAdd скрипт `project-to-daily`.
Сначала о том, откуда берутся задачи. Все мои проекты лежат в заметке **MOC - Projects** (папка `1. Projects` по структуре PARA). Это **канбан-доска** на плагине Kanban: просто карточки задач, которые перетаскиваются из статуса в статус.
<div style="display:flex; flex-wrap:wrap; gap:10px; margin:20px 0; justify-content:center;">
<div style="flex:1 1 120px; min-width:110px; background:#fff5f5; border:1px solid #f0d0d0; border-radius:8px; padding:12px; text-align:center; font-size:0.9em;">💡 <b>Идеи</b></div>
<div style="align-self:center; color:#aaa;">→</div>
<div style="flex:1 1 120px; min-width:110px; background:#fff9ec; border:1px solid #efe0c0; border-radius:8px; padding:12px; text-align:center; font-size:0.9em;">🔄 <b>В работе</b></div>
<div style="align-self:center; color:#aaa;">→</div>
<div style="flex:1 1 120px; min-width:110px; background:#eefaf0; border:1px solid #c8e6d0; border-radius:8px; padding:12px; text-align:center; font-size:0.9em;">✅ <b>Готово</b></div>
</div>
Кнопка «Задача» делает две умные вещи:
1. **Подтягивает задачи из проектов в дневник.** Открывается окно с вашей канбан-доской, вы галочками отмечаете нужные карточки и нажимаете «Добавить» — они появляются в разделе «Мои планы на день».
2. **Двигает задачи по доске при выполнении.** Когда вы отмечаете задачу выполненной (`- [x]`) прямо в дневнике, она автоматически переезжает в **следующую колонку** канбана (например, из «В работе» в «Готово») и убирается из заметки.
> [!example] Есть и «турбо-режим»: если включить тумблер «🏁 В последнюю» (или дописать к задаче значок 🏁), при выполнении она поедет сразу в самую последнюю колонку доски.
### Что в скрипте заменить под себя
Скрипт лежит у меня в `0. Files/4. Templates/Scripts/project-to-daily_01.js`. Если будете брать его себе, в самом верху есть **две строки**, которые нужно настроить под свою систему:
```javascript
const MOC_PATH = "1. Projects/MOC - Projects.md"; // ← путь к ВАШЕЙ канбан-доске
const LAST_COLUMN_MARKER = "🏁"; // ← значок «сразу в последнюю колонку»
```
- **`MOC_PATH`** — укажите путь к вашей главной канбан-доске с проектами (как она названа и в какой папке лежит).
- **`LAST_COLUMN_MARKER`** — значок-метка для «турбо-режима». Можно оставить 🏁 или поставить любой свой.
Ещё один важный момент: скрипт ищет, куда вставлять задачи, по заголовку **`# Мои планы на день`**. Этот заголовок должен быть в вашей ежедневной заметке — иначе задачи добавятся в конец.
> [!note]- Полный код скрипта project-to-daily (нажмите, чтобы раскрыть)
> Подключается через QuickAdd как **User Script** в макросе, а макрос вызывается кнопкой «Задача».
>
> ```javascript
> module.exports = async (params) => {
> const { app, obsidian } = params;
> const { Modal, Component, Notice } = obsidian;
>
> const MOC_PATH = "1. Projects/MOC - Projects.md";
> const LAST_COLUMN_MARKER = "🏁"; // задача с этим маркером едет сразу в последнюю колонку
>
> async function parseKanbanBoard(file) {
> const content = await app.vault.cachedRead(file);
> const lines = content.split('\n');
> const columns = [];
> let currentColumn = null;
> let inFrontmatter = false, frontmatterCount = 0, inSettings = false;
> for (const line of lines) {
> if (line.trim() === '---') { frontmatterCount++; inFrontmatter = frontmatterCount < 2; continue; }
> if (inFrontmatter) continue;
> if (line.includes('%% kanban:settings')) { inSettings = true; continue; }
> if (inSettings) { if (line.includes('%%')) inSettings = false; continue; }
> if (line.startsWith('## ')) {
> currentColumn = { name: line.substring(3).trim(), tasks: [] };
> columns.push(currentColumn); continue;
> }
> if (currentColumn && line.trim().startsWith('- [ ]')) {
> const displayText = line.trim().replace(/^- \[ \] - /, '').replace(/^- \[ \] /, '').trim();
> currentColumn.tasks.push({ display: displayText, raw: line.trim() });
> }
> }
> return columns;
> }
>
> function resolveLink(linkText, sourcePath) {
> const clean = linkText.replace(/^\[\[/, '').replace(/\]\]$/, '').split('|')[0].trim();
> return app.metadataCache.getFirstLinkpathDest(clean, sourcePath);
> }
> function isKanban(file) {
> if (!file) return false;
> return !!app.metadataCache.getFileCache(file)?.frontmatter?.["kanban-plugin"];
> }
> function tryResolveAsFile(displayText, sourcePath) {
> const match = displayText.match(/\[\[([^\]]+)\]\]/);
> if (!match) return null;
> return resolveLink(`[[${match[1]}]]`, sourcePath);
> }
> function cleanText(s) {
> return (s || '').replace(/\[\[([^\]|]+)(\|[^\]]*)?\]\]/g, '$1').trim();
> }
> function waitForMetadataResolved(timeout = 3000) {
> return new Promise((resolve) => {
> let ref = null, timer = null;
> const finish = () => {
> if (ref) app.metadataCache.offref(ref);
> if (timer) clearTimeout(timer);
> resolve();
> };
> ref = app.metadataCache.on('resolved', finish);
> timer = setTimeout(finish, timeout);
> });
> }
> function getAllKanbanBoards() {
> return app.vault.getMarkdownFiles().filter(f =>
> app.metadataCache.getFileCache(f)?.frontmatter?.["kanban-plugin"] === "board"
> );
> }
>
> async function findAndMoveTask(taskText, toLast = false) {
> const boards = getAllKanbanBoards();
> const normalized = cleanText(taskText).toLowerCase();
> for (const board of boards) {
> const content = await app.vault.read(board);
> const lines = content.split('\n');
> let foundIdx = -1;
> let curColName = '', curColStart = -1;
> const cols = [];
> let inFM = false, fmCount = 0, inSet = false;
> for (let i = 0; i < lines.length; i++) {
> const line = lines[i];
> if (line.trim() === '---') { fmCount++; inFM = fmCount < 2; continue; }
> if (inFM) continue;
> if (line.includes('%% kanban:settings')) { inSet = true; continue; }
> if (inSet) { if (line.includes('%%')) inSet = false; continue; }
> if (line.startsWith('## ')) {
> if (curColStart !== -1) cols.push({ name: curColName, start: curColStart });
> curColName = line.substring(3).trim();
> curColStart = i; continue;
> }
> if (line.trim().startsWith('- [ ]') || line.trim().startsWith('- [x]')) {
> const t = cleanText(line.trim().replace(/^- \[[x ]\] - /, '').replace(/^- \[[x ]\] /, '')).toLowerCase();
> if (t === normalized) foundIdx = i;
> }
> }
> if (curColStart !== -1) cols.push({ name: curColName, start: curColStart });
> for (let i = 0; i < cols.length; i++)
> cols[i].end = i + 1 < cols.length ? cols[i + 1].start - 1 : lines.length - 1;
> if (foundIdx === -1) continue;
> let curColIdx = cols.findIndex(c => foundIdx >= c.start && foundIdx <= c.end);
> if (curColIdx === -1) continue;
> const targetIdx = toLast ? cols.length - 1 : curColIdx + 1;
> if (targetIdx >= cols.length || targetIdx <= curColIdx) return null;
> const targetCol = cols[targetIdx];
> const taskLine = lines[foundIdx];
> lines.splice(foundIdx, 1);
> for (const c of cols) {
> if (c.start > foundIdx) c.start--;
> if (c.end >= foundIdx) c.end--;
> }
> let insertPos = cols[targetIdx].start + 1;
> for (let i = cols[targetIdx].start + 1; i <= cols[targetIdx].end; i++) {
> if (lines[i]?.trim().startsWith('- [ ]')) insertPos = i + 1;
> }
> lines.splice(insertPos, 0, taskLine);
> await app.vault.modify(board, lines.join('\n'));
> return { taskText: cleanText(taskText), column: targetCol.name };
> }
> return null;
> }
>
> async function syncCompletedTasks() {
> const activeFile = app.workspace.getActiveFile();
> if (!activeFile) return { moved: [], linesRemoved: 0 };
> const content = await app.vault.read(activeFile);
> const lines = content.split('\n');
> const completed = [];
> const completedLineIndices = [];
> for (let i = 0; i < lines.length; i++) {
> const line = lines[i];
> const m = line.match(/^- \[x\] (.+?)(\s*\|.*)?$/);
> if (m) {
> let text = m[1].trim();
> const toLast = text.includes(LAST_COLUMN_MARKER);
> if (toLast) text = text.split(LAST_COLUMN_MARKER).join('').replace(/\s+/g, ' ').trim();
> completed.push({ text, toLast });
> completedLineIndices.push(i);
> }
> }
> if (completed.length === 0) return { moved: [], linesRemoved: 0 };
> const moved = [];
> const indicesToRemove = [];
> for (let i = 0; i < completed.length; i++) {
> const { text, toLast } = completed[i];
> const result = await findAndMoveTask(text, toLast);
> if (result) { moved.push(result); indicesToRemove.push(completedLineIndices[i]); }
> }
> if (indicesToRemove.length > 0) {
> const newLines = lines.filter((_, idx) => !indicesToRemove.includes(idx));
> await app.vault.modify(activeFile, newLines.join('\n'));
> }
> return { moved, linesRemoved: indicesToRemove.length };
> }
>
> async function addTasksToNote(taskTexts) {
> const activeFile = app.workspace.getActiveFile();
> if (!activeFile) { new Notice('❌ Нет активной заметки', 3000); return false; }
> const content = await app.vault.read(activeFile);
> const lines = content.split('\n');
> let insertAfter = -1;
> for (let i = 0; i < lines.length; i++) {
> if (lines[i].includes('# Мои планы на день')) {
> for (let j = i + 1; j < lines.length; j++) {
> if (lines[j].match(/^# /)) break;
> if (lines[j].match(/^- \[[x ]\]/)) insertAfter = j;
> }
> if (insertAfter === -1) {
> for (let j = i + 1; j < lines.length; j++) {
> if (lines[j].match(/^# /)) break;
> if (lines[j].includes('button-')) insertAfter = j;
> }
> }
> break;
> }
> }
> if (insertAfter === -1) insertAfter = lines.length - 1;
> const insertAt = insertAfter + 1;
> const taskLines = taskTexts.map(text => `- [ ] ${text}`);
> lines.splice(insertAt, 0, ...taskLines);
> await app.vault.modify(activeFile, lines.join('\n'));
> return true;
> }
>
> // ФАЗА 1 — синхронизация выполненных задач
> await new Promise(async (resolve) => {
> const result = await syncCompletedTasks();
> if (result.moved.length === 0) {
> new Notice('ℹ️ Нет выполненных задач для переноса', 2500);
> } else {
> const summary = result.moved.map(m => `✅ ${m.taskText} → ${m.column}`).join('\n');
> new Notice(`Перенесено задач: ${result.moved.length}\n${summary}\n🗑️ Удалено из заметки: ${result.linesRemoved}`, 5000);
> }
> if (result.moved.length > 0) await waitForMetadataResolved();
> resolve();
> });
>
> // ФАЗА 2 — пикер задач из канбана (множественный выбор)
> await new Promise((resolve) => {
> class TaskPickerModal extends Modal {
> constructor() {
> super(app);
> this.comp = new Component(); this.comp.load();
> this.history = [];
> this.selectedCards = new Map();
> this.addBtn = null;
> this.toLastMode = false;
> }
> onOpen() {
> this.modalEl.style.cssText = `width: 860px; max-width: 95vw; height: 80vh; display: flex; flex-direction: column; padding: 0;`;
> this.contentEl.style.cssText = `display: flex; flex-direction: column; height: 100%; overflow: hidden;`;
> this.renderBoard(MOC_PATH, 'MOC - Projects', true);
> }
> onClose() { this.comp.unload(); this.contentEl.empty(); resolve(); }
> async renderBoard(filePath, title, isRoot = false) {
> this.contentEl.empty();
> this.selectedCards.clear();
> const file = app.vault.getAbstractFileByPath(filePath);
> if (!file) { this.contentEl.createDiv({ text: `❌ Файл не найден: ${filePath}` }); return; }
> const columns = await parseKanbanBoard(file);
> const header = this.contentEl.createDiv();
> header.style.cssText = `padding: 18px 24px 14px; border-bottom: 1px solid var(--background-modifier-border); flex-shrink: 0;`;
> header.createDiv({ text: title }).style.cssText = `font-size: 1.2em; font-weight: 600;`;
> header.createDiv({ text: '✓ выбрать · 2 клика — войти в канбан' }).style.cssText = `margin-top: 4px; font-size: 0.8em; color: var(--text-muted);`;
> const body = this.contentEl.createDiv();
> body.style.cssText = `flex: 1; overflow: hidden; display: flex; flex-direction: column;`;
> this.renderKanbanBoard(body, columns, filePath, isRoot);
> const footer = this.contentEl.createDiv();
> footer.style.cssText = `padding: 12px 20px; border-top: 1px solid var(--background-modifier-border); flex-shrink: 0; display: flex; justify-content: space-between; align-items: center;`;
> const backBtn = footer.createEl('button', { text: isRoot ? '' : '← Назад' });
> backBtn.style.cssText = `padding: 7px 14px; border-radius: 6px; cursor: pointer; font-size: 0.9em; visibility: ${isRoot ? 'hidden' : 'visible'};`;
> backBtn.onclick = () => { const prev = this.history.pop(); if (prev) this.renderBoard(prev.path, prev.title, prev.isRoot); };
> const rightGroup = footer.createDiv();
> rightGroup.style.cssText = `display: flex; align-items: center; gap: 10px;`;
> const toLastBtn = rightGroup.createEl('button', { text: `${LAST_COLUMN_MARKER} В последнюю` });
> toLastBtn.style.cssText = `padding: 7px 12px; border-radius: 6px; cursor: pointer; font-size: 0.9em; border: 1px solid var(--background-modifier-border); background: var(--background-secondary); color: var(--text-normal); transition: background 0.15s, color 0.15s, border 0.15s;`;
> const syncToLastBtn = () => {
> if (this.toLastMode) {
> toLastBtn.style.background = 'var(--interactive-accent)'; toLastBtn.style.color = 'white'; toLastBtn.style.borderColor = 'var(--interactive-accent)';
> } else {
> toLastBtn.style.background = 'var(--background-secondary)'; toLastBtn.style.color = 'var(--text-normal)'; toLastBtn.style.borderColor = 'var(--background-modifier-border)';
> }
> };
> toLastBtn.onclick = () => { this.toLastMode = !this.toLastMode; syncToLastBtn(); };
> syncToLastBtn();
> this.addBtn = rightGroup.createEl('button', { text: '+ Добавить задачи (0)' });
> this.addBtn.style.cssText = `padding: 7px 16px; border-radius: 6px; cursor: pointer; font-size: 0.9em; background: var(--interactive-accent); color: white; border: none; opacity: 0.35; pointer-events: none; transition: opacity 0.15s;`;
> this.addBtn.onclick = async () => {
> if (this.selectedCards.size === 0) return;
> const tasks = Array.from(this.selectedCards.values()).map(t => this.toLastMode ? `${t} ${LAST_COLUMN_MARKER}` : t);
> const ok = await addTasksToNote(tasks);
> if (ok) { new Notice(`✅ Добавлено задач: ${tasks.length}`, 3000); this.close(); }
> };
> }
> updateAddButton() {
> if (!this.addBtn) return;
> const count = this.selectedCards.size;
> this.addBtn.textContent = `+ Добавить задачи (${count})`;
> if (count > 0) { this.addBtn.style.opacity = '1'; this.addBtn.style.pointerEvents = 'auto'; }
> else { this.addBtn.style.opacity = '0.35'; this.addBtn.style.pointerEvents = 'none'; }
> }
> renderKanbanBoard(container, columns, sourcePath, isRoot) {
> if (columns.length === 0) {
> container.createDiv({ text: 'Колонки не найдены' }).style.cssText = `text-align:center; padding: 60px; opacity: 0.6;`;
> return;
> }
> const board = container.createDiv();
> board.style.cssText = `display: flex; flex-direction: row; gap: 14px; padding: 16px 20px; overflow-x: auto; overflow-y: hidden; height: 100%; align-items: flex-start; box-sizing: border-box;`;
> columns.forEach(column => {
> const col = board.createDiv();
> col.style.cssText = `min-width: 200px; max-width: 240px; flex-shrink: 0; display: flex; flex-direction: column; background: var(--background-secondary); border-radius: 10px; overflow: hidden; border: 1px solid var(--background-modifier-border); max-height: 100%;`;
> const colHeader = col.createDiv();
> colHeader.style.cssText = `padding: 10px 14px; font-weight: 600; font-size: 0.9em; background: var(--background-modifier-border); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;`;
> colHeader.createSpan({ text: column.name });
> const badge = colHeader.createSpan({ text: String(column.tasks.length) });
> badge.style.cssText = `background: var(--interactive-accent); color: white; border-radius: 10px; padding: 1px 7px; font-size: 0.78em; font-weight: 600;`;
> const taskList = col.createDiv();
> taskList.style.cssText = `padding: 8px; overflow-y: auto; flex: 1;`;
> if (column.tasks.length === 0) {
> taskList.createDiv({ text: 'Нет задач' }).style.cssText = `padding: 12px; font-size: 0.82em; color: var(--text-muted); text-align: center; opacity: 0.6;`;
> }
> column.tasks.forEach(task => {
> const linkedFile = tryResolveAsFile(task.display, sourcePath);
> const canDrill = linkedFile && isKanban(linkedFile);
> const cleanDisplay = task.display.replace(/\[\[([^\]|]+)(\|[^\]]*)?\]\]/g, '$1');
> const cardWrapper = taskList.createDiv();
> cardWrapper.style.cssText = `display: flex; align-items: flex-start; gap: 6px; margin-bottom: 6px; cursor: pointer;`;
> const checkbox = cardWrapper.createEl('input', { type: 'checkbox' });
> checkbox.style.cssText = `margin-top: 10px; cursor: pointer; flex-shrink: 0;`;
> const card = cardWrapper.createDiv();
> card.style.cssText = `padding: 8px 10px; border-radius: 7px; flex: 1; background: var(--background-primary); border: 1px solid ${canDrill ? 'var(--interactive-accent)' : 'var(--background-modifier-border)'}; font-size: 0.88em; line-height: 1.4; transition: background 0.1s, border 0.1s; user-select: none;`;
> card.textContent = cleanDisplay;
> if (canDrill) card.createSpan({ text: ' ↗' }).style.cssText = `opacity: 0.5; font-size: 0.85em;`;
> card.dataset.canDrill = String(canDrill);
> const taskId = `${sourcePath}::${task.display}`;
> const toggleSelection = () => {
> if (this.selectedCards.has(taskId)) {
> this.selectedCards.delete(taskId); checkbox.checked = false;
> card.style.background = 'var(--background-primary)';
> card.style.borderColor = canDrill ? 'var(--interactive-accent)' : 'var(--background-modifier-border)';
> card.style.color = '';
> } else {
> this.selectedCards.set(taskId, task.display); checkbox.checked = true;
> card.style.background = 'var(--interactive-accent)'; card.style.color = 'white';
> card.style.borderColor = 'var(--interactive-accent)';
> }
> this.updateAddButton();
> };
> checkbox.addEventListener('change', (e) => { e.stopPropagation(); toggleSelection(); });
> cardWrapper.addEventListener('click', (e) => { if (e.target !== checkbox) toggleSelection(); });
> card.addEventListener('dblclick', (e) => {
> e.stopPropagation();
> const target = linkedFile || tryResolveAsFile(task.display, sourcePath);
> if (target && isKanban(target)) {
> this.history.push({ path: sourcePath, title: app.vault.getAbstractFileByPath(sourcePath)?.basename || sourcePath, isRoot });
> this.renderBoard(target.path, target.basename, false);
> }
> });
> });
> });
> }
> }
> new TaskPickerModal().open();
> });
> };
> ```
![[main.00_06_17_26.Still009.jpg]]
<div class="cta-section" style="padding: 15px; text-align: center; background-color: #f0f0f0; border-radius: 6px; margin: 20px 0;">
<h3>Для тех, кто не хочет долго разбираться</h3>
<p style="font-size: 0.95em; margin: 10px 0;">Попробуйте мой готовый шаблон Obsidian и начните систематизировать информацию уже сегодня</p>
<a href="https://eltonlabs.org/template" style="display: inline-block; padding: 8px 16px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; cursor: pointer; font-size: 0.95em; margin-top: 10px;">Узнать про шаблон</a>
</div>
---
## Шаг 7. Автоматический перенос невыполненных задач
В шаблоне, помимо кнопок, есть ещё один код — он переносит **невыполненные задачи с прошлого дня** на сегодня. Работает на плагине **Templater**: при создании новой заметки код срабатывает один раз и превращается в обычный список задач.
> [!quote]- 🛠️ Что такое Templater
> **Templater** — плагин для «умных» шаблонов. Обычный шаблон просто вставляет один и тот же текст. А шаблон Templater может **думать**: подставить сегодняшнюю дату, день недели или, как у нас, найти вчерашние невыполненные задачи и перенести их.
>
> Работает он один раз — в момент создания заметки. Код (он пишется между значками `<%` и `%>`) срабатывает и исчезает, оставляя вместо себя обычный готовый текст.
>
> Вам не нужно в нём разбираться: достаточно один раз вставить код в шаблон — дальше всё происходит автоматически.
Логика простая и очень удобная:
<div style="margin:20px 0; background:#f4f6fb; border:1px solid #e0e4ee; border-radius:10px; padding:18px;">
<div style="display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:12px; text-align:center;">
<div style="flex:1 1 160px; min-width:150px;">📄 <b>Вчерашняя заметка</b><br><span style="font-size:0.85em; color:#666;">находим последнюю заметку перед текущим днём</span></div>
<div style="font-size:1.5em; color:#007bff;">→</div>
<div style="flex:1 1 160px; min-width:150px;">🔍 <b>Ищем «- [ ]»</b><br><span style="font-size:0.85em; color:#666;">собираем все незакрытые задачи</span></div>
<div style="font-size:1.5em; color:#007bff;">→</div>
<div style="flex:1 1 160px; min-width:150px;">📥 <b>Сегодня</b><br><span style="font-size:0.85em; color:#666;">вставляем их в новую заметку</span></div>
</div>
</div>
Вот этот код из шаблона (вставляется как есть, ничего менять не нужно — кроме, при желании, пути к папке с ежедневными заметками):
```javascript
<%*
const moment = tp.obsidian.moment;
const fn = tp.file.title;
const date = moment(fn, 'DD-MM-YYYY');
// По воскресеньям добавляем кнопку резервной копии
if (date.isValid() && date.day() === 0) {
tR += '- [ ] `button-local-backup`\n';
}
const pathToDailyNotes = "2. Areas/Дневники/Ежедневные заметки"; // ← ваш путь
const currentDate = moment(tp.file.title, 'DD-MM-YYYY');
const today = moment().startOf('day');
const referenceDate = currentDate.isAfter(today) ? today.clone().add(1, 'day') : currentDate;
function findLatestDailyNoteBeforeDate(beforeDate) {
const folder = app.vault.getAbstractFileByPath(pathToDailyNotes);
if (!folder || !folder.children) return null;
const dailyFiles = folder.children
.filter(file => {
if (file.extension !== "md") return false;
if (!file.name.match(/^\d{2}-\d{2}-\d{4}\.md$/)) return false;
const fileDate = moment(file.name.replace('.md', ''), 'DD-MM-YYYY');
return fileDate.isBefore(beforeDate) && fileDate.isSameOrBefore(today);
})
.sort((a, b) => {
const dateA = moment(a.name.replace('.md', ''), 'DD-MM-YYYY');
const dateB = moment(b.name.replace('.md', ''), 'DD-MM-YYYY');
return dateB.valueOf() - dateA.valueOf();
});
return dailyFiles.length > 0 ? dailyFiles[0] : null;
}
if (!currentDate.isValid()) {
tR += `❌ Не удалось определить дату из названия файла. Формат: DD-MM-YYYY.`;
} else {
const yesterday = referenceDate.clone().subtract(1, 'day').format('DD-MM-YYYY');
let targetFile = app.vault.getAbstractFileByPath(`${pathToDailyNotes}/${yesterday}.md`);
let targetDate = yesterday;
if (!targetFile) {
targetFile = findLatestDailyNoteBeforeDate(referenceDate);
if (targetFile) targetDate = targetFile.name.replace('.md', '');
}
if (!targetFile) {
tR += `❌ Нет ежедневных заметок до ${referenceDate.format('DD-MM-YYYY')}.`;
} else {
const fileContent = await app.vault.read(targetFile);
const tasks = fileContent.split("\n").filter(line => line.trim().startsWith("- [ ]"));
if (tasks.length === 0) {
tR += `✅ Нет невыполненных задач за ${targetDate}.`;
} else {
tR += `## 🔁 Невыполненные задачи из ${targetDate} (последняя заметка)\n\n`;
tR += tasks.join("\n");
}
}
}
%>
```
> [!tip] Если у вашего дневника был перерыв в несколько дней, скрипт всё равно найдёт **последнюю** заметку перед текущим днём и подтянет задачи из неё — ничего не потеряется. Менять нужно только `pathToDailyNotes`, если у вас другая папка.
---
## Шаг 8. Кнопки привычек и питания (Meta Bind)
Дальше в шаблоне идут привычки. Они работают на плагине **Meta Bind**, который позволяет редактировать свойства заметки **прямо из текста** — не нужно листать вверх и открывать панель свойств.
> [!quote]- 🔗 Что такое Meta Bind
> **Meta Bind** связывает свойства заметки (те самые из шага 2) с удобными элементами прямо в тексте: переключателями, галочками, полями ввода.
>
> Без него, чтобы отметить привычку, пришлось бы листать вверх и менять свойство вручную. С ним — вы просто щёлкаете тумблер «🏃 Спорт» по ходу заметки, и галочка сама встаёт в свойствах.
>
> Запись вида `INPUT[toggle:Спорт]` как раз и означает: «поставь здесь переключатель, привязанный к свойству Спорт».
```
`INPUT[toggle:Спорт]` 🏃♂️ Спорт
`INPUT[toggle:Чтение]` 📖 Чтение
`INPUT[toggle:Прогулка]` 🚶♂️ Прогулка
```
Здесь `INPUT[toggle:Спорт]` — это переключатель, привязанный к свойству `Спорт`. Нажали тумблер в теле заметки — галочка автоматически встала в свойствах. А поскольку графики берут данные именно из свойств, ваша привычка тут же попадает в статистику.
Питание устроено так же, только через текстовые поля:
```
🥚 **Завтрак:** `INPUT[text:Завтрак]`
🍕 **Обед:** `INPUT[text:Обед]`
🥧 **Ужин:** `INPUT[text:Ужин]`
🥨 **Перекус:** `INPUT[text:Перекусы]`
**Итого:** `INPUT[text:Итого_ккал]`
`button-calories`
```
В конце стоит кнопка `button-calories` — **«🌮 Посчитать калории»**. Это моя личная кнопка, которая с помощью ИИ прикидывает калорийность по тому, что я вписал в поля питания.
> [!info] Идеально каждый день заполнять не получается — и это нормально. Но даже пара заполненных дней в неделю дают наглядную динамику на графиках привычек и калорий.
![[main.00_08_00_24.Still010.jpg]]
---
## Шаг 9. Мысли — самый важный раздел
И самое главное — в самом низу шаблона раздел **«Мысли»**. Здесь нет ни кода, ни шаблона. Это абсолютно пустое пространство для рассуждений.
Сюда я в течение дня записываю:
- задачи на будущее и идеи;
- проблемы, которые меня беспокоят;
- размышления и наблюдения.
> [!quote] Если я не могу решить задачу прямо сейчас, я не давлю на себя. Я пишу в «Мыслях», *почему* не могу за неё взяться. Часто оказывается, что мозг зациклен на другой, внешней задаче — и осознание этого само снимает ступор.
Эти мысли работают на меня и дальше:
1. **На домашней странице.** Завтра я увижу вчерашние мысли в специальном блоке на главной (Homepage).
2. **В заметке MOC - Дневники.** Там стоит код Dataview, который собирает мысли по месяцам — день за днём. Из них рождаются новые задачи и атомарные заметки.
3. **С телефона.** У меня настроен виджет, который пишет мысль сразу в нужный раздел сегодняшней заметки. Незаменимо, когда идея пришла на улице.
![[main.00_08_53_23.Still011.jpg]]
---
## Бумажные дневники тоже в системе
Я не отказался от бумаги полностью — иногда приятно писать от руки. В моей системе для этого есть кнопка **«📸 Фото в текст»** (ярлык `button-image-2-text`, скрипт `image-to-text`).
Логика проста: фотографирую страницу бумажного блокнота → кнопка распознаёт рукописный текст с помощью ИИ → я могу скопировать его в ежедневную заметку или создать из него новую заметку.
<div style="margin:20px 0; background:#f4f6fb; border:1px solid #e0e4ee; border-radius:10px; padding:18px;">
<div style="display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:12px; text-align:center;">
<div style="flex:1 1 150px; min-width:140px;">📓 <b>Бумажный лист</b></div>
<div style="font-size:1.5em; color:#007bff;">→</div>
<div style="flex:1 1 150px; min-width:140px;">📸 <b>Фото в текст (ИИ)</b></div>
<div style="font-size:1.5em; color:#007bff;">→</div>
<div style="flex:1 1 150px; min-width:140px;">📝 <b>Текст в заметке</b></div>
</div>
</div>
А ещё я люблю рисовать схемы от руки — и любой такой рисунок легко вставляется прямо в заметку (через плагин Excalidraw или ту же кнопку рисунка). Так что ведение дневника в Obsidian **не ограничено по функционалу**: бумага, рисунки, голос, фото — всё стекается в одно место.
![[main.00_09_53_06.Still012.jpg]]
---
## Итог
Вот и вся система. Выглядит масштабно, но собирается она из простых кирпичиков:
<div style="margin:20px 0;">
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><b style="flex-shrink:0; width:34px;">1.</b><span>Плагины: Dataview, Meta Bind, Homepage, Templater, Buttons, QuickAdd.</span></div>
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><b style="flex-shrink:0; width:34px;">2.</b><span>Встроенные «Ежедневные заметки» + шаблон в папке Areas (PARA).</span></div>
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><b style="flex-shrink:0; width:34px;">3.</b><span>Свойства (YAML) для привычек и питания.</span></div>
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><b style="flex-shrink:0; width:34px;">4.</b><span>Виджет Google Calendar через scripts.google.com.</span></div>
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid #eee;"><b style="flex-shrink:0; width:34px;">5.</b><span>Кнопки и скрипт связи дневника с проектами.</span></div>
<div style="display:flex; gap:10px; padding:8px 0;"><b style="flex-shrink:0; width:34px;">6.</b><span>Автоперенос задач, привычки, питание и раздел мыслей.</span></div>
</div>
> [!tip] Главный совет: **не внедряйте всё сразу.** Начните с включённых ежедневных заметок и одной кнопки утреннего ритуала. Когда это войдёт в привычку — добавьте перенос задач, потом календарь, потом проекты. Система должна разгружать мозг, а не нагружать его.
В следующих материалах я покажу, как сделал себе плагин для чтения книг (выделяю фрагменты и создаю из них заметки) и как уточняю информацию у ИИ прямо в процессе чтения.
<div class="cta-section" style="padding: 15px; text-align: center; background-color: #f0f0f0; border-radius: 6px; margin: 20px 0;">
<h3>Для тех, кто не хочет долго разбираться</h3>
<p style="font-size: 0.95em; margin: 10px 0;">Попробуйте мой готовый шаблон Obsidian и начните систематизировать информацию уже сегодня</p>
<a href="https://eltonlabs.org/template" style="display: inline-block; padding: 8px 16px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; cursor: pointer; font-size: 0.95em; margin-top: 10px;">Узнать про шаблон</a>
</div>
---
© 2026 Elton Labs. Все права защищены.