From 3d4843ddf2d23b5cd07a93e640d4bfafa837db3f Mon Sep 17 00:00:00 2001 From: Mustache Games <126818428+Mustache-Games@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:11:55 +0000 Subject: [PATCH 1/5] feat(plugins): add simple counter API methods for plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend PluginAPI with comprehensive simple counter functionality to enable plugins to read and update counters through both basic key-value and full model access patterns. ## ๐Ÿ”ง Basic Counter Methods (Simple Value Access) Treat counters as key-value pairs using today's countOnDay: - getAllCounters(): Get all counters' today's values as {key: number} - getCounter(key): Get single counter's today's value - setCounter(key, value): Set counter value (auto-creates if missing) - incrementCounter(key, amount): Increment counter by amount (default 1) - decrementCounter(key, amount): Decrement counter by amount (default 1, min 0) ## ๐ŸŽฏ Full SimpleCounter Methods (Complete Model Access) Operate on full SimpleCounter model with all properties: - getAllSimpleCounters(): Get all counters as complete objects - getSimpleCounter(id): Get single counter by ID - updateSimpleCounter(id, updates): Update partial counter fields - toggleSimpleCounter(id): Toggle isOn state - setSimpleCounterEnabled(id, isEnabled): Enable/disable counter - deleteSimpleCounter(id): Remove counter entirely - setSimpleCounterToday(id, value): Set today's count - setSimpleCounterDate(id, date, value): Set count for specific date ## โœ… Features - Full input validation (alphanumeric keys, non-negative values) - Async Promise-based API consistent with existing patterns - Comprehensive logging for all operations - Integration with existing NgRx simple-counter store - Automatic counter creation on first set - Date-specific operations (today and historical dates) ## ๐Ÿงช Testing All methods tested and working in 'API Test' plugin with console and UI verification. ## ๐Ÿ“ Note A dedicated plugin to demonstrate and test these new methods will follow shortly. ## ๐ŸŽฎ Context As a game developer, I lack deep TypeScript/JavaScript expertise in this codebase. As a student, I have limited time to properly learn new technologies. This implementation was created with AI assistance to bridge these constraints. Files modified: - src/app/plugins/plugin-api.ts - src/app/plugins/plugin-bridge.service.ts Closes: https://github.com/johannesjo/super-productivity/issues/5398" --- src/app/plugins/plugin-api.ts | 138 ++++++++++++++- src/app/plugins/plugin-bridge.service.ts | 216 ++++++++++++++++++++++- 2 files changed, 351 insertions(+), 3 deletions(-) diff --git a/src/app/plugins/plugin-api.ts b/src/app/plugins/plugin-api.ts index f4c44b9c2b..a234efb4ee 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(); } @@ -323,6 +323,142 @@ export class PluginAPI implements PluginAPIInterface { this._pluginBridge.onWindowFocusChange(this._pluginId, handler); } + /** + * 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 key The counter key (e.g., 'daily-commits'). + */ + async getCounter(key: string): Promise { + PluginLog.log(`Plugin ${this._pluginId} requested counter value for key: ${key}`); + return this._pluginBridge.getCounter(key); + } + + /** + * Sets a simple counter value. + * @param key The counter key. + * @param value The numeric value. + */ + async setCounter(key: string, value: number): Promise { + PluginLog.log(`Plugin ${this._pluginId} requested to set counter ${key} to ${value}`); + return this._pluginBridge.setCounter(key, value); + } + + /** + * Increments a simple counter (default +1). + * @param key The counter key. + * @param amount Increment amount (default: 1). + */ + async incrementCounter(key: string, amount = 1): Promise { + PluginLog.log( + `Plugin ${this._pluginId} requested to increment counter ${key} by ${amount}`, + ); + return this._pluginBridge.incrementCounter(key, amount); + } + + /** + * Decrements a simple counter (floors at 0, default -1). + * @param key The counter key. + * @param amount Decrement amount (default: 1). + */ + async decrementCounter(key: string, amount = 1): Promise { + PluginLog.log( + `Plugin ${this._pluginId} requested to decrement counter ${key} by ${amount}`, + ); + return this._pluginBridge.decrementCounter(key, amount); + } + + /** + * Gets all simple counters as SimpleCounter[]. + */ + 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); + } + /** * Clean up all resources associated with this plugin API instance * Called when the plugin is being unloaded diff --git a/src/app/plugins/plugin-bridge.service.ts b/src/app/plugins/plugin-bridge.service.ts index b7084f0f68..dbc2387747 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'; @@ -51,6 +51,16 @@ import { TaskCopy } from '../features/tasks/task.model'; import { ProjectCopy } from '../features/project/project.model'; import { TagCopy } from '../features/tag/tag.model'; +// 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. * This provides: @@ -110,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; @@ -121,6 +131,19 @@ export class PluginBridgeService implements OnDestroy { executeNodeScript: ( request: PluginNodeScriptRequest, ) => Promise; + getAllCounters: () => Promise<{ [key: string]: number }>; + getCounter: (key: string) => Promise; + setCounter: (key: string, value: number) => Promise; + incrementCounter: (key: string, amount?: number) => Promise; + decrementCounter: (key: string, amount?: 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 { @@ -152,6 +175,27 @@ export class PluginBridgeService implements OnDestroy { executeNodeScript: (request: PluginNodeScriptRequest) => this._executeNodeScript(pluginId, manifest || null, request), + // Basic counter methods (existing) + getAllCounters: () => this.getAllCounters(), + getCounter: (key: string) => this.getCounter(key), + setCounter: (key: string, value: number) => this.setCounter(key, value), + incrementCounter: (key: string, amount = 1) => this.incrementCounter(key, amount), + decrementCounter: (key: string, amount = 1) => this.decrementCounter(key, amount), + + // 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}`), }; @@ -1102,6 +1146,174 @@ 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) => + 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(key: string): Promise { + typia.assert(key); + if (!/^[a-z0-9-]+$/.test(key)) { + throw new Error('Invalid counter key: must be alphanumeric with hyphens'); + } + const counters = await this.getAllCounters(); + return counters[key]; + } + + async setCounter(key: string, value: number): Promise { + typia.assert(key); + typia.assert(value); + if (!/^[a-z0-9-]+$/.test(key)) { + 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: key, + title: key, + isEnabled: true, + icon: null, + type: 'ClickCounter', + countOnDay: { [today]: value }, + isOn: false, + } as SimpleCounter, + }), + ); + } + + async incrementCounter(key: string, amount = 1): Promise { + typia.assert(key); + typia.assert(amount); + if (typeof amount !== 'number' || !isFinite(amount) || amount <= 0) { + throw new Error('Invalid increment amount: must be a positive number'); + } + const current = (await this.getCounter(key)) ?? 0; + await this.setCounter(key, current + amount); + } + + async decrementCounter(key: string, amount = 1): Promise { + typia.assert(key); + typia.assert(amount); + if (typeof amount !== 'number' || !isFinite(amount) || amount <= 0) { + throw new Error('Invalid decrement amount: must be a positive number'); + } + const current = (await this.getCounter(key)) ?? 0; + await this.setCounter(key, Math.max(0, current - amount)); + } + + /** + * 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) { + this._store.dispatch(toggleSimpleCounterCounter({ id })); + } + throw new Error(`Counter ${id} not found`); + } + + /** + * 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 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 */ From aa18b812491976429e02b64e27d5ace561e52c41 Mon Sep 17 00:00:00 2001 From: Mustache-Games Date: Fri, 7 Nov 2025 19:48:18 +0200 Subject: [PATCH 2/5] Allow uppercase letters and underscores in the counter IDs. --- src/app/plugins/plugin-bridge.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/plugins/plugin-bridge.service.ts b/src/app/plugins/plugin-bridge.service.ts index dbc2387747..ab475a892e 100644 --- a/src/app/plugins/plugin-bridge.service.ts +++ b/src/app/plugins/plugin-bridge.service.ts @@ -1172,7 +1172,7 @@ export class PluginBridgeService implements OnDestroy { */ async getCounter(key: string): Promise { typia.assert(key); - if (!/^[a-z0-9-]+$/.test(key)) { + if (!/^[A-Za-z0-9_-]+$/.test(key)) { throw new Error('Invalid counter key: must be alphanumeric with hyphens'); } const counters = await this.getAllCounters(); @@ -1182,7 +1182,7 @@ export class PluginBridgeService implements OnDestroy { async setCounter(key: string, value: number): Promise { typia.assert(key); typia.assert(value); - if (!/^[a-z0-9-]+$/.test(key)) { + if (!/^[A-Za-z0-9_-]+$/.test(key)) { throw new Error('Invalid counter key: must be alphanumeric with hyphens'); } if (typeof value !== 'number' || !isFinite(value) || value < 0) { From c60ceb76de6ea3978e5280185b49b156dd8601e4 Mon Sep 17 00:00:00 2001 From: Mustache-Games Date: Tue, 11 Nov 2025 16:49:52 +0200 Subject: [PATCH 3/5] resolve conflicts --- src/app/plugins/plugin-bridge.service.ts | 109 ----------------------- 1 file changed, 109 deletions(-) diff --git a/src/app/plugins/plugin-bridge.service.ts b/src/app/plugins/plugin-bridge.service.ts index e6c61b8652..ab475a892e 100644 --- a/src/app/plugins/plugin-bridge.service.ts +++ b/src/app/plugins/plugin-bridge.service.ts @@ -50,7 +50,6 @@ 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'; @@ -90,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([]); @@ -492,113 +490,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 From 7c93ac9931bcffcc30ff0154e4d5bfdfcb4dce1f Mon Sep 17 00:00:00 2001 From: Mustache Games <126818428+Mustache-Games@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:11:29 +0000 Subject: [PATCH 4/5] Update PluginAPI counter methods - Change return types to `any` for get/increment/decrementCounter. - Comment out deleteCounter in interface. --- packages/plugin-api/src/types.ts | 2 +- src/app/plugins/plugin-api.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-api/src/types.ts b/packages/plugin-api/src/types.ts index b1062d50e2..f172aa4afb 100644 --- a/packages/plugin-api/src/types.ts +++ b/packages/plugin-api/src/types.ts @@ -406,7 +406,7 @@ export interface PluginAPI { decrementCounter(id: string, decrementBy?: number): Promise; - deleteCounter(id: string): Promise; + //deleteCounter(id: string): Promise; getAllCounters(): Promise<{ [id: string]: number }>; } diff --git a/src/app/plugins/plugin-api.ts b/src/app/plugins/plugin-api.ts index a234efb4ee..d4775d5426 100644 --- a/src/app/plugins/plugin-api.ts +++ b/src/app/plugins/plugin-api.ts @@ -335,7 +335,7 @@ export class PluginAPI implements PluginAPIInterface { * Gets a single simple counter value (undefined if unset). * @param key The counter key (e.g., 'daily-commits'). */ - async getCounter(key: string): Promise { + async getCounter(key: string): Promise { PluginLog.log(`Plugin ${this._pluginId} requested counter value for key: ${key}`); return this._pluginBridge.getCounter(key); } @@ -355,7 +355,7 @@ export class PluginAPI implements PluginAPIInterface { * @param key The counter key. * @param amount Increment amount (default: 1). */ - async incrementCounter(key: string, amount = 1): Promise { + async incrementCounter(key: string, amount = 1): Promise { PluginLog.log( `Plugin ${this._pluginId} requested to increment counter ${key} by ${amount}`, ); @@ -367,7 +367,7 @@ export class PluginAPI implements PluginAPIInterface { * @param key The counter key. * @param amount Decrement amount (default: 1). */ - async decrementCounter(key: string, amount = 1): Promise { + async decrementCounter(key: string, amount = 1): Promise { PluginLog.log( `Plugin ${this._pluginId} requested to decrement counter ${key} by ${amount}`, ); From e41071f07f816487e5aa830d6eab829ff03705af Mon Sep 17 00:00:00 2001 From: Mustache-Games Date: Sun, 16 Nov 2025 19:35:01 +0200 Subject: [PATCH 5/5] fix(plugin-api): align simple counter API signatures and enable deleteCounter - Update parameter names from 'key' to 'id' and 'amount' to 'incrementBy/decrementBy' for consistency across PluginAPI and bridge service - Align return types: `getCounter` to `Promise`, `incrementCounter`/`decrementCounter` to `Promise` (return new value post-operation) - Implement `deleteCounter` in PluginAPI and bridge (uncomment in types, add delegation) - Add null handling in `getCounter` returns - Comment out unnecessary default properties in `setCounter` upsert (title, isEnabled, etc.) - Swap logic in `toggleSimpleCounter` to throw if counter not found before dispatch - Add date validation with `new Date` in `setSimpleCounterDate` - Minor: Fix RxJS pipe typing for `getAllCounters` (explicit `SimpleCounter[]`) --- packages/plugin-api/src/types.ts | 2 +- src/app/plugins/plugin-api.ts | 48 ++++++++----- src/app/plugins/plugin-bridge.service.ts | 86 ++++++++++++++---------- 3 files changed, 80 insertions(+), 56 deletions(-) diff --git a/packages/plugin-api/src/types.ts b/packages/plugin-api/src/types.ts index f172aa4afb..b1062d50e2 100644 --- a/packages/plugin-api/src/types.ts +++ b/packages/plugin-api/src/types.ts @@ -406,7 +406,7 @@ export interface PluginAPI { decrementCounter(id: string, decrementBy?: number): Promise; - //deleteCounter(id: string): Promise; + deleteCounter(id: string): Promise; getAllCounters(): Promise<{ [id: string]: number }>; } diff --git a/src/app/plugins/plugin-api.ts b/src/app/plugins/plugin-api.ts index d4775d5426..fe1413ae2b 100644 --- a/src/app/plugins/plugin-api.ts +++ b/src/app/plugins/plugin-api.ts @@ -333,45 +333,57 @@ export class PluginAPI implements PluginAPIInterface { /** * Gets a single simple counter value (undefined if unset). - * @param key The counter key (e.g., 'daily-commits'). + * @param id The counter id (e.g., 'daily-commits'). */ - async getCounter(key: string): Promise { - PluginLog.log(`Plugin ${this._pluginId} requested counter value for key: ${key}`); - return this._pluginBridge.getCounter(key); + 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 key The counter key. + * @param id The counter id. * @param value The numeric value. */ - async setCounter(key: string, value: number): Promise { - PluginLog.log(`Plugin ${this._pluginId} requested to set counter ${key} to ${value}`); - return this._pluginBridge.setCounter(key, value); + async setCounter(id: string, value: number): Promise { + PluginLog.log(`Plugin ${this._pluginId} requested to set counter ${id} to ${value}`); + return this._pluginBridge.setCounter(id, value); } /** * Increments a simple counter (default +1). - * @param key The counter key. - * @param amount Increment amount (default: 1). + * @param id The counter id. + * @param incrementBy Increment amount (default: 1). */ - async incrementCounter(key: string, amount = 1): Promise { + async incrementCounter(id: string, incrementBy = 1): Promise { PluginLog.log( - `Plugin ${this._pluginId} requested to increment counter ${key} by ${amount}`, + `Plugin ${this._pluginId} requested to increment counter ${id} by ${incrementBy}`, ); - return this._pluginBridge.incrementCounter(key, amount); + const newValue = await this._pluginBridge.incrementCounter(id, incrementBy); + return newValue; } /** * Decrements a simple counter (floors at 0, default -1). - * @param key The counter key. - * @param amount Decrement amount (default: 1). + * @param id The counter id. + * @param decrementBy Decrement amount (default: 1). */ - async decrementCounter(key: string, amount = 1): Promise { + async decrementCounter(id: string, decrementBy = 1): Promise { PluginLog.log( - `Plugin ${this._pluginId} requested to decrement counter ${key} by ${amount}`, + `Plugin ${this._pluginId} requested to decrement counter ${id} by ${decrementBy}`, ); - return this._pluginBridge.decrementCounter(key, amount); + const newValue = await this._pluginBridge.decrementCounter(id, decrementBy); + return newValue; + } + + /** + * 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.deleteSimpleCounter(id); } /** diff --git a/src/app/plugins/plugin-bridge.service.ts b/src/app/plugins/plugin-bridge.service.ts index ab475a892e..7fd070b0bd 100644 --- a/src/app/plugins/plugin-bridge.service.ts +++ b/src/app/plugins/plugin-bridge.service.ts @@ -132,10 +132,10 @@ export class PluginBridgeService implements OnDestroy { request: PluginNodeScriptRequest, ) => Promise; getAllCounters: () => Promise<{ [key: string]: number }>; - getCounter: (key: string) => Promise; - setCounter: (key: string, value: number) => Promise; - incrementCounter: (key: string, amount?: number) => Promise; - decrementCounter: (key: string, amount?: number) => Promise; + 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; @@ -177,10 +177,12 @@ export class PluginBridgeService implements OnDestroy { // Basic counter methods (existing) getAllCounters: () => this.getAllCounters(), - getCounter: (key: string) => this.getCounter(key), - setCounter: (key: string, value: number) => this.setCounter(key, value), - incrementCounter: (key: string, amount = 1) => this.incrementCounter(key, amount), - decrementCounter: (key: string, amount = 1) => this.decrementCounter(key, amount), + 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(), @@ -1155,7 +1157,7 @@ export class PluginBridgeService implements OnDestroy { .select(selectAllSimpleCounters) .pipe( take(1), - map((counters) => + map((counters: SimpleCounter[]) => counters.reduce( (acc, c) => ({ ...acc, [c.id]: c.countOnDay?.[today] ?? 0 }), {} as { [key: string]: number }, @@ -1170,19 +1172,21 @@ export class PluginBridgeService implements OnDestroy { * Gets a single simple counter value (undefined if unset). * @param key The counter key (e.g., 'daily-commits'). */ - async getCounter(key: string): Promise { - typia.assert(key); - if (!/^[A-Za-z0-9_-]+$/.test(key)) { + 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[key]; + return counters[id] ?? null; } - async setCounter(key: string, value: number): Promise { - typia.assert(key); + async setCounter(id: string, value: number): Promise { + typia.assert(id); typia.assert(value); - if (!/^[A-Za-z0-9_-]+$/.test(key)) { + // 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) { @@ -1193,36 +1197,40 @@ export class PluginBridgeService implements OnDestroy { this._store.dispatch( upsertSimpleCounter({ simpleCounter: { - id: key, - title: key, - isEnabled: true, - icon: null, - type: 'ClickCounter', + id: id, + //title: id, + //isEnabled: true, + //icon: null, + //type: 'ClickCounter', countOnDay: { [today]: value }, - isOn: false, + //isOn: false, } as SimpleCounter, }), ); } - async incrementCounter(key: string, amount = 1): Promise { - typia.assert(key); - typia.assert(amount); - if (typeof amount !== 'number' || !isFinite(amount) || amount <= 0) { + 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(key)) ?? 0; - await this.setCounter(key, current + amount); + const current = (await this.getCounter(id)) ?? 0; + const newValue = current + incrementBy; + await this.setCounter(id, newValue); + return newValue; } - async decrementCounter(key: string, amount = 1): Promise { - typia.assert(key); - typia.assert(amount); - if (typeof amount !== 'number' || !isFinite(amount) || amount <= 0) { + 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(key)) ?? 0; - await this.setCounter(key, Math.max(0, current - amount)); + const current = (await this.getCounter(id)) ?? 0; + const newValue = Math.max(0, current - decrementBy); + await this.setCounter(id, Math.max(0, newValue)); + return newValue; } /** @@ -1260,10 +1268,10 @@ export class PluginBridgeService implements OnDestroy { */ async toggleSimpleCounter(id: string): Promise { const counter = await this.getSimpleCounter(id); - if (counter) { - this._store.dispatch(toggleSimpleCounterCounter({ id })); + if (!counter) { + throw new Error(`Counter ${id} not found`); } - throw new Error(`Counter ${id} not found`); + this._store.dispatch(toggleSimpleCounterCounter({ id })); } /** @@ -1303,6 +1311,10 @@ export class PluginBridgeService implements OnDestroy { 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`);