From 8e30643bc8e2f931630540c7c2832b062f63ec4f Mon Sep 17 00:00:00 2001 From: Bella Lawrence Date: Sat, 19 Apr 2025 22:37:19 +1200 Subject: [PATCH 1/4] Add error alert preferences and related IPC handlers --- src/app/browser/preferences/index.tsx | 30 ++++++++++++++++++++++++++- src/app/i18n/locale/en-gb.ts | 6 ++++++ src/app/main/index.ts | 4 +++- src/app/main/ipc.ts | 4 ++++ src/app/main/monitor.ts | 3 +++ src/app/main/util.ts | 9 +++++++- src/app/preload/index.ts | 3 +++ 7 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/app/browser/preferences/index.tsx b/src/app/browser/preferences/index.tsx index 3777e31..b84e2be 100644 --- a/src/app/browser/preferences/index.tsx +++ b/src/app/browser/preferences/index.tsx @@ -30,6 +30,8 @@ function _Preferences(props: { const [login_item, ,, forceRefreshLoginItem] = useAsync(useCallback(() => ipc.getLoginItemSettings(), [ipc])); + const [show_error_alerts, , show_error_alerts_state, forceRefreshErrorAlerts] = useAsync(useCallback(() => ipc.getShowErrorAlerts(), [ipc])); + const setOpenAtLogin = useCallback(async (open_at_login: boolean | 'mixed') => { await ipc.setLoginItemSettings({...login_item!, startup_enabled: !!open_at_login}); forceRefreshLoginItem(); @@ -39,6 +41,11 @@ function _Preferences(props: { forceRefreshLoginItem(); }, [ipc, login_item]); + const setShowErrorAlerts = useCallback(async (show_error_alerts: boolean | 'mixed') => { + await ipc.setShowErrorAlerts(!!show_error_alerts); + forceRefreshErrorAlerts(); + }, [ipc]); + const [discord_users, discord_users_error, discord_users_state, forceRefreshDiscordUsers] = useAsync(useCallback(() => ipc.getDiscordUsers(), [ipc])); @@ -97,7 +104,8 @@ function _Preferences(props: { useEventListener(events, 'window:refresh', () => ( forceRefreshAccounts(), forceRefreshLoginItem(), - forceRefreshDiscordUsers(), forceRefreshDiscordOptions() + forceRefreshDiscordUsers(), forceRefreshDiscordOptions(), + forceRefreshErrorAlerts() ), []); if (!users || @@ -280,6 +288,26 @@ function _Preferences(props: { {t('splatnet3.discord_help_2')} + + + + {t('miscellaneous.heading')} + + + + + setShowErrorAlerts(!(show_error_alerts ?? true))}> + {t('miscellaneous.show_error_alerts')} + + + {t('miscellaneous.show_error_alerts_help')} + + ; } diff --git a/src/app/i18n/locale/en-gb.ts b/src/app/i18n/locale/en-gb.ts index 395e5b4..ed8b37c 100644 --- a/src/app/i18n/locale/en-gb.ts +++ b/src/app/i18n/locale/en-gb.ts @@ -290,6 +290,12 @@ export const preferences_window = { discord_help_1: 'Uses SplatNet 3 to retrieve additional presence information while playing Splatoon 3. You must be using a secondary Nintendo Account that is friends with your main account to fetch your presence, and the secondary account must be able to access SplatNet 3.', discord_help_2: 'When using a presence URL that returns Splatoon 3 data additional presence information will be shown regardless of this setting.', }, + + miscellaneous: { + heading: 'Miscellaneous', + show_error_alerts: 'Show error alerts', + show_error_alerts_help: 'This will show a popup when an error occurs while loading data. This is useful for debugging, but can be annoying if you are not interested in the errors.', + } }; export const friend_window = { diff --git a/src/app/main/index.ts b/src/app/main/index.ts index fc97795..13c7cf0 100644 --- a/src/app/main/index.ts +++ b/src/app/main/index.ts @@ -484,6 +484,7 @@ export class Store extends EventEmitter { error: err, buttons: ['OK', 'Retry'], defaultId: 1, + app: this.app, }); if (response === 1) { @@ -513,6 +514,7 @@ export class Store extends EventEmitter { error: err, buttons: ['OK', 'Retry'], defaultId: 1, + app: this.app, }); if (response === 1) { @@ -520,4 +522,4 @@ export class Store extends EventEmitter { } } } -} +} \ No newline at end of file diff --git a/src/app/main/ipc.ts b/src/app/main/ipc.ts index f5e0634..7b5a5be 100644 --- a/src/app/main/ipc.ts +++ b/src/app/main/ipc.ts @@ -64,6 +64,10 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) { handle('systemPreferences:getloginitem', () => appinstance.store.getLoginItem()); handle('systemPreferences:setloginitem', (e, settings: LoginItemOptions) => appinstance.store.setLoginItem(settings)); + handle('preferences:showerroralerts', () => storage.getItem('preferences.showerroralerts')); + handle('preferences:setshowerroralerts', (e, enabled: boolean) => + storage.setItem('preferences.showerroralerts', enabled)); + handle('update:get', () => appinstance.updater.cache ?? appinstance.updater.check()); handle('update:check', () => appinstance.updater.check()); diff --git a/src/app/main/monitor.ts b/src/app/main/monitor.ts index 1772c57..ff9323e 100644 --- a/src/app/main/monitor.ts +++ b/src/app/main/monitor.ts @@ -63,6 +63,7 @@ export class PresenceMonitorManager { error: err, buttons: ['OK', 'Retry', 'Stop'], defaultId: 0, + app: this.app, }); if (response === 1) { @@ -391,6 +392,7 @@ export class PresenceMonitorManager { error: err, buttons: ['OK', 'Retry'], defaultId: 0, + app: this.app, }); if (response === 1) { @@ -420,6 +422,7 @@ export class PresenceMonitorManager { await showErrorDialog({ message: error.name + ' updating presence monitor', error, + app: this.app, }); } } diff --git a/src/app/main/util.ts b/src/app/main/util.ts index c2187de..2061726 100644 --- a/src/app/main/util.ts +++ b/src/app/main/util.ts @@ -77,10 +77,17 @@ interface ErrorBoxOptions extends MessageBoxOptions { window?: BrowserWindow; } -export function showErrorDialog(options: ErrorBoxOptions) { +export async function showErrorDialog(options: ErrorBoxOptions) { const {error, app, window, ...message_box_options} = options; const detail = ErrorDescription.getErrorDescription(error); + if (app) { + const showErrorAlerts = await app.store.storage.getItem('preferences.showerroralerts') as boolean + if (showErrorAlerts === false) { + return Promise.resolve({ response: 0, checkboxChecked: false }); + } + } + message_box_options.detail = message_box_options.detail ? detail + '\n\n' + message_box_options.detail : detail; diff --git a/src/app/preload/index.ts b/src/app/preload/index.ts index ba3c841..360c034 100644 --- a/src/app/preload/index.ts +++ b/src/app/preload/index.ts @@ -47,6 +47,9 @@ const ipc = { getLoginItemSettings: () => inv('systemPreferences:getloginitem'), setLoginItemSettings: (settings: LoginItemOptions) => inv('systemPreferences:setloginitem', settings), + getShowErrorAlerts: () => inv('preferences:showerroralerts'), + setShowErrorAlerts: (enabled: boolean) => inv('preferences:setshowerroralerts', enabled), + getUpdateData: () => inv('update:get'), checkUpdates: () => inv('update:check'), From f59caa3d6fce82c556ff9ab9f48014b0cfc3fe0a Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 17 Aug 2025 20:52:04 +0100 Subject: [PATCH 2/4] Increase update interval after errors --- src/app/main/index.ts | 4 +--- src/app/main/ipc.ts | 5 ++--- src/app/main/monitor.ts | 5 +++-- src/app/main/util.ts | 9 +-------- src/app/preload/index.ts | 4 ++-- src/util/loop.ts | 17 +++++++++++++++++ 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/app/main/index.ts b/src/app/main/index.ts index cc0182a..b28ee84 100644 --- a/src/app/main/index.ts +++ b/src/app/main/index.ts @@ -586,7 +586,6 @@ export class Store extends EventEmitter { error: err, buttons: ['OK', 'Retry'], defaultId: 1, - app: this.app, }); if (response === 1) { @@ -616,7 +615,6 @@ export class Store extends EventEmitter { error: err, buttons: ['OK', 'Retry'], defaultId: 1, - app: this.app, }); if (response === 1) { @@ -624,4 +622,4 @@ export class Store extends EventEmitter { } } } -} \ No newline at end of file +} diff --git a/src/app/main/ipc.ts b/src/app/main/ipc.ts index 69267e5..9713b11 100644 --- a/src/app/main/ipc.ts +++ b/src/app/main/ipc.ts @@ -90,9 +90,8 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) { handle('systemPreferences:getloginitem', () => appinstance.store.getLoginItem()); handle('systemPreferences:setloginitem', (e, settings: LoginItemOptions) => appinstance.store.setLoginItem(settings)); - handle('preferences:showerroralerts', () => storage.getItem('preferences.showerroralerts')); - handle('preferences:setshowerroralerts', (e, enabled: boolean) => - storage.setItem('preferences.showerroralerts', enabled)); + handle('preferences:getshowerroralerts', () => storage.getItem('ShowErrorAlertsPreference').then(s => s ?? false)); + handle('preferences:setshowerroralerts', (e, show: boolean) => storage.setItem('ShowErrorAlertsPreference', show)); handle('update:get', () => appinstance.updater.cache ?? appinstance.updater.check()); handle('update:check', () => appinstance.updater.check()); diff --git a/src/app/main/monitor.ts b/src/app/main/monitor.ts index 63c1ef2..a81a683 100644 --- a/src/app/main/monitor.ts +++ b/src/app/main/monitor.ts @@ -397,12 +397,14 @@ export class PresenceMonitorManager { return LoopResult.OK; } + const show_error_alerts: boolean = await this.app.store.storage.getItem('ShowErrorAlertsPreference') ?? false; + if (!show_error_alerts) return LoopResult.DEFER_NEXT_UPDATE; + const {response} = await showErrorDialog({ message: err.name + ' updating presence monitor', error: err, buttons: ['OK', 'Retry'], defaultId: 0, - app: this.app, }); if (response === 1) { @@ -432,7 +434,6 @@ export class PresenceMonitorManager { await showErrorDialog({ message: error.name + ' updating presence monitor', error, - app: this.app, }); } } diff --git a/src/app/main/util.ts b/src/app/main/util.ts index 2061726..c2187de 100644 --- a/src/app/main/util.ts +++ b/src/app/main/util.ts @@ -77,17 +77,10 @@ interface ErrorBoxOptions extends MessageBoxOptions { window?: BrowserWindow; } -export async function showErrorDialog(options: ErrorBoxOptions) { +export function showErrorDialog(options: ErrorBoxOptions) { const {error, app, window, ...message_box_options} = options; const detail = ErrorDescription.getErrorDescription(error); - if (app) { - const showErrorAlerts = await app.store.storage.getItem('preferences.showerroralerts') as boolean - if (showErrorAlerts === false) { - return Promise.resolve({ response: 0, checkboxChecked: false }); - } - } - message_box_options.detail = message_box_options.detail ? detail + '\n\n' + message_box_options.detail : detail; diff --git a/src/app/preload/index.ts b/src/app/preload/index.ts index f645830..472332b 100644 --- a/src/app/preload/index.ts +++ b/src/app/preload/index.ts @@ -50,8 +50,8 @@ const ipc = { getLoginItemSettings: () => inv('systemPreferences:getloginitem'), setLoginItemSettings: (settings: LoginItemOptions) => inv('systemPreferences:setloginitem', settings), - getShowErrorAlerts: () => inv('preferences:showerroralerts'), - setShowErrorAlerts: (enabled: boolean) => inv('preferences:setshowerroralerts', enabled), + getShowErrorAlerts: () => inv('preferences:getshowerroralerts'), + setShowErrorAlerts: (show: boolean) => inv('preferences:setshowerroralerts', show), getUpdateData: () => inv('update:get'), checkUpdates: () => inv('update:check'), diff --git a/src/util/loop.ts b/src/util/loop.ts index 6084488..dd0a110 100644 --- a/src/util/loop.ts +++ b/src/util/loop.ts @@ -4,6 +4,7 @@ const debug = createDebug('nxapi:util:loop'); export default abstract class Loop { update_interval = 60; + errors = 0; init(): void | Promise {} @@ -13,8 +14,11 @@ export default abstract class Loop { try { const result = init ? await this.init() : await this.update(); + this.errors = 0; + return result ?? (init ? LoopResult.OK_SKIP_INTERVAL : LoopResult.OK); } catch (err) { + this.errors++; return this.handleError(err as any); } } @@ -23,6 +27,10 @@ export default abstract class Loop { throw err; } + get next_update_interval() { + return this.update_interval * Math.min(this.errors / 2, 20); + } + private is_loop_active = 0; async loop(init = false) { @@ -38,6 +46,13 @@ export default abstract class Loop { await new Promise(rs => setTimeout(this.timeout_resolve = rs, this.update_interval * 1000)); } } + if (result === LoopResult.DEFER_NEXT_UPDATE) { + if (this.skip_interval_once) { + this.skip_interval_once = false; + } else { + await new Promise(rs => setTimeout(this.timeout_resolve = rs, this.next_update_interval * 1000)); + } + } if (result === LoopResult.STOP) { return LoopResult.STOP; } @@ -62,11 +77,13 @@ export default abstract class Loop { const LoopRunOk = Symbol('LoopRunOk'); const LoopRunOkSkipInterval = Symbol('LoopRunOkSkipInterval'); +const LoopRunIncrementInterval = Symbol('LoopRunIncrementInterval'); const LoopRunStop = Symbol('LoopRunStopNow'); export enum LoopResult { OK = LoopRunOk as any, OK_SKIP_INTERVAL = LoopRunOkSkipInterval as any, + DEFER_NEXT_UPDATE = LoopRunIncrementInterval as any, STOP = LoopRunStop as any, } From 05b254e69c74d0b9be29ded15294ed49f874653c Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 17 Aug 2025 20:55:26 +0100 Subject: [PATCH 3/4] Update help text --- src/app/i18n/locale/en-gb.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/i18n/locale/en-gb.ts b/src/app/i18n/locale/en-gb.ts index 808139a..6fbef42 100644 --- a/src/app/i18n/locale/en-gb.ts +++ b/src/app/i18n/locale/en-gb.ts @@ -296,8 +296,8 @@ export const preferences_window = { miscellaneous: { heading: 'Miscellaneous', show_error_alerts: 'Show error alerts', - show_error_alerts_help: 'This will show a popup when an error occurs while loading data. This is useful for debugging, but can be annoying if you are not interested in the errors.', - } + show_error_alerts_help: 'Shows an alert when an error occurs while updating presence. If this is disabled, nxapi will delay updates after errors.', + }, }; export const friend_window = { From 7a2fe3d35c61d7ca276ed689bb497f274047f3d4 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 17 Aug 2025 21:41:11 +0100 Subject: [PATCH 4/4] Use show error alerts preference for SplatNet 3 presence monitoring --- src/app/main/monitor.ts | 4 +++- src/discord/monitor/splatoon3.ts | 2 ++ src/discord/types.ts | 1 + src/util/loop.ts | 16 +++++++++------- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/app/main/monitor.ts b/src/app/main/monitor.ts index a81a683..5b47712 100644 --- a/src/app/main/monitor.ts +++ b/src/app/main/monitor.ts @@ -67,9 +67,11 @@ export class PresenceMonitorManager { error: err, buttons: ['OK', 'Retry', 'Stop'], defaultId: 0, - app: this.app, }); + const show_error_alerts: boolean = await this.app.store.storage.getItem('ShowErrorAlertsPreference') ?? false; + if (!show_error_alerts) return ErrorResult.DEFER; + if (response === 1) { return ErrorResult.RETRY; } diff --git a/src/discord/monitor/splatoon3.ts b/src/discord/monitor/splatoon3.ts index e80d3bd..83d1261 100644 --- a/src/discord/monitor/splatoon3.ts +++ b/src/discord/monitor/splatoon3.ts @@ -91,6 +91,7 @@ export default class SplatNet3Monitor extends EmbeddedLoop { debug('Error authenticating to SplatNet 3', err); const result = await this.discord_presence.handleError(err as Error); if (result === ErrorResult.RETRY) return this.init(); + if (result === ErrorResult.DEFER) return this.errors++, LoopResult.DEFER_NEXT_UPDATE; if (result === ErrorResult.STOP) return LoopResult.STOP; } @@ -168,6 +169,7 @@ export default class SplatNet3Monitor extends EmbeddedLoop { async handleError(err: Error) { const result = await this.discord_presence.handleError(err as Error); if (result === ErrorResult.RETRY) return LoopResult.OK_SKIP_INTERVAL; + if (result === ErrorResult.DEFER) return LoopResult.DEFER_NEXT_UPDATE; this.friend = null; this.discord_presence.refreshPresence(); diff --git a/src/discord/types.ts b/src/discord/types.ts index 787460b..86f8a1d 100644 --- a/src/discord/types.ts +++ b/src/discord/types.ts @@ -148,5 +148,6 @@ export interface ExternalMonitor extends EmbeddedLoop { export enum ErrorResult { STOP, RETRY, + DEFER, IGNORE, } diff --git a/src/util/loop.ts b/src/util/loop.ts index dd0a110..0d4cdfd 100644 --- a/src/util/loop.ts +++ b/src/util/loop.ts @@ -52,6 +52,7 @@ export default abstract class Loop { } else { await new Promise(rs => setTimeout(this.timeout_resolve = rs, this.next_update_interval * 1000)); } + return LoopResult.DEFER_NEXT_UPDATE; } if (result === LoopResult.STOP) { return LoopResult.STOP; @@ -109,25 +110,26 @@ export abstract class EmbeddedLoop extends Loop { private async _run() { this._running++; const i = this._running; + let init = true; try { - const result = await this.loop(true); - if (result === LoopResult.STOP) return; - while (i === this._running) { - const result = await this.loop(); + const result = await this.loop(init); + + if (init && result !== LoopResult.DEFER_NEXT_UPDATE) { + init = false; + } if (result === LoopResult.STOP) { - await this.onStop?.(); + if (!init) await this.onStop?.(); return; } } - if (this._running === 0 && !this.onStop) { + if (this._running === 0 && !init && !this.onStop) { // Run one more time after the loop ends const result = await this.loopRun(); } - } finally { this._running = 0; }