From 131f0961ff451ec47376483178e092c8d7403b27 Mon Sep 17 00:00:00 2001
From: AAGaming <aagaming@riseup.net>
Date: Mon, 5 Aug 2024 14:07:10 -0400
Subject: Rewrite router/tabs/toaster hooks (#661)

---
 frontend/src/components/Toast.tsx | 102 +++++++++++++++++++++++++++++---------
 1 file changed, 79 insertions(+), 23 deletions(-)

(limited to 'frontend/src/components/Toast.tsx')

diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx
index 79e3d864..e86e9337 100644
--- a/frontend/src/components/Toast.tsx
+++ b/frontend/src/components/Toast.tsx
@@ -1,37 +1,38 @@
 import type { ToastData } from '@decky/api';
-import { findModule, joinClassNames } from '@decky/ui';
-import { FunctionComponent } from 'react';
+import { Focusable, Navigation, findClassModule, joinClassNames } from '@decky/ui';
+import { FC, memo } from 'react';
 
-interface ToastProps {
-  toast: ToastData;
-}
+import Logger from '../logger';
 
-export const toastClasses = findModule((mod) => {
-  if (typeof mod !== 'object') return false;
+const logger = new Logger('ToastRenderer');
 
-  if (mod.ToastPlaceholder) {
-    return true;
-  }
+// TODO there are more of these
+export enum ToastLocation {
+  /** Big Picture popup toasts */
+  GAMEPADUI_POPUP = 1,
+  /** QAM Notifications tab */
+  GAMEPADUI_QAM = 3,
+}
 
-  return false;
-});
+interface ToastProps {
+  toast: ToastData;
+  newIndicator?: boolean;
+}
 
-const templateClasses = findModule((mod) => {
-  if (typeof mod !== 'object') return false;
+interface ToastRendererProps extends ToastProps {
+  location: ToastLocation;
+}
 
-  if (mod.ShortTemplate) {
-    return true;
-  }
+const templateClasses = findClassModule((m) => m.ShortTemplate) || {};
 
-  return false;
-});
+// These are memoized as they like to randomly rerender
 
-const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
+const GamepadUIPopupToast: FC<Omit<ToastProps, 'newIndicator'>> = memo(({ toast }) => {
   return (
     <div
       style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties}
       onClick={toast.onClick}
-      className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')}
+      className={joinClassNames(templateClasses.ShortTemplate, toast.className || '', 'DeckyGamepadUIPopupToast')}
     >
       {toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>}
       <div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}>
@@ -43,6 +44,61 @@ const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
       </div>
     </div>
   );
-};
+});
+
+const GamepadUIQAMToast: FC<ToastProps> = memo(({ toast, newIndicator }) => {
+  // The fields aren't mismatched, the logic for these is just a bit weird.
+  return (
+    <Focusable
+      onActivate={() => {
+        toast.onClick?.();
+        Navigation.CloseSideMenus();
+      }}
+      className={joinClassNames(
+        templateClasses.StandardTemplateContainer,
+        toast.className || '',
+        'DeckyGamepadUIQAMToast',
+      )}
+    >
+      <div className={templateClasses.StandardTemplate}>
+        {toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>}
+        <div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}>
+          <div className={templateClasses.Header}>
+            {toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>}
+            {toast.title && <div className={templateClasses.Title}>{toast.title}</div>}
+            {/* timestamp should always be defined by toaster */}
+            {/* TODO check how valve does this */}
+            {toast.timestamp && (
+              <div className={templateClasses.Timestamp}>
+                {toast.timestamp.toLocaleTimeString(undefined, { timeStyle: 'short' })}
+              </div>
+            )}
+          </div>
+          {toast.body && <div className={templateClasses.StandardNotificationDescription}>{toast.body}</div>}
+          {toast.subtext && <div className={templateClasses.StandardNotificationSubText}>{toast.subtext}</div>}
+        </div>
+        {newIndicator && (
+          <div className={templateClasses.NewIndicator}>
+            <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none">
+              <circle fill="currentColor" cx="25" cy="25" r="25"></circle>
+            </svg>
+          </div>
+        )}
+      </div>
+    </Focusable>
+  );
+});
+
+export const ToastRenderer: FC<ToastRendererProps> = memo(({ toast, location, newIndicator }) => {
+  switch (location) {
+    default:
+      logger.warn(`Toast UI not implemented for location ${location}! Falling back to GamepadUIQAMToast.`);
+      return <GamepadUIQAMToast toast={toast} newIndicator={false} />;
+    case ToastLocation.GAMEPADUI_POPUP:
+      return <GamepadUIPopupToast toast={toast} />;
+    case ToastLocation.GAMEPADUI_QAM:
+      return <GamepadUIQAMToast toast={toast} newIndicator={newIndicator} />;
+  }
+});
 
-export default Toast;
+export default ToastRenderer;
-- 
cgit v1.2.3

