Jarasłaŭ Viktorčyk: 3 Integrate plugins into gamepadui Restyle plugins page to match Steam Deck Add types for events and callbacks 13 files changed, 345 insertions(+), 163 deletions(-)
crankshaft/patches: FAILED in 1m36s [Integrate plugins into gamepadui][0] v2 from [Jarasłaŭ Viktorčyk][1] [0]: https://lists.sr.ht/~avery/public-inbox/patches/35075 [1]: mailto:ugzuzg@gmail.com ✗ #836525 FAILED crankshaft/patches/mirror.yml https://builds.sr.ht/~avery/job/836525 ✓ #836524 SUCCESS crankshaft/patches/linux.yml https://builds.sr.ht/~avery/job/836524
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~avery/public-inbox/patches/35075/mbox | git am -3Learn more about email & git
--- .../menu-manager/deck/menu-injector-deck.css | 13 +--- .../menu-manager/deck/menu-injector-deck.tsx | 70 +++++-------------- injected/src/smm.ts | 18 +++++ injected/src/tab-observer.ts | 5 ++ injected/src/types/global.d.ts | 2 + 5 files changed, 46 insertions(+), 62 deletions(-) diff --git a/injected/src/menu-manager/deck/menu-injector-deck.css b/injected/src/menu-manager/deck/menu-injector-deck.css index 4f78a23..2621f37 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.css +++ b/injected/src/menu-manager/deck/menu-injector-deck.css @@ -1,21 +1,12 @@ [data-smm-menu-page-container] { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: auto; - z-index: 999; - width: 100%; - height: calc(100% - 40px); + height: 100%; + padding-top: calc(0px + var(--basicui-header-height)); display: flex; overflow: auto; background-color: #23262e; - opacity: 0; - - pointer-events: none; } [data-smm-menu-page-container] > ul:first-child { diff --git a/injected/src/menu-manager/deck/menu-injector-deck.tsx b/injected/src/menu-manager/deck/menu-injector-deck.tsx index 67b7497..f6949a9 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.tsx +++ b/injected/src/menu-manager/deck/menu-injector-deck.tsx @@ -34,6 +34,7 @@ export class MenuInjectorDeck implements MenuInjector { this.createPageContainer(); this.addPluginsMenuItem(); this.listenToClickEvents(); + this.listenToNavigationChanges(); } private injectMenuStyles() { @@ -56,21 +57,24 @@ export class MenuInjectorDeck implements MenuInjector { {this.menuPage} </div> ); - - document - .querySelector<HTMLDivElement>(DECK_SELECTORS.mainNavMenu) - ?.appendChild(this.pageContainer); } private listenToClickEvents() { this.smm.IPC.on<{ id: string }>( 'csMenuItemClicked', async ({ data: { id: _id } }) => { - this.openPluginsPage(); + window.coolClass.Navigate('/blank/cs-plugins'); + window.coolClass.CloseSideMenus(); } ); } + private listenToNavigationChanges() { + this.smm.addEventListener('switchToPlugins', () => { + this.openPluginsPage(); + }); + } + private addPluginsMenuItem() { window.csMenuItems = [ { @@ -82,23 +86,11 @@ export class MenuInjectorDeck implements MenuInjector { } private openPluginsPage() { - window.csMenuActiveItem = 'plugins'; - - // Close menu - window.coolClass.OpenSideMenu(); + if (this.pageContainer.isConnected) return; - // Make sure we're on a page where we can show the plugin page - // (we'll navigate back when the page is closed) - if ( - document.querySelector(DECK_SELECTORS.topLevelTransitionSwitch)?.children - ?.length === 0 - ) { - window.coolClass.NavigateToLibraryTab(); - this.enteredWithNavigate = true; - } + window.csMenuActiveItem = 'plugins'; this.showPageContainer(); - this.menuListGamepad = new GamepadHandler({ smm: this.smm, root: this.menuList, @@ -110,44 +102,20 @@ export class MenuInjectorDeck implements MenuInjector { }); } - private async closePluginsPage() { - // Fade out the plugin page before removing it - const animation = await this.pageContainer.animate([{ opacity: 0 }], { - duration: 300, - fill: 'forwards', - }).finished; - this.hidePageContainer(); - animation.cancel(); - + private async closePluginsPage(forward = false) { // Clear active menu item window.csMenuActiveItem = undefined; window.csMenuUpdate?.(); - if (this.enteredWithNavigate) { - window.coolClass.NavigateBackOrOpenMenu(); - } - } + deleteAll('[data-smm-menu-page-container]'); - private showPageContainer() { - this.pageContainer.style.opacity = '1'; - this.pageContainer.style.pointerEvents = 'all'; - const header = document.querySelector<HTMLDivElement>( - DECK_SELECTORS.header - ); - if (header) { - header.style.display = 'none'; - } + if (!forward) window.coolClass.NavigateBackOrOpenMenu(); } - private hidePageContainer() { - this.pageContainer.style.opacity = '0'; - this.pageContainer.style.pointerEvents = 'none'; - const header = document.querySelector<HTMLDivElement>( - DECK_SELECTORS.header - ); - if (header) { - header.style.display = 'flex'; - } + private showPageContainer() { + document + .querySelector<HTMLDivElement>(DECK_SELECTORS.topLevelTransitionSwitch) + ?.appendChild(this.pageContainer); } createMenuItem({ id, label, render }: MenuItem) { @@ -181,7 +149,7 @@ export class MenuInjectorDeck implements MenuInjector { closeActivePage() { this.activePluginGamepad?.cleanup(); this.menuListGamepad?.cleanup(); - this.closePluginsPage(); + this.closePluginsPage(true); } private openPluginPage(render: MenuItem['render']) { diff --git a/injected/src/smm.ts b/injected/src/smm.ts index a375f9b..2689405 100644 --- a/injected/src/smm.ts +++ b/injected/src/smm.ts @@ -29,6 +29,7 @@ type SMMEventType = | typeof eventTypeSwitchToCollections | typeof eventTypeSwitchToAppDetails | typeof eventTypeSwitchToAppProperties + | typeof eventTypeSwitchToPlugins | typeof eventTypeLockScreenOpened | typeof eventTypeLockScreenClosed; @@ -38,6 +39,7 @@ type SMMEvent = | EventSwitchToCollections | EventSwitchToAppDetails | EventSwitchToAppProperties + | EventSwitchToPlugins | EventLockScreenOpened | EventLockScreenClosed; @@ -77,6 +79,13 @@ class EventSwitchToAppProperties extends CustomEvent<AppPropsApp> { } } +const eventTypeSwitchToPlugins = 'switchToPlugins' as const; +class EventSwitchToPlugins extends CustomEvent<void> { + constructor() { + super(eventTypeSwitchToPlugins); + } +} + const eventTypeLockScreenOpened = 'lockScreenOpened' as const; class EventLockScreenOpened extends CustomEvent<void> { constructor() { @@ -275,6 +284,15 @@ export class SMM extends EventTarget { this.dispatchEvent(new EventSwitchToAppProperties(app)); } + /** + * @internal + */ + switchToPlugins() { + info('Switched to plugins'); + + this.dispatchEvent(new EventSwitchToPlugins()); + } + /** * @internal */ diff --git a/injected/src/tab-observer.ts b/injected/src/tab-observer.ts index c66c3eb..d8ea9ee 100644 --- a/injected/src/tab-observer.ts +++ b/injected/src/tab-observer.ts @@ -81,6 +81,11 @@ export const createTabObserver = (smm: SMM, mainLibraryEl: HTMLElement) => { } } + if (location.pathname === '/routes/blank/cs-plugins') { + smm.switchToPlugins(); + return; + } + smm.switchToUnknownPage(); }); diff --git a/injected/src/types/global.d.ts b/injected/src/types/global.d.ts index 0b0c628..f082cbf 100644 --- a/injected/src/types/global.d.ts +++ b/injected/src/types/global.d.ts @@ -68,9 +68,11 @@ declare global { // other/none = close menu OpenSideMenu: (menu?: number) => void; ToggleSideMenu: (menu?: number) => void; + CloseSideMenus: () => void; // Currently open menu (same number values as above) m_eOpenSideMenu?: number; + Navigate: (target: string) => void; NavigateToLibraryTab: () => void; NavigateBackOrOpenMenu: () => void; -- 2.37.3
--- injected/src/gamepad/gamepad.ts | 16 +- .../menu-manager/deck/menu-injector-deck.css | 138 +++++++++++++++--- .../menu-manager/deck/menu-injector-deck.tsx | 98 +++++++++++-- injected/src/ui/buttons.tsx | 2 +- injected/src/ui/index.ts | 3 +- 5 files changed, 217 insertions(+), 40 deletions(-) diff --git a/injected/src/gamepad/gamepad.ts b/injected/src/gamepad/gamepad.ts index a7577c5..fbd2b67 100644 --- a/injected/src/gamepad/gamepad.ts +++ b/injected/src/gamepad/gamepad.ts @@ -79,23 +79,27 @@ export class GamepadHandler { async cleanup() { await this.smm.ButtonInterceptors.removeAfter(gamepadRoot(this.id)); await this.smm.ButtonInterceptors.removeInterceptor(gamepadRoot(this.id)); - this.root - .querySelectorAll('.cs-gp-focus') - .forEach((node) => node.classList.remove('cs-gp-focus')); + this.root.querySelectorAll<HTMLElement>('.cs-gp-focus').forEach((node) => { + node.classList.remove('cs-gp-focus'); + node.blur(); + }); } updateFocused(newFocusPath: string) { const curFocus = this.tree[this.focusPath]; if (curFocus) { curFocus.el.classList.remove('cs-gp-focus'); + curFocus.el.blur(); } - document - .querySelectorAll('.cs-gp-focus') - .forEach((node) => node.classList.remove('cs-gp-focus')); + document.querySelectorAll<HTMLElement>('.cs-gp-focus').forEach((node) => { + node.classList.remove('cs-gp-focus'); + node.blur(); + }); const newFocusEl = this.tree[newFocusPath].el; newFocusEl.classList.add('cs-gp-focus'); + newFocusEl.focus(); this.focusPath = newFocusPath; diff --git a/injected/src/menu-manager/deck/menu-injector-deck.css b/injected/src/menu-manager/deck/menu-injector-deck.css index 2621f37..c2f0150 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.css +++ b/injected/src/menu-manager/deck/menu-injector-deck.css @@ -9,47 +9,110 @@ background-color: #23262e; } -[data-smm-menu-page-container] > ul:first-child { - flex: 1 0 auto; +[data-smm-menu-page-container].animate-in { + transition-property: opacity, transform; + transition-duration: 400ms; + transition-timing-function: cubic-bezier(0, 0, 0.1, 1); + transition-delay: 100ms; +} +[data-smm-menu-page-container].animate-out { + transition-property: opacity, transform; + transition-duration: 100ms; + transition-timing-function: cubic-bezier(0.6, 0, 1, 1); +} +[data-smm-menu-page-container].animate-in-end, +[data-smm-menu-page-container].animate-out-start { + opacity: 1; + transform: scale(1); +} +[data-smm-menu-page-container].animate-out-end, +[data-smm-menu-page-container].animate-in-start { + opacity: 0; + transform: scale(0.9); +} - max-width: 200px; - height: 100%; +[data-smm-menu-page-container] h1 { + font-size: 22px !important; +} + +[data-smm-menu-page-container] h2 { + font-size: 16px; +} + +[data-smm-menu-page-container] > ul:first-child { + min-width: 240px; + max-width: 40%; + height: calc(100% - 16px * 2); margin: 0; - padding: 0; + padding: 16px 0; list-style: none; - background-color: rgba(255, 255, 255, 2%); + + background: #23262e; + border-right: 1px solid #0e141b; + + flex: 1; + display: flex; + flex-direction: column; + position: relative; + overflow-x: hidden; } [data-smm-menu-page-container] > ul:first-child > li { cursor: pointer; border: solid 1px transparent; - - transition: all 200ms; } [data-smm-menu-page-container] > ul:first-child > li.cs-gp-focus { outline: none; - border-color: white; +} + +[data-smm-menu-page-container] + > ul:first-child + > li.cs-gp-focus + > .smm-menu-item-button { + transform: scale(1.1); + background: #3d4450; } .smm-menu-item-button { width: 100%; - padding: 8px 24px; + border: none; + background: none; - background-color: rgba(255, 255, 255, 2%); - color: rgba(255, 255, 255, 90%); + font-weight: normal; font-size: 16px; - border: none; + font-style: normal; + line-height: 20px; + text-align: left; + text-decoration: none; + text-indent: 0; + text-shadow: none; + text-transform: none; + letter-spacing: 0px; + color: #fff; + padding: 10px calc(12px + 1.4vw); + color: #b8bcbf; cursor: pointer; - - transition: all 150ms; + display: flex; + flex-direction: row; + transform: scale(1) rotateX(1deg); + line-height: 22px; + scroll-margin: 2.5em 0; + transition-property: transform, background-color; + transition-duration: 0.32s, 0s; + transition-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83); + transform-origin: 12% 50%; + animation-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83); + animation-duration: 0.5s; + animation-fill-mode: forwards; + transform: scale(1) rotateX(1deg); } .smm-menu-item-button:hover { - background-color: rgba(255, 255, 255, 4%); + background-color: #23262e; } .smm-menu-item-button.active { @@ -60,5 +123,46 @@ width: 100%; height: 100%; overflow: auto; - background-color: #23262e; + background: #0e141b; + position: relative; +} + +[data-smm-plugin-page] { + position: absolute; + top: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + +[data-smm-plugin-page].animate-out-top, +[data-smm-plugin-page].animate-out-bottom { + transition-property: opacity, transform; + transition-duration: 80ms; + transition-timing-function: cubic-bezier(0.6, 0, 1, 1); +} +[data-smm-plugin-page].animate-in-top, +[data-smm-plugin-page].animate-in-bottom { + transition-property: opacity, transform; + transition-duration: 320ms; + transition-timing-function: cubic-bezier(0, 0, 0.1, 1); + transition-delay: 80ms; +} + +[data-smm-plugin-page].animate-out-top-start, +[data-smm-plugin-page].animate-out-bottom-start, +[data-smm-plugin-page].animate-in-top-end, +[data-smm-plugin-page].animate-in-bottom-end { + opacity: 1; + transform: translateY(0); +} +[data-smm-plugin-page].animate-out-bottom-end, +[data-smm-plugin-page].animate-in-top-start { + opacity: 0; + transform: translateY(-8%); +} +[data-smm-plugin-page].animate-out-top-end, +[data-smm-plugin-page].animate-in-bottom-start { + opacity: 0; + transform: translateY(8%); } diff --git a/injected/src/menu-manager/deck/menu-injector-deck.tsx b/injected/src/menu-manager/deck/menu-injector-deck.tsx index f6949a9..f8a82a8 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.tsx +++ b/injected/src/menu-manager/deck/menu-injector-deck.tsx @@ -8,6 +8,21 @@ import styles from './menu-injector-deck.css'; // @use-dom-chef +const animate = ( + node: HTMLElement, + animationName: string, + duration: number +) => { + node.classList.add(animationName, `${animationName}-start`); + setTimeout(() => { + node.classList.remove(`${animationName}-start`); + node.classList.add(`${animationName}-end`); + }, 0); + setTimeout(() => { + node.classList.remove(animationName, `${animationName}-end`); + }, duration); +}; + export class MenuInjectorDeck implements MenuInjector { private readonly smm: SMM; private readonly menuManager: MenuManager; @@ -18,17 +33,21 @@ export class MenuInjectorDeck implements MenuInjector { // List of plugins in page private menuList!: HTMLUListElement; private menuItemNodes: Record<string, HTMLLIElement>; + private menuItems: MenuItem[]; // Root to render plugin contents private menuPage!: HTMLDivElement; private menuListGamepad?: GamepadHandler; private activePluginGamepad?: GamepadHandler; + private activePluginId?: string; + private activePluginEl?: HTMLDivElement; constructor(smm: SMM, menuManager: MenuManager) { this.smm = smm; this.menuManager = menuManager; this.enteredWithNavigate = false; this.menuItemNodes = {}; + this.menuItems = []; this.injectMenuStyles(); this.createPageContainer(); @@ -103,11 +122,13 @@ export class MenuInjectorDeck implements MenuInjector { } private async closePluginsPage(forward = false) { + this.activePluginId = undefined; + this.activePluginEl = undefined; // Clear active menu item window.csMenuActiveItem = undefined; window.csMenuUpdate?.(); - deleteAll('[data-smm-menu-page-container]'); + this.hidePageContainer(); if (!forward) window.coolClass.NavigateBackOrOpenMenu(); } @@ -116,11 +137,19 @@ export class MenuInjectorDeck implements MenuInjector { document .querySelector<HTMLDivElement>(DECK_SELECTORS.topLevelTransitionSwitch) ?.appendChild(this.pageContainer); + animate(this.pageContainer, 'animate-in', 600); } - createMenuItem({ id, label, render }: MenuItem) { + private hidePageContainer() { + animate(this.pageContainer, 'animate-out', 100); + setTimeout(() => deleteAll('[data-smm-menu-page-container]'), 100); + } + + createMenuItem(menuItem: MenuItem) { + const { id, label, render } = menuItem; const newMenuItem = dcCreateElement<HTMLLIElement>( <li + tabIndex={0} smm-menu-item={id} data-cs-gp-in-group="root" data-cs-gp-item={id} @@ -128,7 +157,10 @@ export class MenuInjectorDeck implements MenuInjector { Object.values(this.menuItemNodes).length === 0 ? 'true' : 'false' } onClick={() => { - this.openPluginPage(render); + this.openPluginPage(id, render, true); + }} + onFocus={() => { + this.openPluginPage(id, render); }} > <button className="smm-menu-item-button" data-smm-menu-item-button={id}> @@ -139,11 +171,13 @@ export class MenuInjectorDeck implements MenuInjector { this.menuItemNodes[id] = newMenuItem; this.menuList.appendChild(newMenuItem); + this.menuItems.push(menuItem); } removeMenuItem(id: string) { this.menuItemNodes[id]?.remove(); delete this.menuItemNodes[id]; + this.menuItems = this.menuItems.filter((item) => item.id !== id); } closeActivePage() { @@ -152,17 +186,51 @@ export class MenuInjectorDeck implements MenuInjector { this.closePluginsPage(true); } - private openPluginPage(render: MenuItem['render']) { - [...this.menuPage.children].forEach((node) => node.remove()); - render(this.smm, this.menuPage); - this.activePluginGamepad = new GamepadHandler({ - smm: this.smm, - root: this.menuPage, - rootExitCallback: () => { - this.activePluginGamepad?.cleanup(); - [...this.menuPage.children].forEach((node) => node.remove()); - this.menuListGamepad?.updateFocused(this.menuListGamepad.focusPath); - }, - }); + private openPluginPage( + id: string, + render: MenuItem['render'], + navigateIntoView = false + ) { + if (this.activePluginId !== id || !this.activePluginEl) { + const oldIndex = this.menuItems.findIndex( + (item) => item.id === this.activePluginId + ); + const newIndex = this.menuItems.findIndex((item) => item.id === id); + const animationDirection = newIndex > oldIndex ? 'bottom' : 'top'; + + ([...this.menuPage.children] as HTMLElement[]).forEach( + (node: HTMLElement) => { + animate(node, `animate-out-${animationDirection}`, 80); + setTimeout(() => { + // reset scroll after out animation is over + this.menuPage.scrollTop = 0; + node.remove(); + }, 80); + } + ); + + const shouldAnimate = this.activePluginId != null; + this.activePluginId = id; + + this.activePluginEl = dcCreateElement<HTMLDivElement>( + <div data-smm-plugin-page /> + ); + render(this.smm, this.activePluginEl); + this.menuPage.appendChild(this.activePluginEl); + + if (shouldAnimate) + animate(this.activePluginEl, `animate-in-${animationDirection}`, 500); + } + + if (navigateIntoView) { + this.activePluginGamepad = new GamepadHandler({ + smm: this.smm, + root: this.activePluginEl, + rootExitCallback: () => { + this.activePluginGamepad?.cleanup(); + this.menuListGamepad?.updateFocused(this.menuListGamepad.focusPath); + }, + }); + } } } diff --git a/injected/src/ui/buttons.tsx b/injected/src/ui/buttons.tsx index 6103d97..dfd89d5 100644 --- a/injected/src/ui/buttons.tsx +++ b/injected/src/ui/buttons.tsx @@ -25,7 +25,7 @@ const buttonStyles = ` } .cs-button:focus { - outline: none; + outline: white solid 2px; } .cs-button:focus-visible { diff --git a/injected/src/ui/index.ts b/injected/src/ui/index.ts index 0eb5849..1eb9864 100644 --- a/injected/src/ui/index.ts +++ b/injected/src/ui/index.ts @@ -9,7 +9,8 @@ export const registerCustomElements = async () => { // TODO: load these styles from a .css file // Had an issue with esbuild loading the text from the file let styleSheetContents = ` -.cs-gp-focus { +.cs-gp-focus, +.cs-gp-focus:focus { outline: solid 2px white; } `; -- 2.37.3
smm.addEventListener can now automatically determine the event arg type based on the string supplied to it. --- .../menu-manager/deck/menu-injector-deck.tsx | 12 +- injected/src/smm.ts | 124 ++++++++++-------- injected/src/tab-observer.ts | 7 +- 3 files changed, 82 insertions(+), 61 deletions(-) diff --git a/injected/src/menu-manager/deck/menu-injector-deck.tsx b/injected/src/menu-manager/deck/menu-injector-deck.tsx index f8a82a8..0cdef73 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.tsx +++ b/injected/src/menu-manager/deck/menu-injector-deck.tsx @@ -1,7 +1,7 @@ import { dcCreateElement } from '../../dom-chef'; import { GamepadHandler } from '../../gamepad'; import { DECK_SELECTORS } from '../../selectors'; -import { SMM } from '../../smm'; +import { eventTypeSwitchToLocation, SMM } from '../../smm'; import { deleteAll } from '../../util'; import { MenuInjector, MenuItem, MenuManager } from '../menu-manager'; import styles from './menu-injector-deck.css'; @@ -89,8 +89,12 @@ export class MenuInjectorDeck implements MenuInjector { } private listenToNavigationChanges() { - this.smm.addEventListener('switchToPlugins', () => { - this.openPluginsPage(); + this.smm.addEventListener(eventTypeSwitchToLocation, (e) => { + if (e.detail.pathname === '/routes/blank/cs-plugins') { + this.openPluginsPage(); + } else { + this.closeActivePage(); + } }); } @@ -122,6 +126,8 @@ export class MenuInjectorDeck implements MenuInjector { } private async closePluginsPage(forward = false) { + if (!this.pageContainer.isConnected) return; + this.activePluginId = undefined; this.activePluginEl = undefined; // Clear active menu item diff --git a/injected/src/smm.ts b/injected/src/smm.ts index 2689405..32d255a 100644 --- a/injected/src/smm.ts +++ b/injected/src/smm.ts @@ -19,52 +19,28 @@ import { info } from './util'; type PluginId = string; -type AddEventListenerArgs = Parameters<EventTarget['addEventListener']>; - -// TODO: there's probably a better way to do these EventTarget types - -type SMMEventType = - | typeof eventTypeSwitchToUnknownPage - | typeof eventTypeSwitchToHome - | typeof eventTypeSwitchToCollections - | typeof eventTypeSwitchToAppDetails - | typeof eventTypeSwitchToAppProperties - | typeof eventTypeSwitchToPlugins - | typeof eventTypeLockScreenOpened - | typeof eventTypeLockScreenClosed; - -type SMMEvent = - | EventSwitchToUnknownPage - | EventSwitchToHome - | EventSwitchToCollections - | EventSwitchToAppDetails - | EventSwitchToAppProperties - | EventSwitchToPlugins - | EventLockScreenOpened - | EventLockScreenClosed; - -const eventTypeSwitchToUnknownPage = 'switchToUnknownPage' as const; +export const eventTypeSwitchToUnknownPage = 'switchToUnknownPage' as const; class EventSwitchToUnknownPage extends CustomEvent<void> { constructor() { super(eventTypeSwitchToUnknownPage); } } -const eventTypeSwitchToHome = 'switchToHome' as const; +export const eventTypeSwitchToHome = 'switchToHome' as const; class EventSwitchToHome extends CustomEvent<void> { constructor() { super(eventTypeSwitchToHome); } } -const eventTypeSwitchToCollections = 'switchToCollections' as const; +export const eventTypeSwitchToCollections = 'switchToCollections' as const; class EventSwitchToCollections extends CustomEvent<void> { constructor() { super(eventTypeSwitchToCollections); } } -const eventTypeSwitchToAppDetails = 'switchToAppDetails' as const; +export const eventTypeSwitchToAppDetails = 'switchToAppDetails' as const; type eventDetailsSwitchToAppDetails = { appId: string; appName: string }; class EventSwitchToAppDetails extends CustomEvent<eventDetailsSwitchToAppDetails> { constructor(detail: eventDetailsSwitchToAppDetails) { @@ -72,34 +48,79 @@ class EventSwitchToAppDetails extends CustomEvent<eventDetailsSwitchToAppDetails } } -const eventTypeSwitchToAppProperties = 'switchToAppProperties' as const; +export const eventTypeSwitchToAppProperties = 'switchToAppProperties' as const; class EventSwitchToAppProperties extends CustomEvent<AppPropsApp> { constructor(detail: AppPropsApp) { super(eventTypeSwitchToAppProperties, { detail }); } } -const eventTypeSwitchToPlugins = 'switchToPlugins' as const; -class EventSwitchToPlugins extends CustomEvent<void> { - constructor() { - super(eventTypeSwitchToPlugins); +export const eventTypeSwitchToLocation = 'switchToLocation' as const; +type eventDetailsSwitchToLocation = Location; +class EventSwitchToLocation extends CustomEvent<eventDetailsSwitchToLocation> { + constructor(detail: eventDetailsSwitchToLocation) { + super(eventTypeSwitchToLocation, { detail }); } } -const eventTypeLockScreenOpened = 'lockScreenOpened' as const; +export const eventTypeLockScreenOpened = 'lockScreenOpened' as const; class EventLockScreenOpened extends CustomEvent<void> { constructor() { super(eventTypeLockScreenOpened); } } -const eventTypeLockScreenClosed = 'lockScreenClosed' as const; +export const eventTypeLockScreenClosed = 'lockScreenClosed' as const; class EventLockScreenClosed extends CustomEvent<void> { constructor() { super(eventTypeLockScreenClosed); } } +interface EventListenerObjectFor<T extends Event> { + handleEvent(evt: T): void; +} +interface EventListenerFor<T extends Event> { + (evt: T): void; +} +type EventListenerOrEventListenerObjectFor<T extends Event> = + | EventListenerFor<T> + | EventListenerObjectFor<T>; + +type SMMEventGroup<T extends string, E extends Event> = { + type: T; + event: E; +}; + +type SMMEvent = + | SMMEventGroup<typeof eventTypeSwitchToUnknownPage, EventSwitchToUnknownPage> + | SMMEventGroup<typeof eventTypeSwitchToHome, EventSwitchToHome> + | SMMEventGroup<typeof eventTypeSwitchToCollections, EventSwitchToCollections> + | SMMEventGroup<typeof eventTypeSwitchToAppDetails, EventSwitchToAppDetails> + | SMMEventGroup< + typeof eventTypeSwitchToAppProperties, + EventSwitchToAppProperties + > + | SMMEventGroup<typeof eventTypeSwitchToLocation, EventSwitchToLocation> + | SMMEventGroup<typeof eventTypeLockScreenOpened, EventLockScreenOpened> + | SMMEventGroup<typeof eventTypeLockScreenClosed, EventLockScreenClosed>; + +type MapToCallbacks<U extends SMMEvent> = U extends any + ? { + type: U['type']; + callback: EventListenerOrEventListenerObjectFor<U['event']> | null; + options: Parameters<EventTarget['addEventListener']>[2]; + } + : never; +type SMMEventCallback = MapToCallbacks<SMMEvent>; + +type SMMCallbackMap = { + [P in SMMEventCallback['type']]: Extract< + SMMEventCallback, + { type: P } + >['callback']; +}; + /** * @public */ @@ -141,14 +162,7 @@ export class SMM extends EventTarget { // that plugin. private currentPlugin?: string; // Events attached by plugins - private attachedEvents: Record< - PluginId, - { - type: AddEventListenerArgs[0]; - callback: AddEventListenerArgs[1]; - options: AddEventListenerArgs[2]; - }[] - >; + private attachedEvents: Record<PluginId, SMMEventCallback[]>; constructor(entry: Entry) { super(); @@ -287,10 +301,10 @@ export class SMM extends EventTarget { /** * @internal */ - switchToPlugins() { - info('Switched to plugins'); + switchToLocation(loc: Location) { + info('Switched to location'); - this.dispatchEvent(new EventSwitchToPlugins()); + this.dispatchEvent(new EventSwitchToLocation(loc)); } /** @@ -383,15 +397,15 @@ export class SMM extends EventTarget { if (this.attachedEvents[pluginId]) { for (const { type, callback, options } of this.attachedEvents[pluginId]) { - this.removeEventListener(type, callback, options); + this.removeEventListener(type, callback as EventListener, options); } delete this.attachedEvents[pluginId]; } } - addEventListener( - type: SMMEventType, - callback: EventListenerOrEventListenerObject | null, + addEventListener<K extends keyof SMMCallbackMap>( + type: K, + callback: SMMCallbackMap[K], options?: boolean | AddEventListenerOptions ): void { if (this.currentPlugin) { @@ -399,13 +413,17 @@ export class SMM extends EventTarget { this.attachedEvents[this.currentPlugin] = []; } - this.attachedEvents[this.currentPlugin].push({ type, callback, options }); + this.attachedEvents[this.currentPlugin].push({ + type, + callback, + options, + } as SMMEventCallback); } - super.addEventListener(type, callback, options); + super.addEventListener(type, callback as EventListener, options); } - dispatchEvent(event: SMMEvent): boolean { + dispatchEvent(event: SMMEvent['event']): boolean { return super.dispatchEvent(event); } diff --git a/injected/src/tab-observer.ts b/injected/src/tab-observer.ts index d8ea9ee..d9bfad9 100644 --- a/injected/src/tab-observer.ts +++ b/injected/src/tab-observer.ts @@ -14,6 +14,8 @@ export const createTabObserver = (smm: SMM, mainLibraryEl: HTMLElement) => { } const observer = new MutationObserver((_mutationsList) => { + smm.switchToLocation({ ...location }); + if (window.smmUIMode === 'deck') { if (document.querySelector(DECK_SELECTORS.lockScreenContainer)) { if (!smm.onLockScreen) { @@ -81,11 +83,6 @@ export const createTabObserver = (smm: SMM, mainLibraryEl: HTMLElement) => { } } - if (location.pathname === '/routes/blank/cs-plugins') { - smm.switchToPlugins(); - return; - } - smm.switchToUnknownPage(); }); -- 2.37.3
builds.sr.ht <builds@sr.ht>crankshaft/patches: FAILED in 1m36s [Integrate plugins into gamepadui][0] v2 from [Jarasłaŭ Viktorčyk][1] [0]: https://lists.sr.ht/~avery/public-inbox/patches/35075 [1]: mailto:ugzuzg@gmail.com ✗ #836525 FAILED crankshaft/patches/mirror.yml https://builds.sr.ht/~avery/job/836525 ✓ #836524 SUCCESS crankshaft/patches/linux.yml https://builds.sr.ht/~avery/job/836524