Merge pull request #75 from jacobtread/feat-status-bar-view

feat: status bar items
This commit is contained in:
Jacob 2026-03-28 17:39:27 +13:00 committed by GitHub
commit 8ce8b29dd9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 252 additions and 3 deletions

View file

@ -54,6 +54,11 @@ export class TimesheetRowDurationComponent extends Component {
}
}
onunload(): void {
super.onunload();
this.#timeEl?.remove();
}
/**
* Updates the current time duration value based
* on the current time

View file

@ -0,0 +1,74 @@
import { Component } from "obsidian";
import { TimeEntry } from "@/timekeep/schema";
import { TimesheetRowDurationComponent } from "./timesheetRowDuration";
export class TimesheetStatusBarItem extends Component {
/** Parent container element */
#containerEl: HTMLElement;
/** The entry this duration belongs to */
entry: TimeEntry;
/** The time span element */
#wrapperEl: HTMLSpanElement | undefined;
/** Currently tracked background interval for content */
currentContentInterval: number | undefined;
/** Component for rendering the real time updating duration */
duration: TimesheetRowDurationComponent;
/** Callback to open the file when the content is clicked */
onOpen: VoidFunction;
/** Callback to stop the timekeep item */
onStop: VoidFunction;
constructor(
containerEl: HTMLElement,
entry: TimeEntry,
onOpen: VoidFunction,
onStop: VoidFunction
) {
super();
this.#containerEl = containerEl;
this.entry = entry;
this.onOpen = onOpen;
this.onStop = onStop;
}
onload(): void {
super.onload();
const entry = this.entry;
const wrapperEl = this.#containerEl.createDiv({ cls: "timekeep-status-item" });
this.#wrapperEl = wrapperEl;
// const stopButton = wrapperEl.createEl("button", {
// cls: "timekeep-status-item__button",
// });
// this.registerDomEvent(stopButton, "click", this.onStop);
const contentEl = wrapperEl.createDiv({
cls: "timekeep-status-item__content",
title: "Open File",
});
contentEl.createSpan({
cls: "timekeep-status-item__name",
text: entry.name + ":",
});
this.addChild(new TimesheetRowDurationComponent(contentEl, entry));
this.registerDomEvent(contentEl, "click", this.onOpen);
}
onunload(): void {
super.onunload();
this.#wrapperEl?.remove();
}
}

View file

@ -23,12 +23,14 @@ import { load, replaceTimekeepCodeblock, extractTimekeepCodeblocks } from "@/tim
import { stopAllTimekeeps } from "./commands/stopAllTimekeeps";
import { stopFileTimekeeps } from "./commands/stopFileTimekeeps";
import { TimesheetStatusBarItem } from "./components/timesheetStatusBarItem";
import { CustomOutputFormat } from "./output";
import { TimekeepRegistry } from "./service/registry";
import { Timekeep, TimeEntry } from "./timekeep/schema";
import { TimekeepLocatorModal } from "./views/timekeep-locator-modal";
import { TimekeepMarkdownView } from "./views/timekeep-markdown-view";
import { TimekeepMergerModal } from "./views/timekeep-merger-modal";
import { TimekeepStatusBarView } from "./views/timekeep-status-bar-view";
export default class TimekeepPlugin extends Plugin {
settingsStore: Store<TimekeepSettings>;
@ -105,6 +107,11 @@ export default class TimekeepPlugin extends Plugin {
this.registry = new TimekeepRegistry(this.app.vault);
this.registry.concurrencyLimit = settings.registryConcurrencyLimit;
this.registry.onload();
if (settings.statusBarEnabled) {
const containerEl = this.addStatusBarItem();
new TimekeepStatusBarView(containerEl, this.app, this.registry, this.settingsStore);
}
});
this.registerMarkdownCodeBlockProcessor(

View file

@ -7,7 +7,7 @@ import {
type TimekeepWithPosition,
} from "@/timekeep/parser";
type TimekeepRegistryEntry = {
export type TimekeepRegistryEntry = {
file: TFile;
timekeeps: TimekeepWithPosition[];
};

View file

@ -419,5 +419,26 @@ export class TimekeepSettingsTab extends PluginSettingTab {
}));
});
});
// Status bar section
new Setting(this.containerEl)
.setName("Status Bar")
.setDesc(
"Timekeep can show status bar entries for running timers within your vault. This requires that the registry option above is enabled"
)
.setHeading();
new Setting(this.containerEl)
.setName("Enabled")
.setDesc("Whether to enable status bar entries.")
.addToggle((t) => {
t.setValue(settings.statusBarEnabled);
t.onChange((v) => {
this.settingsStore.setState((currentValue) => ({
...currentValue,
statusBarEnabled: v,
}));
});
});
}
}

