summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backend/locales/en-US.json5
-rw-r--r--frontend/src/components/DeckyState.tsx9
-rw-r--r--frontend/src/components/settings/pages/general/NotificationSettings.tsx35
-rw-r--r--frontend/src/components/settings/pages/general/index.tsx5
-rw-r--r--frontend/src/notification-service.tsx51
-rw-r--r--frontend/src/plugin-loader.tsx30
6 files changed, 123 insertions, 12 deletions
diff --git a/backend/locales/en-US.json b/backend/locales/en-US.json
index 1561b006..3e24887c 100644
--- a/backend/locales/en-US.json
+++ b/backend/locales/en-US.json
@@ -186,6 +186,11 @@
},
"updates": {
"header": "Updates"
+ },
+ "notifications": {
+ "header": "Notifications",
+ "decky_updates_label": "Decky update available",
+ "plugin_updates_label": "Plugin updates available"
}
},
"SettingsIndex": {
diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx
index 920985b3..d20c8d86 100644
--- a/frontend/src/components/DeckyState.tsx
+++ b/frontend/src/components/DeckyState.tsx
@@ -1,5 +1,6 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
+import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store';
import { VerInfo } from '../updater';
@@ -13,6 +14,7 @@ interface PublicDeckyState {
hasLoaderUpdate?: boolean;
isLoaderUpdating: boolean;
versionInfo: VerInfo | null;
+ notificationSettings: NotificationSettings;
userInfo: UserInfo | null;
}
@@ -30,6 +32,7 @@ export class DeckyState {
private _hasLoaderUpdate: boolean = false;
private _isLoaderUpdating: boolean = false;
private _versionInfo: VerInfo | null = null;
+ private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS;
private _userInfo: UserInfo | null = null;
public eventBus = new EventTarget();
@@ -44,6 +47,7 @@ export class DeckyState {
hasLoaderUpdate: this._hasLoaderUpdate,
isLoaderUpdating: this._isLoaderUpdating,
versionInfo: this._versionInfo,
+ notificationSettings: this._notificationSettings,
userInfo: this._userInfo,
};
}
@@ -93,6 +97,11 @@ export class DeckyState {
this.notifyUpdate();
}
+ setNotificationSettings(notificationSettings: NotificationSettings) {
+ this._notificationSettings = notificationSettings;
+ this.notifyUpdate();
+ }
+
setUserInfo(userInfo: UserInfo) {
this._userInfo = userInfo;
this.notifyUpdate();
diff --git a/frontend/src/components/settings/pages/general/NotificationSettings.tsx b/frontend/src/components/settings/pages/general/NotificationSettings.tsx
new file mode 100644
index 00000000..21c2fd82
--- /dev/null
+++ b/frontend/src/components/settings/pages/general/NotificationSettings.tsx
@@ -0,0 +1,35 @@
+import { Field, Toggle } from 'decky-frontend-lib';
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { useDeckyState } from '../../../DeckyState';
+
+const NotificationSettings: FC = () => {
+ const { notificationSettings } = useDeckyState();
+ const notificationService = window.DeckyPluginLoader.notificationService;
+
+ const { t } = useTranslation();
+
+ return (
+ <>
+ <Field label={t('SettingsGeneralIndex.notifications.decky_updates_label')}>
+ <Toggle
+ value={notificationSettings.deckyUpdates}
+ onChange={(deckyUpdates) => {
+ notificationService.update({ ...notificationSettings, deckyUpdates });
+ }}
+ />
+ </Field>
+ <Field label={t('SettingsGeneralIndex.notifications.plugin_updates_label')}>
+ <Toggle
+ value={notificationSettings.pluginUpdates}
+ onChange={(pluginUpdates) => {
+ notificationService.update({ ...notificationSettings, pluginUpdates });
+ }}
+ />
+ </Field>
+ </>
+ );
+};
+
+export default NotificationSettings;
diff --git a/frontend/src/components/settings/pages/general/index.tsx b/frontend/src/components/settings/pages/general/index.tsx
index 96ae6782..6fc62a46 100644
--- a/frontend/src/components/settings/pages/general/index.tsx
+++ b/frontend/src/components/settings/pages/general/index.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useDeckyState } from '../../../DeckyState';
import BranchSelect from './BranchSelect';
+import NotificationSettings from './NotificationSettings';
import StoreSelect from './StoreSelect';
import UpdaterSettings from './Updater';
@@ -28,6 +29,10 @@ export default function GeneralSettings({
<StoreSelect />
</DialogControlsSection>
<DialogControlsSection>
+ <DialogControlsSectionHeader>{t('SettingsGeneralIndex.notifications.header')}</DialogControlsSectionHeader>
+ <NotificationSettings />
+ </DialogControlsSection>
+ <DialogControlsSection>
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
<Field label={t('SettingsGeneralIndex.developer_mode.label')}>
<Toggle
diff --git a/frontend/src/notification-service.tsx b/frontend/src/notification-service.tsx
new file mode 100644
index 00000000..188cebd3
--- /dev/null
+++ b/frontend/src/notification-service.tsx
@@ -0,0 +1,51 @@
+import { DeckyState } from './components/DeckyState';
+import { getSetting, setSetting } from './utils/settings';
+
+export interface NotificationSettings {
+ deckyUpdates: boolean;
+ pluginUpdates: boolean;
+}
+
+export const DEFAULT_NOTIFICATION_SETTINGS: NotificationSettings = {
+ deckyUpdates: true,
+ pluginUpdates: true,
+};
+
+/**
+ * A Service class for managing the notification settings
+ *
+ * It's mostly responsible for sending setting updates to the server and keeping the local state in sync.
+ */
+export class NotificationService {
+ constructor(private deckyState: DeckyState) {}
+
+ async init() {
+ const notificationSettings = await getSetting<Partial<NotificationSettings>>('notificationSettings', {});
+
+ // Adding a fallback to the default settings to be backwards compatible if we ever add new notification settings
+ this.deckyState.setNotificationSettings({
+ ...DEFAULT_NOTIFICATION_SETTINGS,
+ ...notificationSettings,
+ });
+ }
+
+ /**
+ * Sends the new notification settings to the server and persists it locally in the decky state
+ *
+ * @param notificationSettings The new notification settings
+ */
+ async update(notificationSettings: NotificationSettings) {
+ await setSetting('notificationSettings', notificationSettings);
+ this.deckyState.setNotificationSettings(notificationSettings);
+ }
+
+ /**
+ * For a specific event, returns true if a notification should be shown
+ *
+ * @param event The notification event that should be checked
+ * @returns true if the notification should be shown
+ */
+ shouldNotify(event: keyof NotificationSettings) {
+ return this.deckyState.publicState().notificationSettings[event];
+ }
+}
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index e2ffc44a..b27a19bb 100644
--- a/frontend/src/plugin-loader.tsx
+++ b/frontend/src/plugin-loader.tsx
@@ -24,6 +24,7 @@ import PluginView from './components/PluginView';
import WithSuspense from './components/WithSuspense';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
+import { NotificationService } from './notification-service';
import { InstallType, Plugin } from './plugin';
import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
@@ -47,7 +48,9 @@ class PluginLoader extends Logger {
private routerHook: RouterHook = new RouterHook();
public toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState();
+
public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
+ public notificationService = new NotificationService(this.deckyState);
private reloadLock: boolean = false;
// stores a list of plugin names which requested to be reloaded
@@ -121,18 +124,20 @@ class PluginLoader extends Logger {
public async notifyUpdates() {
const versionInfo = await this.updateVersion();
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
- this.toaster.toast({
- title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
- body: (
- <TranslationHelper
- trans_class={TranslationClass.PLUGIN_LOADER}
- trans_text="decky_update_available"
- i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
- />
- ),
- onClick: () => Router.Navigate('/decky/settings'),
- });
this.deckyState.setHasLoaderUpdate(true);
+ if (this.notificationService.shouldNotify('deckyUpdates')) {
+ this.toaster.toast({
+ title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
+ body: (
+ <TranslationHelper
+ trans_class={TranslationClass.PLUGIN_LOADER}
+ trans_text="decky_update_available"
+ i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
+ />
+ ),
+ onClick: () => Router.Navigate('/decky/settings'),
+ });
+ }
}
await sleep(7000);
await this.notifyPluginUpdates();
@@ -146,7 +151,7 @@ class PluginLoader extends Logger {
public async notifyPluginUpdates() {
const updates = await this.checkPluginUpdates();
- if (updates?.size > 0) {
+ if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) {
this.toaster.toast({
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
body: (
@@ -220,6 +225,7 @@ class PluginLoader extends Logger {
});
this.hiddenPluginsService.init();
+ this.notificationService.init();
}
public deinit() {