2 года в Obsidian: мой workflow, архитектура заметок, плагины и синхронизация
Введение
Около двух лет я использую 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— вставка Callout -
Ctrl + O— открыть Quick Switcher -
Ctrl + P— открыть Command Palette -
Ctrl + 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. Если у вас есть вопросы по работе со скриптами или структурой — пишите в комментариях!
Постараюсь ответить на все вопросы. Со мной также можно связаться в телеграм (в хабр профиле), там я точно вам отвечу.
Автор: ITsuperiorRF

