# Как я веду ежедневные заметки в 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. Все права защищены.