Afix lints

This commit is contained in:
Chentao Yang 2026-06-13 20:22:58 +02:00
parent 90e1ff3c03
commit 2f9845e940
15 changed files with 1441 additions and 2611 deletions

32
.eslintrc.json Normal file
View file

@ -0,0 +1,32 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"env": {
"browser": true,
"node": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2020,
"project": "./tsconfig.json"
},
"ignorePatterns": [
"main.js",
"node_modules/",
"dist/"
],
"rules": {
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-return": "warn"
}
}

0
eslint Normal file
View file

38
eslint.config.mjs Normal file
View file

@ -0,0 +1,38 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
{
ignores: [
"eslint.config.mjs",
"main.js",
"rollup.config.js",
"node_modules/**",
"dist/**"
],
},
{
files: ["src/**/*.ts"],
extends: [
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/unbound-method": "warn"
},
}
);

View file

@ -2,9 +2,9 @@
"id": "things-toolkit",
"name": "Things Toolkit",
"description": "Sync Things3 completions into daily notes with review stats and privacy-aware macOS access.",
"version": "1.0.0",
"version": "1.0.1",
"author": "yangcht",
"authorUrl": "https://github.com/yangcht",
"isDesktopOnly": true,
"minAppVersion": "1.0.0"
"minAppVersion": "1.0.1"
}

View file

2338
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,28 +6,25 @@
"main": "main.js",
"license": "MIT",
"scripts": {
"lint": "eslint . --ext .ts",
"lint": "eslint src/**/*.ts",
"build": "npm run lint && rollup -c",
"build:nolint": "rollup -c",
"test:watch": "yarn test -- --watch"
},
"dependencies": {
"obsidian": "obsidianmd/obsidian-api#master",
"obsidian-daily-notes-interface": "0.9.2",
"papaparse": "5.3.1",
"tslib": "2.2.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "17.1.0",
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "11.2.0",
"@rollup/plugin-replace": "2.4.1",
"@rollup/plugin-typescript": "8.2.0",
"@eslint/js": "^10.0.1",
"@types/node": "latest",
"@types/papaparse": "5.3.1",
"@typescript-eslint/eslint-plugin": "4.17.0",
"@typescript-eslint/parser": "4.17.0",
"eslint": "7.22.0",
"rollup": "^2.80.0",
"typescript": "4.2.3"
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"eslint": "latest",
"obsidian": "latest",
"typescript": "latest",
"typescript-eslint": "^8.61.0"
}
}

View file