View file

@ -72,6 +72,8 @@ export interface TimekeepSettings {
registryEnabled: boolean;
registryConcurrencyLimit: number;
statusBarEnabled: boolean;
}
export const defaultSettings: TimekeepSettings = {
@ -98,6 +100,8 @@ export const defaultSettings: TimekeepSettings = {
registryEnabled: true,
registryConcurrencyLimit: 15,
statusBarEnabled: true,
};
export function legacySettingsCompatibility(settings: TimekeepSettings): void {

View file

@ -286,3 +286,16 @@
width: 100%;
margin-bottom: 1rem;
}
.timekeep-status-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.timekeep-status-item__content {
display: flex;
align-items: center;
gap: 0.25rem;
cursor: pointer;
}

View file

@ -0,0 +1,95 @@
import { App, MarkdownView } from "obsidian";
import { TimesheetStatusBarItem } from "@/components/timesheetStatusBarItem";
import { TimekeepRegistry, TimekeepRegistryEntry } from "@/service/registry";
import { TimekeepSettings } from "@/settings";
import { Store } from "@/store";
import { getRunningEntry } from "@/timekeep";
import { TimekeepWithPosition } from "@/timekeep/parser";
import { TimeEntry } from "@/timekeep/schema";
export class TimekeepStatusBarView {
/** Parent container element */
#containerEl: HTMLElement;
/** Access to the app instance */
app: App;
/** Access to the app registry */
registry: TimekeepRegistry;
/** Access to the timekeep settings */
settings: Store<TimekeepSettings>;
/** Currently rendered items */
items: TimesheetStatusBarItem[] = [];
constructor(
containerEl: HTMLElement,
app: App,
registry: TimekeepRegistry,
settings: Store<TimekeepSettings>
) {
this.#containerEl = containerEl;
this.app = app;
this.registry = registry;
this.settings = settings;
// Render the initial state
this.render(this.registry.entries.getState());
// Subscribe to changes to re-render
this.registry.entries.subscribe(() => {
this.render(this.registry.entries.getState());
});
}
render(entries: TimekeepRegistryEntry[]) {
// Unload the current children
for (const item of this.items) {
item.unload();
}
// Load the new children
for (const entry of entries) {
for (const timekeep of entry.timekeeps) {
const runningEntry = getRunningEntry(timekeep.timekeep.entries);
if (runningEntry === null) continue;
const item = new TimesheetStatusBarItem(
this.#containerEl,
runningEntry,
() => {
void this.onOpen(entry, timekeep);
},
() => {
this.onStop(entry, timekeep, runningEntry);
}
);
this.items.push(item);
item.load();
}
}
}
onStop(_entry: TimekeepRegistryEntry, _timekeep: TimekeepWithPosition, _timeEntry: TimeEntry) {
// TODO: For future implementation, ...being able to stop entries
}
async onOpen(entry: TimekeepRegistryEntry, timekeep: TimekeepWithPosition) {
const leaf = this.app.workspace.getLeaf();
await leaf.openFile(entry.file);
const view = leaf.view;
if (view instanceof MarkdownView) {
const editor = view.editor;
const line = timekeep.startLine;
// Focus the line we opened to
editor.setCursor({ line: Math.max(line - 1, 0), ch: 0 });
editor.scrollIntoView({ from: { line, ch: 0 }, to: { line, ch: 0 } }, true);
}
}
}

File diff suppressed because one or more lines are too long