Skip to content
This repository has been archived by the owner on Jun 14, 2023. It is now read-only.

Commit

Permalink
Add support for app properties plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
coolavery committed Jul 10, 2022
1 parent 2e00554 commit 7d0e6ef
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 43 deletions.
145 changes: 145 additions & 0 deletions injected/src/app-properties-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { GamepadHandler } from './gamepad';
import { BTN_CODE } from './gamepad/buttons';
import { DECK_SELECTORS } from './selectors';
import { SMM } from './smm';
import { AppPropsApp } from './types/global';

type AppPropertiesMenuRender = (
smm: SMM,
root: HTMLDivElement,
app: AppPropsApp
) => void | Promise<void>;

interface AppPropertiesMenuItem {
id: string;
title: string;
render?: AppPropertiesMenuRender;
// TODO: make this a filtering function instead
appIds?: number[];
}

export class AppPropertiesMenu {
private readonly smm: SMM;
private appPropsMenuOpen: boolean;
private gamepad?: GamepadHandler;
items: AppPropertiesMenuItem[];

constructor(smm: SMM) {
this.smm = smm;
this.appPropsMenuOpen = false;
this.items = [];

window.csGetAppPropsMenuItems = this.getMenuItems.bind(this);

this.smm.addEventListener(
'switchToAppProperties',
(e: CustomEventInit<AppPropsApp>) => {
if (typeof e.detail === 'undefined') {
return;
}

const app = e.detail;

// Make sure this event only happens once after app props is opened
if (this.appPropsMenuOpen) {
return;
}

const appProps = document.querySelector<HTMLDivElement>(
DECK_SELECTORS.appProperties
);
if (!appProps) {
return;
}

this.appPropsMenuOpen = true;

let curPluginPage: string | undefined = undefined;

const observer = new MutationObserver(() => {
if (!document.querySelector(DECK_SELECTORS.appProperties)) {
this.appPropsMenuOpen = false;
observer.disconnect();
}

let found = false;

for (const item of this.getMenuItems({ appid: app.appid })) {
// Check if this plugin's page is visible and we can render into it
const root = document.querySelector<HTMLDivElement>(
`[data-cs-plugin-id="${item.id}"]`
);
if (root) {
// Exit if plugin is already rendered
if (item.id === curPluginPage) {
return;
}

curPluginPage = item.id;
found = true;

// Clear before rendering
root.innerHTML = '';
item.render?.(
this.smm,
root,
JSON.parse(root.dataset!.csPluginData!) as AppPropsApp
);

if (window.smmUIMode === 'deck') {
this.smm.ButtonInterceptors.addInterceptor({
id: `csAppPropertiesMenu`,
handler: (buttonCode) => {
if (
!this.gamepad &&
(buttonCode === BTN_CODE.A ||
buttonCode === BTN_CODE.RIGHT)
) {
this.gamepad = new GamepadHandler({
smm: this.smm,
root,
rootExitCallback: () => {
this.gamepad?.cleanup();
this.gamepad = undefined;
},
});
}
},
});
}

break;
}
}

if (!found) {
curPluginPage = undefined;
this.gamepad?.cleanup();
this.gamepad = undefined;
this.smm.ButtonInterceptors.removeInterceptor(
`csAppPropertiesMenu`
);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
);
}

addMenuItem(item: AppPropertiesMenuItem) {
this.items.push(item);
}

removeMenuItem(id: string) {
this.items = this.items.filter((item) => item.id !== id);
}

private getMenuItems(app: Pick<AppPropsApp, 'appid'>) {
return this.items.filter((item) =>
item.appIds ? item.appIds.includes(app.appid) : true
);
}
}
9 changes: 5 additions & 4 deletions injected/src/entrypoints/app-properties.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SHARED_SELECTORS } from '../selectors';
import { SMM } from '../smm';
import { AppPropsApp } from '../types/global';
import { info, waitForElement } from '../util';

const main = async () => {
Expand All @@ -19,10 +20,10 @@ const main = async () => {

await smm.loadPlugins();

const appPropertiesAppId: number | undefined = (window as any)
.appPropertiesAppId;
if (appPropertiesAppId) {
smm.switchToAppProperties(appPropertiesAppId);
const appPropertiesApp: AppPropsApp | undefined = (window as any)
.appPropertiesApp;
if (appPropertiesApp) {
smm.switchToAppProperties(appPropertiesApp);
} else {
console.error('App ID for app properties context not found.');
}
Expand Down
1 change: 1 addition & 0 deletions injected/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const DECK_SELECTORS = {
quickAccessContainer: '[class*=quickaccessmenu_Container_]',
topLevelTransitionSwitch:
'[class^=topleveltransitionswitch_TopLevelTransitionSwitch_]',
appProperties: '[class*=appproperties_AppProperties]',
};

const createModeSelectors = <
Expand Down
49 changes: 44 additions & 5 deletions injected/src/services/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,52 @@ interface AppDetails {
_fullDetails: Record<string, any>;
}

enum ipcNames {
getCachedDetailsForApp = 'csAppsGetCachedDetailsForApp',
getCachedDetailsForAppRes = 'csAppsGetCachedDetailsForAppRes',
}

export class Apps extends Service {
constructor(...args: ConstructorParameters<typeof Service>) {
super(...args);

if (this.smm.entry === 'library') {
this.smm.IPC.on<{ appId: number }>(
ipcNames.getCachedDetailsForApp,
async ({ data: { appId } }) => {
this.smm.IPC.send<AppDetails | undefined>(
ipcNames.getCachedDetailsForAppRes,
await this.getCachedDetailsForApp(appId)
);
}
);
}
}

/*
Get app details for an app ID from Steam's cache.
TODO: get info for uncached app
TODO: support this from other contexts (IPC probably)
*/
getCachedDetailsForApp(appId: number): AppDetails | undefined {
Get app details for an app ID from Steam's cache.
TODO: get info for uncached app
TODO: support this from other contexts (IPC probably)
*/
async getCachedDetailsForApp(appId: number): Promise<AppDetails | undefined> {
if (this.smm.entry !== 'library') {
this.smm.IPC.send<{ appId: number }>(ipcNames.getCachedDetailsForApp, {
appId,
});
const appDetails = await new Promise<AppDetails | undefined>(
(resolve) => {
this.smm.IPC.on<AppDetails | undefined>(
ipcNames.getCachedDetailsForAppRes,
({ data }) => {
resolve(data);
this.smm.IPC.off(ipcNames.getCachedDetailsForAppRes);
}
);
}
);
return appDetails;
}

const appData =
window.appDetailsStore.m_mapAppData._data.get(appId)?.value.details;
if (!appData) {
Expand Down
7 changes: 4 additions & 3 deletions injected/src/services/inject.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { rpcRequest } from '../rpc';
import { AppPropsApp } from '../types/global';
import { Service } from './service';

export class Inject extends Service {
async injectAppProperties(appId: number) {
async injectAppProperties(app: AppPropsApp) {
const { getRes } = rpcRequest<
{
appId: number;
app: string;
},
{}
>('InjectService.InjectAppProperties', { appId });
>('InjectService.InjectAppProperties', { app: JSON.stringify(app) });
return getRes();
}
}
40 changes: 23 additions & 17 deletions injected/src/smm.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AppPropertiesMenu } from './app-properties-menu';
import { GamepadHandler } from './gamepad';
import { ButtonInterceptors } from './gamepad/button-interceptors';
import { InGameMenu } from './in-game-menu';
Expand All @@ -12,6 +13,7 @@ import { Plugins } from './services/plugins';
import { Store } from './services/store';
import { Toast } from './services/toast';
import { UI } from './services/ui';
import { AppPropsApp } from './types/global';
import { info } from './util';

type PluginId = string;
Expand Down Expand Up @@ -52,9 +54,8 @@ class EventSwitchToAppDetails extends CustomEvent<eventDetailsSwitchToAppDetails
}
}

type eventDetailsSwitchToAppProperties = { appId: number };
class EventSwitchToAppProperties extends CustomEvent<eventDetailsSwitchToAppProperties> {
constructor(detail: eventDetailsSwitchToAppProperties) {
class EventSwitchToAppProperties extends CustomEvent<AppPropsApp> {
constructor(detail: AppPropsApp) {
super('switchToAppProperties', { detail });
}
}
Expand Down Expand Up @@ -86,6 +87,7 @@ export class SMM extends EventTarget {
// TODO: improve types for running in context without menu
readonly MenuManager!: MenuManager;
readonly InGameMenu!: InGameMenu;
readonly AppPropertiesMenu!: AppPropertiesMenu;
readonly FS: FS;
readonly Plugins: Plugins;
readonly IPC: IPC;
Expand All @@ -94,8 +96,7 @@ export class SMM extends EventTarget {
readonly Inject: Inject;
readonly Store: Store;
readonly ButtonInterceptors: ButtonInterceptors;
// TODO: related to types for specific context above, use inherited classes
readonly Apps?: Apps;
readonly Apps: Apps;

readonly serverPort: string;

Expand Down Expand Up @@ -134,16 +135,23 @@ export class SMM extends EventTarget {
this.Inject = new Inject(this);
this.Store = new Store(this);
this.ButtonInterceptors = new ButtonInterceptors(this);
this.Apps = new Apps(this);

if (entry === 'library') {
this.MenuManager = new MenuManager(this);
this.Apps = new Apps(this);
}

if (entry === 'library' || entry === 'quickAccess') {
this.InGameMenu = new InGameMenu(this);
}

if (
entry === 'library' ||
(window.smmUIMode === 'desktop' && entry === 'appProperties')
) {
this.AppPropertiesMenu = new AppPropertiesMenu(this);
}

this.serverPort = window.smmServerPort;

this.attachedEvents = {};
Expand Down Expand Up @@ -206,8 +214,8 @@ export class SMM extends EventTarget {
this.dispatchEvent(new EventSwitchToAppDetails({ appId, appName }));
}

switchToAppProperties(appId: number) {
info('Switched to app properties for app', appId);
switchToAppProperties(app: AppPropsApp) {
info('Switched to app properties for app', app.appid);

/*
In desktop mode, the app properties dialog is in a different context, so
Expand All @@ -218,11 +226,11 @@ export class SMM extends EventTarget {
*/

if (window.smmUIMode === 'desktop' && this.entry !== 'appProperties') {
this.Inject.injectAppProperties(appId);
this.Inject.injectAppProperties(app);
return;
}

this.dispatchEvent(new EventSwitchToAppProperties({ appId }));
this.dispatchEvent(new EventSwitchToAppProperties(app));
}

lockScreenOpened() {
Expand Down Expand Up @@ -311,16 +319,14 @@ export class SMM extends EventTarget {
callback: EventListenerOrEventListenerObject | null,
options?: boolean | AddEventListenerOptions
): void {
if (!this.currentPlugin) {
console.error('[SMM] addEventListener missing this.currentPlugin!');
return;
}
if (this.currentPlugin) {
if (!this.attachedEvents[this.currentPlugin]) {
this.attachedEvents[this.currentPlugin] = [];
}

if (!this.attachedEvents[this.currentPlugin]) {
this.attachedEvents[this.currentPlugin] = [];
this.attachedEvents[this.currentPlugin].push({ type, callback, options });
}

this.attachedEvents[this.currentPlugin].push({ type, callback, options });
super.addEventListener(type, callback, options);
}

Expand Down
4 changes: 2 additions & 2 deletions injected/src/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export interface AppPropsApp {
display_status: number;
installed: boolean;
is_available_on_current_platform: boolean;
status_percentage: number
},
status_percentage: number;
};
size_on_disk: string;
sort_as: string;
steam_deck_compat_category: number;
Expand Down
Loading

0 comments on commit 7d0e6ef

Please sign in to comment.