diff --git a/src/app/plugins/plugin-api.ts b/src/app/plugins/plugin-api.ts index ca1f97d772..fe1413ae2b 100644 --- a/src/app/plugins/plugin-api.ts +++ b/src/app/plugins/plugin-api.ts @@ -260,7 +260,7 @@ export class PluginAPI implements PluginAPIInterface { return this._boundMethods.loadPersistedData(); } - async getConfig(): Promise { + async getConfig(): Promise { PluginLog.log(`Plugin ${this._pluginId} requested configuration`); return this._boundMethods.getConfig(); } @@ -324,7 +324,27 @@ export class PluginAPI implements PluginAPIInterface { } /** - * Set a simple counter value for today + * Gets all simple counters as { [key: string]: number }. + */ + async getAllCounters(): Promise<{ [key: string]: number }> { + PluginLog.log(`Plugin ${this._pluginId} requested all simple counters`); + return this._pluginBridge.getAllCounters(); + } + + /** + * Gets a single simple counter value (undefined if unset). + * @param id The counter id (e.g., 'daily-commits'). + */ + async getCounter(id: string): Promise { + PluginLog.log(`Plugin ${this._pluginId} requested counter value for id: ${id}`); + const value = await this._pluginBridge.getCounter(id); + return value ?? null; + } + + /** + * Sets a simple counter value. + * @param id The counter id. + * @param value The numeric value. */ async setCounter(id: string, value: number): Promise { PluginLog.log(`Plugin ${this._pluginId} requested to set counter ${id} to ${value}`); @@ -332,47 +352,123 @@ export class PluginAPI implements PluginAPIInterface { } /** - * Get a simple counter value for today + * Increments a simple counter (default +1). + * @param id The counter id. + * @param incrementBy Increment amount (default: 1). */ - async getCounter(id: string): Promise { - PluginLog.log(`Plugin ${this._pluginId} requested to get counter ${id}`); - return this._pluginBridge.getCounter(id); - } - - /** - * Increment a simple counter value for today - */ - async incrementCounter(id: string, incrementBy: number = 1): Promise { + async incrementCounter(id: string, incrementBy = 1): Promise { PluginLog.log( `Plugin ${this._pluginId} requested to increment counter ${id} by ${incrementBy}`, ); - return this._pluginBridge.incrementCounter(id, incrementBy); + const newValue = await this._pluginBridge.incrementCounter(id, incrementBy); + return newValue; } /** - * Decrement a simple counter value for today + * Decrements a simple counter (floors at 0, default -1). + * @param id The counter id. + * @param decrementBy Decrement amount (default: 1). */ - async decrementCounter(id: string, decrementBy: number = 1): Promise { + async decrementCounter(id: string, decrementBy = 1): Promise { PluginLog.log( `Plugin ${this._pluginId} requested to decrement counter ${id} by ${decrementBy}`, ); - return this._pluginBridge.decrementCounter(id, decrementBy); + const newValue = await this._pluginBridge.decrementCounter(id, decrementBy); + return newValue; } /** - * Delete a simple counter + * Deletes a simple counter. + * @param id The counter ID. */ async deleteCounter(id: string): Promise { PluginLog.log(`Plugin ${this._pluginId} requested to delete counter ${id}`); - return this._pluginBridge.deleteCounter(id); + return this._pluginBridge.deleteSimpleCounter(id); } /** - * Get all simple counters with their values for today + * Gets all simple counters as SimpleCounter[]. */ - async getAllCounters(): Promise<{ [id: string]: number }> { - PluginLog.log(`Plugin ${this._pluginId} requested to get all counters`); - return this._pluginBridge.getAllCounters(); + async getAllSimpleCounters(): Promise { + PluginLog.log(`Plugin ${this._pluginId} requested all simple counters (full model)`); + return this._pluginBridge.getAllSimpleCounters(); + } + + /** + * Gets a single simple counter by ID. + * @param id The counter ID. + */ + async getSimpleCounter(id: string): Promise { + PluginLog.log(`Plugin ${this._pluginId} requested simple counter ${id}`); + return this._pluginBridge.getSimpleCounter(id); + } + + /** + * Updates a simple counter (partial). + * @param id The counter ID. + * @param updates Partial updates. + */ + async updateSimpleCounter(id: string, updates: Partial): Promise { + PluginLog.log( + `Plugin ${this._pluginId} requested to update simple counter ${id}`, + updates, + ); + return this._pluginBridge.updateSimpleCounter(id, updates); + } + + /** + * Toggles a simple counter's isOn state. + * @param id The counter ID. + */ + async toggleSimpleCounter(id: string): Promise { + PluginLog.log(`Plugin ${this._pluginId} requested to toggle simple counter ${id}`); + return this._pluginBridge.toggleSimpleCounter(id); + } + + /** + * Sets a simple counter's isEnabled state. + * @param id The counter ID. + * @param isEnabled Enabled state. + */ + async setSimpleCounterEnabled(id: string, isEnabled: boolean): Promise { + PluginLog.log( + `Plugin ${this._pluginId} requested to set simple counter ${id} enabled: ${isEnabled}`, + ); + return this._pluginBridge.setSimpleCounterEnabled(id, isEnabled); + } + + /** + * Deletes a simple counter. + * @param id The counter ID. + */ + async deleteSimpleCounter(id: string): Promise { + PluginLog.log(`Plugin ${this._pluginId} requested to delete simple counter ${id}`); + return this._pluginBridge.deleteSimpleCounter(id); + } + + /** + * Sets a simple counter value for today. + * @param id The counter ID. + * @param value The numeric value. + */ + async setSimpleCounterToday(id: string, value: number): Promise { + PluginLog.log( + `Plugin ${this._pluginId} requested to set simple counter ${id} today to ${value}`, + ); + return this._pluginBridge.setSimpleCounterToday(id, value); + } + + /** + * Sets a simple counter value for a specific date. + * @param id The counter ID. + * @param date The date (`YYYY-MM-DD`). + * @param value The numeric value. + */ + async setSimpleCounterDate(id: string, date: string, value: number): Promise { + PluginLog.log( + `Plugin ${this._pluginId} requested to set simple counter ${id} on ${date} to ${value}`, + ); + return this._pluginBridge.setSimpleCounterDate(id, date, value); } /** diff --git a/src/app/plugins/plugin-bridge.service.ts b/src/app/plugins/plugin-bridge.service.ts index 7a2ea648d1..7fd070b0bd 100644 --- a/src/app/plugins/plugin-bridge.service.ts +++ b/src/app/plugins/plugin-bridge.service.ts @@ -34,7 +34,7 @@ import { WorkContextService } from '../features/work-context/work-context.servic import { ProjectService } from '../features/project/project.service'; import { TagService } from '../features/tag/tag.service'; import typia from 'typia'; -import { first } from 'rxjs/operators'; +import { first, take, map } from 'rxjs/operators'; import { selectTaskByIdWithSubTaskData } from '../features/tasks/store/task.selectors'; import { PluginUserPersistenceService } from './plugin-user-persistence.service'; import { PluginConfigService } from './plugin-config.service'; @@ -50,7 +50,16 @@ import { Log, PluginLog } from '../core/log'; import { TaskCopy } from '../features/tasks/task.model'; import { ProjectCopy } from '../features/project/project.model'; import { TagCopy } from '../features/tag/tag.model'; -import { SimpleCounterService } from '../features/simple-counter/simple-counter.service'; + +// New imports for simple counters +import { selectAllSimpleCounters } from '../features/simple-counter/store/simple-counter.reducer'; +import { SimpleCounter } from '../features/simple-counter/simple-counter.model'; +import { + upsertSimpleCounter, + updateSimpleCounter, + deleteSimpleCounter, + toggleSimpleCounterCounter, +} from '../features/simple-counter/store/simple-counter.actions'; /** * PluginBridge acts as an intermediary layer between plugins and the main application services. @@ -80,7 +89,6 @@ export class PluginBridgeService implements OnDestroy { private _injector = inject(Injector); private _translateService = inject(TranslateService); private _syncWrapperService = inject(SyncWrapperService); - private _simpleCounterService = inject(SimpleCounterService); // Track header buttons registered by plugins private readonly _headerButtons = signal([]); @@ -112,7 +120,7 @@ export class PluginBridgeService implements OnDestroy { ): { persistDataSynced: (dataStr: string) => Promise; loadPersistedData: () => Promise; - getConfig: () => Promise; + getConfig: () => Promise; registerHeaderButton: (cfg: PluginHeaderBtnCfg) => void; registerMenuEntry: (cfg: Omit) => void; registerSidePanelButton: (cfg: Omit) => void; @@ -123,6 +131,19 @@ export class PluginBridgeService implements OnDestroy { executeNodeScript: ( request: PluginNodeScriptRequest, ) => Promise; + getAllCounters: () => Promise<{ [key: string]: number }>; + getCounter: (id: string) => Promise; + setCounter: (id: string, value: number) => Promise; + incrementCounter: (id: string, incrementBy?: number) => Promise; + decrementCounter: (id: string, decrementBy?: number) => Promise; + getAllSimpleCounters: () => Promise; + getSimpleCounter: (id: string) => Promise; + updateSimpleCounter: (id: string, updates: Partial) => Promise; + toggleSimpleCounter: (id: string) => Promise; + setSimpleCounterEnabled: (id: string, isEnabled: boolean) => Promise; + deleteSimpleCounter: (id: string) => Promise; + setSimpleCounterToday: (id: string, value: number) => Promise; + setSimpleCounterDate: (id: string, date: string, value: number) => Promise; log: ReturnType; } { return { @@ -154,6 +175,29 @@ export class PluginBridgeService implements OnDestroy { executeNodeScript: (request: PluginNodeScriptRequest) => this._executeNodeScript(pluginId, manifest || null, request), + // Basic counter methods (existing) + getAllCounters: () => this.getAllCounters(), + getCounter: (id: string) => this.getCounter(id), + setCounter: (id: string, value: number) => this.setCounter(id, value), + incrementCounter: (id: string, incrementBy = 1) => + this.incrementCounter(id, incrementBy), + decrementCounter: (id: string, decrementBy = 1) => + this.decrementCounter(id, decrementBy), + + // Full SimpleCounter methods (new) + getAllSimpleCounters: () => this.getAllSimpleCounters(), + getSimpleCounter: (id: string) => this.getSimpleCounter(id), + updateSimpleCounter: (id: string, updates: Partial) => + this.updateSimpleCounter(id, updates), + toggleSimpleCounter: (id: string) => this.toggleSimpleCounter(id), + setSimpleCounterEnabled: (id: string, isEnabled: boolean) => + this.setSimpleCounterEnabled(id, isEnabled), + deleteSimpleCounter: (id: string) => this.deleteSimpleCounter(id), + setSimpleCounterToday: (id: string, value: number) => + this.setSimpleCounterToday(id, value), + setSimpleCounterDate: (id: string, date: string, value: number) => + this.setSimpleCounterDate(id, date, value), + // Logging log: Log.withContext(`${pluginId}`), }; @@ -448,113 +492,6 @@ export class PluginBridgeService implements OnDestroy { PluginLog.log('PluginBridge: Tag updated successfully', { tagId, updates }); } - /** - * Set a simple counter value for today - */ - async setCounter(id: string, value: number): Promise { - typia.assert(id); - typia.assert(value); - - this._simpleCounterService.setCounterToday(id, value); - PluginLog.log('PluginBridge: Counter set successfully', { id, value }); - } - - /** - * Get a simple counter value for today - */ - async getCounter(id: string): Promise { - typia.assert(id); - - const counter = await this._simpleCounterService.simpleCounters$ - .pipe(first()) - .toPromise(); - const simpleCounter = counter?.find((c) => c.id === id); - - if (!simpleCounter) { - return null; - } - - const dateService = this._injector.get( - await import('../core/date/date.service').then((m) => m.DateService), - ); - const today = dateService.todayStr(); - return simpleCounter.countOnDay[today] || 0; - } - - /** - * Increment a simple counter value for today - */ - async incrementCounter(id: string, incrementBy: number = 1): Promise { - typia.assert(id); - typia.assert(incrementBy); - - this._simpleCounterService.increaseCounterToday(id, incrementBy); - - // Get the new value - const newValue = await this.getCounter(id); - PluginLog.log('PluginBridge: Counter incremented successfully', { - id, - incrementBy, - newValue, - }); - return newValue ?? incrementBy; - } - - /** - * Decrement a simple counter value for today - */ - async decrementCounter(id: string, decrementBy: number = 1): Promise { - typia.assert(id); - typia.assert(decrementBy); - - this._simpleCounterService.decreaseCounterToday(id, decrementBy); - - // Get the new value - const newValue = await this.getCounter(id); - PluginLog.log('PluginBridge: Counter decremented successfully', { - id, - decrementBy, - newValue, - }); - return newValue ?? -decrementBy; - } - - /** - * Delete a simple counter - */ - async deleteCounter(id: string): Promise { - typia.assert(id); - - this._simpleCounterService.deleteSimpleCounter(id); - PluginLog.log('PluginBridge: Counter deleted successfully', { id }); - } - - /** - * Get all simple counters with their values for today - */ - async getAllCounters(): Promise<{ [id: string]: number }> { - const counters = await this._simpleCounterService.simpleCounters$ - .pipe(first()) - .toPromise(); - - if (!counters) { - return {}; - } - - const dateService = this._injector.get( - await import('../core/date/date.service').then((m) => m.DateService), - ); - const today = dateService.todayStr(); - - const result: { [id: string]: number } = {}; - counters.forEach((counter) => { - result[counter.id] = counter.countOnDay[today] || 0; - }); - - PluginLog.log('PluginBridge: Retrieved all counters', result); - return result; - } - /** * Reorder tasks in a project or parent task * @param taskIds - Array of task IDs in the new order @@ -1211,6 +1148,184 @@ export class PluginBridgeService implements OnDestroy { handler(this._isWindowFocused); } + /** + * Gets all simple counters as { [key: string]: number }. + */ + async getAllCounters(): Promise<{ [key: string]: number }> { + const today = new Date().toISOString().split('T')[0]; + const countersArray = await this._store + .select(selectAllSimpleCounters) + .pipe( + take(1), + map((counters: SimpleCounter[]) => + counters.reduce( + (acc, c) => ({ ...acc, [c.id]: c.countOnDay?.[today] ?? 0 }), + {} as { [key: string]: number }, + ), + ), + ) + .toPromise(); + return countersArray || {}; + } + + /** + * Gets a single simple counter value (undefined if unset). + * @param key The counter key (e.g., 'daily-commits'). + */ + async getCounter(id: string): Promise { + typia.assert(id); + // Allow uppercase, lowercase, numbers, hyphens, underscores + if (!/^[A-Za-z0-9_-]+$/.test(id)) { + throw new Error('Invalid counter key: must be alphanumeric with hyphens'); + } + const counters = await this.getAllCounters(); + return counters[id] ?? null; + } + + async setCounter(id: string, value: number): Promise { + typia.assert(id); + typia.assert(value); + // Allow uppercase, lowercase, numbers, hyphens, underscores + if (!/^[A-Za-z0-9_-]+$/.test(id)) { + throw new Error('Invalid counter key: must be alphanumeric with hyphens'); + } + if (typeof value !== 'number' || !isFinite(value) || value < 0) { + throw new Error('Invalid counter value: must be a non-negative number'); + } + const today = new Date().toISOString().split('T')[0]; + // Upsert the counter (creates if not exists) + this._store.dispatch( + upsertSimpleCounter({ + simpleCounter: { + id: id, + //title: id, + //isEnabled: true, + //icon: null, + //type: 'ClickCounter', + countOnDay: { [today]: value }, + //isOn: false, + } as SimpleCounter, + }), + ); + } + + async incrementCounter(id: string, incrementBy = 1): Promise { + typia.assert(id); + typia.assert(incrementBy); + if (typeof incrementBy !== 'number' || !isFinite(incrementBy) || incrementBy <= 0) { + throw new Error('Invalid increment amount: must be a positive number'); + } + const current = (await this.getCounter(id)) ?? 0; + const newValue = current + incrementBy; + await this.setCounter(id, newValue); + return newValue; + } + + async decrementCounter(id: string, decrementBy = 1): Promise { + typia.assert(id); + typia.assert(decrementBy); + if (typeof decrementBy !== 'number' || !isFinite(decrementBy) || decrementBy <= 0) { + throw new Error('Invalid decrement amount: must be a positive number'); + } + const current = (await this.getCounter(id)) ?? 0; + const newValue = Math.max(0, current - decrementBy); + await this.setCounter(id, Math.max(0, newValue)); + return newValue; + } + + /** + * Gets all simple counters as SimpleCounter[]. + */ + async getAllSimpleCounters(): Promise { + return this._store.select(selectAllSimpleCounters).pipe(take(1)).toPromise(); + } + + /** + * Gets a single simple counter by ID. + * @param id The counter ID. + */ + async getSimpleCounter(id: string): Promise { + const all = await this.getAllSimpleCounters(); + return all.find((c) => c.id === id); + } + + /** + * Updates a simple counter (partial). + * @param id The counter ID. + * @param updates Partial updates. + */ + async updateSimpleCounter(id: string, updates: Partial): Promise { + this._store.dispatch( + updateSimpleCounter({ + simpleCounter: { id, changes: updates }, + }), + ); + } + + /** + * Toggles a simple counter's isOn state. + * @param id The counter ID. + */ + async toggleSimpleCounter(id: string): Promise { + const counter = await this.getSimpleCounter(id); + if (!counter) { + throw new Error(`Counter ${id} not found`); + } + this._store.dispatch(toggleSimpleCounterCounter({ id })); + } + + /** + * Sets a simple counter's isEnabled state. + * @param id The counter ID. + * @param isEnabled Enabled state. + */ + async setSimpleCounterEnabled(id: string, isEnabled: boolean): Promise { + return this.updateSimpleCounter(id, { isEnabled }); + } + + /** + * Deletes a simple counter. + * @param id The counter ID. + */ + async deleteSimpleCounter(id: string): Promise { + this._store.dispatch(deleteSimpleCounter({ id })); + } + + /** + * Sets a simple counter value for today. + * @param id The counter ID. + * @param value The numeric value. + */ + async setSimpleCounterToday(id: string, value: number): Promise { + const today = new Date().toISOString().split('T')[0]; + return this.setSimpleCounterDate(id, today, value); + } + + /** + * Sets a simple counter value for a specific date. + * @param id The counter ID. + * @param date The date (`YYYY-MM-DD`). + * @param value The numeric value. + */ + async setSimpleCounterDate(id: string, date: string, value: number): Promise { + if (!date.match(/^\d{4}-\d{2}-\d{2}$/)) { + throw new Error('Invalid date format: use YYYY-MM-DD'); + } + const dateObj = new Date(date); + if (isNaN(dateObj.getTime())) { + throw new Error('Invalid date: must be valid YYYY-MM-DD'); + } + const counter = await this.getSimpleCounter(id); + if (!counter) { + throw new Error(`Counter ${id} not found`); + } + if (typeof value !== 'number' || !isFinite(value) || value < 0) { + throw new Error('Invalid counter value: must be a non-negative number'); + } + const newCountOnDay = { ...counter.countOnDay, [date]: value }; + return this.updateSimpleCounter(id, { countOnDay: newCountOnDay }); + } + /** * Clean up all resources when service is destroyed */