diff --git a/main.py b/main.py index 6423b50..7dd7857 100644 --- a/main.py +++ b/main.py @@ -4,9 +4,8 @@ from logging import getLogger starter_config_data = { - "music_enabled": False, - "music_library_only": False, - "selected_pack": "Default" + "selected_pack": "Default", + "selected_music": "None" } starter_config_string = json.dumps(starter_config_data) @@ -220,9 +219,8 @@ async def parse_packs(self, packsDir : str): packData = Pack(packPath, pack) if (packData.name not in [p.name for p in self.soundPacks]): - if (packData.music == False): - self.soundPacks.append(packData) - Log("Audio Loader - Sound pack {} added".format(packData.name)) + self.soundPacks.append(packData) + Log("Audio Loader - Sound pack {} added".format(packData.name)) except Exception as e: Log("Audio Loader - Error parsing sound pack: {}".format(e)) @@ -254,9 +252,8 @@ async def _load(self): async def _main(self): self.soundPacks = [] self.config = { - "music_enabled": False, - "music_library_only": False, - "selected_pack": "Default" + "selected_pack": "Default", + "selected_music": "None" } self.remote = RemoteInstall(self) diff --git a/package.json b/package.json index 77bc3dc..7eb223d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "SDH-AudioLoader", - "version": "1.0.1", + "version": "1.1.0", "description": "Replaces and adds Steam Deck game UI sounds", "scripts": { "build": "shx rm -rf dist && rollup -c", diff --git a/plugin.json b/plugin.json index cf2536a..bd1e3be 100644 --- a/plugin.json +++ b/plugin.json @@ -4,7 +4,7 @@ "flags": ["debug", "_root"], "publish": { "tags": ["style", "media", "music"], - "description": "Replaces Steam UI sound effects with custom sounds.", + "description": "Replaces Steam UI sound effects with custom sounds and adds music to menus.", "image": "https://i.imgur.com/0Xuc1pr.png" } } diff --git a/src/SteamClient.d.ts b/src/SteamClient.d.ts new file mode 100644 index 0000000..e6673a9 --- /dev/null +++ b/src/SteamClient.d.ts @@ -0,0 +1,135 @@ +// Lovingly borrowed from https://github.com/hulkrelax/deckfaqs under the MIT License + +// Non-exhaustive definition of the SteamClient that is available in the SP tab +// This object has a lot more properties/methods than are listed here +declare namespace SteamClient { + const Apps: { + GetAllShortcuts(): Promise; + RegisterForGameActionStart( + callback: ( + actionType: number, + strAppId: string, + actionName: string + ) => any + ): RegisteredEvent; + }; + const InstallFolder: { + GetInstallFolders(): Promise; + }; + const GameSessions: { + RegisterForAppLifetimeNotifications( + callback: (appState: AppState) => any + ): RegisteredEvent; + }; + const BrowserView: { + Create(): any; + CreatePopup(): any; + Destroy(e: any): void; + }; + + const Storage: { + GetJSON(key: string): Promise; + SetObject(key: string, value: {}): Promise; + }; +} + +declare const enum DisplayStatus { + Invalid = 0, + Launching = 1, + Uninstalling = 2, + Installing = 3, + Running = 4, + Validating = 5, + Updating = 6, + Downloading = 7, + Synchronizing = 8, + ReadyToInstall = 9, + ReadyToPreload = 10, + ReadyToLaunch = 11, + RegionRestricted = 12, + PresaleOnly = 13, + InvalidPlatform = 14, + PreloadComplete = 16, + BorrowerLocked = 17, + UpdatePaused = 18, + UpdateQueued = 19, + UpdateRequired = 20, + UpdateDisabled = 21, + DownloadPaused = 22, + DownloadQueued = 23, + DownloadRequired = 24, + DownloadDisabled = 25, + LicensePending = 26, + LicenseExpired = 27, + AvailForFree = 28, + AvailToBorrow = 29, + AvailGuestPass = 30, + Purchase = 31, + Unavailable = 32, + NotLaunchable = 33, + CloudError = 34, + CloudOutOfDate = 35, + Terminating = 36, +} + +type AppState = { + unAppID: number; + nInstanceID: number; + bRunning: boolean; +}; + +declare namespace appStore { + function GetAppOverviewByGameID(appId: number): AppOverview; +} + +type RegisteredEvent = { + unregister(): void; +}; + +type Shortcut = { + appid: number; + data: { + bIsApplication: true; + strAppName: string; + strSortAs: string; + strExePath: string; + strShortcutPath: string; + strArguments: string; + strIconPath: string; + }; +}; + +type AppOverview = { + appid: string; + display_name: string; + display_status: DisplayStatus; + sort_as: string; +}; + +type App = { + nAppID: number; + strAppName: string; + strSortAs: string; + rtLastPlayed: number; + strUsedSize: string; + strDLCSize: string; + strWorkshopSize: string; + strStagedSize: string; +}; + +type InstallFolder = { + nFolderIndex: number; + strFolderPath: string; + strUserLabel: string; + strDriveName: string; + strCapacity: string; + strFreeSpace: string; + strUsedSize: string; + strDLCSize: string; + strWorkshopSize: string; + strStagedSize: string; + bIsDefaultFolder: boolean; + bIsMounted: boolean; + bIsFixed: boolean; + vecApps: App[]; +}; diff --git a/src/index.tsx b/src/index.tsx index 4dcec33..a1516dc 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,7 +9,6 @@ import { Router, beforePatch, unpatch, - ToggleField, SidebarNavigation, } from "decky-frontend-lib"; import { VFC, useMemo, useEffect, useState } from "react"; @@ -26,13 +25,13 @@ import { const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { const { activeSound, + gamesRunning, setActiveSound, soundPacks, - setSoundPacks, - musicEnabled, - setMusicEnabled, - musicLibraryOnly, - setMusicLibraryOnly, + menuMusic, + setMenuMusic, + selectedMusic, + setSelectedMusic, } = useGlobalState(); const [dummyFuncResult, setDummyResult] = useState(false); @@ -45,57 +44,41 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { dummyFuncTest(); }, []); - function reloadPlugin() { - dummyFuncTest(); - python.resolve(python.reloadPacksDir(), () => { - python.resolve(python.getSoundPacks(), (data: any) => { - setSoundPacks(data); - }); - }); - - python.resolve(python.getConfig(), (data: any) => { - // This just has fallbacks incase the fetch fails or the config is improperly formatted - setActiveSound(data?.selected_pack || "Default"); - setMusicEnabled(data?.music_enabled || false); - setMusicLibraryOnly(data?.music_library_only || false); - }); - - unpatch( - AudioParent.GamepadUIAudio.m_AudioPlaybackManager.__proto__, - "PlayAudioURL" - ); - - beforePatch( - AudioParent.GamepadUIAudio.m_AudioPlaybackManager.__proto__, - "PlayAudioURL", - (args) => { - let newSoundURL: string = ""; - switch (activeSound) { - case "Default": - newSoundURL = args[0]; - break; - default: - const currentPack = soundPacks.find((e) => e.name === activeSound); - if (currentPack?.ignore.includes(args[0].slice(8))) { - newSoundURL = args[0]; - break; - } - newSoundURL = args[0].replace( - "sounds/", - `sounds_custom/${currentPack?.path || "/error"}/` - ); - break; - } - args[0] = newSoundURL; - return [newSoundURL]; - } - ); + function restartMusicPlayer(newMusic: string) { + if (menuMusic !== null) { + menuMusic.StopPlayback(); + setMenuMusic(null); + } + // This makes sure if you are in a game, music doesn't start playing + if (newMusic !== "None" && gamesRunning.length === 0) { + const currentPack = soundPacks.find((e) => e.name === newMusic); + const newMenuMusic = + AudioParent.GamepadUIAudio.AudioPlaybackManager.PlayAudioURLWithRepeats( + `/sounds_custom/${currentPack?.path || "/error"}/menu_music.mp3`, + 999 // if someone complains this isn't infinite, just say it's a Feature™ for if you go afk + ); + setMenuMusic(newMenuMusic); + } } const SoundPackDropdownOptions = useMemo(() => { return [ { label: "Default", data: -1 }, ...soundPacks + // Only shows sound packs + .filter((e) => !e.data.music) + .map((p, index) => ({ label: p.name, data: index })) + // TODO: because this sorts after assigning indexes, the sort might make the indexes out of order, make sure this doesn't happen + .sort((a, b) => a.label.localeCompare(b.label)), + ]; + }, [soundPacks]); + + const MusicPackDropdownOptions = useMemo(() => { + return [ + { label: "None", data: -1 }, + ...soundPacks + // Only show music packs + .filter((e) => e.data.music) .map((p, index) => ({ label: p.name, data: index })) // TODO: because this sorts after assigning indexes, the sort might make the indexes out of order, make sure this doesn't happen .sort((a, b) => a.label.localeCompare(b.label)), @@ -133,9 +116,8 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { setActiveSound(option.label); const configObj = { - music_enabled: musicEnabled, - music_library_only: musicLibraryOnly, selected_pack: option.label, + selected_music: selectedMusic, }; python.setConfig(configObj); }} @@ -146,23 +128,25 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => { bottomSeparator={false} label="Music" menuLabel="Music" - rgOptions={[{ label: "Coming Soon", data: 0 }]} - selectedOption={0} - disabled={true} + rgOptions={MusicPackDropdownOptions} + selectedOption={ + MusicPackDropdownOptions.find((e) => e.label === selectedMusic) + ?.data ?? -1 + } + onChange={async (option) => { + setSelectedMusic(option.label); + + const configObj = { + selected_pack: activeSound, + selected_music: option.label, + }; + python.setConfig(configObj); + restartMusicPlayer(option.label); + }} /> - - - - - = ({}) => { Manage Packs - - reloadPlugin()} - > - Reload Plugin - - ); @@ -214,6 +189,7 @@ export default definePlugin((serverApi: ServerAPI) => { python.setServer(serverApi); const state: GlobalState = new GlobalState(); + let menuMusic: any = null; beforePatch( AudioParent.GamepadUIAudio.m_AudioPlaybackManager.__proto__, @@ -244,17 +220,72 @@ export default definePlugin((serverApi: ServerAPI) => { } ); - // For some reason when I didn't make these arrow functions they gave me "can't set property of undefined errors", so just leave them as arrow functions - python.resolve(python.getSoundPacks(), (data: any) => { - state.setSoundPacks(data); - }); - python.resolve(python.getConfig(), (data: any) => { - // This just has fallbacks incase the fetch fails or the config is improperly formatted - state.setActiveSound(data?.selected_pack || "Default"); - state.setMusicEnabled(data?.music_enabled || false); - state.setMusicLibraryOnly(data?.music_library_only || false); + python.resolve(python.getSoundPacks(), (packs: any) => { + state.setSoundPacks(packs); + // This is nested in here so that all data has loaded before it attempts to find audio paths + python.resolve(python.getConfig(), (data: any) => { + // This sets the config data in globalState + state.setActiveSound(data?.selected_pack || "Default"); + const configSelectedMusic = data?.selected_music || "None"; + state.setSelectedMusic(configSelectedMusic); + + // Plays menu music initially + // TODO: Add check if game is currently running + if (configSelectedMusic !== "None") { + const { soundPacks } = state.getPublicState(); + const currentPack = soundPacks.find( + (e) => e.name === configSelectedMusic + ); + menuMusic = + AudioParent.GamepadUIAudio.AudioPlaybackManager.PlayAudioURLWithRepeats( + `/sounds_custom/${currentPack?.path || "/error"}/menu_music.mp3`, + 999 // if someone complains this isn't infinite, just say it's a Feature™ for if you go afk + ); + state.setMenuMusic(menuMusic); + } + }); }); + const AppStateRegistrar = + // SteamClient is something exposed by the SP tab of SteamUI, it's not a decky-frontend-lib thingy, but you can still call it normally + // Refer to the SteamClient.d.ts or just console.log(SteamClient) to see all of it's methods + SteamClient.GameSessions.RegisterForAppLifetimeNotifications( + (update: AppState) => { + const { soundPacks, menuMusic, selectedMusic, gamesRunning } = + state.getPublicState(); + if (selectedMusic !== "None") { + if (update.bRunning) { + // Because gamesRunning is in globalState, array methods like push and splice don't work + state.setGamesRunning([...gamesRunning, update.unAppID]); + if (menuMusic != null) { + menuMusic.StopPlayback(); + state.setMenuMusic(null); + } + } else { + state.setGamesRunning( + gamesRunning.filter((e) => e !== update.unAppID) + ); + + // I'm re-using the filter here because I don't believe the getPublicState() method will update the values if they are changed + if (gamesRunning.filter((e) => e !== update.unAppID).length === 0) { + const currentMusic = soundPacks.find( + (e) => e.name === selectedMusic + ); + const newMenuMusic = + AudioParent.GamepadUIAudio.AudioPlaybackManager.PlayAudioURLWithRepeats( + `/sounds_custom/${ + currentMusic?.path || "/error" + }/menu_music.mp3`, + 999 // if someone complains this isn't infinite, just say it's a Feature™ for if you go afk + ); + // Update menuMusic in globalState after every change so that it reflects the changes the next time it checks + state.setMenuMusic(newMenuMusic); + } + } + } + } + ); + serverApi.routerHook.addRoute("/audiopack-manager", () => ( @@ -270,10 +301,16 @@ export default definePlugin((serverApi: ServerAPI) => { ), icon: , onDismount: () => { + if (menuMusic != null) { + menuMusic.StopPlayback(); + menuMusic = null; + } + unpatch( AudioParent.GamepadUIAudio.m_AudioPlaybackManager.__proto__, "PlayAudioURL" ); + AppStateRegistrar.unregister(); }, }; }); diff --git a/src/pack-manager/PackBrowserPage.tsx b/src/pack-manager/PackBrowserPage.tsx index 87edfa9..587e9db 100644 --- a/src/pack-manager/PackBrowserPage.tsx +++ b/src/pack-manager/PackBrowserPage.tsx @@ -30,12 +30,15 @@ export const PackBrowserPage: VFC = () => { } = useGlobalState(); async function fetchPackDb() { - const response = await python.fetchPackDb(); - if (response.success) { - setBrowserPackList(JSON.parse(response.result.body)); - } else { - console.log("AudioLoader - Fetching PackDb Failed"); - } + python.resolve(python.fetchPackDb(), (response: any) => { + if (response.body) { + setBrowserPackList(JSON.parse(response.body)); + } else { + console.log( + "AudioLoader - Fetching PackDb Failed, no json string was returned by the fetch" + ); + } + }); } function fetchLocalPacks() { @@ -62,7 +65,9 @@ export const PackBrowserPage: VFC = () => { // This checks for the theme name !e.name.toLowerCase().includes(searchFieldValue.toLowerCase()) && // This checks for the author name - !e.author.toLowerCase().includes(searchFieldValue.toLowerCase()) + !e.author.toLowerCase().includes(searchFieldValue.toLowerCase()) && + // This checks for the description + !e.description.toLowerCase().includes(searchFieldValue.toLowerCase()) ) { // return false just means it won't show in the list return false; @@ -320,7 +325,9 @@ export const PackBrowserPage: VFC = () => { e.description ) : ( - No Description Provided + + No description provided. + )} diff --git a/src/pack-manager/UninstallPage.tsx b/src/pack-manager/UninstallPage.tsx index 8f15aaa..a0869ed 100644 --- a/src/pack-manager/UninstallPage.tsx +++ b/src/pack-manager/UninstallPage.tsx @@ -12,8 +12,10 @@ export const UninstallPage: VFC = () => { setSoundPacks, activeSound, setActiveSound, - musicEnabled, - musicLibraryOnly, + selectedMusic, + setSelectedMusic, + menuMusic, + setMenuMusic, } = useGlobalState(); const [isUninstalling, setUninstalling] = useState(false); @@ -30,15 +32,26 @@ export const UninstallPage: VFC = () => { setUninstalling(true); python.resolve(python.deletePack(listEntry.data.name), () => { fetchLocalPacks(); - if (activeSound === listEntry.data.name) { + if ( + activeSound === listEntry.data.name || + selectedMusic === listEntry.data.name + ) { console.log( - "Audio Loader - Attempted to uninstall applied sound, changing applied sound to Default" + "Audio Loader - Attempted to uninstall applied sound/music, changing applied packs to Default" ); - setActiveSound("Default"); + if (activeSound === listEntry.data.name) setActiveSound("Default"); + if (selectedMusic === listEntry.data.name) { + setSelectedMusic("None"); + if (menuMusic !== null) { + menuMusic.StopPlayback(); + setMenuMusic(null); + } + } const configObj = { - music_enabled: musicEnabled, - music_library_only: musicLibraryOnly, - selected_pack: "Default", + selected_pack: + activeSound === listEntry.data.name ? "Default" : activeSound, + selected_music: + selectedMusic === listEntry.data.name ? "None" : activeSound, }; python.setConfig(configObj); } @@ -49,9 +62,7 @@ export const UninstallPage: VFC = () => { if (soundPacks.length === 0) { return ( - - No custom themes installed, find some in the 'Browse Themes' tab. - + No packs installed, find some in the 'Browse Packs' tab. ); } diff --git a/src/python.ts b/src/python.ts index 8f07ad3..4c23add 100644 --- a/src/python.ts +++ b/src/python.ts @@ -32,7 +32,7 @@ export function setServer(s: ServerAPI) { export async function fetchPackDb(): Promise { return server!.fetchNoCors( - "https://github.com/EMERALD0874/AudioLoader-PackDB/releases/download/1.0.0/packs.json", + "https://github.com/EMERALD0874/AudioLoader-PackDB/releases/download/1.1.0/packs.json", { method: "GET" } ); } diff --git a/src/state/GlobalState.tsx b/src/state/GlobalState.tsx index cbd91cb..418b6f6 100644 --- a/src/state/GlobalState.tsx +++ b/src/state/GlobalState.tsx @@ -3,10 +3,11 @@ import { createContext, FC, useContext, useEffect, useState } from "react"; import { Pack, packDbEntry } from "../classes"; interface PublicGlobalState { + menuMusic: any; + gamesRunning: Number[]; activeSound: string; soundPacks: Pack[]; - musicEnabled: boolean; - musicLibraryOnly: boolean; + selectedMusic: string; browserPackList: packDbEntry[]; searchFieldValue: string; selectedSort: number; @@ -17,10 +18,11 @@ interface PublicGlobalState { // The localThemeEntry interface refers to the theme data as given by the python function, the Theme class refers to a theme after it has been formatted and the generate function has been added interface PublicGlobalStateContext extends PublicGlobalState { + setMenuMusic(value: any): void; + setGamesRunning(gameArr: Number[]): void; setActiveSound(value: string): void; setSoundPacks(packArr: Pack[]): void; - setMusicEnabled(value: boolean): void; - setMusicLibraryOnly(value: boolean): void; + setSelectedMusic(value: string): void; setBrowserPackList(packArr: packDbEntry[]): void; setSearchValue(value: string): void; setSort(value: number): void; @@ -30,10 +32,11 @@ interface PublicGlobalStateContext extends PublicGlobalState { // This class creates the getter and setter functions for all of the global state data. export class GlobalState { + private menuMusic: any = null; + private gamesRunning: Number[] = []; private activeSound: string = "Default"; private soundPacks: Pack[] = []; - private musicEnabled: boolean = false; - private musicLibraryOnly: boolean = false; + private selectedMusic: string = "None"; private browserPackList: packDbEntry[] = []; private searchFieldValue: string = ""; private selectedSort: number = 3; @@ -48,10 +51,11 @@ export class GlobalState { getPublicState() { return { + menuMusic: this.menuMusic, + gamesRunning: this.gamesRunning, activeSound: this.activeSound, soundPacks: this.soundPacks, - musicEnabled: this.musicEnabled, - musicLibraryOnly: this.musicLibraryOnly, + selectedMusic: this.selectedMusic, browserPackList: this.browserPackList, searchFieldValue: this.searchFieldValue, selectedSort: this.selectedSort, @@ -60,6 +64,16 @@ export class GlobalState { }; } + setMenuMusic(value: any) { + this.menuMusic = value; + this.forceUpdate(); + } + + setGamesRunning(gameArr: Number[]) { + this.gamesRunning = gameArr; + this.forceUpdate(); + } + setActiveSound(value: string) { this.activeSound = value; this.forceUpdate(); @@ -79,13 +93,8 @@ export class GlobalState { this.forceUpdate(); } - setMusicEnabled(value: boolean) { - this.musicEnabled = value; - this.forceUpdate(); - } - - setMusicLibraryOnly(value: boolean) { - this.musicLibraryOnly = value; + setSelectedMusic(value: string) { + this.selectedMusic = value; this.forceUpdate(); } @@ -146,14 +155,15 @@ export const GlobalStateContextProvider: FC = ({ globalStateClass.eventBus.removeEventListener("stateUpdate", onUpdate); }, []); + const setMenuMusic = (value: any) => globalStateClass.setMenuMusic(value); + const setGamesRunning = (gameArr: Number[]) => + globalStateClass.setGamesRunning(gameArr); const setActiveSound = (value: string) => globalStateClass.setActiveSound(value); const setSoundPacks = (packArr: Pack[]) => globalStateClass.setSoundPacks(packArr); - const setMusicEnabled = (value: boolean) => - globalStateClass.setMusicEnabled(value); - const setMusicLibraryOnly = (value: boolean) => - globalStateClass.setMusicLibraryOnly(value); + const setSelectedMusic = (value: string) => + globalStateClass.setSelectedMusic(value); const setBrowserPackList = (packArr: packDbEntry[]) => globalStateClass.setBrowserPackList(packArr); const setSearchValue = (value: string) => @@ -167,10 +177,11 @@ export const GlobalStateContextProvider: FC = ({