Введение
Около двух лет я использую Obsidian как основное приложение для ведения заметок, планирования и написания статей. В этой статье я хочу поделиться своим полным workflow: от распределения заметок по папкам и горячих клавиш до детального Cheatsheet по хранилищу и организации метода Zettelkasten.
Сразу уточню: Obsidian становится действительно удобным только тогда, когда человек сам собирает под себя хранилище и находить подходящий ему метод. Ниже я разбираю реализацию по частям, чтобы каждый мог изучить интересующий блок, взять полезные идеи или настроить всё аналогично у себя.
Ниже — краткий план того, о чём будет статья:
Архитектура папок (где и что хранится, а также подробный
Cheatsheet.md, который я составлял для себя).Плагины, которые я использую.
Настройка Homepage и каталога книг.
Синхронизация ПК + телефон (плагин Git + Termux на телефоне, у меня репозиторий в Gitea).
Архитектура
Это исчерпывающее руководство по моей системе заметок и записей (Obsidian и Habitica). Если я что-то забуду — вся информация здесь.
Obsidian
Термины и правила
Файлы-якоря (taglinks) — замена тегов в их привычном понимании. Смысл в том, что это те же теги, но в виде отдельных файлов. Их преимущество — явная наглядность и возможность добавить внутреннюю логику (например, указать прямые ссылки на другие файлы для быстрого поиска или расписать описание самого тега). Имеют вид
[[Мой якорь]], где скобки обозначают ссылку на файл.Индекс (index) — taglink с расширенной внутренней логикой. Как правило, у такого index-файла много связей с другими «полноценными» заметами. В начале имени любого индекса ставится
00, а название пишется с большой буквы. Например, taglink[[физика]]превращается в[[00 Физика]]. Это нужно для того, чтобы с первого взгляда отделить индекс от обычного taglink.-
Теги (tags) — классические теги с решёткой (например:
#тег). Используются для неявного указания свойств файла. Я указываю тип файла (#plan,#note,#zettel,#book,#index) для быстрого поиска. Если это#zettel-файл, я добавляю теги прогресса#baby,#teen,#adult:#baby— заметка только на стадии зарождения;#teen— заметка в процессе написания;#adult— заметка самодостаточна и всегда полезна (вечнозелёная).
Важно помнить: даже если заметка уже «выросла», её всё равно можно дополнять. Для этого создаётся новый файл со ссылкой на старый, который раскрывает тему дальше или с другого ракурса.
Типы файлов
note— для простых и оперативных записей (которые со временем могут потерять актуальность);plan— для планов, программ и проектов;zettel— для постоянных знаний в Zettelkasten (статусы:#baby,#teen,#adult);book— для конспектов и карточек книг;index— для индексов.
Файловая структура
_files
Здесь хранятся все медиафайлы, используемые в заметках (фотографии, схемы, рисунки и т.д.).
_templates
Здесь хранятся шаблоны для разных типов файлов.
-
[[Book temp]]— шаблон для книг в папкеBooks. Состоит из:title(название книги),cover(ссылка на обложку),author(автор),genre(жанр),tags(#book),taglinks(ссылки на файлы-якоря),status(статус: прочитана / читаю / в планах),current_page(текущая страница),total_pages(всего страниц),date_started(дата начала чтения),date_ended(дата окончания чтения),other_internal_links(дополнительные ссылки на связанные файлы; по умолчанию стоит ссылка на каталог книг),other_external_links(дополнительные ссылки на внешние ресурсы).
-
[[Index temp]]— шаблон для индексов в папкеIndexes. Состоит из:created_at(дата и время создания),tags(#index),other_internal_links(дополнительные внутренние ссылки),other_external_links(дополнительные внешние ссылки).
-
[[Note temp]]— шаблон для заметок в папкеPlans & Notes. Состоит из:created_at(дата и время создания),tags(#note),taglinks(ссылки на файлы-якоря),other_internal_links(дополнительные внутренние ссылки),other_external_links(дополнительные внешние ссылки).
-
[[Plan temp]]— шаблон для планов и программ в папкеPlans & Notes. Состоит из:created_at(дата и время создания),tags(#plan),taglinks(ссылки на файлы-якоря),other_internal_links(дополнительные внутренние ссылки),other_external_links(дополнительные внешние ссылки).
-
[[Zettel temp]]— шаблон для записей в папкеZettelkasten. Состоит из:created_at(дата и время создания),tags(#zettelи#baby, который затем меняется на#teenи#adultпо мере готовности),taglinks(ссылки на файлы-якоря),other_internal_links(дополнительные внутренние ссылки),other_external_links(дополнительные внешние ссылки).
Taglinks
Папка для хранения всех файлов-якорей.
Indexes
Хранилище всех индексов (файлов с внутренней логикой). Позволяет быстро находить нужные темы через систему связей.
Plans & Notes
Здесь хранятся все планы, программы (
plans) и обычные заметки (notes). Отдельная папка нужна, чтобы не засорять основную базу, так как эти записи не имеют бесконечной ценности.
Zettelkasten
Главная база знаний. Сюда попадают идеи и материалы, содержащие ценные умозаключения, которые пригодятся в будущем.
Главный критерий: «Хотел бы я, чтобы эта информация осталась со мной надолго?». Если ответ «да» — материал ценен и его нужно зафиксировать.
-
Rough Notes (Черновики):
Что это: Папка для «сырых» и только зарождающихся заметок. Все записи в Zettelkasten начинаются отсюда.
Что внутри: Заметки с потенциалом, еще не получившие законченную форму. Это «рассада» моей базы знаний.
Критерий: Если заметка неполная или требует доработки — она остаётся здесь.
-
Grown Notes (Выросшие заметки):
Что это: Основное хранилище ценных и сформированных заметок.
Что внутри: Доработанные записи с законченной мыслью, понятные без стороннего контекста.
Критерий: Как только заметка становится самодостаточной, я перемещаю её из
Rough NotesвGrown Notes.
Books
Папка для прочитанных книг. Главный файл — Каталог, в котором наглядно отображаются все книги.
Создаётся файл с названием книги, применяется и заполняется шаблон. Ниже заголовка можно писать краткие мысли. Если требуется полноценный разбор книги, создаётся отдельный файл, который отправляется в
Zettelkasten.
Остальные файлы
[[Cheatsheet.md]]— исчерпывающая шпаргалка по системе (этот файл);[[Homepage.md]]— домашняя страница с ключевыми ссылками;[[Inbox.md]]— файл для быстрых заметок и невыполненных за день дел;Bookmarks — встроенные закладки Obsidian для мгновенного доступа к частым файлам.
Основные горячие клавиши
Alt + S— вставка шаблонаAlt + Q— вставка CalloutCtrl + O— открыть Quick SwitcherCtrl + P— открыть Command PaletteCtrl + E— переключить режим просмотра (Current View)Ctrl + ЛКМ— открыть ссылку в новой вкладкеCtrl + Shift + F— поиск по всем файламCtrl + ↑— переместить строку вверхCtrl + ↓— переместить строку внизAlt + 1— замена сниппета (Snippet Replacement)Alt + A— свернуть все списки и заголовкиAlt + D— развернуть все списки и заголовки
Как правильно записывать информацию?
Найти интересный материал.
Прочитать и разбирать его до полного понимания.
Закрыть источник и записать суть в Obsidian в удобном формате (рисунок, таблица, список, текст).
Задать себе вопросы: «Будет ли польза от этой заметки в будущем?» и «В каком конкретном сценарии я это применю?». Если ответы положительные — оставляем.
Проставить ссылки (
taglinks,other_internal_links), теги и проверить связность с другими файлами (если это не#index).Открыть первоисточник и сверить точность записей, при необходимости скорректировать.
Перечитать заметку и проверить, понятен ли материал. Если есть пробелы — повторить шаг 6, пока заметка не станет на 100% понятной и автономной.
При возникновении сопутствующих вопросов создать новую связанную заметку и пройти алгоритм с 1-го пункта.
Habitica
Привычки (habits)
Регулярные или пассивные действия, которые выполняются в течение дня.
Дела (tasks)
Быстрые разовые задачи с коротким сроком жизни (1–2 дня): загрузить посудомойку, помыть пол и т.д.
Ежедневные дела (recurring tasks)
Обязательные регулярные задачи, формирующие распорядок дня.
Отложенные (deferred)
Задачи на перспективу («когда-нибудь»). Находятся в мобильном приложении (HabitNow / Habitica) для оперативного доступа с телефона. Задачи из Obsidian
Inboxкасаются именно рабочих процессов за ПК, поэтому их удобнее вести за компьютером.
Плагины, которые я использую
Better Word Count — подсчёт слов и символов.
Commander — кастомизация интерфейса и добавление быстрых команд.
Dataview — агрегация и отображение сложных данных через запросы.
Excalidraw — рукописные схемы и подготовка к экзаменам на графическом планшете.
Git — автоматическая синхронизация с репозиторием.
Homepage — настройка стартовой домашней страницы.
Ink — быстрое создание рукописных заметок.
List Callouts — стильное оформление списков и акцентов.
Templater — расширенная работа с шаблонами и скриптами.
Text Snippets — автозамена текста на заранее заготовленные шаблоны.
Настройка Homepage
Файл домашней страницы (Homepage.md):
--- cssclasses: dashboard --- <div class="fade-banner"> <img src="_files/sandpile.jpg" alt="Banner"> <div class="banner-gradient"></div> </div> # ?️ My Dashboard - ## ? [[3 - Plans & Notes/NetDevSecOps.canvas|NetDevSecOps]] - ## ? [[5 - Books/00 Каталог.md|Каталог книг]] - ## ? [[Inbox]] --- - ### ?️ Тренировки - [[Тренировки зал full-body]] - [[3 - Plans & Notes/Тренировки лето новые.md|Тренировки лето]] - [[Доп. Тренировки (3 в неделю)]] - ### ? Цели и Планы - [[3 - Plans & Notes/Цели на 2026.md|Цели на 2026]] - [[3 - Plans & Notes/Еженедельный план 2026.md|Еженедельный план 2026]] - [[2 - Indexes/00 Хочу.md|Все что я хочу]] - ### ? Полезные материалы - [[Interview Questions|Вопросы DevOps Advanced]] - [[DevOps Questions Basic Level|Вопросы DevOps Basic]] - [[Механизм минимальных затрат]]
CSS-сниппет для оформления панели (простой стиль без лишних элементов):
.fade-banner { position: relative; width: 100%; height: 200px; overflow: hidden; margin-bottom: 25px; border-radius: 8px; } .fade-banner img { width: 100%; height: 100%; object-fit: cover; } .banner-gradient { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to bottom, transparent 50%, var(--background-primary) 100%); } .dashboard { font-family: inherit; padding-left: 25px !important; padding-right: 25px !important; padding-top: 20px !important; } .dashboard .markdown-preview-section { max-width: 100%; } .dashboard h1 { font-size: 2em; font-weight: 600; margin-bottom: 20px; } .dashboard div>ul>li>b, .dashboard div>ul>li>strong { display: block; font-size: 1.2em; font-weight: 600; margin-bottom: 12px; color: var(--text-normal); } .dashboard div>ul>li { background-color: transparent !important; border: none !important; box-shadow: none !important; padding: 10px !important; } .dashboard div>ul { list-style: none; display: flex; column-gap: 40px; row-gap: 25px; flex-flow: row wrap; padding-left: 0px !important; } .dashboard div>ul>li { min-width: 260px; flex: 1; } .dashboard div.markdown-preview-section>div>ul>li>.list-bullet { display: none !important; } .dashboard.markdown-rendered.show-indentation-guide li>ul::before, .dashboard.markdown-rendered.show-indentation-guide li>ol::before { display: none; } div.markdown-preview-section>div>ul.has-list-bullet>li { padding-left: 0px !important; } .dashboard ul.dataview { row-gap: 0px !important; list-style-type: disc !important; } .dashboard-welcome { font-size: 1.25em; font-weight: 800; letter-spacing: 7px; text-transform: uppercase; color: #FFFFFF !important; text-shadow: 0px 2px 4px rgba(255, 255, 255, 0.6), 0px 4px 10px rgba(0, 0, 0, 0.4); margin-bottom: -15px; opacity: 1; text-align: center; display: block; }
Настройка каталога книг
Основной файл каталога (работает на dataviewjs):
const bookPages = dv.pages('"5 - Books"').where(p => p.title); const container = this.container; const style = document.createElement("style"); style.textContent = ` .book-table { width: 100%; border-collapse: collapse; font-family: Cascadia Mono, monospace; margin-top: 1rem; font-size: 15px; } .book-table th { background: rgba(138, 43, 226, 0.15); color: #d6bcfa; padding: 12px 14px; border-bottom: 2px solid rgba(214, 188, 250, 0.3); font-weight: 600; text-align: left; } .book-table td { padding: 12px 14px; border-bottom: 1px solid rgba(214, 188, 250, 0.15); color: #e9d8fd; vertical-align: middle; } .book-table tr { transition: all 0.2s ease; } .book-table tr:hover { background: rgba(138, 43, 226, 0.12) !important; } .filter-container { background: rgba(138, 43, 226, 0.1); backdrop-filter: blur(10px); border-radius: 12px; padding: 1.2rem; margin-bottom: 2rem; border: 1px solid rgba(214, 188, 250, 0.2); display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; } .filter-select, .filter-search { padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(214, 188, 250, 0.3); background: rgba(26, 26, 32, 0.8); color: #e9d8fd; font-size: 14px; outline: none; transition: all 0.2s ease; height: 40px; box-sizing: border-box; font-family: Cascadia Mono, monospace; } .filter-select:hover, .filter-search:focus { border-color: rgba(214, 188, 250, 0.6); box-shadow: 0 0 0 4px rgba(138, 43, 226, 0.15); } .filter-search { flex: 1; min-width: 250px; } .book-title-link { color: #d6bcfa; text-decoration: none; font-weight: 600; font-size: 16px; transition: color 0.2s; line-height: 1.3; } .book-title-link:hover { color: #e9d8fd; } .book-meta-text { font-size: 12px; color: rgba(214, 188, 250, 0.7); margin-top: 3px; } .status-dropdown { padding: 4px 10px; border-radius: 14px; font-size: 13px; font-weight: 600; border: 1px solid transparent; outline: none; cursor: pointer; transition: all 0.2s; width: 130px; text-align: center; height: 36px; box-sizing: border-box; font-family: Cascadia Mono, monospace; } .page-num-input { width: 55px; background: rgba(138, 43, 226, 0.15); border: 1px solid rgba(214, 188, 250, 0.3); color: #e9d8fd; border-radius: 6px; padding: 4px 6px; text-align: center; font-size: 13px; font-family: Cascadia Mono, monospace; outline: none; transition: all 0.2s; box-sizing: border-box; } .page-num-input:focus { border-color: rgba(214, 188, 250, 0.6); background: rgba(26, 26, 32, 0.9); box-shadow: 0 0 0 3px rgba(138, 43, 226, 0.2); } .page-num-input::-webkit-outer-spin-button, .page-num-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .page-num-input { -moz-appearance: textfield; } .progress-bar-container { background: rgba(214, 188, 250, 0.15); border-radius: 12px; height: 6px; width: 100%; overflow: hidden; } .progress-bar-fill { background: linear-gradient(90deg, #8A2BE2, #9370DB); height: 100%; transition: width 0.3s ease; } .book-cover-img { width: 75px; height: 108px; border-radius: 8px; object-fit: cover; box-shadow: 0 6px 14px rgba(0,0,0,0.4); border: 1px solid rgba(214, 188, 250, 0.2); transition: transform 0.2s; cursor: pointer; } .book-cover-img:hover { transform: scale(1.05); } .total-page-label { font-size: 12px; color: rgba(214, 188, 250, 0.8); white-space: nowrap; } /* Мобильная оптимизация */ @media (max-width: 600px) { .book-table th, .book-table td { padding: 8px 6px; font-size: 13px; } .book-cover-img { width: 50px; height: 72px; } .book-title-link { font-size: 14px; } .book-meta-text { font-size: 11px; } .status-dropdown { width: 105px; font-size: 12px; height: 32px; padding: 2px 4px; } .page-num-input { width: 45px; font-size: 12px; padding: 3px 2px; } .total-page-label { font-size: 11px; } .filter-container { padding: 0.8rem; flex-direction: column; align-items: stretch; gap: 0.6rem; } .filter-select, .filter-search { width: 100%; } } `; document.head.appendChild(style); const filterContainer = document.createElement("div"); filterContainer.className = "filter-container"; const genreSelect = document.createElement("select"); genreSelect.className = "filter-select"; const allOption = document.createElement("option"); allOption.textContent = "Все жанры"; allOption.value = ""; genreSelect.appendChild(allOption); const allGenres = new Set(); for (let p of bookPages) { if (p.genre) { const genres = Array.isArray(p.genre) ? p.genre : [p.genre]; genres.forEach(g => g && allGenres.add(g)); } } for (let genre of Array.from(allGenres).sort()) { const opt = document.createElement("option"); opt.textContent = genre; opt.value = genre; opt.style.background = "#1a1a20"; genreSelect.appendChild(opt); } filterContainer.appendChild(genreSelect); const statusSelect = document.createElement("select"); statusSelect.className = "filter-select"; const statusOptions = [ { value: "", text: "Все статусы" }, { value: "reading", text: "Читаю" }, { value: "completed", text: "Прочитано" }, { value: "planned", text: "В планах" } ]; statusOptions.forEach(o => { const opt = document.createElement("option"); opt.textContent = o.text; opt.value = o.value; opt.style.background = "#1a1a20"; statusSelect.appendChild(opt); }); filterContainer.appendChild(statusSelect); const searchInput = document.createElement("input"); searchInput.type = "text"; searchInput.className = "filter-search"; searchInput.placeholder = "? Поиск по названию или автору..."; filterContainer.appendChild(searchInput); container.appendChild(filterContainer); function determineBookStatus(page) { if (page.status) return page.status; const today = new Date().toISOString().split('T')[0]; if (page.date_ended && page.date_ended <= today) return "completed"; if (page.date_started) return "reading"; const total = page.total_pages || 100; const current = page.current_page || 0; if (total > 0 && current >= total) return "completed"; if (page.current_page && page.current_page > 0) return "reading"; return "planned"; } function applyStatusStyle(selectElement, status) { if (status === "completed") { selectElement.style.background = "rgba(102, 221, 170, 0.15)"; selectElement.style.color = "#66ddaa"; selectElement.style.borderColor = "rgba(102, 221, 170, 0.25)"; } else if (status === "reading") { selectElement.style.background = "rgba(147, 112, 219, 0.15)"; selectElement.style.color = "#9370db"; selectElement.style.borderColor = "rgba(147, 112, 219, 0.25)"; } else { selectElement.style.background = "rgba(214, 188, 250, 0.15)"; selectElement.style.color = "#d6bcfa"; selectElement.style.borderColor = "rgba(214, 188, 250, 0.25)"; } } async function updatePageProperty(page, property, value) { try { const file = app.vault.getAbstractFileByPath(page.file.path); if (!file) return false; await app.fileManager.processFrontMatter(file, (fm) => { if (value === undefined || value === "") { delete fm[property]; } else { fm[property] = value; } }); page[property] = value; return true; } catch (e) { console.error(e); return false; } } async function removePageProperty(page, property) { try { const file = app.vault.getAbstractFileByPath(page.file.path); if (!file) return false; await app.fileManager.processFrontMatter(file, (fm) => { delete fm[property]; }); delete page[property]; return true; } catch (e) { console.error(e); return false; } } function renderTable(selectedGenre = "", selectedStatus = "", searchTerm = "") { const oldTable = container.querySelector(".book-table"); if (oldTable) oldTable.remove(); const table = document.createElement("table"); table.className = "book-table"; const header = table.insertRow(); ["Обложка", "Книга", "Статус", "Прогресс"].forEach(t => { const th = document.createElement("th"); th.textContent = t; header.appendChild(th); }); for (let p of bookPages) { const genreMatch = !selectedGenre || (p.genre && ((Array.isArray(p.genre) && p.genre.includes(selectedGenre)) || p.genre === selectedGenre)); const actualStatus = determineBookStatus(p); const statusMatch = !selectedStatus || actualStatus === selectedStatus; const searchMatch = !searchTerm || (p.title && p.title.toLowerCase().includes(searchTerm.toLowerCase())) || (p.author && p.author.toLowerCase().includes(searchTerm.toLowerCase())); if (!(genreMatch && statusMatch && searchMatch)) continue; let total = p.total_pages || 100; let current = p.current_page || 0; let percent = total > 0 ? Math.round((current / total) * 100) : 0; const row = table.insertRow(); row.onclick = (e) => { if (!e.target.closest('a') && !e.target.closest('select') && !e.target.closest('input') && !e.target.closest('img')) { window.location.href = p.file.path; } }; const cellCover = row.insertCell(); if (p.cover) { const img = document.createElement("img"); img.src = p.cover; img.className = "book-cover-img"; img.onclick = () => window.location.href = p.file.path; cellCover.appendChild(img); } else { const placeholder = document.createElement("div"); placeholder.className = "book-cover-img"; placeholder.style.background = "rgba(138, 43, 226, 0.12)"; placeholder.style.display = "flex"; placeholder.style.alignItems = "center"; placeholder.style.justifyContent = "center"; placeholder.style.fontSize = "22px"; placeholder.style.border = "1px dashed rgba(214, 188, 250, 0.2)"; placeholder.innerHTML = "?"; placeholder.onclick = () => window.location.href = p.file.path; cellCover.appendChild(placeholder); } const cellInfo = row.insertCell(); const infoWrapper = document.createElement("div"); infoWrapper.style.display = "flex"; infoWrapper.style.flexDirection = "column"; const link = document.createElement("a"); link.href = p.file.path; link.textContent = p.title || p.file.name; link.className = "internal-link book-title-link"; const metaText = document.createElement("div"); metaText.className = "book-meta-text"; const author = p.author || "Автор не указан"; const genre = Array.isArray(p.genre) ? p.genre.join(", ") : (p.genre || ""); metaText.textContent = author + (genre ? ` • ${genre}` : ""); infoWrapper.appendChild(link); infoWrapper.appendChild(metaText); cellInfo.appendChild(infoWrapper); const cellStatus = row.insertCell(); const sel = document.createElement("select"); sel.className = "status-dropdown"; const opts = [ { value: "planned", text: "? В планах" }, { value: "reading", text: "? Читаю" }, { value: "completed", text: "✓ Готово" } ]; opts.forEach(o => { const opt = document.createElement("option"); opt.value = o.value; opt.textContent = o.text; opt.style.background = "#1a1a20"; if (o.value === actualStatus) opt.selected = true; sel.appendChild(opt); }); applyStatusStyle(sel, actualStatus); cellStatus.appendChild(sel); sel.onchange = async () => { const newStatus = sel.value; applyStatusStyle(sel, newStatus); if (newStatus !== actualStatus) { let success = await updatePageProperty(p, "status", newStatus); if (success) { if (actualStatus === "completed" && newStatus !== "completed") { const newCurrent = Math.max(0, Math.floor(total * 0.9)); await updatePageProperty(p, "current_page", newCurrent); await removePageProperty(p, "date_ended"); } else if (newStatus === "completed") { await updatePageProperty(p, "current_page", total); await updatePageProperty(p, "date_ended", new Date().toISOString().split('T')[0]); } renderTable(genreSelect.value, statusSelect.value, searchInput.value); } } }; const cellProgress = row.insertCell(); const progressWrapper = document.createElement("div"); progressWrapper.style.display = "flex"; progressWrapper.style.flexDirection = "column"; progressWrapper.style.gap = "6px"; progressWrapper.style.minWidth = "120px"; const barContainer = document.createElement("div"); barContainer.className = "progress-bar-container"; const barFill = document.createElement("div"); barFill.className = "progress-bar-fill"; barFill.style.width = `${percent}%`; barContainer.appendChild(barFill); progressWrapper.appendChild(barContainer); const inputRow = document.createElement("div"); inputRow.style.display = "flex"; inputRow.style.alignItems = "center"; inputRow.style.gap = "4px"; inputRow.style.justifyContent = "flex-end"; const numInput = document.createElement("input"); numInput.type = "number"; numInput.className = "page-num-input"; numInput.value = current; numInput.min = 0; numInput.max = total; const label = document.createElement("span"); label.className = "total-page-label"; label.textContent = `/${total} (${percent}%)`; inputRow.appendChild(numInput); inputRow.appendChild(label); progressWrapper.appendChild(inputRow); cellProgress.appendChild(progressWrapper); const saveProgressValue = async () => { let val = parseInt(numInput.value); if (isNaN(val)) val = current; val = Math.max(0, Math.min(val, total)); if (val !== current) { let success = await updatePageProperty(p, "current_page", val); if (success) { if (val >= total) { await updatePageProperty(p, "status", "completed"); await updatePageProperty(p, "date_ended", new Date().toISOString().split('T')[0]); } else if (actualStatus === "completed" && val < total) { await updatePageProperty(p, "status", "reading"); await removePageProperty(p, "date_ended"); } else if (actualStatus === "planned" && val > 0) { await updatePageProperty(p, "status", "reading"); await updatePageProperty(p, "date_started", new Date().toISOString().split('T')[0]); } renderTable(genreSelect.value, statusSelect.value, searchInput.value); } } else { numInput.value = current; } }; numInput.onblur = saveProgressValue; numInput.onkeydown = (ev) => { if (ev.key === "Enter") numInput.blur(); if (ev.key === "Escape") { numInput.value = current; numInput.blur(); } }; } container.appendChild(table); } genreSelect.onchange = () => renderTable(genreSelect.value, statusSelect.value, searchInput.value); statusSelect.onchange = () => renderTable(genreSelect.value, statusSelect.value, searchInput.value); searchInput.oninput = () => renderTable(genreSelect.value, statusSelect.value, searchInput.value); renderTable();
Синхронизация ПК и смартфона (Git + Termux + Gitea)
Для независимой синхронизации без сторонних платных сервисов я использую связку Git, собственный инстанс Gitea и Termux на Android:
На ПК: В самом Obsidian настроен плагин
Obsidian Git. Он автоматически коммитит и отправляет (push) изменения в мой закрытый репозиторий на Gitea при закрытии программы или каждые 10–15 минут.-
На телефоне:
Настроен Termux с установленным пакетом
gitи SSH-ключами для доступа к Gitea.Синхронизацию я запускаю через комманду (alias)
gsyncв ~/.zshrc (или ~/.bashrc), которая делает сначала pull, потом push репозитория. Да, приходится заходить в терминал и писать gsync, но меня пока что устраивает (alias gsync="cd ~/storage/shared/Obsidian/Vault && git pull origin main && git add . && git commit -m 'mobile auto-sync' && git push origin main")Также уточню, что git-crypt я не использую, так как репозиторий находится в моем self-hosted Gitea, данные никуда не утекают.
Почему именно Gitea: Полный контроль над данными, отсутствие ограничений на размер файлов и бесплатный приватный сервер.
Обязательно стоит добавить .gitignore: (чтобы не было конфликтов веток)
.obsidian/workspace.json .obsidian/workspace-mobile.json .obsidian/cache .trash/
Также вот неплохая статья с гайдом по настройке (подход немного отличается от моего)
Заключение
Эта система складывалась постепенно и продолжает эволюционировать. Главный вывод за два года использования — хранилище не должно быть идеальным с первого дня. Оно должно помогать думать и работать, а не забирать всё время на свою настройку.
Надеюсь, мой workflow подаст вам пару интересных идей для организации собственного цифрового сада в Obsidian. Если у вас есть вопросы по работе со скриптами или структурой — пишите в комментариях!
Постараюсь ответить на все вопросы. Со мной также можно связаться в телеграм (в хабр профиле), там я точно вам отвечу.