Bump to version 1.6.4

This commit is contained in:
Chentao Yang 2026-06-23 22:46:43 +02:00
parent ca1952d0c3
commit f0b8c405d9
9 changed files with 185 additions and 119 deletions

View file

@ -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.3",
"version": "1.6.4",
"author": "yangcht",
"authorUrl": "https://github.com/yangcht",
"isDesktopOnly": false,

25
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-things-toolkit-plugin",
"version": "1.6.3",
"version": "1.6.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-things-toolkit-plugin",
"version": "1.6.3",
"version": "1.6.4",
"license": "MIT",
"dependencies": {
"obsidian-daily-notes-interface": "0.9.2",
@ -15,6 +15,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@obsidian-typings/obsidian-public-latest": "^6.16.0",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
@ -690,6 +691,26 @@
"license": "MIT",
"peer": true
},
"node_modules/@obsidian-typings/obsidian-public-1.12.7": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/@obsidian-typings/obsidian-public-1.12.7/-/obsidian-public-1.12.7-6.20.0.tgz",
"integrity": "sha512-z5b7n7niCfkcg65+9D4MM333U1pgiVmJTbtU0jU3V62hcTHTSEqscyvOV6iISpdl4ecuOcssUGWljsBCBEzX9g==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/node": ">=16.0.0"
}
},
"node_modules/@obsidian-typings/obsidian-public-latest": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@obsidian-typings/obsidian-public-latest/-/obsidian-public-latest-6.18.0.tgz",
"integrity": "sha512-Vjvw1V5VccZQHurz5KlyrKNsQ5akOZZUSVKj1u1k7gt5+qhNBH/Zo5TPOr2Vv7KrqGgY9ckHG0SAOarhLMUMjg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@obsidian-typings/obsidian-public-1.12.7": "^6.20.0"
}
},
"node_modules/@rollup/plugin-commonjs": {
"version": "29.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-things-toolkit-plugin",
"version": "1.6.3",
"version": "1.6.4",
"type": "module",
"description": "Sync Things3 completions into Obsidian daily notes with review stats",
"author": "yangcht",
@ -9,8 +9,9 @@
"scripts": {
"lint": "eslint src/**/*.ts",
"test": "tsx --test test/*.test.ts",
"typecheck": "tsc --noEmit",
"build": "npm run lint && npm test && npm run typecheck && rollup -c",
"typecheck": "tsc --noEmit -p tsconfig.json",
"test:typecheck": "tsc --noEmit -p tsconfig.test.json",
"build": "npm run lint && npm test && npm run typecheck && npm run test:typecheck && rollup -c",
"build:nolint": "rollup -c",
"release:create": "npm run build && VERSION=$(node -p \"require('./manifest.json').version\") && gh release create \"$VERSION\" main.js manifest.json styles.css --title \"$VERSION\" --notes \"Release $VERSION\"",
"release:upload": "npm run build && VERSION=$(node -p \"require('./manifest.json').version\") && (gh release delete-asset \"$VERSION\" versions.json -y >/dev/null 2>&1 || true) && gh release upload \"$VERSION\" main.js manifest.json styles.css --clobber",
@ -23,6 +24,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@obsidian-typings/obsidian-public-latest": "^6.16.0",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
@ -37,4 +39,4 @@
"typescript": "latest",
"typescript-eslint": "^8.61.0"
}
}
}

View file

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

View file

