diff --git a/defaults/python/lib/constants.py b/defaults/python/lib/constants.py index d140b92..dd0f5c5 100644 --- a/defaults/python/lib/constants.py +++ b/defaults/python/lib/constants.py @@ -15,7 +15,7 @@ def get_user(): CURRENT_USER = get_user() BUDDY_API_VERSION = 4 -CONFIG_VERSION_LITERAL = typing.Literal[18] +CONFIG_VERSION_LITERAL = typing.Literal[19] CONFIG_DIR = str(pathlib.Path("/home", CURRENT_USER, ".config", "moondeck")) CONFIG_FILENAME = "settings.json" LOG_FILE = "/tmp/moondeck.log" diff --git a/defaults/python/lib/settings.py b/defaults/python/lib/settings.py index a47fc10..853cdd6 100644 --- a/defaults/python/lib/settings.py +++ b/defaults/python/lib/settings.py @@ -48,6 +48,11 @@ class HostResolution(TypedDict): dimensions: List[Dimension] +class SunshineAppsSettings(TypedDict): + showQuickAccessButton: bool + lastSelectedOverride: str + + class HostSettings(TypedDict): hostInfoPort: int buddyPort: int @@ -60,6 +65,7 @@ class HostSettings(TypedDict): resolution: HostResolution hostApp: HostApp runnerTimeouts: RunnerTimeouts + sunshineApps: SunshineAppsSettings class GameSessionSettings(TypedDict): @@ -250,6 +256,10 @@ def _migrate_settings(data: Dict[str, Any]) -> Dict[str, Any]: if data["version"] == 17: data["version"] = 18 data["buttonPosition"]["zIndex"] = "" + if data["version"] == 18: + data["version"] = 19 + for host in data["hostSettings"].keys(): + data["hostSettings"][host]["sunshineApps"] = { "showQuickAccessButton": False, "lastSelectedOverride": "Default" } return data diff --git a/src/components/changelogview/changelogview.tsx b/src/components/changelogview/changelogview.tsx index b86eb27..b3ee2fc 100644 --- a/src/components/changelogview/changelogview.tsx +++ b/src/components/changelogview/changelogview.tsx @@ -7,7 +7,13 @@ export const ChangelogView: VFC = () => { +
• Migrate plugin to use new Decky API.
+
• Sunshine app sync button can now be added to the QAM.
+
• Last used app resolution override will be used when adding new Sunshine apps.
+ + } focusable={true} /> = ({ currentHostSettings, settingsManager } return ( - + = ({ connectivityManager, settingsMan ? : <> + diff --git a/src/components/quicksettingsview/sunshineappspanel.tsx b/src/components/quicksettingsview/sunshineappspanel.tsx new file mode 100644 index 0000000..c56ddeb --- /dev/null +++ b/src/components/quicksettingsview/sunshineappspanel.tsx @@ -0,0 +1,34 @@ +import { BuddyProxy, UserSettings } from "../../lib"; +import { Field, PanelSection, PanelSectionRow } from "@decky/ui"; +import { CurrentHostSettings } from "../../hooks"; +import { SunshineAppsSyncButton } from "../shared"; +import { VFC } from "react"; + +interface Props { + buddyProxy: BuddyProxy; + currentHostSettings: CurrentHostSettings | null; + currentSettings: UserSettings | null; +} + +export const SunshineAppsPanel: VFC = ({ buddyProxy, currentHostSettings, currentSettings }) => { + if (currentHostSettings === null || currentSettings === null) { + return null; + } + + if (!currentHostSettings.sunshineApps.showQuickAccessButton) { + return null; + } + + return ( + + + + + + + + ); +}; diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts index 5a8a65e..3471300 100644 --- a/src/components/shared/index.ts +++ b/src/components/shared/index.ts @@ -9,5 +9,6 @@ export * from "./resolutionselectiondropdown"; export * from "./serverstatusfield"; export * from "./statusfield"; export * from "./settingsloadingfield"; +export * from "./sunshineappssyncbutton"; export * from "./textinput"; export * from "./togglefield"; diff --git a/src/components/sunshineappsview/syncbutton.tsx b/src/components/shared/sunshineappssyncbutton.tsx similarity index 58% rename from src/components/sunshineappsview/syncbutton.tsx rename to src/components/shared/sunshineappssyncbutton.tsx index da0b96b..75c7747 100644 --- a/src/components/sunshineappsview/syncbutton.tsx +++ b/src/components/shared/sunshineappssyncbutton.tsx @@ -1,14 +1,14 @@ import { AppDetails, ConfirmModal, DialogButton, showModal } from "@decky/ui"; -import { BuddyProxy, addShortcut, getMoonDeckManagedMark, logger, removeShortcut, restartSteamClient, setAppLaunchOptions } from "../../lib"; -import { VFC } from "react"; +import { BuddyProxy, HostSettings, UserSettings, addShortcut, getAllExternalAppDetails, getMoonDeckManagedMark, logger, removeShortcut, restartSteamClient, setAppLaunchOptions, setAppResolutionOverride } from "../../lib"; +import { VFC, useState } from "react"; interface Props { - shortcuts: AppDetails[]; - moonDeckHostApps: string[]; - hostName: string; + shortcuts?: AppDetails[]; buddyProxy: BuddyProxy; - moonlightExecPath: string | null; - refreshApps: () => void; + settings: UserSettings; + hostSettings: HostSettings; + noConfirmationDialog?: boolean; + refreshApps?: () => void; } function makeExecPath(moonlightExecPath: string | null): string { @@ -31,13 +31,23 @@ function makeLaunchOptions(appName: string, hostName: string, customExec: boolea return `${getMoonDeckManagedMark()} %command% ${customExec ? "" : "run com.moonlight_stream.Moonlight"} stream '${escapedHostName}' '${escapedAppName}'`; } -async function updateLaunchOptions(appId: number, appName: string, hostName: string, customExec: boolean): Promise { +async function updateLaunchOptions(appId: number, appName: string, hostName: string, customExec: boolean): Promise { if (!await setAppLaunchOptions(appId, makeLaunchOptions(appName, hostName, customExec))) { logger.error(`Failed to set shortcut launch options for ${appName}!`); + return false; } + return true; } -async function syncShortcuts(shortcuts: AppDetails[], moonDeckHostApps: string[], hostName: string, buddyProxy: BuddyProxy, moonlightExecPath: string | null, refreshApps: () => void): Promise { +async function updateResolutionOverride(appId: number, appName: string, resolution: string): Promise { + if (!await setAppResolutionOverride(appId, resolution)) { + logger.warn(`Failed to set app resolution override for ${appName} to ${resolution}!`); + return false; + } + return true; +} + +async function syncShortcuts(shortcuts: AppDetails[], moonDeckHostApps: string[], hostName: string, buddyProxy: BuddyProxy, moonlightExecPath: string | null, resOverride: string, refreshApps?: () => void): Promise { let sunshineApps = await buddyProxy.getGamestreamAppNames(); if (sunshineApps === null) { logger.toast("Failed to get Gamestream app list!", { output: "error" }); @@ -61,6 +71,7 @@ async function syncShortcuts(shortcuts: AppDetails[], moonDeckHostApps: string[] const appsToAdd = new Set(); const appsToUpdate: AppDetails[] = []; const appsToRemove: AppDetails[] = []; + let success = true; // Check which apps need to be added or updated for (const sunshineApp of sunshineApps) { @@ -86,46 +97,80 @@ async function syncShortcuts(shortcuts: AppDetails[], moonDeckHostApps: string[] for (const app of appsToAdd) { const appId = await addExternalShortcut(app, execPath); if (appId !== null) { - await updateLaunchOptions(appId, app, hostName, customExec); + success = await updateLaunchOptions(appId, app, hostName, customExec) && success; + success = await updateResolutionOverride(appId, app, resOverride) && success; + } else { + success = false; } } // Update the launch options only for (const details of appsToUpdate) { - await updateLaunchOptions(details.unAppID, details.strDisplayName, hostName, customExec); + success = await updateLaunchOptions(details.unAppID, details.strDisplayName, hostName, customExec) && success; } // Remove them bastards! for (const details of appsToRemove) { - await removeShortcut(details.unAppID); + success = await removeShortcut(details.unAppID) && success; } if (appsToAdd.size > 0 || appsToUpdate.length > 0 || appsToRemove.length > 0) { if (appsToRemove.length > 0) { restartSteamClient(); } else { - refreshApps(); - logger.toast(`${appsToAdd.size + appsToUpdate.length}/${sunshineApps.length} app(s) were synced.`, { output: "log" }); + refreshApps?.(); + if (success) { + logger.toast(`${appsToAdd.size + appsToUpdate.length}/${sunshineApps.length} app(s) were synced.`, { output: "log" }); + } else { + logger.toast("Some app(s) failed to sync synced.", { output: "error" }); + } } } else { logger.toast("Apps are in sync.", { output: "log" }); } } -export const SyncButton: VFC = ({ shortcuts, moonDeckHostApps, hostName, buddyProxy, moonlightExecPath, refreshApps }) => { +export const SunshineAppsSyncButton: VFC = ({ shortcuts, buddyProxy, settings, hostSettings, noConfirmationDialog, refreshApps }) => { + const [syncing, setSyncing] = useState(false); + const handleClick = (): void => { + const onOk = (): void => { + (async () => { + setSyncing(true); + await syncShortcuts( + shortcuts ?? await getAllExternalAppDetails(), + hostSettings.hostApp.apps, + hostSettings.hostName, + buddyProxy, + settings.useMoonlightExec ? settings.moonlightExecPath || null : null, + hostSettings.sunshineApps.lastSelectedOverride, + refreshApps); + })() + .catch((e) => logger.critical(e)) + .finally(() => { setSyncing(false); }); + }; + + if (syncing) { + return; + } + + if (noConfirmationDialog) { + onOk(); + return; + } + showModal( { syncShortcuts(shortcuts, moonDeckHostApps, hostName, buddyProxy, moonlightExecPath, refreshApps).catch((e) => logger.critical(e)); }} + onOK={onOk} /> ); }; return ( - handleClick()}> - Sync + handleClick()}> + Sync Apps ); }; diff --git a/src/components/sunshineappsview/batchresoverridebutton.tsx b/src/components/sunshineappsview/batchresoverridebutton.tsx index 342dd71..886a22c 100644 --- a/src/components/sunshineappsview/batchresoverridebutton.tsx +++ b/src/components/sunshineappsview/batchresoverridebutton.tsx @@ -1,11 +1,12 @@ import { AppDetails, DialogButton, showModal } from "@decky/ui"; -import { HostSettings, logger, setAppResolutionOverride } from "../../lib"; +import { HostSettings, SettingsManager, logger, setAppResolutionOverride } from "../../lib"; import { VFC, useState } from "react"; import { BatchResOverrideModal } from "./batchresoverridemodal"; interface Props { hostSettings: HostSettings; shortcuts: AppDetails[]; + settingsManager: SettingsManager; } async function applyResolution(shortcuts: AppDetails[], resolution: string): Promise { @@ -22,7 +23,7 @@ async function applyResolution(shortcuts: AppDetails[], resolution: string): Pro logger.toast(`Applied ${resolution} to ${counter}/${shortcuts.length} app(s).`, { output: "log" }); } -export const BatchResOverrideButton: VFC = ({ hostSettings, shortcuts }) => { +export const BatchResOverrideButton: VFC = ({ hostSettings, shortcuts, settingsManager }) => { const [busy, setBusy] = useState(false); const doApply = (resolution: string): void => { if (busy) { @@ -30,7 +31,10 @@ export const BatchResOverrideButton: VFC = ({ hostSettings, shortcuts }) } setBusy(true); - applyResolution(shortcuts, resolution).catch((e) => logger.critical(e)).finally(() => setBusy(false)); + applyResolution(shortcuts, resolution) + .then(() => { settingsManager.updateHost((settings) => { settings.sunshineApps.lastSelectedOverride = resolution; }); }) + .catch((e) => logger.critical(e)) + .finally(() => setBusy(false)); }; const handleClick = (): void => { @@ -42,7 +46,7 @@ export const BatchResOverrideButton: VFC = ({ hostSettings, shortcuts }) return ( handleClick()}> - { busy ? "Working..." : "Select resolution" } + {busy ? "Working..." : "Select resolution"} ); }; diff --git a/src/components/sunshineappsview/sunshineappsview.tsx b/src/components/sunshineappsview/sunshineappsview.tsx index 9409a16..1679088 100644 --- a/src/components/sunshineappsview/sunshineappsview.tsx +++ b/src/components/sunshineappsview/sunshineappsview.tsx @@ -1,12 +1,11 @@ import { AppDetails, DialogBody, DialogButton, DialogControlsSection, DialogControlsSectionHeader, Field, Navigation } from "@decky/ui"; import { BuddyProxy, SettingsManager, getAllExternalAppDetails, logger } from "../../lib"; +import { LabelWithIcon, SunshineAppsSyncButton, ToggleField } from "../shared"; import { ReactNode, VFC, useEffect, useState } from "react"; import { useCurrentHostSettings, useCurrentSettings } from "../../hooks"; import { BatchResOverrideButton } from "./batchresoverridebutton"; import { HostOff } from "../icons"; -import { LabelWithIcon } from "../shared"; import { PurgeButton } from "./purgebutton"; -import { SyncButton } from "./syncbutton"; interface Props { settingsManager: SettingsManager; @@ -47,8 +46,14 @@ export const SunshineAppsView: VFC = ({ settingsManager, buddyProxy }) => description="Steam client might be restarted afterwards!" childrenContainerWidth="fixed" > - + + settingsManager.updateHost((settings) => { settings.sunshineApps.showQuickAccessButton = value; })} + />
; } @@ -74,7 +79,7 @@ export const SunshineAppsView: VFC = ({ settingsManager, buddyProxy }) => } let batchResOverrideButton: ReactNode = null; - if (hostSettings && shortcuts.length > 0) { + if (hostSettings) { batchResOverrideButton = App Resolution Override @@ -85,7 +90,7 @@ export const SunshineAppsView: VFC = ({ settingsManager, buddyProxy }) =>
Steam shortcut's resolution property needs to be set for all Sunshine apps to match the Moonlight client resolution.
Otherwise the gamescope may try to apply scaling and you might get a blurry text/image.

-
Note: this is already automatically done for MoonDeck-Steam apps, for MoonDeck-Sunshine apps you must do it manually.
+
Note: this is already automatically done for MoonDeck-Steam apps, for synced Sunshine apps you must do it manually.
} focusable={true} @@ -93,8 +98,13 @@ export const SunshineAppsView: VFC = ({ settingsManager, buddyProxy }) => + Override for newly added apps (last used): {hostSettings.sunshineApps.lastSelectedOverride} + + } > - +
; } diff --git a/src/lib/serverproxy.ts b/src/lib/serverproxy.ts index 8ac35c3..a360e4f 100644 --- a/src/lib/serverproxy.ts +++ b/src/lib/serverproxy.ts @@ -109,6 +109,10 @@ export class ServerProxy { appUpdate: currentSettings?.runnerTimeouts.appUpdate ?? appUpdateDefault, streamEnd: currentSettings?.runnerTimeouts.streamEnd ?? streamEndDefault, wakeOnLan: currentSettings?.runnerTimeouts.wakeOnLan ?? wakeOnLanDefault + }, + sunshineApps: { + showQuickAccessButton: currentSettings?.sunshineApps.showQuickAccessButton ?? false, + lastSelectedOverride: currentSettings?.sunshineApps.lastSelectedOverride ?? "Default" } }; diff --git a/src/lib/settingsmanager.ts b/src/lib/settingsmanager.ts index ad0c0e0..52769ad 100644 --- a/src/lib/settingsmanager.ts +++ b/src/lib/settingsmanager.ts @@ -64,6 +64,11 @@ export interface HostResolution { dimensions: Dimension[]; } +export interface SunshineAppsSettings { + showQuickAccessButton: boolean; + lastSelectedOverride: string; +} + export interface HostSettings { hostInfoPort: number; buddyPort: number; @@ -76,6 +81,7 @@ export interface HostSettings { resolution: HostResolution; hostApp: HostApp; runnerTimeouts: RunnerTimeouts; + sunshineApps: SunshineAppsSettings; } export interface GameSessionSettings { diff --git a/src/steam-utils/restartSteamClient.ts b/src/steam-utils/restartSteamClient.ts index 52ffdd7..6b30d2a 100644 --- a/src/steam-utils/restartSteamClient.ts +++ b/src/steam-utils/restartSteamClient.ts @@ -11,7 +11,7 @@ import { logger } from "../lib/logger"; */ export function restartSteamClient(): void { try { - (SteamClient as SteamClientEx).User.StartRestart(); + (SteamClient as SteamClientEx).User.StartRestart(true); } catch (error) { logger.critical(error); } diff --git a/src/steam-utils/shared.ts b/src/steam-utils/shared.ts index f3c72df..3913570 100644 --- a/src/steam-utils/shared.ts +++ b/src/steam-utils/shared.ts @@ -30,7 +30,7 @@ export interface SteamClientEx { RegisterForLoginStateChange: (callback: (username: string) => void) => { unregister: () => void }; RegisterForPrepareForSystemSuspendProgress: (callback: (info: SystemSuspendInfo) => void) => { unregister: () => void }; RegisterForResumeSuspendedGamesProgress: (callback: (info: SystemResumeInfo) => void) => { unregister: () => void }; - StartRestart: () => void; + StartRestart: (param: boolean) => void; }; System: { DisplayManager: {