From 68353a76315ab644befbc2a3f6e6f129eacb7d3f Mon Sep 17 00:00:00 2001 From: Chentao Yang Date: Sat, 13 Jun 2026 20:44:39 +0200 Subject: [PATCH] chore: untrack main.js (release asset only), update yarn.lock --- main.js | 1749 ----------------------------------------------------- yarn.lock | 196 +++++- 2 files changed, 177 insertions(+), 1768 deletions(-) delete mode 100644 main.js diff --git a/main.js b/main.js deleted file mode 100644 index 49c5bcd..0000000 --- a/main.js +++ /dev/null @@ -1,1749 +0,0 @@ -'use strict'; - -var obsidian = require('obsidian'); -var child_process = require('child_process'); -var os = require('os'); -var fs = require('fs'); -var path = require('path'); - -function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } - -var obsidian__default = /*#__PURE__*/_interopDefaultLegacy(obsidian); - -const DEFAULT_DAILY_NOTE_FORMAT = "YYYY-MM-DD"; -const DEFAULT_WEEKLY_NOTE_FORMAT = "gggg-[W]ww"; -const DEFAULT_MONTHLY_NOTE_FORMAT = "YYYY-MM"; - -function shouldUsePeriodicNotesSettings(periodicity) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const periodicNotes = window.app.plugins.getPlugin("periodic-notes"); - return periodicNotes && periodicNotes.settings?.[periodicity]?.enabled; -} -/** - * Read the user settings for the `daily-notes` plugin - * to keep behavior of creating a new note in-sync. - */ -function getDailyNoteSettings() { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { internalPlugins, plugins } = window.app; - if (shouldUsePeriodicNotesSettings("daily")) { - const { format, folder, template } = plugins.getPlugin("periodic-notes")?.settings?.daily || {}; - return { - format: format || DEFAULT_DAILY_NOTE_FORMAT, - folder: folder?.trim() || "", - template: template?.trim() || "", - }; - } - const { folder, format, template } = internalPlugins.getPluginById("daily-notes")?.instance?.options || {}; - return { - format: format || DEFAULT_DAILY_NOTE_FORMAT, - folder: folder?.trim() || "", - template: template?.trim() || "", - }; - } - catch (err) { - console.info("No custom daily note settings found!", err); - } -} -/** - * Read the user settings for the `weekly-notes` plugin - * to keep behavior of creating a new note in-sync. - */ -function getWeeklyNoteSettings() { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pluginManager = window.app.plugins; - const calendarSettings = pluginManager.getPlugin("calendar")?.options; - const periodicNotesSettings = pluginManager.getPlugin("periodic-notes") - ?.settings?.weekly; - if (shouldUsePeriodicNotesSettings("weekly")) { - return { - format: periodicNotesSettings.format || DEFAULT_WEEKLY_NOTE_FORMAT, - folder: periodicNotesSettings.folder?.trim() || "", - template: periodicNotesSettings.template?.trim() || "", - }; - } - const settings = calendarSettings || {}; - return { - format: settings.weeklyNoteFormat || DEFAULT_WEEKLY_NOTE_FORMAT, - folder: settings.weeklyNoteFolder?.trim() || "", - template: settings.weeklyNoteTemplate?.trim() || "", - }; - } - catch (err) { - console.info("No custom weekly note settings found!", err); - } -} -/** - * Read the user settings for the `periodic-notes` plugin - * to keep behavior of creating a new note in-sync. - */ -function getMonthlyNoteSettings() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pluginManager = window.app.plugins; - try { - const settings = (shouldUsePeriodicNotesSettings("monthly") && - pluginManager.getPlugin("periodic-notes")?.settings?.monthly) || - {}; - return { - format: settings.format || DEFAULT_MONTHLY_NOTE_FORMAT, - folder: settings.folder?.trim() || "", - template: settings.template?.trim() || "", - }; - } - catch (err) { - console.info("No custom monthly note settings found!", err); - } -} - -// Credit: @creationix/path.js -function join(...partSegments) { - // Split the inputs into a list of path commands. - let parts = []; - for (let i = 0, l = partSegments.length; i < l; i++) { - parts = parts.concat(partSegments[i].split("/")); - } - // Interpret the path commands to get the new resolved path. - const newParts = []; - for (let i = 0, l = parts.length; i < l; i++) { - const part = parts[i]; - // Remove leading and trailing slashes - // Also remove "." segments - if (!part || part === ".") - continue; - // Push new path segments. - else - newParts.push(part); - } - // Preserve the initial slash if there was one. - if (parts[0] === "") - newParts.unshift(""); - // Turn back into a single string path. - return newParts.join("/"); -} -async function ensureFolderExists(path) { - const dirs = path.replace(/\\/g, "/").split("/"); - dirs.pop(); // remove basename - if (dirs.length) { - const dir = join(...dirs); - if (!window.app.vault.getAbstractFileByPath(dir)) { - await window.app.vault.createFolder(dir); - } - } -} -async function getNotePath(directory, filename) { - if (!filename.endsWith(".md")) { - filename += ".md"; - } - const path = obsidian__default['default'].normalizePath(join(directory, filename)); - await ensureFolderExists(path); - return path; -} -async function getTemplateInfo(template) { - const { metadataCache, vault } = window.app; - const templatePath = obsidian__default['default'].normalizePath(template); - if (templatePath === "/") { - return Promise.resolve(["", null]); - } - try { - const templateFile = metadataCache.getFirstLinkpathDest(templatePath, ""); - const contents = await vault.cachedRead(templateFile); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const IFoldInfo = window.app.foldManager.load(templateFile); - return [contents, IFoldInfo]; - } - catch (err) { - console.error(`Failed to read the daily note template '${templatePath}'`, err); - new obsidian__default['default'].Notice("Failed to read the daily note template"); - return ["", null]; - } -} - -/** - * dateUID is a way of weekly identifying daily/weekly/monthly notes. - * They are prefixed with the granularity to avoid ambiguity. - */ -function getDateUID(date, granularity = "day") { - const ts = date.clone().startOf(granularity).format(); - return `${granularity}-${ts}`; -} -function removeEscapedCharacters(format) { - return format.replace(/\[[^\]]*\]/g, ""); // remove everything within brackets -} -/** - * XXX: When parsing dates that contain both week numbers and months, - * Moment choses to ignore the week numbers. For the week dateUID, we - * want the opposite behavior. Strip the MMM from the format to patch. - */ -function isFormatAmbiguous(format, granularity) { - if (granularity === "week") { - const cleanFormat = removeEscapedCharacters(format); - return (/w{1,2}/i.test(cleanFormat) && - (/M{1,4}/.test(cleanFormat) || /D{1,4}/.test(cleanFormat))); - } - return false; -} -function getDateFromFile(file, granularity) { - return getDateFromFilename(file.basename, granularity); -} -function getDateFromFilename(filename, granularity) { - const getSettings = { - day: getDailyNoteSettings, - week: getWeeklyNoteSettings, - month: getMonthlyNoteSettings, - }; - const format = getSettings[granularity]().format.split("/").pop(); - const noteDate = window.moment(filename, format, true); - if (!noteDate.isValid()) { - return null; - } - if (isFormatAmbiguous(format, granularity)) { - if (granularity === "week") { - const cleanFormat = removeEscapedCharacters(format); - if (/w{1,2}/i.test(cleanFormat)) { - return window.moment(filename, - // If format contains week, remove day & month formatting - format.replace(/M{1,4}/g, "").replace(/D{1,4}/g, ""), false); - } - } - } - return noteDate; -} - -class DailyNotesFolderMissingError extends Error { -} -/** - * This function mimics the behavior of the daily-notes plugin - * so it will replace {{date}}, {{title}}, and {{time}} with the - * formatted timestamp. - * - * Note: it has an added bonus that it's not 'today' specific. - */ -async function createDailyNote(date) { - const app = window.app; - const { vault } = app; - const moment = window.moment; - const { template, format, folder } = getDailyNoteSettings(); - const [templateContents, IFoldInfo] = await getTemplateInfo(template); - const filename = date.format(format); - const normalizedPath = await getNotePath(folder, filename); - try { - const createdFile = await vault.create(normalizedPath, templateContents - .replace(/{{\s*date\s*}}/gi, filename) - .replace(/{{\s*time\s*}}/gi, moment().format("HH:mm")) - .replace(/{{\s*title\s*}}/gi, filename) - .replace(/{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { - const now = moment(); - const currentDate = date.clone().set({ - hour: now.get("hour"), - minute: now.get("minute"), - second: now.get("second"), - }); - if (calc) { - currentDate.add(parseInt(timeDelta, 10), unit); - } - if (momentFormat) { - return currentDate.format(momentFormat.substring(1).trim()); - } - return currentDate.format(format); - }) - .replace(/{{\s*yesterday\s*}}/gi, date.clone().subtract(1, "day").format(format)) - .replace(/{{\s*tomorrow\s*}}/gi, date.clone().add(1, "d").format(format))); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - app.foldManager.save(createdFile, IFoldInfo); - return createdFile; - } - catch (err) { - console.error(`Failed to create file: '${normalizedPath}'`, err); - new obsidian__default['default'].Notice("Unable to create new file."); - } -} -function getDailyNote(date, dailyNotes) { - return dailyNotes[getDateUID(date, "day")] ?? null; -} -function getAllDailyNotes() { - /** - * Find all daily notes in the daily note folder - */ - const { vault } = window.app; - const { folder } = getDailyNoteSettings(); - const dailyNotesFolder = vault.getAbstractFileByPath(obsidian__default['default'].normalizePath(folder)); - if (!dailyNotesFolder) { - throw new DailyNotesFolderMissingError("Failed to find daily notes folder"); - } - const dailyNotes = {}; - obsidian__default['default'].Vault.recurseChildren(dailyNotesFolder, (note) => { - if (note instanceof obsidian__default['default'].TFile) { - const date = getDateFromFile(note, "day"); - if (date) { - const dateString = getDateUID(date, "day"); - dailyNotes[dateString] = note; - } - } - }); - return dailyNotes; -} -var createDailyNote_1 = createDailyNote; -var getAllDailyNotes_1 = getAllDailyNotes; -var getDailyNote_1 = getDailyNote; - -class ConfirmationModal extends obsidian.Modal { - constructor(app, config) { - super(app); - this.config = config; - const { cta, onAccept, text, title } = config; - this.containerEl.addClass('mod-confirmation'); - this.titleEl.setText(title); - this.contentEl.setText(text); - 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(); - this.accepted = true; - this.close(); - onAccept(e); - }); - const cancelBtnEl = this.buttonContainerEl.createEl("button", { text: "Never mind" }); - cancelBtnEl.addEventListener("click", e => { - e.preventDefault(); - this.close(); - }); - } - onClose() { - if (!this.accepted) { - this.config.onCancel?.(); - } - } -} - -function getEditorForFile(app, file) { - let editor = null; - app.workspace.iterateAllLeaves((leaf) => { - if (leaf.view instanceof obsidian.MarkdownView && leaf.view.file === file) { - const view = leaf.view; - editor = view.editor || view.sourceMode?.cmEditor; - } - }); - return editor; -} - -function getHeadingLevel(line = "") { - const heading = line.match(/^(#{1,6})\s+\S/); - return heading ? heading[1].length : null; -} -function toHeading(title, level, addEmptyLine) { - const emptyLine = addEmptyLine ? "\n" : ""; - const hash = "".padStart(level, "#"); - return `${emptyLine}${hash} ${title}`; -} -function getTab(useTab, tabSize) { - if (useTab) { - return "\t"; - } - return "".padStart(tabSize, " "); -} -function groupBy(arr, predicate) { - return arr.reduce((acc, elem) => { - const val = predicate(elem); - acc[val] = acc[val] || []; - acc[val].push(elem); - return acc; - }, {}); -} -function isMacOS() { - return navigator.appVersion.indexOf("Mac") !== -1; -} -async function updateSection(app, file, heading, sectionContents) { - const headingLevel = getHeadingLevel(heading); - if (!headingLevel) { - throw new Error(`Invalid logbook section heading: ${heading}`); - } - const { vault } = app; - const fileContents = await vault.read(file); - const fileLines = fileContents.split("\n"); - let logbookSectionLineNum = -1; - let nextSectionLineNum = -1; - for (let i = 0; i < fileLines.length; i++) { - if (fileLines[i].trim() === heading) { - logbookSectionLineNum = i; - } - else if (logbookSectionLineNum !== -1) { - const currLevel = getHeadingLevel(fileLines[i]); - if (currLevel && currLevel <= headingLevel) { - nextSectionLineNum = i; - break; - } - } - } - 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(logbookSectionLineNum, nextSectionLineNum !== -1 ? nextSectionLineNum : fileLines.length) - .join("\n") - .trimEnd(); - if (currentSection === sectionContents.trimEnd()) { - return false; - } - const from = { line: logbookSectionLineNum, ch: 0 }; - const to = nextSectionLineNum !== -1 - ? { line: nextSectionLineNum, ch: 0 } - : { 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; - } - } - // 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) : []; - const currentSection = fileLines - .slice(logbookSectionLineNum, nextSectionLineNum !== -1 ? nextSectionLineNum : fileLines.length) - .join("\n") - .trimEnd(); - if (currentSection === sectionContents.trimEnd()) { - 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")); - return true; - } -} -function getSectionContents(fileContents, heading) { - const headingLevel = getHeadingLevel(heading); - if (!headingLevel) { - return ""; - } - const fileLines = fileContents.split("\n"); - let sectionLineNum = -1; - let nextSectionLineNum = fileLines.length; - for (let i = 0; i < fileLines.length; i++) { - if (fileLines[i].trim() === heading) { - sectionLineNum = i; - } - else if (sectionLineNum !== -1) { - const currLevel = getHeadingLevel(fileLines[i]); - if (currLevel && currLevel <= headingLevel) { - nextSectionLineNum = i; - break; - } - } - } - if (sectionLineNum === -1) { - return ""; - } - return fileLines.slice(sectionLineNum, nextSectionLineNum).join("\n"); -} -function countThingsTasksInSection(fileContents, heading) { - const sectionContents = getSectionContents(fileContents, heading); - if (!sectionContents) { - return 0; - } - return sectionContents - .split("\n") - .filter((line) => /^- \[[ xXcC]\] .*things:\/\/\/show\?id=/.test(line)) - .length; -} - -class ToolkitRenderer { - constructor(app, settings) { - this.app = app; - this.settings = settings; - this.renderTask = this.renderTask.bind(this); - } - renderTask(task) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const vault = this.app.vault; - 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())) - .map((tag) => `#${prefix}${tag}`) - .join(" "); - 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}`) - : ""; - return [ - `- [${task.cancelled ? this.settings.canceledMark : 'x'}] ${taskTitle}`, - ...notes, - ...task.subtasks.map((subtask) => `${tab}- [${subtask.completed ? "x" : " "}] ${subtask.title}`), - ] - .filter((line) => !!line) - .join("\n"); - } - render(tasks) { - const { sectionHeading, doesSyncProject, doesAddNewlineBeforeHeadings } = this.settings; - const headings = groupBy(tasks, (task) => task.area || (doesSyncProject ? task.project : "") || ""); - const headingLevel = getHeadingLevel(sectionHeading); - const output = [sectionHeading]; - Object.entries(headings).map(([heading, tasks]) => { - if (heading !== "") { - output.push(toHeading(heading, headingLevel + 1, doesAddNewlineBeforeHeadings)); - } - output.push(...tasks.map(this.renderTask)); - }); - return output.join("\n"); - } -} - -const VIEW_TYPE_THINGS_TOOLKIT_REVIEW = "things-toolkit-review"; -const RATING_LABELS = { - good: "Good day", - steady: "Steady day", - improve: "Needs improvement", -}; -class ThingsToolkitReviewView extends obsidian.ItemView { - constructor(leaf, plugin) { - super(leaf); - this.plugin = plugin; - this.selectedDate = obsidian.moment().format("YYYY-MM-DD"); - } - getViewType() { - return VIEW_TYPE_THINGS_TOOLKIT_REVIEW; - } - getDisplayText() { - return "Things toolkit review"; - } - getIcon() { - return "calendar-check"; - } - async onOpen() { - await this.plugin.refreshRecentDailyStats(); - this.display(); - } - display() { - const { contentEl } = this; - contentEl.empty(); - contentEl.addClass("things-toolkit-review"); - this.renderSummary(contentEl); - this.renderCalendar(contentEl); - this.renderSelectedDay(contentEl); - } - renderSummary(containerEl) { - const today = obsidian.moment().format("YYYY-MM-DD"); - const todayCount = this.plugin.getTaskCountForDay(today); - const weeklyCount = this.getRecentTaskTotal(7); - const streak = this.plugin.getCurrentCompletionStreak(); - const summaryEl = containerEl.createDiv("things-toolkit-review-summary"); - this.addSummaryMetric(summaryEl, String(todayCount), "Today"); - this.addSummaryMetric(summaryEl, String(weeklyCount), "Last 7 days"); - this.addSummaryMetric(summaryEl, String(streak), "Current streak"); - } - addSummaryMetric(containerEl, value, label) { - const metricEl = containerEl.createDiv("things-toolkit-review-metric"); - metricEl.createDiv({ - cls: "things-toolkit-review-metric-value", - text: value, - }); - metricEl.createDiv({ - cls: "things-toolkit-review-metric-label", - text: label, - }); - } - renderCalendar(containerEl) { - const dates = this.getRecentDates(this.plugin.getReviewWindowDayCount()); - const startDate = dates[0]; - const endDate = dates[dates.length - 1]; - containerEl.createEl("h4", { text: "Review calendar" }); - containerEl.createDiv({ - cls: "things-toolkit-review-range", - text: `${startDate.format("MMM D, YYYY")} - ${endDate.format("MMM D, YYYY")}`, - }); - for (let month = startDate.clone().startOf("month"); month.isSameOrBefore(endDate, "month"); month.add(1, "month")) { - this.renderMonth(containerEl, month.clone(), startDate, endDate); - } - } - renderSelectedDay(containerEl) { - const dateKey = this.selectedDate; - const date = obsidian.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"); - detailEl.createEl("h4", { - text: date.format("dddd, MMM D, YYYY"), - }); - detailEl.createDiv({ - cls: "things-toolkit-review-detail-count", - text: `${stat?.taskCount || 0} completed tasks • Week ${date.isoWeek()}`, - }); - const contextEl = detailEl.createDiv("things-toolkit-review-context"); - this.addContextMetric(contextEl, String(this.getWeekTotal(date)), `Week ${date.isoWeek()} total`); - this.addContextMetric(contextEl, String(this.getMonthTotal(date)), `${date.format("MMM")} total`); - this.addContextMetric(contextEl, String(this.getMonthActiveDays(date)), "Active days"); - const ratingEl = detailEl.createDiv("things-toolkit-review-rating"); - Object.keys(RATING_LABELS).forEach((rating) => { - const buttonEl = ratingEl.createEl("button", { - text: RATING_LABELS[rating], - }); - if (review.rating === rating) { - buttonEl.addClass("is-active"); - } - buttonEl.addEventListener("click", async () => { - await this.plugin.writeDayReview(dateKey, { rating }); - this.display(); - }); - }); - const textareaEl = detailEl.createEl("textarea", { - cls: "things-toolkit-review-reflection", - attr: { - placeholder: "One short note about what worked, what slipped, or what to adjust tomorrow.", - }, - }); - textareaEl.value = review.reflection || ""; - const actionsEl = detailEl.createDiv("things-toolkit-review-actions"); - actionsEl - .createEl("button", { text: "Save reflection" }) - .addEventListener("click", async () => { - await this.plugin.writeDayReview(dateKey, { - reflection: textareaEl.value.trim(), - }); - new obsidian.Notice("Things review saved"); - this.display(); - }); - actionsEl - .createEl("button", { text: "Open daily note" }) - .addEventListener("click", async () => { - await this.plugin.openDailyNote(dateKey); - }); - } - renderMonth(containerEl, month, startDate, endDate) { - const monthEl = containerEl.createDiv("things-toolkit-review-month"); - const summary = this.getMonthSummary(month); - const headerEl = monthEl.createDiv("things-toolkit-review-month-header"); - headerEl.createDiv({ - cls: "things-toolkit-review-month-title", - text: month.format("MMMM YYYY"), - }); - headerEl.createDiv({ - cls: "things-toolkit-review-month-meta", - text: `${summary.total} tasks • ${summary.activeDays} active days • best ${summary.bestDay}`, - }); - const gridEl = monthEl.createDiv("things-toolkit-review-calendar-grid"); - ["Wk", "M", "T", "W", "T", "F", "S", "S"].forEach((label) => { - gridEl.createDiv({ - cls: "things-toolkit-review-calendar-heading", - text: label, - }); - }); - const monthEnd = month.clone().endOf("month").startOf("day"); - const finalWeekStart = monthEnd.clone().startOf("isoWeek"); - for (let weekStart = month.clone().startOf("isoWeek"); weekStart.isSameOrBefore(finalWeekStart, "day"); weekStart.add(1, "week")) { - gridEl.createDiv({ - cls: "things-toolkit-review-week-number", - text: String(weekStart.isoWeek()), - }); - for (let dayOffset = 0; dayOffset < 7; dayOffset++) { - const date = weekStart.clone().add(dayOffset, "days"); - if (!date.isSame(month, "month") || - date.isBefore(startDate, "day") || - date.isAfter(endDate, "day")) { - gridEl.createDiv("things-toolkit-review-day-spacer"); - } - else { - this.renderCalendarDay(gridEl, date); - } - } - } - } - renderCalendarDay(containerEl, date) { - const dateKey = date.format("YYYY-MM-DD"); - const count = this.plugin.getTaskCountForDay(dateKey); - const review = this.plugin.options.dailyReviews[dateKey]; - const buttonEl = containerEl.createEl("button", { - cls: "things-toolkit-review-day", - }); - buttonEl.setAttribute("aria-label", `${date.format("MMM D, YYYY")}: ${count} tasks, week ${date.isoWeek()}`); - if (dateKey === this.selectedDate) { - buttonEl.addClass("is-selected"); - } - if (review?.rating) { - buttonEl.addClass(`is-${review.rating}`); - } - if (count > 0) { - buttonEl.addClass("has-tasks"); - } - buttonEl.createDiv({ - cls: "things-toolkit-review-day-number", - text: date.format("D"), - }); - buttonEl.createDiv({ - cls: "things-toolkit-review-day-count", - text: String(count), - }); - buttonEl.addEventListener("click", async () => { - this.selectedDate = dateKey; - this.display(); - await this.plugin.openDailyNote(dateKey); - }); - } - addContextMetric(containerEl, value, label) { - const metricEl = containerEl.createDiv("things-toolkit-review-context-metric"); - metricEl.createDiv({ - cls: "things-toolkit-review-context-value", - text: value, - }); - metricEl.createDiv({ - cls: "things-toolkit-review-context-label", - text: label, - }); - } - getRecentDates(dayCount) { - const end = obsidian.moment().startOf("day"); - const start = end.clone().subtract(dayCount - 1, "days"); - const dates = []; - for (let date = start; date.isSameOrBefore(end); date.add(1, "day")) { - dates.push(date.clone()); - } - return dates; - } - getRecentTaskTotal(dayCount) { - return this.getRecentDates(dayCount).reduce((sum, date) => sum + this.plugin.getTaskCountForDay(date.format("YYYY-MM-DD")), 0); - } - getWeekTotal(date) { - return this.sumDateRange(date.clone().startOf("isoWeek"), date.clone().endOf("isoWeek")); - } - getMonthTotal(date) { - return this.sumDateRange(date.clone().startOf("month"), date.clone().endOf("month")); - } - getMonthActiveDays(date) { - const end = date.clone().endOf("month"); - let activeDays = 0; - for (let day = date.clone().startOf("month"); day.isSameOrBefore(end, "day"); day.add(1, "day")) { - if (this.plugin.getTaskCountForDay(day.format("YYYY-MM-DD")) > 0) { - activeDays++; - } - } - return activeDays; - } - getMonthSummary(month) { - const end = month.clone().endOf("month"); - let activeDays = 0; - let bestCount = 0; - let bestDay = "0"; - let total = 0; - for (let day = month.clone().startOf("month"); day.isSameOrBefore(end, "day"); day.add(1, "day")) { - const count = this.plugin.getTaskCountForDay(day.format("YYYY-MM-DD")); - total += count; - if (count > 0) { - activeDays++; - } - if (count > bestCount) { - bestCount = count; - bestDay = `${day.format("MMM D")} (${count})`; - } - } - return { activeDays, bestDay, total }; - } - sumDateRange(start, end) { - let total = 0; - for (let date = start.clone().startOf("day"); date.isSameOrBefore(end, "day"); date.add(1, "day")) { - total += this.plugin.getTaskCountForDay(date.format("YYYY-MM-DD")); - } - return total; - } -} - -// Documented here: https://culturedcode.com/things/support/articles/2982272/ -const THINGS_DB_PATH_START = "~/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/"; -const THINGS_DB_PATH_END = "Things Database.thingsdatabase/main.sqlite"; - -var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; - -function createCommonjsModule(fn) { - var module = { exports: {} }; - return fn(module, module.exports), module.exports; -} - -/* @license -Papa Parse -v5.3.1 -https://github.com/mholt/PapaParse -License: MIT -*/ - -var papaparse_min = createCommonjsModule(function (module, exports) { -!function(e,t){module.exports=t();}(commonjsGlobal,function s(){var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=n&&/blob:/i.test((f.location||{}).protocol),a={},h=0,b={parse:function(e,t){var i=(t=t||{}).dynamicTyping||!1;M(i)&&(t.dynamicTypingFunction=i,i={});if(t.dynamicTyping=i,t.transform=!!M(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var r=function(){if(!b.WORKERS_SUPPORTED)return !1;var e=(i=f.URL||f.webkitURL||null,r=s.toString(),b.BLOB_URL||(b.BLOB_URL=i.createObjectURL(new Blob(["(",r,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var i,r;return t.onmessage=_,t.id=h++,a[t.id]=t}();return r.userStep=t.step,r.userChunk=t.chunk,r.userComplete=t.complete,r.userError=t.error,t.step=M(t.step),t.chunk=M(t.chunk),t.complete=M(t.complete),t.error=M(t.error),delete t.worker,void r.postMessage({input:e,config:t,workerId:r.id})}var n=null;"string"==typeof e?n=t.download?new l(t):new p(t):!0===e.readable&&M(e.read)&&M(e.on)?n=new g(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var n=!1,_=!0,m=",",y="\r\n",s='"',a=s+s,i=!1,r=null,o=!1;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return -1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter);("boolean"==typeof t.quotes||"function"==typeof t.quotes||Array.isArray(t.quotes))&&(n=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(i=t.skipEmptyLines);"string"==typeof t.newline&&(y=t.newline);"string"==typeof t.quoteChar&&(s=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");r=t.columns;}void 0!==t.escapeChar&&(a=t.escapeChar+s);"boolean"==typeof t.escapeFormulae&&(o=t.escapeFormulae);}();var h=new RegExp(j(s),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,i);if("object"==typeof e[0])return u(r||Object.keys(e[0]),e,i)}else if("object"==typeof e)return "string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:"object"==typeof e.data[0]?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],i);throw new Error("Unable to serialize unrecognized input");function u(e,t,i){var r="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(M(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0;}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!M(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0;},this._sendError=function(e){M(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1});};}function l(e){var r;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded();}:function(){this._readChunk();},this.stream=function(e){this._input=e,this._nextChunk();},this._readChunk=function(){if(this._finished)this._chunkLoaded();else {if(r=new XMLHttpRequest,this._config.withCredentials&&(r.withCredentials=this._config.withCredentials),n||(r.onload=v(this._chunkLoaded,this),r.onerror=v(this._chunkError,this)),r.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)r.setRequestHeader(t,e[t]);}if(this._config.chunkSize){var i=this._start+this._config.chunkSize-1;r.setRequestHeader("Range","bytes="+this._start+"-"+i);}try{r.send(this._config.downloadRequestBody);}catch(e){this._chunkError(e.message);}n&&0===r.status&&this._chunkError();}},this._chunkLoaded=function(){4===r.readyState&&(r.status<200||400<=r.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:r.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return -1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(r),this.parseChunk(r.responseText)));},this._chunkError=function(e){var t=r.statusText||e;this._sendError(new Error(t));};}function c(e){var r,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((r=new FileReader).onload=v(this._chunkLoaded,this),r.onerror=v(this._chunkError,this)):r=new FileReaderSync,this._nextChunk();},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result);},this._chunkError=function(){this._sendError(r.error);};}function p(e){var i;u.call(this,e=e||{}),this.stream=function(e){return i=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=i.substring(0,t),i=i.substring(t)):(e=i,i=""),this._finished=!i,this.parseChunk(e)}};}function g(e){u.call(this,e=e||{});var t=[],i=!0,r=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause();},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume();},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError);},this._checkIsFinished=function(){r&&1===t.length&&(this._finished=!0);},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):i=!0;},this._streamData=v(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),i&&(i=!1,this._checkIsFinished(),this.parseChunk(t.shift()));}catch(e){this._streamError(e);}},this),this._streamError=v(function(e){this._streamCleanUp(),this._sendError(e);},this),this._streamEnd=v(function(){this._streamCleanUp(),r=!0,this._streamData("");},this),this._streamCleanUp=v(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError);},this);}function i(m){var a,o,h,r=Math.pow(2,53),n=-r,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,u=/^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))$/,t=this,i=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(M(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else {if(g(),0===c.data.length)return;i+=e.data.length,m.preview&&i>m.preview?o.abort():(c.data=c.data[0],p(c,t));}};}function y(e){return "greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){if(c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),m.skipEmptyLines)for(var e=0;e=l.length?"__parsed_extra":l[i]),m.transform&&(s=m.transform(s,n)),s=v(n,s),"__parsed_extra"===n?(r[n]=r[n]||[],r[n].push(s)):r[n]=s;}return m.header&&(i>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+i,f+t):i=r.length/2?"\r\n":"\r"}(e,r)),h=!1,m.delimiter)M(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else {var n=function(e,t,i,r,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=D)return C(!0)}else for(m=F,F++;;){if(-1===(m=r.indexOf(S,m+1)))return i||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:F}),E();if(m===n-1)return E(r.substring(F,m).replace(_,S));if(S!==L||r[m+1]!==L){if(S===L||0===m||r[m-1]!==L){-1!==p&&p=D)return C(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:F}),m++;}}else m++;}return E();function k(e){h.push(e),d=F;}function b(e){var t=0;if(-1!==e){var i=r.substring(m+1,e);i&&""===i.trim()&&(t=i.length);}return t}function E(e){return i||(void 0===e&&(e=r.substring(F)),f.push(e),F=n,k(f),o&&R()),C()}function w(e){F=e,k(f),f=[],g=r.indexOf(x,F);}function C(e){return {data:h,errors:u,meta:{delimiter:O,linebreak:x,aborted:z,truncated:!!e,cursor:d+(t||0)}}}function R(){T(C()),h=[],u=[];}},this.abort=function(){z=!0;},this.getCharIndex=function(){return F};}function _(e){var t=e.data,i=a[t.workerId],r=!1;if(t.error)i.userError(t.error,t.file);else if(t.results&&t.results.data){var n={abort:function(){r=!0,m(t.workerId,{data:[],errors:[],meta:{aborted:!0}});},pause:y,resume:y};if(M(i.userStep)){for(var s=0;s { - const stdOut = []; - const stdErr = []; - let settled = false; - const finish = (result) => { - if (!settled) { - settled = true; - done(result); - } - }; - const spawned = child_process.spawn("sqlite3", ["-csv", "-header", "-readonly", dbPath, query], { detached: true }); - spawned.stdout.on("data", (buffer) => { - stdOut.push(buffer); - }); - spawned.stderr.on("data", (buffer) => { - stdErr.push(buffer); - }); - spawned.on("error", (err) => { - stdErr.push(Buffer.from(String(err.stack), "ascii")); - }); - spawned.on("close", (code) => finish({ stdErr, stdOut, code })); - spawned.on("exit", (code) => finish({ stdErr, stdOut, code })); - }); -} -async function querySqliteDB(dbPath, query) { - const { stdOut, stdErr } = await handleSqliteQuery(dbPath, query); - if (stdErr.length) { - const error = Buffer.concat(stdErr).toString("utf-8"); - return Promise.reject(error); - } - return parseCSV(stdOut); -} - -const TASK_FETCH_LIMIT = 1000; -const DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS = 365; -const MIN_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS = 365; -const APPLESCRIPT_RECORD_SEPARATOR = "\x1e"; -const STATUS_COMPLETED = "3"; -// Info on how to find the Things db file here: -// https://culturedcode.com/things/support/articles/2982272/ -const baseDir = THINGS_DB_PATH_START.replace("~", os.homedir()); -const STATUS_CANCELLED = 2; -class ThingsSqlitePrivacyError extends Error { - constructor(message) { - super(message); - this.name = "ThingsSqlitePrivacyError"; - } -} -function getThingsSqlitePath() { - let dataFiles; - try { - dataFiles = fs.readdirSync(baseDir); - } - catch (err) { - if (isFileAccessDeniedError(err)) { - throw new ThingsSqlitePrivacyError("macOS privacy is blocking direct access to the Things database"); - } - throw err; - } - const dataPath = dataFiles.find((file) => file.startsWith("ThingsData")) ?? ""; - const dbPath = dataPath ? path.join(baseDir, dataPath, THINGS_DB_PATH_END) : ""; - if (!dbPath || !fs.existsSync(dbPath)) { - throw new Error("Things database not found"); - } - return dbPath; -} -function isFileAccessDeniedError(err) { - const error = err; - return (error?.code === "EPERM" || - error?.code === "EACCES" || - String(error?.message || "").includes("operation not permitted")); -} -function getErrorMessage(err) { - return err instanceof Error ? err.message : String(err); -} -function buildAccessStatus(source, message, sqliteBlocked) { - return { - message, - source, - sqliteBlocked, - updatedAt: window.moment().unix(), - }; -} -function getRepairCutoff(latestSyncTime, repairLookbackDays) { - const incrementalCutoff = window.moment.unix(latestSyncTime).startOf("day"); - const repairCutoff = window.moment() - .subtract(Math.max(1, Number(repairLookbackDays) || 1), "days") - .startOf("day"); - if (latestSyncTime > 0 && incrementalCutoff.isBefore(repairCutoff)) { - return incrementalCutoff; - } - return repairCutoff; -} -function getLastStopDate(records) { - const recordsWithStopDate = records.filter((record) => record.stopDate); - return recordsWithStopDate[recordsWithStopDate.length - 1]?.stopDate ?? null; -} -function runAppleScript(script) { - return new Promise((resolve, reject) => { - const stdOut = []; - const stdErr = []; - let settled = false; - const finish = (err) => { - if (settled) { - return; - } - settled = true; - if (err) { - reject(err); - } - else { - resolve(Buffer.concat(stdOut).toString("utf-8")); - } - }; - const spawned = child_process.spawn("osascript", ["-e", script]); - spawned.stdout.on("data", (buffer) => stdOut.push(buffer)); - spawned.stderr.on("data", (buffer) => stdErr.push(buffer)); - spawned.on("error", finish); - spawned.on("close", (code) => { - if (code === 0) { - finish(); - } - else { - finish(new Error(Buffer.concat(stdErr).toString("utf-8"))); - } - }); - }); -} -function getAppleScriptCutoff(latestSyncTime, fallbackLookbackDays, repairLookbackDays) { - const configuredLookbackDays = Math.max(1, Number(fallbackLookbackDays) || DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS); - const lookbackDays = Math.max(configuredLookbackDays, repairLookbackDays, MIN_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS); - const lookbackCutoff = getRepairCutoff(latestSyncTime, lookbackDays); - if (latestSyncTime > 0) { - const incrementalCutoff = window.moment.unix(latestSyncTime).startOf("day"); - if (lookbackCutoff.isBefore(incrementalCutoff)) { - console.debug(`[Things Toolkit] repairing the last ${lookbackDays} days from Things AppleScript.`); - return lookbackCutoff; - } - return incrementalCutoff; - } - console.warn(`[Things Toolkit] SQLite is unavailable. Falling back to the last ${lookbackDays} days from Things AppleScript instead of scanning the full Logbook.`); - return lookbackCutoff; -} -function buildThingsToolkitAppleScript(cutoff) { - return ` -set cutoff to current date -set year of cutoff to ${cutoff.year()} -set month of cutoff to ${cutoff.month() + 1} -set day of cutoff to ${cutoff.date()} -set time of cutoff to ${cutoff.diff(cutoff.clone().startOf("day"), "seconds")} -set sep to ASCII character 30 -set out to "" -tell application "Things3" - set completedItems to to dos of list "Logbook" whose status is completed and completion date > cutoff - repeat with taskItem in completedItems - set out to out & (_private_experimental_ json of taskItem) & sep - end repeat - set canceledItems to to dos of list "Logbook" whose status is canceled and cancellation date > cutoff - repeat with taskItem in canceledItems - set out to out & (_private_experimental_ json of taskItem) & sep - end repeat -end tell -return out -`; -} -function toUnixTime(dateString) { - if (!dateString) { - return 0; - } - return Math.floor(new Date(dateString).getTime() / 1000); -} -function buildTaskRecordsFromAppleScriptJson(records) { - return records.flatMap((record) => { - const isCancelled = record.status === "canceled"; - const stopDate = toUnixTime(isCancelled ? record.cancellationDate : record.completionDate); - const tags = (record.tagNames || "") - .split(",") - .map((tag) => tag.trim()) - .filter((tag) => !!tag); - const task = { - uuid: record.id, - title: record.name, - notes: record.notes || "", - area: record.area?.name, - project: record.project?.name, - startDate: toUnixTime(record.creationDate), - stopDate, - status: isCancelled ? String(STATUS_CANCELLED) : STATUS_COMPLETED, - }; - if (tags.length === 0) { - return [{ ...task }]; - } - return tags.map((tag) => ({ ...task, tag })); - }); -} -async function getTasksFromThingsAppleScript(latestSyncTime, fallbackLookbackDays, repairLookbackDays, accessStatus) { - const cutoff = getAppleScriptCutoff(latestSyncTime, fallbackLookbackDays, repairLookbackDays); - console.debug(`[Things Toolkit] fetching tasks from Things AppleScript after ${cutoff.format()}...`); - const output = await runAppleScript(buildThingsToolkitAppleScript(cutoff)); - const records = output - .split(APPLESCRIPT_RECORD_SEPARATOR) - .map((line) => line.trim()) - .filter((line) => !!line) - .map((line) => JSON.parse(line)); - const taskRecords = buildTaskRecordsFromAppleScriptJson(records); - console.debug(`[Things Toolkit] fetched ${records.length} tasks from Things AppleScript`); - return { - accessStatus, - taskRecords, - checklistRecords: [], - source: "applescript", - cutoffTime: cutoff.unix(), - isLimited: latestSyncTime === 0, - repairLookbackDays: window.moment().startOf("day").diff(cutoff, "days"), - }; -} -function buildTasksFromSQLRecords(taskRecords, checklistRecords) { - const tasks = {}; - taskRecords.forEach(({ tag, ...task }) => { - const id = task.uuid; - const { status, title, ...other } = task; - if (tasks[id]) { - if (tag && !tasks[id].tags.includes(tag)) { - tasks[id].tags.push(tag); - } - } - else { - tasks[id] = { - ...other, - cancelled: STATUS_CANCELLED === Number.parseInt(status), - title: (title || "").trimEnd(), - subtasks: [], - tags: tag ? [tag] : [], - }; - } - }); - checklistRecords.forEach(({ taskId, title, stopDate }) => { - const task = tasks[taskId]; - const subtaskTitle = (title || "").trimEnd(); - if (!subtaskTitle) { - return; - } - const subtask = { - completed: !!stopDate, - title: subtaskTitle, - }; - // checklist item might be completed before task - if (task) { - if (task.subtasks) { - task.subtasks.push(subtask); - } - else { - task.subtasks = [subtask]; - } - } - }); - return Object.values(tasks).sort((a, b) => { - const stopDateDiff = b.stopDate - a.stopDate; - if (stopDateDiff !== 0) { - return stopDateDiff; - } - return a.uuid.localeCompare(b.uuid); - }); -} -async function getTasksFromThingsDb(latestSyncTime) { - return querySqliteDB(getThingsSqlitePath(), `SELECT - TMTask.uuid as uuid, - TMTask.title as title, - TMTask.notes as notes, - TMTask.startDate as startDate, - TMTask.stopDate as stopDate, - TMTask.status as status, - TMArea.title as area, - TMTag.title as tag, - TMProject.title as project - FROM - TMTask - LEFT JOIN TMTaskTag - ON TMTaskTag.tasks = TMTask.uuid - LEFT JOIN TMTag - ON TMTag.uuid = TMTaskTag.tags - LEFT JOIN TMArea - ON TMTask.area = TMArea.uuid - LEFT JOIN TMTask TMProject - ON TMProject.uuid = TMTask.project - WHERE - TMTask.trashed = 0 - AND TMTask.stopDate IS NOT NULL - AND TMTask.stopDate > ${latestSyncTime} - ORDER BY - TMTask.stopDate - LIMIT ${TASK_FETCH_LIMIT} - `); -} -async function getChecklistItemsThingsDb(latestSyncTime) { - return querySqliteDB(getThingsSqlitePath(), `SELECT - task as taskId, - title as title, - stopDate as stopDate - FROM - TMChecklistItem - WHERE - stopDate > ${latestSyncTime} - AND title IS NOT "" - ORDER BY - stopDate - LIMIT ${TASK_FETCH_LIMIT} - `); -} -async function getTasksFromThingsSqlite(latestSyncTime) { - const taskRecords = []; - let isSyncCompleted = false; - let stopTime = window.moment.unix(latestSyncTime).startOf("day").unix(); - while (!isSyncCompleted) { - console.debug("[Things Toolkit] fetching tasks from sqlite db..."); - const batch = await getTasksFromThingsDb(stopTime); - isSyncCompleted = batch.length < TASK_FETCH_LIMIT; - const lastStopDate = getLastStopDate(batch); - if (lastStopDate) { - stopTime = lastStopDate; - } - taskRecords.push(...batch); - console.debug(`[Things Toolkit] fetched ${batch.length} tasks from sqlite db`); - } - return taskRecords; -} -async function getChecklistItemsFromThingsSqlite(latestSyncTime) { - const checklistItems = []; - let isSyncCompleted = false; - let stopTime = latestSyncTime; - while (!isSyncCompleted) { - console.debug("[Things Toolkit] fetching checklist items from sqlite db..."); - const batch = await getChecklistItemsThingsDb(stopTime); - isSyncCompleted = batch.length < TASK_FETCH_LIMIT; - const lastStopDate = getLastStopDate(batch); - if (lastStopDate) { - stopTime = lastStopDate; - } - checklistItems.push(...batch); - console.debug(`[Things Toolkit] fetched ${batch.length} checklist items from sqlite db`); - } - return checklistItems; -} -async function fetchThingsToolkit(latestSyncTime, fallbackLookbackDays, accessMode, repairLookbackDays) { - const cutoff = getRepairCutoff(latestSyncTime, repairLookbackDays); - const cutoffTime = cutoff.unix(); - if (accessMode === "applescript") { - return getTasksFromThingsAppleScript(latestSyncTime, fallbackLookbackDays, repairLookbackDays, buildAccessStatus("applescript", "Using Things AppleScript. Direct database access is disabled in settings.", false)); - } - try { - const taskRecords = await getTasksFromThingsSqlite(cutoffTime); - const checklistRecords = await getChecklistItemsFromThingsSqlite(cutoffTime); - return { - accessStatus: buildAccessStatus("sqlite", "Using direct Things database access.", false), - taskRecords, - checklistRecords, - source: "sqlite", - cutoffTime, - isLimited: false, - repairLookbackDays: window.moment().startOf("day").diff(cutoff, "days"), - }; - } - catch (err) { - if (accessMode === "sqlite") { - throw new Error(`Things SQLite access failed: ${getErrorMessage(err)}`); - } - const sqliteBlocked = err instanceof ThingsSqlitePrivacyError || isFileAccessDeniedError(err); - const message = sqliteBlocked - ? "macOS privacy is blocking direct access to the Things database. Using Things AppleScript instead." - : `Direct Things database access is unavailable. Using Things AppleScript instead. (${getErrorMessage(err)})`; - console.warn("[Things Toolkit] Things SQLite DB is unavailable; falling back to Things AppleScript", err); - console.warn("[Things Toolkit] Checklist items are skipped because Things AppleScript does not expose them."); - try { - return await getTasksFromThingsAppleScript(latestSyncTime, fallbackLookbackDays, repairLookbackDays, buildAccessStatus("applescript", message, sqliteBlocked)); - } - catch (appleScriptErr) { - throw new Error(`Things AppleScript access failed after SQLite was unavailable: ${getErrorMessage(appleScriptErr)}`); - } - } -} - -const DEFAULT_SECTION_HEADING = "## Things"; -const DEFAULT_SYNC_FREQUENCY_SECONDS = 30 * 60; // Every 30 minutes -const DEFAULT_REVIEW_WINDOW_DAYS = 365; -const DEFAULT_TAG_PREFIX = "things/"; -const DEFAULT_CANCELLED_MARK = "c"; -const DEFAULT_SETTINGS = Object.freeze({ - hasAcceptedDisclaimer: false, - latestSyncTime: 0, - appleScriptFallbackLookbackDays: DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS, - thingsAccessMode: "auto", - thingsAccessStatus: undefined, - reviewWindowDays: DEFAULT_REVIEW_WINDOW_DAYS, - dailyStats: {}, - dailyReviews: {}, - doesSyncNoteBody: true, - doesSyncProject: false, - doesAddNewlineBeforeHeadings: false, - isSyncEnabled: false, - syncInterval: DEFAULT_SYNC_FREQUENCY_SECONDS, - sectionHeading: DEFAULT_SECTION_HEADING, - tagPrefix: DEFAULT_TAG_PREFIX, - canceledMark: DEFAULT_CANCELLED_MARK -}); -class ThingsToolkitSettingsTab extends obsidian.PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - display() { - this.containerEl.empty(); - this.addResetLastSyncSetting(); - new obsidian.Setting(this.containerEl).setName("Sync Engine").setHeading(); - this.addThingsAccessModeSetting(); - this.addThingsAccessStatusSetting(); - this.addSyncEnabledSetting(); - this.addSyncIntervalSetting(); - this.addAppleScriptFallbackLookbackSetting(); - new obsidian.Setting(this.containerEl).setName("Daily Notes").setHeading(); - this.addSectionHeadingSetting(); - this.addDoesSyncNoteBodySetting(); - this.addDoesSyncProjectSetting(); - this.addDoesAddNewlineBeforeHeadingsSetting(); - new obsidian.Setting(this.containerEl).setName("Imported Tags").setHeading(); - this.addTagPrefixSetting(); - this.addCanceledMarkSetting(); - new obsidian.Setting(this.containerEl).setName("Review Calendar").setHeading(); - this.addReviewWindowDaysSetting(); - } - addSectionHeadingSetting() { - new obsidian.Setting(this.containerEl) - .setName("Section heading") - .setDesc("Markdown heading to replace or append when adding Things items to a daily note") - .addText((textfield) => { - textfield.setValue(this.plugin.options.sectionHeading); - textfield.onChange(async (rawSectionHeading) => { - const sectionHeading = this.normalizeSectionHeading(rawSectionHeading); - this.plugin.writeOptions({ sectionHeading }); - }); - }); - } - addReviewWindowDaysSetting() { - new obsidian.Setting(this.containerEl) - .setName("Review window") - .setDesc("Number of recent days to show and repair in the review calendar") - .addText((textfield) => { - textfield.setValue(String(this.plugin.options.reviewWindowDays)); - textfield.inputEl.type = "number"; - textfield.inputEl.onblur = (e) => { - const reviewWindowDays = Math.max(30, Math.floor(Number(e.target.value) || DEFAULT_REVIEW_WINDOW_DAYS)); - textfield.setValue(String(reviewWindowDays)); - this.plugin.writeOptions({ reviewWindowDays }); - }; - }); - } - normalizeSectionHeading(rawSectionHeading) { - const sectionHeading = rawSectionHeading.trim(); - if (!sectionHeading) { - return DEFAULT_SECTION_HEADING; - } - if (/^#{1,6}\s+\S/.test(sectionHeading)) { - return sectionHeading; - } - return `## ${sectionHeading.replace(/^#+\s*/, "")}`; - } - addSyncEnabledSetting() { - new obsidian.Setting(this.containerEl) - .setName("Enable periodic syncing") - .addToggle((toggle) => { - toggle.setValue(this.plugin.options.isSyncEnabled); - toggle.onChange(async (isSyncEnabled) => { - this.plugin.writeOptions({ isSyncEnabled }); - }); - }); - } - addThingsAccessModeSetting() { - new obsidian.Setting(this.containerEl) - .setName("Things access") - .setDesc("Auto tries the Things database first, then uses AppleScript when macOS privacy blocks direct access.") - .addDropdown((dropdown) => { - dropdown - .addOption("auto", "Auto") - .addOption("applescript", "AppleScript") - .addOption("sqlite", "SQLite only"); - dropdown.setValue(this.plugin.options.thingsAccessMode); - dropdown.onChange(async (thingsAccessMode) => { - await this.plugin.writeOptions({ thingsAccessMode }); - this.display(); - }); - }); - } - addThingsAccessStatusSetting() { - const accessStatus = this.plugin.options.thingsAccessStatus; - const statusText = accessStatus - ? `${accessStatus.message} Checked ${obsidian.moment.unix(accessStatus.updatedAt).fromNow()}.` - : "Not checked yet. Run Sync now to test Things access."; - new obsidian.Setting(this.containerEl) - .setName("macOS privacy status") - .setDesc(statusText) - .addButton((button) => { - button.setButtonText("Full Disk Access"); - button.onClick(() => { - this.openSystemSettings("x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"); - }); - }) - .addButton((button) => { - button.setButtonText("Automation"); - button.onClick(() => { - this.openSystemSettings("x-apple.systempreferences:com.apple.preference.security?Privacy_Automation"); - }); - }); - } - openSystemSettings(url) { - child_process.spawn("open", [url]); - } - addDoesSyncNoteBodySetting() { - new obsidian.Setting(this.containerEl) - .setName("Include notes") - .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 }); - }); - }); - } - addDoesSyncProjectSetting() { - new obsidian.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(async (doesSyncProject) => { - this.plugin.writeOptions({ doesSyncProject }); - }); - }); - } - addSyncIntervalSetting() { - new obsidian.Setting(this.containerEl) - .setName("Sync frequency") - .setDesc("Number of seconds the plugin will wait before syncing again") - .addText((textfield) => { - textfield.setValue(String(this.plugin.options.syncInterval)); - textfield.inputEl.type = "number"; - textfield.inputEl.onblur = (e) => { - const syncInterval = Math.max(60, Math.floor(Number(e.target.value) || DEFAULT_SYNC_FREQUENCY_SECONDS)); - textfield.setValue(String(syncInterval)); - this.plugin.writeOptions({ syncInterval }); - }; - }); - } - addAppleScriptFallbackLookbackSetting() { - new obsidian.Setting(this.containerEl) - .setName("AppleScript fallback lookback") - .setDesc(`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.inputEl.type = "number"; - textfield.inputEl.onblur = (e) => { - const appleScriptFallbackLookbackDays = Math.max(1, Math.floor(Number(e.target.value) || DEFAULT_APPLESCRIPT_FALLBACK_LOOKBACK_DAYS)); - textfield.setValue(String(appleScriptFallbackLookbackDays)); - this.plugin.writeOptions({ appleScriptFallbackLookbackDays }); - }; - }); - } - addTagPrefixSetting() { - new obsidian.Setting(this.containerEl) - .setName("Tag prefix") - .setDesc("Prefix added to Things tags when imported into Obsidian (e.g. #things/work)") - .addText((textfield) => { - textfield.setValue(this.plugin.options.tagPrefix); - textfield.onChange(async (tagPrefix) => { - this.plugin.writeOptions({ tagPrefix }); - }); - }); - } - addCanceledMarkSetting() { - new obsidian.Setting(this.containerEl) - .setName("Canceled mark") - .setDesc("Mark character to use for canceled tasks") - .addText((textfield) => { - textfield.setValue(this.plugin.options.canceledMark); - textfield.onChange(async (canceledMark) => { - this.plugin.writeOptions({ canceledMark }); - }); - }); - } - addDoesAddNewlineBeforeHeadingsSetting() { - new obsidian.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(async (doesAddNewlineBeforeHeadings) => { - this.plugin.writeOptions({ doesAddNewlineBeforeHeadings }); - }); - }); - } - addResetLastSyncSetting() { - const { latestSyncTime } = this.plugin.options; - const { syncStatus } = this.plugin; - const syncTime = latestSyncTime > 0 - ? obsidian.moment.unix(this.plugin.options.latestSyncTime).fromNow() - : 'Never'; - new obsidian.Setting(this.containerEl) - .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.syncLogbook(); - this.display(); - }); - }) - .addButton(button => { - button.setButtonText('Reset sync history'); - button.setClass('mod-danger'); - button.setDisabled(syncStatus.isSyncing); - button.onClick(async () => { - await 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.'); - }); - } -} - -class ThingsToolkitPlugin extends obsidian.Plugin { - constructor() { - super(...arguments); - this.syncStatus = { - isSyncing: false, - message: "", - }; - } - async onload() { - if (!isMacOS()) { - console.info("Failed to load Things Toolkit plugin. Platform not supported"); - return; - } - this.scheduleNextSync = this.scheduleNextSync.bind(this); - this.syncLogbook = this.syncLogbook.bind(this); - this.tryToScheduleSync = this.tryToScheduleSync.bind(this); - this.tryToSyncLogbook = this.tryToSyncLogbook.bind(this); - this.registerView(VIEW_TYPE_THINGS_TOOLKIT_REVIEW, (leaf) => new ThingsToolkitReviewView(leaf, this)); - this.addCommand({ - id: "sync-things-toolkit", - name: "Sync", - callback: () => setTimeout(() => this.tryToSyncLogbook(), 20), - }); - this.addCommand({ - id: "open-things-toolkit-review", - name: "Open review", - callback: () => this.activateReviewView(), - }); - this.addRibbonIcon("calendar-check", "Open Things toolkit review", () => this.activateReviewView()); - await this.loadOptions(); - this.statusBarEl = this.addStatusBarItem(); - this.statusBarEl.addClass("things-toolkit-status"); - this.statusBarEl.addEventListener("click", () => this.activateReviewView()); - this.updateStatusBar(); - this.app.workspace.onLayoutReady(() => { - this.refreshRecentDailyStats().then(() => { - this.updateStatusBar(); - this.refreshReviewViews(); - }); - }); - this.settingsTab = new ThingsToolkitSettingsTab(this.app, this); - this.addSettingTab(this.settingsTab); - if (this.options.hasAcceptedDisclaimer && this.options.isSyncEnabled) { - if (this.app.workspace.layoutReady) { - this.scheduleNextSync(); - } - else { - this.registerEvent(this.app.workspace.on("layout-ready", this.scheduleNextSync)); - } - } - } - async activateReviewView() { - const { workspace } = this.app; - let leaf = workspace.getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW)[0]; - if (!leaf) { - leaf = workspace.getRightLeaf(false); - if (!leaf) { - return; - } - await leaf.setViewState({ - type: VIEW_TYPE_THINGS_TOOLKIT_REVIEW, - active: true, - }); - } - await workspace.revealLeaf(leaf); - } - async tryToSyncLogbook() { - 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(); - } - } - async tryToScheduleSync() { - 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(); - } - } - async syncLogbook() { - if (this.syncStatus.isSyncing) { - new obsidian.Notice("Things Toolkit sync already running"); - return; - } - this.syncStatus = { - isSyncing: true, - message: "Syncing...", - }; - this.settingsTab?.display(); - const logbookRenderer = new ToolkitRenderer(this.app, this.options); - const dailyNotes = getAllDailyNotes_1(); - const latestSyncTime = this.options.latestSyncTime || 0; - let taskCount = 0; - let dayCount = 0; - let changedDayCount = 0; - try { - const fetchResult = await fetchThingsToolkit(latestSyncTime, this.options.appleScriptFallbackLookbackDays, this.options.thingsAccessMode, this.getReviewWindowDayCount()); - const tasks = buildTasksFromSQLRecords(fetchResult.taskRecords, fetchResult.checklistRecords); - taskCount = tasks.length; - const daysToTasks = groupBy(tasks.filter((task) => task.stopDate), (task) => window.moment.unix(task.stopDate).startOf("day").format()); - const dayEntries = Object.entries(daysToTasks).sort(([a], [b]) => window.moment(a).diff(window.moment(b))); - dayCount = dayEntries.length; - const dailyStats = this.buildDailyStats(dayEntries, fetchResult.source, window.moment().unix()); - for (const [dateStr, tasks] of dayEntries) { - const date = window.moment(dateStr); - let dailyNote = getDailyNote_1(date, dailyNotes); - if (!dailyNote) { - dailyNote = await createDailyNote_1(date); - } - const didChange = await updateSection(this.app, dailyNote, this.options.sectionHeading, logbookRenderer.render(tasks)); - if (didChange) { - changedDayCount++; - } - } - await this.writeOptions({ - latestSyncTime: window.moment().unix(), - dailyStats, - thingsAccessStatus: fetchResult.accessStatus, - }); - const sourceLabel = fetchResult.source === "applescript" - ? `Things AppleScript, repairing last ${fetchResult.repairLookbackDays || this.options.appleScriptFallbackLookbackDays} days` - : `Things SQLite, repairing last ${fetchResult.repairLookbackDays || this.getReviewWindowDayCount()} days`; - this.syncStatus = { - isSyncing: false, - message: `Last result: ${taskCount} tasks, ${changedDayCount}/${dayCount} notes updated via ${sourceLabel}. ${fetchResult.accessStatus.message}`, - }; - new obsidian.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 obsidian.Notice("Things Toolkit sync failed"); - } - finally { - await this.refreshRecentDailyStats(); - this.updateStatusBar(); - this.refreshReviewViews(); - this.settingsTab?.display(); - this.scheduleNextSync(); - } - } - buildDailyStats(dayEntries, source, syncedAt) { - const dailyStats = { ...(this.options.dailyStats || {}) }; - dayEntries.forEach(([dateStr, tasks]) => { - const dateKey = window.moment(dateStr).format("YYYY-MM-DD"); - dailyStats[dateKey] = { - taskCount: tasks.length, - source, - syncedAt, - }; - }); - return dailyStats; - } - getTaskCountForDay(dateKey) { - return this.options.dailyStats?.[dateKey]?.taskCount || 0; - } - getReviewWindowDayCount() { - return Math.max(7, Math.floor(Number(this.options.reviewWindowDays) || 365)); - } - async refreshRecentDailyStats(dayCount = this.getReviewWindowDayCount()) { - const dailyStats = { ...(this.options.dailyStats || {}) }; - const dailyNotes = getAllDailyNotes_1(); - const syncedAt = window.moment().unix(); - const end = window.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"); - const dailyNote = getDailyNote_1(date, dailyNotes); - if (!dailyNote) { - dailyStats[dateKey] = { - taskCount: 0, - source: "daily-note", - syncedAt, - }; - continue; - } - const fileContents = await this.app.vault.read(dailyNote); - dailyStats[dateKey] = { - taskCount: countThingsTasksInSection(fileContents, this.options.sectionHeading), - source: "daily-note", - syncedAt, - }; - } - await this.writeOptions({ dailyStats }); - } - getCurrentCompletionStreak() { - let streak = 0; - for (let date = window.moment().startOf("day"); this.getTaskCountForDay(date.format("YYYY-MM-DD")) > 0; date = date.subtract(1, "day")) { - streak++; - } - return streak; - } - async writeDayReview(dateKey, diff) { - const dailyReviews = { ...(this.options.dailyReviews || {}) }; - const nextReview = { - ...(dailyReviews[dateKey] || {}), - ...diff, - updatedAt: window.moment().unix(), - }; - if (!nextReview.rating && !nextReview.reflection) { - delete dailyReviews[dateKey]; - } - else { - dailyReviews[dateKey] = nextReview; - } - await this.writeOptions({ dailyReviews }); - this.refreshReviewViews(); - } - async openDailyNote(dateKey) { - const date = window.moment(dateKey, "YYYY-MM-DD"); - const dailyNotes = getAllDailyNotes_1(); - let dailyNote = getDailyNote_1(date, dailyNotes); - if (!dailyNote) { - dailyNote = await createDailyNote_1(date); - } - await this.app.workspace.getLeaf(false).openFile(dailyNote); - } - refreshReviewViews() { - this.app.workspace - .getLeavesOfType(VIEW_TYPE_THINGS_TOOLKIT_REVIEW) - .forEach((leaf) => { - if (leaf.view instanceof ThingsToolkitReviewView) { - leaf.view.display(); - } - }); - } - updateStatusBar() { - if (!this.statusBarEl) { - return; - } - const todayKey = window.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"); - } - cancelScheduledSync() { - if (this.syncTimeoutId !== undefined) { - window.clearTimeout(this.syncTimeoutId); - } - } - scheduleNextSync() { - const now = window.moment().unix(); - this.cancelScheduledSync(); - if (!this.options.isSyncEnabled || !this.options.syncInterval) { - console.debug("[Things Toolkit] scheduling skipped, no syncInterval set"); - return; - } - const { latestSyncTime, syncInterval } = this.options; - const secondsUntilNextSync = latestSyncTime + syncInterval - now; - const nextSync = Math.max(secondsUntilNextSync * 1000, 20); - console.debug(`[Things Toolkit] next sync scheduled in ${nextSync}ms`); - this.syncTimeoutId = window.setTimeout(this.syncLogbook, nextSync); - } - async loadOptions() { - this.options = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - 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) { - this.options = Object.assign(this.options, diff); - // Sync toggled on/off - if (diff.isSyncEnabled !== undefined) { - if (diff.isSyncEnabled) { - this.tryToScheduleSync(); - } - else { - this.cancelScheduledSync(); - } - } - else if (diff.syncInterval !== undefined && this.options.isSyncEnabled) { - // reschedule if interval changed - this.tryToScheduleSync(); - } - await this.saveData(this.options); - } -} - -module.exports = ThingsToolkitPlugin; diff --git a/yarn.lock b/yarn.lock index ab536dd..326a6b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -103,11 +103,62 @@ resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + "@marijn/find-cluster-break@^1.0.0": version "1.0.2" resolved "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz" integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g== +"@rollup/plugin-commonjs@^29.0.3": + version "29.0.3" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz" + integrity sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + fdir "^6.2.0" + is-reference "1.2.1" + magic-string "^0.30.3" + picomatch "^4.0.2" + +"@rollup/plugin-node-resolve@^16.0.3": + version "16.0.3" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz" + integrity sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/plugin-typescript@^12.3.0": + version "12.3.0" + resolved "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz" + integrity sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big== + dependencies: + "@rollup/pluginutils" "^5.1.0" + resolve "^1.22.1" + +"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.1.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz" + integrity sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" + +"@rollup/rollup-darwin-arm64@4.62.0": + version "4.62.0" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz" + integrity sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw== + "@types/codemirror@5.60.8": version "5.60.8" resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz" @@ -120,17 +171,7 @@ resolved "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz" integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== -"@types/estree@*": - version "0.0.39" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - -"@types/estree@^1.0.6": - version "1.0.9" - resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz" - integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== - -"@types/estree@^1.0.8": +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8", "@types/estree@1.0.9": version "1.0.9" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz" integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== @@ -154,6 +195,11 @@ dependencies: "@types/node" "*" +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + "@types/tern@*": version "0.23.9" resolved "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz" @@ -289,6 +335,11 @@ brace-expansion@^5.0.5: dependencies: balanced-match "^4.0.2" +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + crelt@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz" @@ -315,6 +366,16 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" @@ -409,6 +470,11 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" @@ -429,7 +495,7 @@ fast-levenshtein@^2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fdir@^6.5.0: +fdir@^6.2.0, fdir@^6.5.0: version "6.5.0" resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== @@ -462,6 +528,11 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz" integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" @@ -469,6 +540,13 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +hasown@^2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + ignore@^5.2.0: version "5.3.2" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" @@ -484,6 +562,13 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +is-core-module@^2.16.1: + version "2.16.2" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz" + integrity sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA== + dependencies: + hasown "^2.0.3" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -496,6 +581,18 @@ is-glob@^4.0.0, is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-reference@1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -538,6 +635,13 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +magic-string@^0.30.3: + version "0.30.21" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + minimatch@^10.2.2: version "10.2.5" resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz" @@ -631,7 +735,12 @@ path-key@^3.1.0: resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -"picomatch@^3 || ^4", picomatch@^4.0.4: +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +"picomatch@^3 || ^4", picomatch@^4.0.2, picomatch@^4.0.4: version "4.0.4" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz" integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== @@ -646,6 +755,50 @@ punycode@^2.1.0: resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +resolve@^1.22.1: + version "1.22.12" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^2.14.0||^3.0.0||^4.0.0, rollup@^2.68.0||^3.0.0||^4.0.0, rollup@^2.78.0||^3.0.0||^4.0.0, rollup@^4.62.0: + version "4.62.0" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz" + integrity sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA== + dependencies: + "@types/estree" "1.0.9" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.62.0" + "@rollup/rollup-android-arm64" "4.62.0" + "@rollup/rollup-darwin-arm64" "4.62.0" + "@rollup/rollup-darwin-x64" "4.62.0" + "@rollup/rollup-freebsd-arm64" "4.62.0" + "@rollup/rollup-freebsd-x64" "4.62.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.62.0" + "@rollup/rollup-linux-arm-musleabihf" "4.62.0" + "@rollup/rollup-linux-arm64-gnu" "4.62.0" + "@rollup/rollup-linux-arm64-musl" "4.62.0" + "@rollup/rollup-linux-loong64-gnu" "4.62.0" + "@rollup/rollup-linux-loong64-musl" "4.62.0" + "@rollup/rollup-linux-ppc64-gnu" "4.62.0" + "@rollup/rollup-linux-ppc64-musl" "4.62.0" + "@rollup/rollup-linux-riscv64-gnu" "4.62.0" + "@rollup/rollup-linux-riscv64-musl" "4.62.0" + "@rollup/rollup-linux-s390x-gnu" "4.62.0" + "@rollup/rollup-linux-x64-gnu" "4.62.0" + "@rollup/rollup-linux-x64-musl" "4.62.0" + "@rollup/rollup-openbsd-x64" "4.62.0" + "@rollup/rollup-openharmony-arm64" "4.62.0" + "@rollup/rollup-win32-arm64-msvc" "4.62.0" + "@rollup/rollup-win32-ia32-msvc" "4.62.0" + "@rollup/rollup-win32-x64-gnu" "4.62.0" + "@rollup/rollup-win32-x64-msvc" "4.62.0" + fsevents "~2.3.2" + semver@^7.7.3: version "7.8.4" resolved "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz" @@ -668,6 +821,11 @@ style-mod@^4.1.0: resolved "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz" integrity sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ== +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + tinyglobby@^0.2.15: version "0.2.17" resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz" @@ -681,16 +839,16 @@ ts-api-utils@^2.5.0: resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz" integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== +tslib@*, tslib@2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz" + integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== + tslib@2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz" integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== -tslib@2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz" - integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" @@ -708,7 +866,7 @@ typescript-eslint@^8.61.0: "@typescript-eslint/typescript-estree" "8.61.0" "@typescript-eslint/utils" "8.61.0" -typescript@>=4.8.4, "typescript@>=4.8.4 <6.1.0", typescript@latest: +typescript@>=3.7.0, typescript@>=4.8.4, "typescript@>=4.8.4 <6.1.0", typescript@latest: version "6.0.3" resolved "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz" integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==