@ -50,7 +50,7 @@ export interface ISettings {
canceledMark: string;
}
export const DEFAULT_SETTINGS = Object.freeze({
export const DEFAULT_SETTINGS: Readonly<ISettings> = Object.freeze({
hasAcceptedDisclaimer: false,
latestSyncTime: 0,
appleScriptFallbackLookbackDays: DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS,
@ -67,22 +67,22 @@ export const DEFAULT_SETTINGS = Object.freeze({
syncInterval: DEFAULT_SYNC_FREQUENCY_SECONDS,
sectionHeading: DEFAULT_SECTION_HEADING,
tagPrefix: DEFAULT_TAG_PREFIX,
canceledMark: DEFAULT_CANCELLED_MARK
canceledMark: DEFAULT_CANCELLED_MARK,
});
export class ThingsToolkitSettingsTab extends PluginSettingTab {
private plugin: ThingsToolkitPlugin;
private readonly toolkitPlugin: ThingsToolkitPlugin;
constructor(app: App, plugin: ThingsToolkitPlugin) {
super(app, plugin);
this.plugin = plugin;
this.toolkitPlugin = plugin;
}
display(): void {
this.containerEl.empty();
new Setting(this.containerEl).setName("Sync Engine").setHeading();
if (this.plugin.isSyncSupported()) {
if (this.toolkitPlugin.isSyncSupported()) {
this.addResetLastSyncSetting();
this.addThingsAccessModeSetting();
this.addThingsAccessStatusSetting();
@ -114,10 +114,10 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
"Markdown heading to replace or append when adding Things items to a daily note"
)
.addText((textfield) => {
textfield.setValue(this.plugin.options.sectionHeading);
textfield.setValue(this.toolkitPlugin.options.sectionHeading);
textfield.onChange((rawSectionHeading) => {
const sectionHeading = this.normalizeSectionHeading(rawSectionHeading);
void this.plugin.writeOptions({ sectionHeading });
void this.toolkitPlugin.writeOptions({ sectionHeading });
});
});
}
@ -125,35 +125,38 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
addReviewWindowDaysSetting(): void {
new Setting(this.containerEl)
.setName("Review window")
.setDesc(
"Number of recent days to show and repair in the review calendar"
)
.setDesc("Number of recent days to show and repair in the review calendar")
.addText((textfield) => {
textfield.setValue(String(this.plugin.options.reviewWindowDays));
textfield.setValue(String(this.toolkitPlugin.options.reviewWindowDays));
textfield.inputEl.type = "number";
textfield.inputEl.onblur = (e: FocusEvent) => {
const target = e.target;
textfield.inputEl.onblur = (event: FocusEvent) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
const reviewWindowDays = Math.max(
30,
Math.floor(Number(target.value) || DEFAULT_REVIEW_WINDOW_DAYS)
);
textfield.setValue(String(reviewWindowDays));
void this.plugin.writeOptions({ reviewWindowDays });
void this.toolkitPlugin.writeOptions({ reviewWindowDays });
};
});
}
normalizeSectionHeading(rawSectionHeading: string): string {
const sectionHeading = rawSectionHeading.trim();
if (!sectionHeading) {
return DEFAULT_SECTION_HEADING;
}
if (/^#{1,6}\s+\S/.test(sectionHeading)) {
return sectionHeading;
}
return `## ${sectionHeading.replace(/^#+\s*/, "")}`;
}
@ -161,9 +164,9 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
new Setting(this.containerEl)
.setName("Enable periodic syncing")
.addToggle((toggle) => {
toggle.setValue(this.plugin.options.isSyncEnabled);
toggle.setValue(this.toolkitPlugin.options.isSyncEnabled);
toggle.onChange((isSyncEnabled) => {
void this.plugin.writeOptions({ isSyncEnabled });
void this.toolkitPlugin.writeOptions({ isSyncEnabled });
});
});
}
@ -187,19 +190,22 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
.addOption("auto", "Auto")
.addOption("applescript", "AppleScript")
.addOption("sqlite", "SQLite only");
dropdown.setValue(this.plugin.options.thingsAccessMode);
dropdown.setValue(this.toolkitPlugin.options.thingsAccessMode);
dropdown.onChange(async (value: string) => {
const thingsAccessMode = value as ThingsAccessMode;
await this.plugin.writeOptions({ thingsAccessMode });
await this.toolkitPlugin.writeOptions({ thingsAccessMode });
this.display();
});
});
}
addThingsAccessStatusSetting(): void {
const accessStatus = this.plugin.options.thingsAccessStatus;
const accessStatus = this.toolkitPlugin.options.thingsAccessStatus;
const statusText = accessStatus
? `${accessStatus.message} Checked ${moment.unix(accessStatus.updatedAt).fromNow()}.`
? `${accessStatus.message} Checked ${moment
.unix(accessStatus.updatedAt)
.fromNow()}.`
: "Not checked yet. Run Sync now to test Things access.";
new Setting(this.containerEl)
@ -208,7 +214,7 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
.addButton((button) => {
button.setButtonText("Full Disk Access");
button.onClick(() => {
void this.openSystemSettings(
this.openSystemSettings(
"x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
);
});
@ -216,7 +222,7 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
.addButton((button) => {
button.setButtonText("Automation");
button.onClick(() => {
void this.openSystemSettings(
this.openSystemSettings(
"x-apple.systempreferences:com.apple.preference.security?Privacy_Automation"
);
});
@ -230,25 +236,27 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
addDoesSyncNoteBodySetting(): void {
new Setting(this.containerEl)
.setName("Include notes")
.setDesc('Includes MD notes of a task into the synced Obsidian document')
.setDesc("Includes MD notes of a task into the synced Obsidian document")
.addToggle((toggle) => {
toggle.setValue(this.plugin.options.doesSyncNoteBody);
toggle.setValue(this.toolkitPlugin.options.doesSyncNoteBody);
toggle.onChange((doesSyncNoteBody) => {
void this.plugin.writeOptions({ doesSyncNoteBody });
void this.toolkitPlugin.writeOptions({ doesSyncNoteBody });
});
});
}
addDoesSyncProjectSetting(): void {
new Setting(this.containerEl)
.setName("Include project")
.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((doesSyncProject) => {
void this.plugin.writeOptions({ doesSyncProject });
});
.setName("Include project")
.setDesc(
"If the Things task belongs to a project, use project name as header instead of area"
)
.addToggle((toggle) => {
toggle.setValue(this.toolkitPlugin.options.doesSyncProject);
toggle.onChange((doesSyncProject) => {
void this.toolkitPlugin.writeOptions({ doesSyncProject });
});
});
}
addSyncIntervalSetting(): void {
@ -256,19 +264,21 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
.setName("Sync frequency")
.setDesc("Number of seconds the plugin will wait before syncing again")
.addText((textfield) => {
textfield.setValue(String(this.plugin.options.syncInterval));
textfield.setValue(String(this.toolkitPlugin.options.syncInterval));
textfield.inputEl.type = "number";
textfield.inputEl.onblur = (e: FocusEvent) => {
const target = e.target;
textfield.inputEl.onblur = (event: FocusEvent) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
const syncInterval = Math.max(
60,
Math.floor(Number(target.value) || DEFAULT_SYNC_FREQUENCY_SECONDS)
);
textfield.setValue(String(syncInterval));
void this.plugin.writeOptions({ syncInterval });
void this.toolkitPlugin.writeOptions({ syncInterval });
};
});
}
@ -280,19 +290,27 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
`Days to repair when macOS blocks direct Things database access. The recent review window always uses at least ${MIN_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS} days.`
)
.addText((textfield) => {
textfield.setValue(String(this.plugin.options.appleScriptFallbackLookbackDays));
textfield.setValue(
String(this.toolkitPlugin.options.appleScriptFallbackLookbackDays)
);
textfield.inputEl.type = "number";
textfield.inputEl.onblur = (e: FocusEvent) => {
const target = e.target;
textfield.inputEl.onblur = (event: FocusEvent) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
const appleScriptFallbackLookbackDays = Math.max(
1,
Math.floor(Number(target.value) || DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS)
Math.floor(
Number(target.value) || DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS
)
);
textfield.setValue(String(appleScriptFallbackLookbackDays));
void this.plugin.writeOptions({ appleScriptFallbackLookbackDays });
void this.toolkitPlugin.writeOptions({
appleScriptFallbackLookbackDays,
});
};
});
}
@ -304,77 +322,87 @@ export class ThingsToolkitSettingsTab extends PluginSettingTab {
"Prefix added to Things tags when imported into Obsidian (e.g. #things/work)"
)
.addText((textfield) => {
textfield.setValue(this.plugin.options.tagPrefix);
textfield.setValue(this.toolkitPlugin.options.tagPrefix);
textfield.onChange((tagPrefix) => {
void this.plugin.writeOptions({ tagPrefix });
void this.toolkitPlugin.writeOptions({ tagPrefix });
});
});
}
addCanceledMarkSetting(): void {
new Setting(this.containerEl)
.setName("Canceled mark")
.setDesc(
"Mark character to use for canceled tasks"
)
.addText((textfield) => {
textfield.setValue(this.plugin.options.canceledMark);
textfield.onChange((canceledMark) => {
void this.plugin.writeOptions({ canceledMark });
});
.setName("Canceled mark")
.setDesc("Mark character to use for canceled tasks")
.addText((textfield) => {
textfield.setValue(this.toolkitPlugin.options.canceledMark);
textfield.onChange((canceledMark) => {
void this.toolkitPlugin.writeOptions({ canceledMark });
});
});
}
addDoesAddNewlineBeforeHeadingsSetting(): void {
new Setting(this.containerEl)
.setName("Empty line before headings")
.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((doesAddNewlineBeforeHeadings) => {
void this.plugin.writeOptions({ doesAddNewlineBeforeHeadings });
.setName("Empty line before headings")
.setDesc(
"When grouping tasks with headings by area or project, add an empty line before that heading"
)
.addToggle((toggle) => {
toggle.setValue(
this.toolkitPlugin.options.doesAddNewlineBeforeHeadings
);
toggle.onChange((doesAddNewlineBeforeHeadings) => {
void this.toolkitPlugin.writeOptions({
doesAddNewlineBeforeHeadings,
});
});
});
}
addResetLastSyncSetting(): void {
const { latestSyncTime } = this.plugin.options;
const { syncStatus } = this.plugin;
const syncTime = latestSyncTime > 0
? moment.unix(this.plugin.options.latestSyncTime).fromNow()
: 'Never';
const { latestSyncTime } = this.toolkitPlugin.options;
const { syncStatus } = this.toolkitPlugin;
const syncTime =
latestSyncTime > 0
? moment.unix(this.toolkitPlugin.options.latestSyncTime).fromNow()
: "Never";
new Setting(this.containerEl)
.setDesc(createFragment(el => {
el.appendText('Last sync: ');
el.createSpan({ cls: 'u-pop', text: syncTime });
.setDesc(
createFragment((el) => {
el.appendText("Last sync: ");
el.createSpan({ cls: "u-pop", text: syncTime });
if (syncStatus.message) {
el.createEl("br");
el.appendText(syncStatus.message);
}
}))
.addButton(button => {
button.setButtonText(syncStatus.isSyncing ? 'Syncing...' : 'Sync now');
button.setClass('mod-cta');
button.setDisabled(syncStatus.isSyncing);
button.onClick(async () => {
button.setDisabled(true);
await this.plugin.tryToSyncLogbook();
this.display();
});
})
.addButton(button => {
button.setButtonText('Reset sync history');
button.setClass('mod-danger');
button.setDisabled(syncStatus.isSyncing);
button.onClick(() => {
void this.plugin.writeOptions({ latestSyncTime: 0 });
this.display();
});
})
.addExtraButton(component => {
component.setIcon('lucide-info');
component.setTooltip('Resetting sync history will rewrite the configured Things section in matching daily notes.');
)
.addButton((button) => {
button.setButtonText(syncStatus.isSyncing ? "Syncing..." : "Sync now");
button.setClass("mod-cta");
button.setDisabled(syncStatus.isSyncing);
button.onClick(async () => {
button.setDisabled(true);
await this.toolkitPlugin.tryToSyncLogbook();
this.display();
});
})
.addButton((button) => {
button.setButtonText("Reset sync history");
button.setClass("mod-danger");
button.setDisabled(syncStatus.isSyncing);
button.onClick(() => {
void this.toolkitPlugin.writeOptions({ latestSyncTime: 0 });
this.display();
});
})
.addExtraButton((component) => {
component.setIcon("lucide-info");
component.setTooltip(
"Resetting sync history will rewrite the configured Things section in matching daily notes."
);
});
}
}
}

View file

@ -9,10 +9,10 @@
"importHelpers": true,
"strict": true,
"noImplicitAny": true,
"types": ["node"],
"lib": ["DOM", "ES2022"],
"types": ["node", "@obsidian-typings/obsidian-public-latest"],
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "test/**/*.ts"],
"exclude": ["node_modules", "main.js"]
}
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "main.js", "test/**/*.ts"]
}

7
tsconfig.test.json Normal file
View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node", "@obsidian-typings/obsidian-public-latest"]
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}

View file

@ -8,5 +8,6 @@
"1.6.0": "1.1.0",
"1.6.1": "1.13.1",
"1.6.2": "1.13.1",
"1.6.3": "1.13.1"
"1.6.3": "1.13.1",
"1.6.4": "1.13.1"
}

View file

@ -118,6 +118,18 @@
resolved "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@obsidian-typings/obsidian-public-1.12.7@^6.20.0":
version "6.20.0"
resolved "https://registry.npmjs.org/@obsidian-typings/obsidian-public-1.12.7/-/obsidian-public-1.12.7-6.20.0.tgz"
integrity sha512-z5b7n7niCfkcg65+9D4MM333U1pgiVmJTbtU0jU3V62hcTHTSEqscyvOV6iISpdl4ecuOcssUGWljsBCBEzX9g==
"@obsidian-typings/obsidian-public-latest@^6.16.0":
version "6.18.0"
resolved "https://registry.npmjs.org/@obsidian-typings/obsidian-public-latest/-/obsidian-public-latest-6.18.0.tgz"
integrity sha512-Vjvw1V5VccZQHurz5KlyrKNsQ5akOZZUSVKj1u1k7gt5+qhNBH/Zo5TPOr2Vv7KrqGgY9ckHG0SAOarhLMUMjg==
dependencies:
"@obsidian-typings/obsidian-public-1.12.7" "^6.20.0"
"@rollup/plugin-commonjs@^29.0.3":
version "29.0.3"
resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz"
@ -186,7 +198,7 @@
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/node@*", "@types/node@latest":
"@types/node@*", "@types/node@>=16.0.0", "@types/node@latest":
version "25.9.3"
resolved "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz"
integrity sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==
@ -565,11 +577,6 @@ flatted@^3.2.9:
resolved "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz"
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"