mirror of
https://github.com/yangcht/obsidian-things-toolkit.git
synced 2026-07-22 07:45:44 +00:00
Merge pull request #1 from yangcht/fix/release-attestations-and-assets
Fix/release attestations and assets
This commit is contained in:
commit
90e1ff3c03
8 changed files with 254 additions and 176 deletions
|
|
@ -23,12 +23,10 @@
|
|||
"@rollup/plugin-node-resolve": "11.2.0",
|
||||
"@rollup/plugin-replace": "2.4.1",
|
||||
"@rollup/plugin-typescript": "8.2.0",
|
||||
"@types/moment": "2.13.0",
|
||||
"@types/papaparse": "5.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.17.0",
|
||||
"@typescript-eslint/parser": "4.17.0",
|
||||
"eslint": "7.22.0",
|
||||
"moment": "^2.30.1",
|
||||
"rollup": "^2.80.0",
|
||||
"typescript": "4.2.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
import { Editor } from "codemirror";
|
||||
import { App, MarkdownView, TFile } from "obsidian";
|
||||
import { App, MarkdownView, TFile, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
type MarkdownEditor = Editor & {
|
||||
replaceRange: Editor["replaceRange"];
|
||||
};
|
||||
|
||||
type MarkdownViewWithEditor = MarkdownView & {
|
||||
editor?: MarkdownEditor;
|
||||
sourceMode?: { cmEditor?: MarkdownEditor };
|
||||
};
|
||||
|
||||
function isMarkdownViewWithEditor(view: unknown): view is MarkdownViewWithEditor {
|
||||
return view instanceof MarkdownView;
|
||||
}
|
||||
|
||||
export function getEditorForFile(app: App, file: TFile): MarkdownEditor | null {
|
||||
let editor = null;
|
||||
app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (leaf.view instanceof MarkdownView && leaf.view.file === file) {
|
||||
const view = leaf.view as MarkdownView & {
|
||||
editor?: MarkdownEditor;
|
||||
sourceMode?: { cmEditor?: MarkdownEditor };
|
||||
};
|
||||
editor = view.editor || view.sourceMode?.cmEditor;
|
||||
let editor: MarkdownEditor | null = null;
|
||||
app.workspace.iterateAllLeaves((leaf: WorkspaceLeaf) => {
|
||||
const { view } = leaf;
|
||||
if (isMarkdownViewWithEditor(view) && view.file === file) {
|
||||
editor = view.editor ?? view.sourceMode?.cmEditor ?? null;
|
||||
}
|
||||
});
|
||||
return editor;
|
||||
|
|
|
|||
240
src/index.ts
240
src/index.ts
|
|
@ -1,11 +1,17 @@
|
|||
import type moment from "moment";
|
||||
import { Notice, Plugin, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import {
|
||||
Notice,
|
||||
Platform,
|
||||
Plugin,
|
||||
TFile,
|
||||
WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
import {
|
||||
createDailyNote,
|
||||
getDailyNote,
|
||||
getAllDailyNotes,
|
||||
getDailyNote,
|
||||
} from "obsidian-daily-notes-interface";
|
||||
|
||||
import { getMoment, type MomentLike } from "./moment";
|
||||
import { ConfirmationModal } from "./modal";
|
||||
import { ToolkitRenderer } from "./renderer";
|
||||
import {
|
||||
|
|
@ -19,7 +25,6 @@ import {
|
|||
ISettings,
|
||||
ThingsToolkitSettingsTab,
|
||||
} from "./settings";
|
||||
|
||||
import {
|
||||
buildTasksFromSQLRecords,
|
||||
fetchThingsToolkit,
|
||||
|
|
@ -32,10 +37,28 @@ import {
|
|||
updateSection,
|
||||
} from "./textUtils";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
moment: typeof moment;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
function getDateKey(date: MomentLike): string {
|
||||
return date.format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
export interface ISyncStatus {
|
||||
|
|
@ -44,20 +67,18 @@ export interface ISyncStatus {
|
|||
}
|
||||
|
||||
export default class ThingsToolkitPlugin extends Plugin {
|
||||
public options: ISettings;
|
||||
public options!: ISettings;
|
||||
public syncStatus: ISyncStatus = {
|
||||
isSyncing: false,
|
||||
message: "",
|
||||
};
|
||||
private syncTimeoutId: number;
|
||||
private settingsTab: ThingsToolkitSettingsTab;
|
||||
private statusBarEl: HTMLElement;
|
||||
private syncTimeoutId?: number;
|
||||
private settingsTab?: ThingsToolkitSettingsTab;
|
||||
private statusBarEl?: HTMLElement;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
if (!isMacOS()) {
|
||||
console.info(
|
||||
"Failed to load Things Toolkit plugin. Platform not supported"
|
||||
);
|
||||
if (!Platform.isDesktopApp || !isMacOS()) {
|
||||
console.info("Failed to load Things Toolkit plugin. Platform not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -72,29 +93,33 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
);
|
||||
|
||||
this.addCommand({
|
||||
id: "sync-things-toolkit",
|
||||
id: "sync",
|
||||
name: "Sync",
|
||||
callback: () => setTimeout(() => this.tryToSyncLogbook(), 20),
|
||||
callback: () => window.setTimeout(() => {
|
||||
void this.tryToSyncLogbook();
|
||||
}, 20),
|
||||
});
|
||||
this.addCommand({
|
||||
id: "open-things-toolkit-review",
|
||||
id: "open-review",
|
||||
name: "Open review",
|
||||
callback: () => this.activateReviewView(),
|
||||
callback: () => {
|
||||
void this.activateReviewView();
|
||||
},
|
||||
});
|
||||
|
||||
this.addRibbonIcon(
|
||||
"calendar-check",
|
||||
"Open Things toolkit review",
|
||||
() => this.activateReviewView()
|
||||
);
|
||||
this.addRibbonIcon("calendar-check", "Open Things toolkit review", () => {
|
||||
void this.activateReviewView();
|
||||
});
|
||||
|
||||
await this.loadOptions();
|
||||
this.statusBarEl = this.addStatusBarItem();
|
||||
this.statusBarEl.addClass("things-toolkit-status");
|
||||
this.statusBarEl.addEventListener("click", () => this.activateReviewView());
|
||||
this.statusBarEl.addEventListener("click", () => {
|
||||
void this.activateReviewView();
|
||||
});
|
||||
this.updateStatusBar();
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.refreshRecentDailyStats().then(() => {
|
||||
getWorkspace(this).onLayoutReady(() => {
|
||||
void this.refreshRecentDailyStats().then(() => {
|
||||
this.updateStatusBar();
|
||||
this.refreshReviewViews();
|
||||
});
|
||||
|
|
@ -104,21 +129,23 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
this.addSettingTab(this.settingsTab);
|
||||
|
||||
if (this.options.hasAcceptedDisclaimer && this.options.isSyncEnabled) {
|
||||
if (this.app.workspace.layoutReady) {
|
||||
if (getWorkspace(this).layoutReady) {
|
||||
this.scheduleNextSync();
|
||||
} else {
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("layout-ready", this.scheduleNextSync)
|
||||
getWorkspace(this).on("layout-ready", () => {
|
||||
this.scheduleNextSync();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async activateReviewView(): Promise<void> {
|
||||
const { workspace } = this.app;
|
||||
let leaf = workspace.getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW)[0];
|
||||
const workspace = getWorkspace(this);
|
||||
let leaf = workspace.getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW)[0] as ReviewLeaf | undefined;
|
||||
if (!leaf) {
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
leaf = workspace.getRightLeaf(false) as ReviewLeaf | null;
|
||||
if (!leaf) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -133,43 +160,46 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
async tryToSyncLogbook(): Promise<void> {
|
||||
if (this.options.hasAcceptedDisclaimer) {
|
||||
await this.syncLogbook();
|
||||
} else {
|
||||
new ConfirmationModal(this.app, {
|
||||
cta: "Sync",
|
||||
onAccept: async () => {
|
||||
await this.writeOptions({ hasAcceptedDisclaimer: true });
|
||||
await this.syncLogbook();
|
||||
},
|
||||
text:
|
||||
"Enabling sync will repair the configured review window from Things3 into Obsidian. This can create or modify many daily notes. Make sure to test the plugin in a test vault before continuing.",
|
||||
title: "Sync Now?",
|
||||
}).open();
|
||||
return;
|
||||
}
|
||||
|
||||
new ConfirmationModal(this.app, {
|
||||
cta: "Sync",
|
||||
onAccept: async () => {
|
||||
await this.writeOptions({ hasAcceptedDisclaimer: true });
|
||||
await this.syncLogbook();
|
||||
},
|
||||
text:
|
||||
"Enabling sync will repair the configured review window from Things3 into Obsidian. This can create or modify many daily notes. Make sure to test the plugin in a test vault before continuing.",
|
||||
title: "Sync Now?",
|
||||
}).open();
|
||||
}
|
||||
|
||||
async tryToScheduleSync(): Promise<void> {
|
||||
if (this.options.hasAcceptedDisclaimer) {
|
||||
this.scheduleNextSync();
|
||||
} else {
|
||||
new ConfirmationModal(this.app, {
|
||||
cta: "Sync",
|
||||
onAccept: async () => {
|
||||
await this.writeOptions({ hasAcceptedDisclaimer: true });
|
||||
this.scheduleNextSync();
|
||||
},
|
||||
onCancel: async () => {
|
||||
await this.writeOptions({ isSyncEnabled: false });
|
||||
// update the settings tab display
|
||||
this.settingsTab.display();
|
||||
},
|
||||
text:
|
||||
"Enabling sync will repair the configured review window from Things3 into Obsidian. This can create or modify many daily notes. Make sure to test the plugin in a test vault before continuing.",
|
||||
title: "Sync Now?",
|
||||
}).open();
|
||||
return;
|
||||
}
|
||||
|
||||
new ConfirmationModal(this.app, {
|
||||
cta: "Sync",
|
||||
onAccept: async () => {
|
||||
await this.writeOptions({ hasAcceptedDisclaimer: true });
|
||||
this.scheduleNextSync();
|
||||
},
|
||||
onCancel: () => {
|
||||
void this.writeOptions({ isSyncEnabled: false }).then(() => {
|
||||
this.settingsTab?.display();
|
||||
});
|
||||
},
|
||||
text:
|
||||
"Enabling sync will repair the configured review window from Things3 into Obsidian. This can create or modify many daily notes. Make sure to test the plugin in a test vault before continuing.",
|
||||
title: "Sync Now?",
|
||||
}).open();
|
||||
}
|
||||
|
||||
async syncLogbook(): Promise<void> {
|
||||
const moment = getMoment();
|
||||
if (this.syncStatus.isSyncing) {
|
||||
new Notice("Things Toolkit sync already running");
|
||||
return;
|
||||
|
|
@ -204,31 +234,34 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
|
||||
const daysToTasks: Record<string, ITask[]> = groupBy(
|
||||
tasks.filter((task) => task.stopDate),
|
||||
(task) => window.moment.unix(task.stopDate).startOf("day").format()
|
||||
(task) => moment.unix(task.stopDate).startOf("day").format()
|
||||
);
|
||||
const dayEntries = Object.entries(daysToTasks).sort(([a], [b]) =>
|
||||
window.moment(a).diff(window.moment(b))
|
||||
moment(a).diff(moment(b))
|
||||
);
|
||||
dayCount = dayEntries.length;
|
||||
const dailyStats = this.buildDailyStats(
|
||||
dayEntries,
|
||||
fetchResult.source,
|
||||
window.moment().unix()
|
||||
moment().unix()
|
||||
);
|
||||
|
||||
for (const [dateStr, tasks] of dayEntries) {
|
||||
const date = window.moment(dateStr);
|
||||
|
||||
for (const [dateStr, groupedTasks] of dayEntries) {
|
||||
const date = moment(dateStr);
|
||||
let dailyNote = getDailyNote(date, dailyNotes);
|
||||
if (!dailyNote) {
|
||||
dailyNote = await createDailyNote(date);
|
||||
}
|
||||
|
||||
if (!isTFile(dailyNote)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const didChange = await updateSection(
|
||||
this.app,
|
||||
dailyNote,
|
||||
this.options.sectionHeading,
|
||||
logbookRenderer.render(tasks)
|
||||
logbookRenderer.render(groupedTasks)
|
||||
);
|
||||
if (didChange) {
|
||||
changedDayCount++;
|
||||
|
|
@ -236,7 +269,7 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
}
|
||||
|
||||
await this.writeOptions({
|
||||
latestSyncTime: window.moment().unix(),
|
||||
latestSyncTime: moment().unix(),
|
||||
dailyStats,
|
||||
thingsAccessStatus: fetchResult.accessStatus,
|
||||
});
|
||||
|
|
@ -274,9 +307,10 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
source: IDailyLogbookStat["source"],
|
||||
syncedAt: number
|
||||
): Record<string, IDailyLogbookStat> {
|
||||
const moment = getMoment();
|
||||
const dailyStats = { ...(this.options.dailyStats || {}) };
|
||||
dayEntries.forEach(([dateStr, tasks]) => {
|
||||
const dateKey = window.moment(dateStr).format("YYYY-MM-DD");
|
||||
const dateKey = moment(dateStr).format("YYYY-MM-DD");
|
||||
dailyStats[dateKey] = {
|
||||
taskCount: tasks.length,
|
||||
source,
|
||||
|
|
@ -291,23 +325,21 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
}
|
||||
|
||||
getReviewWindowDayCount(): number {
|
||||
return Math.max(
|
||||
7,
|
||||
Math.floor(Number(this.options.reviewWindowDays) || 365)
|
||||
);
|
||||
return Math.max(7, Math.floor(Number(this.options.reviewWindowDays) || 365));
|
||||
}
|
||||
|
||||
async refreshRecentDailyStats(dayCount = this.getReviewWindowDayCount()): Promise<void> {
|
||||
const moment = getMoment();
|
||||
const dailyStats = { ...(this.options.dailyStats || {}) };
|
||||
const dailyNotes = getAllDailyNotes();
|
||||
const syncedAt = window.moment().unix();
|
||||
const end = window.moment().startOf("day");
|
||||
const syncedAt = moment().unix();
|
||||
const end = moment().startOf("day");
|
||||
const start = end.clone().subtract(dayCount - 1, "days");
|
||||
|
||||
for (let date = start; date.isSameOrBefore(end); date.add(1, "day")) {
|
||||
const dateKey = date.format("YYYY-MM-DD");
|
||||
for (let date = start.clone(); date.isSameOrBefore(end); date.add(1, "day")) {
|
||||
const dateKey = getDateKey(date);
|
||||
const dailyNote = getDailyNote(date, dailyNotes);
|
||||
if (!dailyNote) {
|
||||
if (!isTFile(dailyNote)) {
|
||||
dailyStats[dateKey] = {
|
||||
taskCount: 0,
|
||||
source: "daily-note",
|
||||
|
|
@ -318,10 +350,7 @@ 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,
|
||||
};
|
||||
|
|
@ -331,11 +360,12 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
}
|
||||
|
||||
getCurrentCompletionStreak(): number {
|
||||
const moment = getMoment();
|
||||
let streak = 0;
|
||||
for (
|
||||
let date = window.moment().startOf("day");
|
||||
this.getTaskCountForDay(date.format("YYYY-MM-DD")) > 0;
|
||||
date = date.subtract(1, "day")
|
||||
let date = moment().startOf("day");
|
||||
this.getTaskCountForDay(getDateKey(date)) > 0;
|
||||
date = date.clone().subtract(1, "day")
|
||||
) {
|
||||
streak++;
|
||||
}
|
||||
|
|
@ -346,11 +376,12 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
dateKey: string,
|
||||
diff: Partial<IDailyLogbookReview>
|
||||
): Promise<void> {
|
||||
const moment = getMoment();
|
||||
const dailyReviews = { ...(this.options.dailyReviews || {}) };
|
||||
const nextReview = {
|
||||
...(dailyReviews[dateKey] || {}),
|
||||
...diff,
|
||||
updatedAt: window.moment().unix(),
|
||||
updatedAt: moment().unix(),
|
||||
};
|
||||
if (!nextReview.rating && !nextReview.reflection) {
|
||||
delete dailyReviews[dateKey];
|
||||
|
|
@ -362,17 +393,21 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async openDailyNote(dateKey: string): Promise<void> {
|
||||
const date = window.moment(dateKey, "YYYY-MM-DD");
|
||||
const moment = getMoment();
|
||||
const date = moment(dateKey, "YYYY-MM-DD");
|
||||
const dailyNotes = getAllDailyNotes();
|
||||
let dailyNote = getDailyNote(date, dailyNotes);
|
||||
if (!dailyNote) {
|
||||
dailyNote = await createDailyNote(date);
|
||||
}
|
||||
await this.app.workspace.getLeaf(false).openFile(dailyNote as TFile);
|
||||
if (!isTFile(dailyNote)) {
|
||||
throw new Error("Daily note could not be opened as a file");
|
||||
}
|
||||
await (getWorkspace(this).getLeaf(false) as ReviewLeaf).openFile(dailyNote);
|
||||
}
|
||||
|
||||
refreshReviewViews(): void {
|
||||
this.app.workspace
|
||||
getWorkspace(this)
|
||||
.getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW)
|
||||
.forEach((leaf) => {
|
||||
if (leaf.view instanceof ThingsToolkitReviewView) {
|
||||
|
|
@ -382,18 +417,16 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
}
|
||||
|
||||
updateStatusBar(): void {
|
||||
const moment = getMoment();
|
||||
if (!this.statusBarEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const todayKey = window.moment().format("YYYY-MM-DD");
|
||||
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"
|
||||
);
|
||||
this.statusBarEl.setAttribute("aria-label", "Open Things toolkit review");
|
||||
}
|
||||
|
||||
cancelScheduledSync(): void {
|
||||
|
|
@ -403,7 +436,8 @@ export default class ThingsToolkitPlugin extends Plugin {
|
|||
}
|
||||
|
||||
scheduleNextSync(): void {
|
||||
const now = window.moment().unix();
|
||||
const moment = getMoment();
|
||||
const now = moment().unix();
|
||||
|
||||
this.cancelScheduledSync();
|
||||
if (!this.options.isSyncEnabled || !this.options.syncInterval) {
|
||||
|
|
@ -416,31 +450,29 @@ 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(this.syncLogbook, nextSync);
|
||||
this.syncTimeoutId = window.setTimeout(() => {
|
||||
void this.syncLogbook();
|
||||
}, nextSync);
|
||||
}
|
||||
|
||||
async loadOptions(): Promise<void> {
|
||||
this.options = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
this.options = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) as ISettings;
|
||||
if (!this.options.hasAcceptedDisclaimer) {
|
||||
// In case the user quits before accepting sync modal,
|
||||
// this keep the settings in sync
|
||||
this.options.isSyncEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async writeOptions(diff: Partial<ISettings>): Promise<void> {
|
||||
this.options = Object.assign(this.options, diff);
|
||||
this.options = Object.assign({}, this.options, diff);
|
||||
|
||||
// Sync toggled on/off
|
||||
if (diff.isSyncEnabled !== undefined) {
|
||||
if (diff.isSyncEnabled) {
|
||||
this.tryToScheduleSync();
|
||||
await this.tryToScheduleSync();
|
||||
} else {
|
||||
this.cancelScheduledSync();
|
||||
}
|
||||
} else if (diff.syncInterval !== undefined && this.options.isSyncEnabled) {
|
||||
// reschedule if interval changed
|
||||
this.tryToScheduleSync();
|
||||
await this.tryToScheduleSync();
|
||||
}
|
||||
|
||||
await this.saveData(this.options);
|
||||
|
|
|
|||
28
src/modal.ts
28
src/modal.ts
|
|
@ -2,15 +2,15 @@ import { App, Modal } from "obsidian";
|
|||
|
||||
interface IConfirmationDialogParams {
|
||||
cta: string;
|
||||
onAccept: (...args: unknown[]) => Promise<void>;
|
||||
onCancel?: (...args: unknown[]) => void;
|
||||
onAccept: () => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
text: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export class ConfirmationModal extends Modal {
|
||||
private config: IConfirmationDialogParams;
|
||||
private accepted: boolean;
|
||||
private accepted = false;
|
||||
private buttonContainerEl: HTMLElement;
|
||||
|
||||
constructor(app: App, config: IConfirmationDialogParams) {
|
||||
|
|
@ -19,28 +19,30 @@ export class ConfirmationModal extends Modal {
|
|||
|
||||
const { cta, onAccept, text, title } = config;
|
||||
|
||||
this.containerEl.addClass('mod-confirmation');
|
||||
this.containerEl.addClass("mod-confirmation");
|
||||
this.titleEl.setText(title);
|
||||
this.contentEl.setText(text);
|
||||
|
||||
this.buttonContainerEl = this.modalEl.createDiv('modal-button-container');
|
||||
this.buttonContainerEl = this.modalEl.createDiv("modal-button-container");
|
||||
|
||||
const acceptBtnEl = this.buttonContainerEl.createEl("button", {
|
||||
cls: "mod-cta",
|
||||
text: cta,
|
||||
});
|
||||
acceptBtnEl.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
acceptBtnEl.addEventListener("click", (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
this.accepted = true;
|
||||
this.close();
|
||||
onAccept(e);
|
||||
void onAccept();
|
||||
});
|
||||
|
||||
const cancelBtnEl = this.buttonContainerEl.createEl("button", { text: "Never mind" });
|
||||
cancelBtnEl.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
this.close();
|
||||
});
|
||||
const cancelBtnEl = this.buttonContainerEl.createEl("button", {
|
||||
text: "Never mind",
|
||||
});
|
||||
cancelBtnEl.addEventListener("click", (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
|
|
|
|||
35
src/moment.ts
Normal file
35
src/moment.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
export interface MomentLike {
|
||||
unix(): number;
|
||||
startOf(unit: string): MomentLike;
|
||||
endOf(unit: string): MomentLike;
|
||||
format(pattern?: string): string;
|
||||
diff(input: string | MomentLike, unit?: string): number;
|
||||
clone(): MomentLike;
|
||||
subtract(amount: number, unit: string): MomentLike;
|
||||
add(amount: number, unit: string): MomentLike;
|
||||
isSameOrBefore(input: MomentLike, unit?: string): boolean;
|
||||
isBefore(input: MomentLike, unit?: string): boolean;
|
||||
isAfter(input: MomentLike, unit?: string): boolean;
|
||||
isSame(input: MomentLike, unit?: string): boolean;
|
||||
isoWeek(): number;
|
||||
year(): number;
|
||||
month(): number;
|
||||
date(): number;
|
||||
fromNow(): string;
|
||||
}
|
||||
|
||||
export interface MomentFactory {
|
||||
(): MomentLike;
|
||||
(input: string, format?: string, strict?: boolean): MomentLike;
|
||||
unix(timestamp: number): MomentLike;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
moment: MomentFactory;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMoment(): MomentFactory {
|
||||
return window.moment;
|
||||
}
|
||||
|
|
@ -3,6 +3,15 @@ import { ISettings } from "./settings";
|
|||
import { ISubTask, ITask } from "./things";
|
||||
import { getHeadingLevel, getTab, groupBy, toHeading } from "./textUtils";
|
||||
|
||||
interface VaultConfigReader {
|
||||
getConfig(key: "useTab"): boolean;
|
||||
getConfig(key: "tabSize"): number;
|
||||
}
|
||||
|
||||
function getVaultConfig(app: App): VaultConfigReader {
|
||||
return app.vault as unknown as VaultConfigReader;
|
||||
}
|
||||
|
||||
export class ToolkitRenderer {
|
||||
private app: App;
|
||||
private settings: ISettings;
|
||||
|
|
@ -14,31 +23,33 @@ export class ToolkitRenderer {
|
|||
}
|
||||
|
||||
renderTask(task: ITask): string {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const vault = this.app.vault as any;
|
||||
const vault = getVaultConfig(this.app);
|
||||
const tab = getTab(vault.getConfig("useTab"), vault.getConfig("tabSize"));
|
||||
const prefix = this.settings.tagPrefix;
|
||||
|
||||
const tags = Array.from(new Set(task.tags
|
||||
.filter((tag) => !!tag)
|
||||
.map((tag) => tag.replace(/\s+/g, "-").toLowerCase())
|
||||
.sort()
|
||||
))
|
||||
const tags = Array.from(
|
||||
new Set(
|
||||
task.tags
|
||||
.filter((tag) => !!tag)
|
||||
.map((tag) => tag.replace(/\s+/g, "-").toLowerCase())
|
||||
.sort()
|
||||
)
|
||||
)
|
||||
.map((tag) => `#${prefix}${tag}`)
|
||||
.join(" ");
|
||||
|
||||
const taskTitle = `[${task.title}](things:///show?id=${task.uuid}) ${tags}`.trimEnd()
|
||||
const taskTitle = `[${task.title}](things:///show?id=${task.uuid}) ${tags}`.trimEnd();
|
||||
|
||||
const notes = this.settings.doesSyncNoteBody
|
||||
? String(task.notes || "")
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.filter((line) => !!line)
|
||||
.map((noteLine) => `${tab}${noteLine}`)
|
||||
: ""
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.filter((line) => !!line)
|
||||
.map((noteLine) => `${tab}${noteLine}`)
|
||||
: [];
|
||||
|
||||
return [
|
||||
`- [${task.cancelled ? this.settings.canceledMark : 'x'}] ${taskTitle}`,
|
||||
`- [${task.cancelled ? this.settings.canceledMark : "x"}] ${taskTitle}`,
|
||||
...notes,
|
||||
...task.subtasks.map(
|
||||
(subtask: ISubTask) =>
|
||||
|
|
@ -51,15 +62,20 @@ export class ToolkitRenderer {
|
|||
|
||||
public render(tasks: ITask[]): string {
|
||||
const { sectionHeading, doesSyncProject, doesAddNewlineBeforeHeadings } = this.settings;
|
||||
const headings = groupBy<ITask>(tasks, (task) => task.area || (doesSyncProject ? task.project : "") || "");
|
||||
const headingLevel = getHeadingLevel(sectionHeading);
|
||||
const headings = groupBy<ITask>(
|
||||
tasks,
|
||||
(task) => task.area || (doesSyncProject ? task.project : "") || ""
|
||||
);
|
||||
const headingLevel = getHeadingLevel(sectionHeading) ?? 2;
|
||||
|
||||
const output = [sectionHeading];
|
||||
Object.entries(headings).map(([heading, tasks]) => {
|
||||
Object.entries(headings).forEach(([heading, groupedTasks]) => {
|
||||
if (heading !== "") {
|
||||
output.push(toHeading(heading, headingLevel + 1, doesAddNewlineBeforeHeadings));
|
||||
output.push(
|
||||
toHeading(heading, headingLevel + 1, doesAddNewlineBeforeHeadings)
|
||||
);
|
||||
}
|
||||
output.push(...tasks.map(this.renderTask));
|
||||
output.push(...groupedTasks.map(this.renderTask));
|
||||
});
|
||||
|
||||
return output.join("\n");
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const TASK_FETCH_LIMIT = 1000;
|
|||
interface ISpawnResults {
|
||||
stdOut: Buffer[];
|
||||
stdErr: Buffer[];
|
||||
code: number;
|
||||
code: number | null;
|
||||
}
|
||||
|
||||
function parseCSV<T>(csv: Buffer[]): T[] {
|
||||
|
|
@ -47,10 +47,10 @@ async function handleSqliteQuery(
|
|||
});
|
||||
|
||||
spawned.on("error", (err: Error) => {
|
||||
stdErr.push(Buffer.from(String(err.stack), "ascii"));
|
||||
stdErr.push(Buffer.from(err.stack ?? err.message, "utf-8"));
|
||||
});
|
||||
spawned.on("close", (code: number) => finish({ stdErr, stdOut, code }));
|
||||
spawned.on("exit", (code: number) => finish({ stdErr, stdOut, code }));
|
||||
spawned.on("close", (code: number | null) => finish({ stdErr, stdOut, code }));
|
||||
spawned.on("exit", (code: number | null) => finish({ stdErr, stdOut, code }));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -58,10 +58,10 @@ export async function querySqliteDB<T>(
|
|||
dbPath: string,
|
||||
query: string
|
||||
): Promise<T[]> {
|
||||
const { stdOut, stdErr } = await handleSqliteQuery(dbPath, query);
|
||||
if (stdErr.length) {
|
||||
const error = Buffer.concat(stdErr).toString("utf-8");
|
||||
return Promise.reject(error);
|
||||
const { stdOut, stdErr, code } = await handleSqliteQuery(dbPath, query);
|
||||
if (stdErr.length || code !== 0) {
|
||||
const error = Buffer.concat(stdErr).toString("utf-8") || `sqlite3 exited with code ${String(code)}`;
|
||||
throw new Error(error);
|
||||
}
|
||||
return parseCSV<T>(stdOut);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { App, TFile } from "obsidian";
|
||||
import { Platform } from "obsidian";
|
||||
import { getEditorForFile } from "./fileUtils";
|
||||
|
||||
export function getHeadingLevel(line = ""): number | null {
|
||||
|
|
@ -32,7 +33,7 @@ export function groupBy<T>(
|
|||
}
|
||||
|
||||
export function isMacOS(): boolean {
|
||||
return navigator.appVersion.indexOf("Mac") !== -1;
|
||||
return Platform.isMacOS;
|
||||
}
|
||||
|
||||
export async function updateSection(
|
||||
|
|
@ -67,9 +68,6 @@ export async function updateSection(
|
|||
|
||||
const editor = getEditorForFile(app, file);
|
||||
if (editor) {
|
||||
// if the "## Logbook" header exists, we just replace the
|
||||
// section. If it doesn't, we need to append it to the end
|
||||
// if the file and add `\n` for separation.
|
||||
if (logbookSectionLineNum !== -1) {
|
||||
const currentSection = fileLines
|
||||
.slice(
|
||||
|
|
@ -89,16 +87,14 @@ export async function updateSection(
|
|||
: { line: fileLines.length, ch: 0 };
|
||||
editor.replaceRange(`${sectionContents}\n`, from, to);
|
||||
return true;
|
||||
} else {
|
||||
const pos = { line: fileLines.length, ch: 0 };
|
||||
editor.replaceRange(`\n\n${sectionContents}`, pos, pos);
|
||||
return true;
|
||||
}
|
||||
|
||||
const pos = { line: fileLines.length, ch: 0 };
|
||||
editor.replaceRange(`\n\n${sectionContents}`, pos, pos);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Editor is not open, modify the file on disk...
|
||||
if (logbookSectionLineNum !== -1) {
|
||||
// Section already exists, just replace
|
||||
const prefix = fileLines.slice(0, logbookSectionLineNum);
|
||||
const suffix =
|
||||
nextSectionLineNum !== -1 ? fileLines.slice(nextSectionLineNum) : [];
|
||||
|
|
@ -114,19 +110,12 @@ export async function updateSection(
|
|||
return false;
|
||||
}
|
||||
|
||||
await vault.process(
|
||||
file,
|
||||
() => [...prefix, sectionContents, ...suffix].join("\n")
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
// Section does not exist, append to end of file.
|
||||
await vault.process(
|
||||
file,
|
||||
() => [...fileLines, "", sectionContents].join("\n")
|
||||
);
|
||||
await vault.process(file, () => [...prefix, sectionContents, ...suffix].join("\n"));
|
||||
return true;
|
||||
}
|
||||
|
||||
await vault.process(file, () => [...fileLines, "", sectionContents].join("\n"));
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getSectionContents(
|
||||
|
|
|
|||
Loading…
Reference in a new issue