Remove compiled files from repo

This commit is contained in:
Nick Twort 2025-12-09 08:00:51 +11:00
parent a3602ed818
commit 66ac00f68f
2 changed files with 3 additions and 534 deletions

4
.gitignore vendored
View file

@ -7,4 +7,6 @@ npm-debug.log*
node_modules/
*.tsbuildinfo
*.tsbuildinfo
main.js

533
main.js
View file

@ -1,533 +0,0 @@
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// main.ts
var main_exports = {};
__export(main_exports, {
default: () => HarvestPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian = require("obsidian");
function parseQuery(source) {
const tokens = source.trim().split(/\s+/).map((t) => t.toUpperCase());
if (tokens.length < 2)
throw new Error("Query is too short.");
const type = tokens[0];
if (type !== "LIST" /* LIST */ && type !== "SUMMARY" /* SUMMARY */) {
throw new Error(`Invalid query type: ${type}. Must be LIST or SUMMARY.`);
}
const { from, to } = parseTimeRange(tokens.slice(1));
return { type, from, to };
}
function parseTimeRange(tokens) {
const today = new Date();
const formatDate = (date) => {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${year}-${month}-${day}`;
};
let from;
let to;
switch (tokens[0]) {
case "TODAY":
from = today;
to = today;
break;
case "WEEK": {
const dayOfWeek = today.getDay();
const firstDayOfWeek = new Date(today.setDate(today.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1)));
from = firstDayOfWeek;
to = new Date(new Date(firstDayOfWeek).setDate(firstDayOfWeek.getDate() + 6));
break;
}
case "MONTH":
from = new Date(today.getFullYear(), today.getMonth(), 1);
to = new Date(today.getFullYear(), today.getMonth() + 1, 0);
break;
case "PAST": {
const count = parseInt(tokens[1]);
if (isNaN(count) || tokens[2] !== "DAYS")
throw new Error("Invalid PAST format. Use 'PAST <number> DAYS'.");
to = today;
from = new Date(new Date().setDate(today.getDate() - (count - 1)));
break;
}
case "FROM": {
if (tokens.length < 4 || tokens[2] !== "TO")
throw new Error("Invalid FROM...TO format.");
from = new Date(tokens[1]);
to = new Date(tokens[3]);
if (isNaN(from.getTime()) || isNaN(to.getTime()))
throw new Error("Invalid date format in FROM...TO. Use YYYY-MM-DD.");
break;
}
default:
throw new Error(`Unknown time range specifier: ${tokens[0]}`);
}
return { from: formatDate(from), to: formatDate(to) };
}
function renderReport(container, entries, query) {
container.empty();
const wrapper = container.createDiv({ cls: "harvest-report" });
if (entries.length === 0) {
wrapper.createEl("p", { text: "No time entries found for the selected period." });
return;
}
if (query.type === "LIST" /* LIST */) {
renderList(wrapper, entries);
} else if (query.type === "SUMMARY" /* SUMMARY */) {
renderSummary(wrapper, entries);
}
}
function renderList(container, entries) {
const table = container.createEl("table", { cls: "harvest-table" });
const thead = table.createTHead();
const headerRow = thead.insertRow();
headerRow.createEl("th", { text: "Project" });
headerRow.createEl("th", { text: "Task" });
headerRow.createEl("th", { text: "Date" });
headerRow.createEl("th", { text: "Hours" });
const tbody = table.createTBody();
for (const entry of entries) {
const row = tbody.insertRow();
row.createEl("td", { text: entry.project.name });
row.createEl("td", { text: entry.task.name });
row.createEl("td", { text: entry.spent_date });
row.createEl("td", { text: entry.hours.toFixed(2), cls: "harvest-hours" });
}
}
function renderSummary(container, entries) {
let totalHours = 0;
const projectTotals = {};
for (const entry of entries) {
totalHours += entry.hours;
const projectName = entry.project.name;
if (!projectTotals[projectName]) {
projectTotals[projectName] = 0;
}
projectTotals[projectName] += entry.hours;
}
container.createEl("h3", { text: "Time summary" });
const summaryDiv = container.createDiv({ cls: "harvest-summary" });
summaryDiv.createEl("p").createEl("strong", { text: `Total hours: ${totalHours.toFixed(2)}` });
const barChartContainer = summaryDiv.createDiv({ cls: "harvest-barchart-container" });
const colors = ["#84b65a", "#c25956", "#59a7c2", "#c29b59", "#8e59c2", "#c2598e", "#5ac28a"];
let colorIndex = 0;
const sortedProjects = Object.keys(projectTotals).sort((a, b) => projectTotals[b] - projectTotals[a]);
for (const projectName of sortedProjects) {
const projectHours = projectTotals[projectName];
const percentage = totalHours > 0 ? projectHours / totalHours * 100 : 0;
const color = colors[colorIndex % colors.length];
const bar = barChartContainer.createDiv({ cls: "harvest-barchart-bar" });
bar.style.setProperty("--bar-width", `${percentage}%`);
bar.style.setProperty("--bar-color", color);
bar.title = `${projectName}: ${projectHours.toFixed(2)} hours`;
colorIndex++;
}
const legendContainer = summaryDiv.createDiv({ cls: "harvest-barchart-legend" });
colorIndex = 0;
for (const projectName of sortedProjects) {
const projectHours = projectTotals[projectName];
const color = colors[colorIndex % colors.length];
const legendItem = legendContainer.createDiv({ cls: "harvest-legend-item" });
const colorSwatch = legendItem.createDiv({ cls: "harvest-legend-swatch" });
colorSwatch.style.backgroundColor = color;
legendItem.createSpan({ text: `${projectName}: ${projectHours.toFixed(2)} hours` });
colorIndex++;
}
}
var hqlProcessor = (plugin) => async (source, el, ctx) => {
try {
const query = parseQuery(source);
if (!query)
return;
el.setText("Loading Harvest report...");
const entries = await plugin.getTimeEntries(query);
if (entries) {
renderReport(el, entries, query);
} else {
el.setText("Failed to fetch Harvest report.");
}
} catch (e) {
el.setText(`Error processing Harvest query: ${e.message}`);
}
};
var DEFAULT_SETTINGS = {
personalAccessToken: "",
accountId: "",
pollingInterval: 5,
// Default to 5 minutes
folderProjectCache: {}
};
var HarvestPlugin = class extends import_obsidian.Plugin {
constructor() {
super(...arguments);
this.runningTimer = null;
this.projectCache = [];
// Cache for the combined project list
this.userId = null;
}
async onload() {
await this.loadSettings();
this.statusBarItemEl = this.addStatusBarItem();
this.statusBarItemEl.setText("Harvest");
this.statusBarItemEl.addClass("mod-clickable");
this.statusBarItemEl.addEventListener("click", () => void this.toggleTimer());
this.addSettingTab(new HarvestSettingTab(this.app, this));
if (this.settings.personalAccessToken && this.settings.accountId) {
await this.fetchCurrentUserId();
}
void this.fetchAllTrackableProjects();
this.addCommand({
id: "start-timer",
name: "Start timer",
callback: () => {
void new ProjectSuggestModal(this.app, this, this.app.workspace.getActiveFile()).open();
}
});
this.addCommand({
id: "stop-timer",
name: "Stop timer",
callback: async () => {
if (this.runningTimer) {
await this.stopTimer(this.runningTimer.id);
} else {
new import_obsidian.Notice("No timer is currently running.");
}
}
});
this.addCommand({
id: "toggle-timer",
name: "Toggle timer",
callback: async () => {
await this.toggleTimer();
}
});
this.addCommand({
id: "refresh-projects",
name: "Refresh projects",
callback: async () => {
new import_obsidian.Notice("Refreshing project list from Harvest...");
await this.fetchAllTrackableProjects(true);
new import_obsidian.Notice("Project list has been updated.");
}
});
this.registerMarkdownCodeBlockProcessor("harvest", hqlProcessor(this));
const pollingMinutes = this.settings.pollingInterval > 0 ? this.settings.pollingInterval : 5;
this.timerInterval = window.setInterval(() => void this.updateRunningTimer(), pollingMinutes * 60 * 1e3);
void this.updateRunningTimer();
}
onunload() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
async request(endpoint, method = "GET", body = null) {
if (!this.settings.personalAccessToken || !this.settings.accountId) {
new import_obsidian.Notice("Harvest API credentials are not set.");
return null;
}
const headers = {
"Authorization": `Bearer ${this.settings.personalAccessToken}`,
"Harvest-Account-Id": this.settings.accountId,
"User-Agent": "Obsidian Harvest Integration",
"Content-Type": "application/json"
};
try {
const response = await (0, import_obsidian.requestUrl)({
url: `https://api.harvestapp.com/v2${endpoint}`,
method,
headers,
body: body ? JSON.stringify(body) : void 0
});
if (response.status >= 400) {
new import_obsidian.Notice(`Harvest API error: ${response.json.message || response.status}`);
return null;
}
return response.json;
} catch (error) {
new import_obsidian.Notice("Failed to connect to Harvest API.");
console.error("Harvest API request error:", error);
return null;
}
}
async fetchCurrentUserId() {
const me = await this.request("/users/me");
if (me && me.id) {
this.userId = me.id;
} else {
this.userId = null;
new import_obsidian.Notice("Could not retrieve Harvest user ID.");
console.error("Failed to fetch Harvest user ID.");
}
}
async getTimeEntries(query) {
if (!this.userId) {
new import_obsidian.Notice("Harvest user ID not found. Cannot fetch your time entries.");
return [];
}
const endpoint = `/time_entries?from=${query.from}&to=${query.to}&user_id=${this.userId}`;
const data = await this.request(endpoint);
if (data && data.time_entries) {
return data.time_entries;
}
return [];
}
async fetchAllTrackableProjects(forceRefresh = false) {
if (this.projectCache.length > 0 && !forceRefresh) {
return this.projectCache;
}
const managedProjects = await this.getManagedProjects();
const recentProjects = await this.getRecentProjectsFromTimeEntries();
const combinedProjectMap = /* @__PURE__ */ new Map();
managedProjects.forEach((proj) => combinedProjectMap.set(proj.id, proj));
recentProjects.forEach((proj) => {
if (!combinedProjectMap.has(proj.id)) {
combinedProjectMap.set(proj.id, proj);
}
});
const sortedProjects = Array.from(combinedProjectMap.values()).sort((a, b) => a.name.localeCompare(b.name));
this.projectCache = sortedProjects;
return this.projectCache;
}
async getManagedProjects() {
let allProjects = [];
let page = 1;
let totalPages = 1;
do {
const data = await this.request(`/projects?is_active=true&page=${page}`);
if (data && data.projects) {
allProjects = allProjects.concat(data.projects);
totalPages = data.total_pages;
page++;
} else {
break;
}
} while (page <= totalPages);
return allProjects;
}
async getRecentProjectsFromTimeEntries() {
if (!this.userId)
return [];
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const fromDate = thirtyDaysAgo.toISOString().split("T")[0];
const data = await this.request(`/time_entries?from=${fromDate}&user_id=${this.userId}`);
if (!data || !data.time_entries)
return [];
const recentProjectsMap = /* @__PURE__ */ new Map();
data.time_entries.forEach((entry) => {
if (entry.project && !recentProjectsMap.has(entry.project.id)) {
recentProjectsMap.set(entry.project.id, entry.project);
}
});
return Array.from(recentProjectsMap.values());
}
async startTimer(projectId, taskId, activeFile) {
if (activeFile && activeFile.parent) {
const folderPath = activeFile.parent.path;
this.settings.folderProjectCache[folderPath] = { projectId, taskId };
await this.saveSettings();
}
const today = new Date();
const year = today.getFullYear();
const month = (today.getMonth() + 1).toString().padStart(2, "0");
const day = today.getDate().toString().padStart(2, "0");
const spentDate = `${year}-${month}-${day}`;
if (this.userId) {
const data = await this.request(`/time_entries?from=${spentDate}&to=${spentDate}&user_id=${this.userId}`);
if (data && data.time_entries) {
const existingEntry = data.time_entries.find(
(entry) => entry.project.id === projectId && entry.task.id === taskId
);
if (existingEntry) {
const result2 = await this.request(`/time_entries/${existingEntry.id}/restart`, "PATCH");
if (result2) {
new import_obsidian.Notice("Timer restarted!");
void this.updateRunningTimer();
}
return;
}
}
}
const body = {
project_id: projectId,
task_id: taskId,
spent_date: spentDate
};
const result = await this.request("/time_entries", "POST", body);
if (result) {
new import_obsidian.Notice("Timer started!");
void this.updateRunningTimer();
}
}
async updateRunningTimer() {
if (!this.userId)
return;
const data = await this.request(`/time_entries?is_running=true&user_id=${this.userId}`);
if (data && data.time_entries && data.time_entries.length > 0) {
this.runningTimer = data.time_entries[0];
const { project, task, hours } = this.runningTimer;
this.statusBarItemEl.setText(`Harvest: ${project.name} - ${task.name} (${hours.toFixed(2)}h)`);
} else {
this.runningTimer = null;
this.statusBarItemEl.setText("Harvest: no timer running");
}
}
async stopTimer(timerId) {
const result = await this.request(`/time_entries/${timerId}/stop`, "PATCH");
if (result) {
new import_obsidian.Notice("Timer stopped.");
void this.updateRunningTimer();
}
}
async toggleTimer() {
await this.updateRunningTimer();
if (this.runningTimer) {
new import_obsidian.Notice("Stopping timer...");
await this.stopTimer(this.runningTimer.id);
} else {
new import_obsidian.Notice("No timer running. Starting a new one...");
new ProjectSuggestModal(this.app, this, this.app.workspace.getActiveFile()).open();
}
}
};
var ProjectSuggestModal = class extends import_obsidian.FuzzySuggestModal {
constructor(app, plugin, activeFile) {
super(app);
this.plugin = plugin;
this.activeFile = activeFile;
}
getItems() {
let projects = [...this.plugin.projectCache];
if (this.activeFile && this.activeFile.parent) {
const folderPath = this.activeFile.parent.path;
const cachedInfo = this.plugin.settings.folderProjectCache[folderPath];
if (cachedInfo) {
const cachedProjectIndex = projects.findIndex((p) => p.id === cachedInfo.projectId);
if (cachedProjectIndex > -1) {
const cachedProject = projects.splice(cachedProjectIndex, 1)[0];
projects.unshift(cachedProject);
}
}
}
return projects;
}
getItemText(project) {
return project.name;
}
renderSuggestion(match, el) {
var _a;
const project = match.item;
el.createEl("div", { text: project.name });
el.createEl("small", { text: ((_a = project.client) == null ? void 0 : _a.name) || "No client" });
}
onChooseItem(project) {
void this.handleProjectChoice(project);
}
async handleProjectChoice(project) {
let tasks = project.task_assignments;
if (!tasks) {
const data = await this.plugin.request(`/projects/${project.id}/task_assignments`);
tasks = data == null ? void 0 : data.task_assignments;
}
if (tasks && tasks.length > 0) {
new TaskSuggestModal(this.app, this.plugin, project, tasks, this.activeFile).open();
} else {
new import_obsidian.Notice("No tasks found for this project.");
}
}
};
var TaskSuggestModal = class extends import_obsidian.FuzzySuggestModal {
constructor(app, plugin, project, tasks, activeFile) {
super(app);
this.plugin = plugin;
this.project = project;
this.tasks = tasks;
this.activeFile = activeFile;
}
getItems() {
let tasks = [...this.tasks];
if (this.activeFile && this.activeFile.parent) {
const folderPath = this.activeFile.parent.path;
const cachedInfo = this.plugin.settings.folderProjectCache[folderPath];
if (cachedInfo && cachedInfo.projectId === this.project.id) {
const cachedTaskIndex = tasks.findIndex((t) => t.task.id === cachedInfo.taskId);
if (cachedTaskIndex > -1) {
const cachedTask = tasks.splice(cachedTaskIndex, 1)[0];
tasks.unshift(cachedTask);
}
}
}
return tasks;
}
getItemText(taskAssignment) {
return taskAssignment.task.name;
}
renderSuggestion(match, el) {
el.createEl("div", { text: match.item.task.name });
}
onChooseItem(taskAssignment) {
void this.plugin.startTimer(this.project.id, taskAssignment.task.id, this.activeFile);
}
};
var HarvestSettingTab = class extends import_obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
new import_obsidian.Setting(containerEl).setName("Credentials").setHeading();
new import_obsidian.Setting(containerEl).setName("Personal access token").setDesc("Get this from the developers section of your Harvest ID.").addText((text) => text.setPlaceholder("Enter your token").setValue(this.plugin.settings.personalAccessToken).onChange(async (value) => {
this.plugin.settings.personalAccessToken = value;
await this.plugin.saveSettings();
if (this.plugin.settings.accountId) {
await this.plugin.fetchCurrentUserId();
}
}));
new import_obsidian.Setting(containerEl).setName("Account ID").setDesc("You can also find this on the same page as your token.").addText((text) => text.setPlaceholder("Enter your account ID").setValue(this.plugin.settings.accountId).onChange(async (value) => {
this.plugin.settings.accountId = value;
await this.plugin.saveSettings();
if (this.plugin.settings.personalAccessToken) {
await this.plugin.fetchCurrentUserId();
}
}));
new import_obsidian.Setting(containerEl).setName("Configuration").setHeading();
new import_obsidian.Setting(containerEl).setName("Polling interval").setDesc("How often to check for a running timer, in minutes. Requires a reload to take effect.").addText((text) => text.setPlaceholder("Default: 5").setValue(String(this.plugin.settings.pollingInterval)).onChange(async (value) => {
const interval = parseInt(value);
if (!isNaN(interval) && interval > 0) {
this.plugin.settings.pollingInterval = interval;
await this.plugin.saveSettings();
}
}));
}
};