@ -1,5 +1,18 @@
import { Editor } from "codemirror";
import { App, MarkdownView, TFile, WorkspaceLeaf } from "obsidian";
import { App, Editor, MarkdownView, TFile, WorkspaceLeaf } from "obsidian";
export function getEditorForFile(app: App, file: TFile): Editor | null {
let editor: Editor | null = null;
app.workspace.iterateAllLeaves((leaf: WorkspaceLeaf) => {
const view = leaf.view;
if (view instanceof MarkdownView && view.file === file) {
editor = view.editor;
}
});
return editor;
}
type MarkdownEditor = Editor & {
replaceRange: Editor["replaceRange"];

View file

@ -37,22 +37,6 @@ import {
updateSection,
} from "./textUtils";
type ReviewLeaf = WorkspaceLeaf & {
setViewState(state: { type: string; active: boolean }): Promise<void>;
openFile(file: TFile): Promise<void>;
};
type WorkspaceWithHelpers = typeof app.workspace & {
getLeavesOfType(type: string): WorkspaceLeaf[];
getRightLeaf(split: boolean): WorkspaceLeaf | null;
revealLeaf(leaf: WorkspaceLeaf): Promise<void>;
getLeaf(newLeaf?: boolean): WorkspaceLeaf;
};
function getWorkspace(plugin: Plugin): WorkspaceWithHelpers {
return plugin.app.workspace as WorkspaceWithHelpers;
}
function isTFile(file: unknown): file is TFile {
return file instanceof TFile;
}
@ -72,13 +56,16 @@ export default class ThingsToolkitPlugin extends Plugin {
isSyncing: false,
message: "",
};
private syncTimeoutId?: number;
private settingsTab?: ThingsToolkitSettingsTab;
private statusBarEl?: HTMLElement;
async onload(): Promise<void> {
if (!Platform.isDesktopApp || !isMacOS()) {
console.info("Failed to load Things Toolkit plugin. Platform not supported");
console.info(
"Failed to load Things Toolkit plugin. Platform not supported"
);
return;
}
@ -95,10 +82,13 @@ export default class ThingsToolkitPlugin extends Plugin {
this.addCommand({
id: "sync",
name: "Sync",
callback: () => window.setTimeout(() => {
void this.tryToSyncLogbook();
}, 20),
callback: () => {
window.setTimeout(() => {
void this.tryToSyncLogbook();
}, 20);
},
});
this.addCommand({
id: "open-review",
name: "Open review",
@ -112,13 +102,16 @@ export default class ThingsToolkitPlugin extends Plugin {
});
await this.loadOptions();
this.statusBarEl = this.addStatusBarItem();
this.statusBarEl.addClass("things-toolkit-status");
this.statusBarEl.addEventListener("click", () => {
void this.activateReviewView();
});
this.updateStatusBar();
getWorkspace(this).onLayoutReady(() => {
this.app.workspace.onLayoutReady(() => {
void this.refreshRecentDailyStats().then(() => {
this.updateStatusBar();
this.refreshReviewViews();
@ -129,11 +122,11 @@ export default class ThingsToolkitPlugin extends Plugin {
this.addSettingTab(this.settingsTab);
if (this.options.hasAcceptedDisclaimer && this.options.isSyncEnabled) {
if (getWorkspace(this).layoutReady) {
if (this.app.workspace.layoutReady) {
this.scheduleNextSync();
} else {
this.registerEvent(
getWorkspace(this).on("layout-ready", () => {
this.app.workspace.on("layout-ready", () => {
this.scheduleNextSync();
})
);
@ -142,19 +135,24 @@ export default class ThingsToolkitPlugin extends Plugin {
}
async activateReviewView(): Promise<void> {
const workspace = getWorkspace(this);
let leaf = workspace.getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW)[0] as ReviewLeaf | undefined;
let leaf = this.app.workspace.getLeavesOfType(
VIEW_TYPE_THINGS_TOOLKIT_REVIEW
)[0];
if (!leaf) {
leaf = workspace.getRightLeaf(false) as ReviewLeaf | null;
leaf = this.app.workspace.getRightLeaf(false);
if (!leaf) {
return;
}
await leaf.setViewState({
type: VIEW_TYPE_THINGS_TOOLKIT_REVIEW,
active: true,
});
}
await workspace.revealLeaf(leaf);
await this.app.workspace.revealLeaf(leaf);
}
async tryToSyncLogbook(): Promise<void> {
@ -175,7 +173,7 @@ export default class ThingsToolkitPlugin extends Plugin {
}).open();
}
async tryToScheduleSync(): Promise<void> {
tryToScheduleSync(): void {
if (this.options.hasAcceptedDisclaimer) {
this.scheduleNextSync();
return;
@ -200,6 +198,7 @@ export default class ThingsToolkitPlugin extends Plugin {
async syncLogbook(): Promise<void> {
const moment = getMoment();
if (this.syncStatus.isSyncing) {
new Notice("Things Toolkit sync already running");
return;
@ -214,9 +213,6 @@ export default class ThingsToolkitPlugin extends Plugin {
const logbookRenderer = new ToolkitRenderer(this.app, this.options);
const dailyNotes = getAllDailyNotes();
const latestSyncTime = this.options.latestSyncTime || 0;
let taskCount = 0;
let dayCount = 0;
let changedDayCount = 0;
try {
const fetchResult = await fetchThingsToolkit(
@ -230,16 +226,19 @@ export default class ThingsToolkitPlugin extends Plugin {
fetchResult.taskRecords,
fetchResult.checklistRecords
);
taskCount = tasks.length;
const taskCount = tasks.length;
const daysToTasks: Record<string, ITask[]> = groupBy(
tasks.filter((task) => task.stopDate),
(task) => moment.unix(task.stopDate).startOf("day").format()
);
const dayEntries = Object.entries(daysToTasks).sort(([a], [b]) =>
moment(a).diff(moment(b))
);
dayCount = dayEntries.length;
const dayCount = dayEntries.length;
let changedDayCount = 0;
const dailyStats = this.buildDailyStats(
dayEntries,
fetchResult.source,
@ -249,6 +248,7 @@ export default class ThingsToolkitPlugin extends Plugin {
for (const [dateStr, groupedTasks] of dayEntries) {
const date = moment(dateStr);
let dailyNote = getDailyNote(date, dailyNotes);
if (!dailyNote) {
dailyNote = await createDailyNote(date);
}
@ -263,6 +263,7 @@ export default class ThingsToolkitPlugin extends Plugin {
this.options.sectionHeading,
logbookRenderer.render(groupedTasks)
);
if (didChange) {
changedDayCount++;
}
@ -274,24 +275,34 @@ export default class ThingsToolkitPlugin extends Plugin {
thingsAccessStatus: fetchResult.accessStatus,
});
const repairLookbackDays =
fetchResult.repairLookbackDays ||
(fetchResult.source === "applescript"
? this.options.appleScriptFallbackLookbackDays
: this.getReviewWindowDayCount());
const sourceLabel =
fetchResult.source === "applescript"
? `Things AppleScript, repairing last ${fetchResult.repairLookbackDays || this.options.appleScriptFallbackLookbackDays} days`
: `Things SQLite, repairing last ${fetchResult.repairLookbackDays || this.getReviewWindowDayCount()} days`;
? `Things AppleScript, repairing last ${repairLookbackDays} days`
: `Things SQLite, repairing last ${repairLookbackDays} days`;
this.syncStatus = {
isSyncing: false,
message: `Last result: ${taskCount} tasks, ${changedDayCount}/${dayCount} notes updated via ${sourceLabel}. ${fetchResult.accessStatus.message}`,
};
new Notice(
`Things Toolkit sync complete (${taskCount} tasks, ${changedDayCount} notes updated)`
);
} catch (err) {
console.error("[Things Toolkit] Sync failed", err);
const errorMessage = err instanceof Error ? err.message : String(err);
this.syncStatus = {
isSyncing: false,
message: `Last result: sync failed. ${errorMessage}`,
};
new Notice("Things Toolkit sync failed");
} finally {
await this.refreshRecentDailyStats();
@ -308,15 +319,20 @@ export default class ThingsToolkitPlugin extends Plugin {
syncedAt: number
): Record<string, IDailyLogbookStat> {
const moment = getMoment();
const dailyStats = { ...(this.options.dailyStats || {}) };
const dailyStats: Record<string, IDailyLogbookStat> = {
...(this.options.dailyStats || {}),
};
dayEntries.forEach(([dateStr, tasks]) => {
const dateKey = moment(dateStr).format("YYYY-MM-DD");
dailyStats[dateKey] = {
taskCount: tasks.length,
source,
syncedAt,
};
});
return dailyStats;
}
@ -328,17 +344,26 @@ export default class ThingsToolkitPlugin extends Plugin {
return Math.max(7, Math.floor(Number(this.options.reviewWindowDays) || 365));
}
async refreshRecentDailyStats(dayCount = this.getReviewWindowDayCount()): Promise<void> {
async refreshRecentDailyStats(
dayCount = this.getReviewWindowDayCount()
): Promise<void> {
const moment = getMoment();
const dailyStats = { ...(this.options.dailyStats || {}) };
const dailyStats: Record<string, IDailyLogbookStat> = {
...(this.options.dailyStats || {}),
};
const dailyNotes = getAllDailyNotes();
const syncedAt = moment().unix();
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")) {
for (
let date = start.clone();
date.isSameOrBefore(end);
date.add(1, "day")
) {
const dateKey = getDateKey(date);
const dailyNote = getDailyNote(date, dailyNotes);
if (!isTFile(dailyNote)) {
dailyStats[dateKey] = {
taskCount: 0,
@ -350,7 +375,10 @@ export default class ThingsToolkitPlugin extends Plugin {
const fileContents = await this.app.vault.read(dailyNote);
dailyStats[dateKey] = {
taskCount: countThingsTasksInSection(fileContents, this.options.sectionHeading),
taskCount: countThingsTasksInSection(
fileContents,
this.options.sectionHeading
),
source: "daily-note",
syncedAt,
};
@ -362,6 +390,7 @@ export default class ThingsToolkitPlugin extends Plugin {
getCurrentCompletionStreak(): number {
const moment = getMoment();
let streak = 0;
for (
let date = moment().startOf("day");
this.getTaskCountForDay(getDateKey(date)) > 0;
@ -369,6 +398,7 @@ export default class ThingsToolkitPlugin extends Plugin {
) {
streak++;
}
return streak;
}
@ -377,17 +407,22 @@ export default class ThingsToolkitPlugin extends Plugin {
diff: Partial<IDailyLogbookReview>
): Promise<void> {
const moment = getMoment();
const dailyReviews = { ...(this.options.dailyReviews || {}) };
const nextReview = {
const dailyReviews: Record<string, IDailyLogbookReview> = {
...(this.options.dailyReviews || {}),
};
const nextReview: IDailyLogbookReview = {
...(dailyReviews[dateKey] || {}),
...diff,
updatedAt: moment().unix(),
};
if (!nextReview.rating && !nextReview.reflection) {
delete dailyReviews[dateKey];
} else {
dailyReviews[dateKey] = nextReview;
}
await this.writeOptions({ dailyReviews });
this.refreshReviewViews();
}
@ -397,27 +432,35 @@ export default class ThingsToolkitPlugin extends Plugin {
const date = moment(dateKey, "YYYY-MM-DD");
const dailyNotes = getAllDailyNotes();
let dailyNote = getDailyNote(date, dailyNotes);
if (!dailyNote) {
dailyNote = await createDailyNote(date);
}
if (!isTFile(dailyNote)) {
throw new Error("Daily note could not be opened as a file");
}
await (getWorkspace(this).getLeaf(false) as ReviewLeaf).openFile(dailyNote);
await this.app.workspace.getLeaf(false).openFile(dailyNote);
}
refreshReviewViews(): void {
getWorkspace(this)
.getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW)
.forEach((leaf) => {
if (leaf.view instanceof ThingsToolkitReviewView) {
leaf.view.display();
}
});
const leaves: WorkspaceLeaf[] = this.app.workspace.getLeavesOfType(
VIEW_TYPE_THINGS_TOOLKIT_REVIEW
);
leaves.forEach((leaf: WorkspaceLeaf) => {
const view = leaf.view;
if (view instanceof ThingsToolkitReviewView) {
view.display();
}
});
}
updateStatusBar(): void {
const moment = getMoment();
if (!this.statusBarEl) {
return;
}
@ -425,6 +468,7 @@ export default class ThingsToolkitPlugin extends Plugin {
const todayKey = moment().format("YYYY-MM-DD");
const todayCount = this.getTaskCountForDay(todayKey);
const streak = this.getCurrentCompletionStreak();
this.statusBarEl.setText(`Things: ${todayCount} today | ${streak}d streak`);
this.statusBarEl.setAttribute("aria-label", "Open Things toolkit review");
}
@ -432,6 +476,7 @@ export default class ThingsToolkitPlugin extends Plugin {
cancelScheduledSync(): void {
if (this.syncTimeoutId !== undefined) {
window.clearTimeout(this.syncTimeoutId);
this.syncTimeoutId = undefined;
}
}
@ -440,6 +485,7 @@ export default class ThingsToolkitPlugin extends Plugin {
const now = moment().unix();
this.cancelScheduledSync();
if (!this.options.isSyncEnabled || !this.options.syncInterval) {
console.debug("[Things Toolkit] scheduling skipped, no syncInterval set");
return;
@ -450,13 +496,20 @@ export default class ThingsToolkitPlugin extends Plugin {
const nextSync = Math.max(secondsUntilNextSync * 1000, 20);
console.debug(`[Things Toolkit] next sync scheduled in ${nextSync}ms`);
this.syncTimeoutId = window.setTimeout(() => {
void this.syncLogbook();
}, nextSync);
}
async loadOptions(): Promise<void> {
this.options = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) as ISettings;
const loadedData = (await this.loadData()) as Partial<ISettings> | null;
this.options = {
...DEFAULT_SETTINGS,
...(loadedData ?? {}),
};
if (!this.options.hasAcceptedDisclaimer) {
this.options.isSyncEnabled = false;
}
@ -467,14 +520,14 @@ export default class ThingsToolkitPlugin extends Plugin {
if (diff.isSyncEnabled !== undefined) {
if (diff.isSyncEnabled) {
await this.tryToScheduleSync();
this.tryToScheduleSync();
} else {
this.cancelScheduledSync();
}
} else if (diff.syncInterval !== undefined && this.options.isSyncEnabled) {
await this.tryToScheduleSync();
this.tryToScheduleSync();
}
await this.saveData(this.options);
}
}
}

View file

@ -75,7 +75,7 @@ export class ToolkitRenderer {
toHeading(heading, headingLevel + 1, doesAddNewlineBeforeHeadings)
);
}
output.push(...groupedTasks.map(this.renderTask));
output.push(...groupedTasks.map((task) => this.renderTask(task)));
});
return output.join("\n");

View file

@ -1,6 +1,7 @@
import { ItemView, Notice, WorkspaceLeaf, moment } from "obsidian";
import { ItemView, Notice, WorkspaceLeaf } from "obsidian";
import type ThingsToolkitPlugin from "./index";
import { getMoment, type MomentLike } from "./moment";
import { DayReviewRating } from "./settings";
export const VIEW_TYPE_THINGS_TOOLKIT_REVIEW = "things-toolkit-review";
@ -14,11 +15,12 @@ const RATING_LABELS: Record<DayReviewRating, string> = {
export class ThingsToolkitReviewView extends ItemView {
private plugin: ThingsToolkitPlugin;
private selectedDate: string;
private readonly moment = getMoment();
constructor(leaf: WorkspaceLeaf, plugin: ThingsToolkitPlugin) {
super(leaf);
this.plugin = plugin;
this.selectedDate = moment().format("YYYY-MM-DD");
this.selectedDate = this.moment().format("YYYY-MM-DD");
}
getViewType(): string {
@ -49,7 +51,7 @@ export class ThingsToolkitReviewView extends ItemView {
}
private renderSummary(containerEl: HTMLElement): void {
const today = moment().format("YYYY-MM-DD");
const today = this.moment().format("YYYY-MM-DD");
const todayCount = this.plugin.getTaskCountForDay(today);
const weeklyCount = this.getRecentTaskTotal(7);
const streak = this.plugin.getCurrentCompletionStreak();
@ -98,7 +100,7 @@ export class ThingsToolkitReviewView extends ItemView {
private renderSelectedDay(containerEl: HTMLElement): void {
const dateKey = this.selectedDate;
const date = moment(dateKey, "YYYY-MM-DD");
const date = this.moment(dateKey, "YYYY-MM-DD");
const review = this.plugin.options.dailyReviews[dateKey] || {};
const stat = this.plugin.options.dailyStats[dateKey];
const detailEl = containerEl.createDiv("things-toolkit-review-detail");
@ -136,9 +138,10 @@ export class ThingsToolkitReviewView extends ItemView {
if (review.rating === rating) {
buttonEl.addClass("is-active");
}
buttonEl.addEventListener("click", async () => {
await this.plugin.writeDayReview(dateKey, { rating });
this.display();
buttonEl.addEventListener("click", () => {
void this.plugin.writeDayReview(dateKey, { rating }).then(() => {
this.display();
});
});
});
@ -153,26 +156,27 @@ export class ThingsToolkitReviewView extends ItemView {
const actionsEl = detailEl.createDiv("things-toolkit-review-actions");
actionsEl
.createEl("button", { text: "Save reflection" })
.addEventListener("click", async () => {
await this.plugin.writeDayReview(dateKey, {
.addEventListener("click", () => {
void this.plugin.writeDayReview(dateKey, {
reflection: textareaEl.value.trim(),
}).then(() => {
new Notice("Things review saved");
this.display();
});
new Notice("Things review saved");
this.display();
});
actionsEl
.createEl("button", { text: "Open daily note" })
.addEventListener("click", async () => {
await this.plugin.openDailyNote(dateKey);
.addEventListener("click", () => {
void this.plugin.openDailyNote(dateKey);
});
}
private renderMonth(
containerEl: HTMLElement,
month: moment.Moment,
startDate: moment.Moment,
endDate: moment.Moment
month: MomentLike,
startDate: MomentLike,
endDate: MomentLike
): void {
const monthEl = containerEl.createDiv("things-toolkit-review-month");
const summary = this.getMonthSummary(month);
@ -221,7 +225,7 @@ export class ThingsToolkitReviewView extends ItemView {
}
}
private renderCalendarDay(containerEl: HTMLElement, date: moment.Moment): void {
private renderCalendarDay(containerEl: HTMLElement, date: MomentLike): void {
const dateKey = date.format("YYYY-MM-DD");
const count = this.plugin.getTaskCountForDay(dateKey);
const review = this.plugin.options.dailyReviews[dateKey];
@ -252,10 +256,10 @@ export class ThingsToolkitReviewView extends ItemView {
text: String(count),
});
buttonEl.addEventListener("click", async () => {
buttonEl.addEventListener("click", () => {
this.selectedDate = dateKey;
this.display();
await this.plugin.openDailyNote(dateKey);
void this.plugin.openDailyNote(dateKey);
});
}
@ -275,10 +279,10 @@ export class ThingsToolkitReviewView extends ItemView {
});
}
private getRecentDates(dayCount: number): moment.Moment[] {
const end = moment().startOf("day");
private getRecentDates(dayCount: number): MomentLike[] {
const end = this.moment().startOf("day");
const start = end.clone().subtract(dayCount - 1, "days");
const dates: moment.Moment[] = [];
const dates: MomentLike[] = [];
for (let date = start; date.isSameOrBefore(end); date.add(1, "day")) {
dates.push(date.clone());
}
@ -292,21 +296,21 @@ export class ThingsToolkitReviewView extends ItemView {
);
}
private getWeekTotal(date: moment.Moment): number {
private getWeekTotal(date: MomentLike): number {
return this.sumDateRange(
date.clone().startOf("isoWeek"),
date.clone().endOf("isoWeek")
);
}
private getMonthTotal(date: moment.Moment): number {
private getMonthTotal(date: MomentLike): number {
return this.sumDateRange(
date.clone().startOf("month"),
date.clone().endOf("month")
);
}
private getMonthActiveDays(date: moment.Moment): number {
private getMonthActiveDays(date: MomentLike): number {
const end = date.clone().endOf("month");
let activeDays = 0;
for (
@ -321,7 +325,7 @@ export class ThingsToolkitReviewView extends ItemView {
return activeDays;
}
private getMonthSummary(month: moment.Moment): {
private getMonthSummary(month: MomentLike): {
activeDays: number;
bestDay: string;
total: number;
@ -351,7 +355,7 @@ export class ThingsToolkitReviewView extends ItemView {
return { activeDays, bestDay, total };
}
private sumDateRange(start: moment.Moment, end: moment.Moment): number {
private sumDateRange(start: MomentLike, end: MomentLike): number {
let total = 0;
for (
let date = start.clone().startOf("day");

View file

@ -55,7 +55,7 @@ export const DEFAULT_SETTINGS = Object.freeze({
hasAcceptedDisclaimer: false,
latestSyncTime: 0,
appleScriptFallbackLookbackDays: DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS,
thingsAccessMode: "auto" as ThingsAccessMode,
thingsAccessMode: "auto",
thingsAccessStatus: undefined,
reviewWindowDays: DEFAULT_REVIEW_WINDOW_DAYS,
dailyStats: {},
@ -113,9 +113,9 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
)
.addText((textfield) => {
textfield.setValue(this.plugin.options.sectionHeading);
textfield.onChange(async (rawSectionHeading) => {
textfield.onChange((rawSectionHeading) => {
const sectionHeading = this.normalizeSectionHeading(rawSectionHeading);
this.plugin.writeOptions({ sectionHeading });
void this.plugin.writeOptions({ sectionHeading });
});
});
}
@ -130,12 +130,16 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
textfield.setValue(String(this.plugin.options.reviewWindowDays));
textfield.inputEl.type = "number";
textfield.inputEl.onblur = (e: FocusEvent) => {
const target = e.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
const reviewWindowDays = Math.max(
30,
Math.floor(Number((<HTMLInputElement>e.target).value) || DEFAULT_REVIEW_WINDOW_DAYS)
Math.floor(Number(target.value) || DEFAULT_REVIEW_WINDOW_DAYS)
);
textfield.setValue(String(reviewWindowDays));
this.plugin.writeOptions({ reviewWindowDays });
void this.plugin.writeOptions({ reviewWindowDays });
};
});
}
@ -156,8 +160,8 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
.setName("Enable periodic syncing")
.addToggle((toggle) => {
toggle.setValue(this.plugin.options.isSyncEnabled);
toggle.onChange(async (isSyncEnabled) => {
this.plugin.writeOptions({ isSyncEnabled });
toggle.onChange((isSyncEnabled) => {
void this.plugin.writeOptions({ isSyncEnabled });
});
});
}
@ -218,8 +222,8 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
.setDesc('Includes MD notes of a task into the synced Obsidian document')
.addToggle((toggle) => {
toggle.setValue(this.plugin.options.doesSyncNoteBody);
toggle.onChange(async (doesSyncNoteBody) => {
this.plugin.writeOptions({ doesSyncNoteBody })
toggle.onChange((doesSyncNoteBody) => {
void this.plugin.writeOptions({ doesSyncNoteBody });
});
});
}
@ -230,8 +234,8 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
.setDesc("If the Things task belongs to a project, use project name as header instead of area")
.addToggle((toggle) => {
toggle.setValue(this.plugin.options.doesSyncProject);
toggle.onChange(async (doesSyncProject) => {
this.plugin.writeOptions({ doesSyncProject })
toggle.onChange((doesSyncProject) => {
void this.plugin.writeOptions({ doesSyncProject });
});
});
}
@ -244,12 +248,16 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
textfield.setValue(String(this.plugin.options.syncInterval));
textfield.inputEl.type = "number";
textfield.inputEl.onblur = (e: FocusEvent) => {
const target = e.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
const syncInterval = Math.max(
60,
Math.floor(Number((<HTMLInputElement>e.target).value) || DEFAULT_SYNC_FREQUENCY_SECONDS)
Math.floor(Number(target.value) || DEFAULT_SYNC_FREQUENCY_SECONDS)
);
textfield.setValue(String(syncInterval));
this.plugin.writeOptions({ syncInterval });
void this.plugin.writeOptions({ syncInterval });
};
});
}
@ -264,12 +272,16 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
textfield.setValue(String(this.plugin.options.appleScriptFallbackLookbackDays));
textfield.inputEl.type = "number";
textfield.inputEl.onblur = (e: FocusEvent) => {
const target = e.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
const appleScriptFallbackLookbackDays = Math.max(
1,
Math.floor(Number((<HTMLInputElement>e.target).value) || DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS)
Math.floor(Number(target.value) || DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS)
);
textfield.setValue(String(appleScriptFallbackLookbackDays));
this.plugin.writeOptions({ appleScriptFallbackLookbackDays });
void this.plugin.writeOptions({ appleScriptFallbackLookbackDays });
};
});
}
@ -282,8 +294,8 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
)
.addText((textfield) => {
textfield.setValue(this.plugin.options.tagPrefix);
textfield.onChange(async (tagPrefix) => {
this.plugin.writeOptions({ tagPrefix });
textfield.onChange((tagPrefix) => {
void this.plugin.writeOptions({ tagPrefix });
});
});
}
@ -296,8 +308,8 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
)
.addText((textfield) => {
textfield.setValue(this.plugin.options.canceledMark);
textfield.onChange(async (canceledMark) => {
this.plugin.writeOptions({ canceledMark });
textfield.onChange((canceledMark) => {
void this.plugin.writeOptions({ canceledMark });
});
});
}
@ -308,8 +320,8 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
.setDesc("When grouping tasks with headings by area or project, add an empty line before that heading")
.addToggle((toggle) => {
toggle.setValue(this.plugin.options.doesAddNewlineBeforeHeadings);
toggle.onChange(async (doesAddNewlineBeforeHeadings) => {
this.plugin.writeOptions({ doesAddNewlineBeforeHeadings });
toggle.onChange((doesAddNewlineBeforeHeadings) => {
void this.plugin.writeOptions({ doesAddNewlineBeforeHeadings });
});
});
}
@ -344,8 +356,8 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
button.setButtonText('Reset sync history');
button.setClass('mod-danger');
button.setDisabled(syncStatus.isSyncing);
button.onClick(async () => {
await this.plugin.writeOptions({ latestSyncTime: 0 });
button.onClick(() => {
void this.plugin.writeOptions({ latestSyncTime: 0 });
this.display();
});
})

View file

@ -2,7 +2,8 @@ import * as os from "os";
import * as fs from "fs";
import * as path from "path";
import { spawn } from "child_process";
import type moment from "moment";
import { getMoment, type MomentLike } from "./moment";
import { THINGS_DB_PATH_START, THINGS_DB_PATH_END } from "./constants";
import { querySqliteDB } from "./sqlite";
@ -142,16 +143,17 @@ function buildAccessStatus(
message,
source,
sqliteBlocked,
updatedAt: window.moment().unix(),
updatedAt: getMoment()().unix(),
};
}
function getRepairCutoff(
latestSyncTime: number,
repairLookbackDays: number
): moment.Moment {
const incrementalCutoff = window.moment.unix(latestSyncTime).startOf("day");
const repairCutoff = window.moment()
): MomentLike {
const moment = getMoment();
const incrementalCutoff = moment.unix(latestSyncTime).startOf("day");
const repairCutoff = moment()
.subtract(Math.max(1, Number(repairLookbackDays) || 1), "days")
.startOf("day");
@ -205,7 +207,8 @@ function getAppleScriptCutoff(
latestSyncTime: number,
fallbackLookbackDays: number,
repairLookbackDays: number
): moment.Moment {
): MomentLike {
const moment = getMoment();
const configuredLookbackDays = Math.max(
1,
Number(fallbackLookbackDays) || DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS
@ -218,7 +221,7 @@ function getAppleScriptCutoff(
const lookbackCutoff = getRepairCutoff(latestSyncTime, lookbackDays);
if (latestSyncTime > 0) {
const incrementalCutoff = window.moment.unix(latestSyncTime).startOf("day");
const incrementalCutoff = moment.unix(latestSyncTime).startOf("day");
if (lookbackCutoff.isBefore(incrementalCutoff)) {
console.debug(
`[Things Toolkit] repairing the last ${lookbackDays} days from Things AppleScript.`
@ -234,7 +237,7 @@ function getAppleScriptCutoff(
return lookbackCutoff;
}
function buildThingsToolkitAppleScript(cutoff: moment.Moment): string {
function buildThingsToolkitAppleScript(cutoff: MomentLike): string {
return `
set cutoff to current date
set year of cutoff to ${cutoff.year()}
@ -302,6 +305,7 @@ async function getTasksFromThingsAppleScript(
repairLookbackDays: number,
accessStatus: IThingsAccessStatus
): Promise<IThingsToolkitFetchResult> {
const moment = getMoment();
const cutoff = getAppleScriptCutoff(
latestSyncTime,
fallbackLookbackDays,
@ -329,7 +333,7 @@ async function getTasksFromThingsAppleScript(
source: "applescript",
cutoffTime: cutoff.unix(),
isLimited: latestSyncTime === 0,
repairLookbackDays: window.moment().startOf("day").diff(cutoff, "days"),
repairLookbackDays: moment().startOf("day").diff(cutoff, "days"),
};
}
@ -448,9 +452,10 @@ async function getChecklistItemsThingsDb(
async function getTasksFromThingsSqlite(
latestSyncTime: number
): Promise<ITaskRecord[]> {
const moment = getMoment();
const taskRecords: ITaskRecord[] = [];
let isSyncCompleted = false;
let stopTime = window.moment.unix(latestSyncTime).startOf("day").unix();
let stopTime = moment.unix(latestSyncTime).startOf("day").unix();
while (!isSyncCompleted) {
console.debug("[Things Toolkit] fetching tasks from sqlite db...");
@ -507,6 +512,7 @@ export async function fetchThingsToolkit(
accessMode: ThingsAccessMode,
repairLookbackDays: number
): Promise<IThingsToolkitFetchResult> {
const moment = getMoment();
const cutoff = getRepairCutoff(latestSyncTime, repairLookbackDays);
const cutoffTime = cutoff.unix();
@ -538,11 +544,13 @@ export async function fetchThingsToolkit(
source: "sqlite",
cutoffTime,
isLimited: false,
repairLookbackDays: window.moment().startOf("day").diff(cutoff, "days"),
repairLookbackDays: moment().startOf("day").diff(cutoff, "days"),
};
} catch (err) {
if (accessMode === "sqlite") {
throw new Error(`Things SQLite access failed: ${getErrorMessage(err)}`);
throw new Error(`Things SQLite access failed: ${getErrorMessage(err)}`, {
cause: err,
});
}
const sqliteBlocked =
@ -568,7 +576,8 @@ export async function fetchThingsToolkit(
);
} catch (appleScriptErr) {
throw new Error(
`Things AppleScript access failed after SQLite was unavailable: ${getErrorMessage(appleScriptErr)}`
`Things AppleScript access failed after SQLite was unavailable: ${getErrorMessage(appleScriptErr)}`,
{ cause: appleScriptErr }
);
}
}

View file

@ -5,12 +5,15 @@
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "esnext",
"allowJs": true,
"noImplicitAny": true,
"target": "ES2020",
"moduleResolution": "node",
"importHelpers": true,
"lib": ["dom", "esnext"]
"strict": true,
"noImplicitAny": true,
"types": ["node"],
"lib": ["DOM", "ES2020"],
"skipLibCheck": true
},
"include": ["**/*.ts"]
}
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "main.js"]
}

1273
yarn.lock

File diff suppressed because it is too large Load diff