mirror of
https://github.com/yangcht/obsidian-things-toolkit.git
synced 2026-07-22 07:45:44 +00:00
Persist daily reviews in daily note frontmatter
This commit is contained in:
parent
48dfbf866c
commit
afc6fdb93b
7 changed files with 218 additions and 32 deletions
|
|
@ -2,7 +2,7 @@
|
|||
"id": "things-toolkit",
|
||||
"name": "Things Toolkit",
|
||||
"description": "Sync Things3 completions into daily notes with review stats and privacy-aware macOS access.",
|
||||
"version": "1.6.8",
|
||||
"version": "1.7.0",
|
||||
"author": "yangcht",
|
||||
"authorUrl": "https://github.com/yangcht",
|
||||
"isDesktopOnly": false,
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-things-toolkit-plugin",
|
||||
"version": "1.6.8",
|
||||
"version": "1.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-things-toolkit-plugin",
|
||||
"version": "1.6.8",
|
||||
"version": "1.7.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"obsidian": "1.13.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-things-toolkit-plugin",
|
||||
"version": "1.6.8",
|
||||
"version": "1.7.0",
|
||||
"type": "module",
|
||||
"description": "Sync Things3 completions into Obsidian daily notes with review stats",
|
||||
"author": "yangcht",
|
||||
|
|
|
|||
139
src/index.ts
139
src/index.ts
|
|
@ -15,8 +15,13 @@ import {
|
|||
|
||||
import { getMoment, type MomentLike } from "./moment";
|
||||
import { ConfirmationModal } from "./modal";
|
||||
import { ToolkitRenderer } from "./renderer";
|
||||
import { getNextSyncDelayMs } from "./scheduler";
|
||||
import { ToolkitRenderer } from "./renderer";
|
||||
import {
|
||||
isDailyReviewEmpty,
|
||||
readDailyReviewFromFrontmatter,
|
||||
writeDailyReviewToFrontmatter,
|
||||
} from "./reviewPersistence";
|
||||
import {
|
||||
ThingsToolkitReviewView,
|
||||
VIEW_TYPE_THINGS_TOOLKIT_REVIEW,
|
||||
|
|
@ -126,14 +131,12 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
this.selectReviewDateFromFile(this.app.workspace.getActiveFile());
|
||||
});
|
||||
|
||||
if (this.isSyncSupported()) {
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
void this.refreshRecentDailyStats().then(() => {
|
||||
this.updateStatusBar();
|
||||
this.refreshReviewViews();
|
||||
});
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
void this.refreshDailyReviewStateFromVault().then(() => {
|
||||
this.updateStatusBar();
|
||||
this.refreshReviewViews();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.settingsTab = new ThingsToolkitSettingsTab(this.app, this);
|
||||
this.addSettingTab(this.settingsTab);
|
||||
|
|
@ -375,7 +378,7 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
|
||||
new Notice("Things Toolkit sync failed");
|
||||
} finally {
|
||||
await this.refreshRecentDailyStats();
|
||||
await this.refreshDailyReviewStateFromVault();
|
||||
this.updateStatusBar();
|
||||
this.refreshReviewViews();
|
||||
this.settingsTab?.display();
|
||||
|
|
@ -457,6 +460,97 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
await this.writeOptions({ dailyStats });
|
||||
}
|
||||
|
||||
async refreshDailyReviewStateFromVault(): Promise<void> {
|
||||
await this.migrateDailyReviewsToFrontmatter();
|
||||
await this.refreshRecentDailyStats();
|
||||
await this.refreshRecentDailyReviews();
|
||||
}
|
||||
|
||||
async refreshRecentDailyReviews(
|
||||
dayCount = this.getReviewWindowDayCount()
|
||||
): Promise<void> {
|
||||
const moment = getMoment();
|
||||
const dailyReviews: Record<string, IDailyLogbookReview> = {
|
||||
...(this.options.dailyReviews || {}),
|
||||
};
|
||||
const dailyNotes = getAllDailyNotes();
|
||||
const end = moment().startOf("day");
|
||||
const start = end.clone().subtract(dayCount - 1, "days");
|
||||
|
||||
for (
|
||||
let date = start.clone();
|
||||
date.isSameOrBefore(end);
|
||||
date.add(1, "day")
|
||||
) {
|
||||
const dateKey = getDateKey(date);
|
||||
const dailyNote = getDailyNote(toDailyNoteDate(date), dailyNotes);
|
||||
|
||||
if (!isTFile(dailyNote)) {
|
||||
delete dailyReviews[dateKey];
|
||||
continue;
|
||||
}
|
||||
|
||||
const review = this.readDailyReviewFromFile(dailyNote);
|
||||
if (review) {
|
||||
dailyReviews[dateKey] = review;
|
||||
} else {
|
||||
delete dailyReviews[dateKey];
|
||||
}
|
||||
}
|
||||
|
||||
await this.writeOptions({ dailyReviews });
|
||||
}
|
||||
|
||||
private readDailyReviewFromFile(file: TFile): IDailyLogbookReview | null {
|
||||
const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
|
||||
return readDailyReviewFromFrontmatter(frontmatter);
|
||||
}
|
||||
|
||||
private async migrateDailyReviewsToFrontmatter(): Promise<void> {
|
||||
const dailyReviews = this.options.dailyReviews || {};
|
||||
const entries = Object.entries(dailyReviews).filter(
|
||||
([, review]) => !isDailyReviewEmpty(review)
|
||||
);
|
||||
|
||||
for (const [dateKey, review] of entries) {
|
||||
const dailyNote = await this.getOrCreateDailyNoteForDateKey(dateKey);
|
||||
const existingReview = this.readDailyReviewFromFile(dailyNote);
|
||||
|
||||
if (existingReview) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.app.fileManager.processFrontMatter(dailyNote, (frontmatter) => {
|
||||
if (readDailyReviewFromFrontmatter(frontmatter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
writeDailyReviewToFrontmatter(frontmatter, review);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async getOrCreateDailyNoteForDateKey(dateKey: string): Promise<TFile> {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateKey)) {
|
||||
throw new Error(`Invalid daily review date: ${dateKey}`);
|
||||
}
|
||||
|
||||
const moment = getMoment();
|
||||
const date = moment(dateKey, "YYYY-MM-DD");
|
||||
const dailyNotes = getAllDailyNotes();
|
||||
let dailyNote = getDailyNote(toDailyNoteDate(date), dailyNotes);
|
||||
|
||||
if (!dailyNote) {
|
||||
dailyNote = await createDailyNote(toDailyNoteDate(date));
|
||||
}
|
||||
|
||||
if (!isTFile(dailyNote)) {
|
||||
throw new Error("Daily note could not be resolved as a file");
|
||||
}
|
||||
|
||||
return dailyNote;
|
||||
}
|
||||
|
||||
getCurrentCompletionStreak(): number {
|
||||
const moment = getMoment();
|
||||
let streak = 0;
|
||||
|
|
@ -487,7 +581,17 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
updatedAt: moment().unix(),
|
||||
};
|
||||
|
||||
if (!nextReview.rating && !nextReview.reflection) {
|
||||
if (nextReview.reflection !== undefined) {
|
||||
nextReview.reflection = nextReview.reflection.trim();
|
||||
}
|
||||
|
||||
const dailyNote = await this.getOrCreateDailyNoteForDateKey(dateKey);
|
||||
|
||||
await this.app.fileManager.processFrontMatter(dailyNote, (frontmatter) => {
|
||||
writeDailyReviewToFrontmatter(frontmatter, nextReview);
|
||||
});
|
||||
|
||||
if (isDailyReviewEmpty(nextReview)) {
|
||||
delete dailyReviews[dateKey];
|
||||
} else {
|
||||
dailyReviews[dateKey] = nextReview;
|
||||
|
|
@ -498,18 +602,7 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async openDailyNote(dateKey: string): Promise<void> {
|
||||
const moment = getMoment();
|
||||
const date = moment(dateKey, "YYYY-MM-DD");
|
||||
const dailyNotes = getAllDailyNotes();
|
||||
let dailyNote = getDailyNote(toDailyNoteDate(date), dailyNotes);
|
||||
|
||||
if (!dailyNote) {
|
||||
dailyNote = await createDailyNote(toDailyNoteDate(date));
|
||||
}
|
||||
|
||||
if (!isTFile(dailyNote)) {
|
||||
throw new Error("Daily note could not be opened as a file");
|
||||
}
|
||||
const dailyNote = await this.getOrCreateDailyNoteForDateKey(dateKey);
|
||||
|
||||
const leaf = this.getDailyNoteOpenLeaf();
|
||||
await leaf.openFile(dailyNote, { active: true });
|
||||
|
|
@ -622,4 +715,4 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
|
||||
await this.saveData(this.options);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/reviewPersistence.ts
Normal file
94
src/reviewPersistence.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import type { DayReviewRating, IDailyLogbookReview } from "./settings";
|
||||
|
||||
export const REVIEW_FRONTMATTER_KEY = "things_toolkit_review";
|
||||
|
||||
const VALID_RATINGS = new Set<DayReviewRating>(["good", "steady", "improve"]);
|
||||
|
||||
type FrontmatterRecord = Record<string, unknown>;
|
||||
|
||||
function isRecord(value: unknown): value is FrontmatterRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeRating(value: unknown): DayReviewRating | undefined {
|
||||
return typeof value === "string" && VALID_RATINGS.has(value as DayReviewRating)
|
||||
? (value as DayReviewRating)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function normalizeReflection(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function normalizeUpdatedAt(value: unknown): number | undefined {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const timestamp = Date.parse(value);
|
||||
if (Number.isFinite(timestamp)) {
|
||||
return Math.floor(timestamp / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isDailyReviewEmpty(review: IDailyLogbookReview): boolean {
|
||||
return !review.rating && !normalizeReflection(review.reflection);
|
||||
}
|
||||
|
||||
export function readDailyReviewFromFrontmatter(
|
||||
frontmatter: FrontmatterRecord | null | undefined
|
||||
): IDailyLogbookReview | null {
|
||||
if (!frontmatter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawReview = frontmatter[REVIEW_FRONTMATTER_KEY];
|
||||
if (!isRecord(rawReview)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const review: IDailyLogbookReview = {
|
||||
rating: normalizeRating(rawReview.rating),
|
||||
reflection: normalizeReflection(rawReview.reflection),
|
||||
updatedAt: normalizeUpdatedAt(rawReview.updatedAt ?? rawReview.updated),
|
||||
};
|
||||
|
||||
return isDailyReviewEmpty(review) ? null : review;
|
||||
}
|
||||
|
||||
export function writeDailyReviewToFrontmatter(
|
||||
frontmatter: FrontmatterRecord,
|
||||
review: IDailyLogbookReview
|
||||
): void {
|
||||
if (isDailyReviewEmpty(review)) {
|
||||
delete frontmatter[REVIEW_FRONTMATTER_KEY];
|
||||
return;
|
||||
}
|
||||
|
||||
const nextReview: FrontmatterRecord = {};
|
||||
|
||||
if (review.rating) {
|
||||
nextReview.rating = review.rating;
|
||||
}
|
||||
|
||||
const reflection = normalizeReflection(review.reflection);
|
||||
if (reflection) {
|
||||
nextReview.reflection = reflection;
|
||||
}
|
||||
|
||||
if (typeof review.updatedAt === "number" && Number.isFinite(review.updatedAt)) {
|
||||
nextReview.updatedAt = review.updatedAt;
|
||||
nextReview.updated = new Date(review.updatedAt * 1000).toISOString();
|
||||
}
|
||||
|
||||
frontmatter[REVIEW_FRONTMATTER_KEY] = nextReview;
|
||||
}
|
||||
|
|
@ -34,9 +34,7 @@ export class ThingsToolkitReviewView extends ItemView {
|
|||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
if (this.plugin.isSyncSupported()) {
|
||||
await this.plugin.refreshRecentDailyStats();
|
||||
}
|
||||
await this.plugin.refreshDailyReviewStateFromVault();
|
||||
this.display();
|
||||
}
|
||||
|
||||
|
|
@ -394,4 +392,4 @@ export class ThingsToolkitReviewView extends ItemView {
|
|||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,5 +13,6 @@
|
|||
"1.6.5": "1.13.1",
|
||||
"1.6.6": "1.13.1",
|
||||
"1.6.7": "1.13.1",
|
||||
"1.6.8": "1.27.0"
|
||||
"1.6.8": "1.12.0",
|
||||
"1.7.0": "1.12.0"
|
||||
}
|
||||
Loading…
Reference in a new issue