Skip to content

Commit

Permalink
Sunshine apps improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
FrogTheFrog committed Aug 18, 2024
1 parent 0a0db3a commit de903e5
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 33 deletions.
2 changes: 1 addition & 1 deletion defaults/python/lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions defaults/python/lib/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class HostResolution(TypedDict):
dimensions: List[Dimension]


class SunshineAppsSettings(TypedDict):
showQuickAccessButton: bool
lastSelectedOverride: str


class HostSettings(TypedDict):
hostInfoPort: int
buddyPort: int
Expand All @@ -60,6 +65,7 @@ class HostSettings(TypedDict):
resolution: HostResolution
hostApp: HostApp
runnerTimeouts: RunnerTimeouts
sunshineApps: SunshineAppsSettings


class GameSessionSettings(TypedDict):
Expand Down Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion src/components/changelogview/changelogview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ export const ChangelogView: VFC<unknown> = () => {
<DialogControlsSection>
<Field
label="1.7.2"
description="Migrate plugin to use new Decky API."
description={
<>
<div>&bull; Migrate plugin to use new Decky API.</div>
<div>&bull; Sunshine app sync button can now be added to the QAM.</div>
<div>&bull; Last used app resolution override will be used when adding new Sunshine apps.</div>
</>
}
focusable={true}
/>
<Field
Expand Down
2 changes: 1 addition & 1 deletion src/components/quicksettingsview/hostapppanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const HostAppPanel: VFC<Props> = ({ currentHostSettings, settingsManager
}

return (
<PanelSection title="APP NAME">
<PanelSection title="STREAMING APP NAME">
<PanelSectionRow>
<Field
childrenContainerWidth="fixed"
Expand Down
2 changes: 2 additions & 0 deletions src/components/quicksettingsview/quicksettingsview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HostAppPanel } from "./hostapppanel";
import { HostCommandPanel } from "./hostcommandpanel";
import { HostStatusPanel } from "./hoststatuspanel";
import { ResolutionPanel } from "./resolutionpanel";
import { SunshineAppsPanel } from "./sunshineappspanel";
import { VFC } from "react";
import { useQuickAccessVisible } from "@decky/api";

Expand All @@ -28,6 +29,7 @@ export const QuickSettingsView: VFC<Props> = ({ connectivityManager, settingsMan
? <GameSessionPanel appData={appData} moonDeckAppLauncher={moonDeckAppLauncher} />
: <>
<HostStatusPanel currentHostSettings={currentHostSettings} currentSettings={currentSettings} settingsManager={settingsManager} serverStatus={serverStatus} serverRefreshStatus={serverRefreshStatus} buddyStatus={buddyStatus} buddyRefreshStatus={buddyRefreshStatus} />
<SunshineAppsPanel buddyProxy={connectivityManager.buddyProxy} currentHostSettings={currentHostSettings} currentSettings={currentSettings} />
<HostAppPanel currentHostSettings={currentHostSettings} settingsManager={settingsManager} />
<ResolutionPanel currentHostSettings={currentHostSettings} settingsManager={settingsManager} />
</>
Expand Down
34 changes: 34 additions & 0 deletions src/components/quicksettingsview/sunshineappspanel.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ buddyProxy, currentHostSettings, currentSettings }) => {
if (currentHostSettings === null || currentSettings === null) {
return null;
}

if (!currentHostSettings.sunshineApps.showQuickAccessButton) {
return null;
}

return (
<PanelSection title="SUNSHINE APPS">
<PanelSectionRow>
<Field
childrenContainerWidth="fixed"
spacingBetweenLabelAndChild="none"
>
<SunshineAppsSyncButton buddyProxy={buddyProxy} settings={currentSettings} hostSettings={currentHostSettings} noConfirmationDialog={true} />
</Field>
</PanelSectionRow>
</PanelSection>
);
};
1 change: 1 addition & 0 deletions src/components/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<void> {
async function updateLaunchOptions(appId: number, appName: string, hostName: string, customExec: boolean): Promise<boolean> {
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<void> {
async function updateResolutionOverride(appId: number, appName: string, resolution: string): Promise<boolean> {
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<void> {
let sunshineApps = await buddyProxy.getGamestreamAppNames();
if (sunshineApps === null) {
logger.toast("Failed to get Gamestream app list!", { output: "error" });
Expand All @@ -61,6 +71,7 @@ async function syncShortcuts(shortcuts: AppDetails[], moonDeckHostApps: string[]
const appsToAdd = new Set<string>();
const appsToUpdate: AppDetails[] = [];
const appsToRemove: AppDetails[] = [];
let success = true;

// Check which apps need to be added or updated
for (const sunshineApp of sunshineApps) {
Expand All @@ -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<Props> = ({ shortcuts, moonDeckHostApps, hostName, buddyProxy, moonlightExecPath, refreshApps }) => {
export const SunshineAppsSyncButton: VFC<Props> = ({ 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(
<ConfirmModal
strTitle="Are you sure you want to sync?"
strDescription="This action cannot be undone."
onOK={() => { syncShortcuts(shortcuts, moonDeckHostApps, hostName, buddyProxy, moonlightExecPath, refreshApps).catch((e) => logger.critical(e)); }}
onOK={onOk}
/>
);
};

return (
<DialogButton onClick={() => handleClick()}>
Sync
<DialogButton disabled={syncing} onClick={() => handleClick()}>
Sync Apps
</DialogButton>
);
};
12 changes: 8 additions & 4 deletions src/components/sunshineappsview/batchresoverridebutton.tsx
Original file line number Diff line number Diff line change
@@ -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<void> {
Expand All @@ -22,15 +23,18 @@ async function applyResolution(shortcuts: AppDetails[], resolution: string): Pro
logger.toast(`Applied ${resolution} to ${counter}/${shortcuts.length} app(s).`, { output: "log" });
}

export const BatchResOverrideButton: VFC<Props> = ({ hostSettings, shortcuts }) => {
export const BatchResOverrideButton: VFC<Props> = ({ hostSettings, shortcuts, settingsManager }) => {
const [busy, setBusy] = useState<boolean>(false);
const doApply = (resolution: string): void => {
if (busy) {
return;
}

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 => {
Expand All @@ -42,7 +46,7 @@ export const BatchResOverrideButton: VFC<Props> = ({ hostSettings, shortcuts })

return (
<DialogButton disabled={busy} onClick={() => handleClick()}>
{ busy ? "Working..." : "Select resolution" }
{busy ? "Working..." : "Select resolution"}
</DialogButton>
);
};
22 changes: 16 additions & 6 deletions src/components/sunshineappsview/sunshineappsview.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -47,8 +46,14 @@ export const SunshineAppsView: VFC<Props> = ({ settingsManager, buddyProxy }) =>
description="Steam client might be restarted afterwards!"
childrenContainerWidth="fixed"
>
<SyncButton shortcuts={shortcuts} moonDeckHostApps={hostSettings.hostApp.apps} hostName={hostSettings.hostName} buddyProxy={buddyProxy} moonlightExecPath={settings.useMoonlightExec ? settings.moonlightExecPath || null : null} refreshApps={refreshApps} />
<SunshineAppsSyncButton shortcuts={shortcuts} buddyProxy={buddyProxy} settings={settings} hostSettings={hostSettings} refreshApps={refreshApps} />
</Field>
<ToggleField
label="Show button in Quick Access Menu"
description="Confirmation dialog will not be displayed when using it!"
value={hostSettings.sunshineApps.showQuickAccessButton}
setValue={(value) => settingsManager.updateHost((settings) => { settings.sunshineApps.showQuickAccessButton = value; })}
/>
</DialogControlsSection>;
}

Expand All @@ -74,7 +79,7 @@ export const SunshineAppsView: VFC<Props> = ({ settingsManager, buddyProxy }) =>
}

let batchResOverrideButton: ReactNode = null;
if (hostSettings && shortcuts.length > 0) {
if (hostSettings) {
batchResOverrideButton =
<DialogControlsSection>
<DialogControlsSectionHeader>App Resolution Override</DialogControlsSectionHeader>
Expand All @@ -85,16 +90,21 @@ export const SunshineAppsView: VFC<Props> = ({ settingsManager, buddyProxy }) =>
<div>Steam shortcut's resolution property needs to be set for all Sunshine apps to match the Moonlight client resolution.</div>
<div>Otherwise the gamescope may try to apply scaling and you might get a blurry text/image.</div>
<br />
<div>Note: this is already automatically done for MoonDeck-Steam apps, for MoonDeck-Sunshine apps you must do it manually.</div>
<div>Note: this is already automatically done for MoonDeck-Steam apps, for synced Sunshine apps you must do it manually.</div>
</>
}
focusable={true}
/>
<Field
label="Select and apply resolution"
childrenContainerWidth="fixed"
description={
<>
Override for newly added apps (last used): {hostSettings.sunshineApps.lastSelectedOverride}
</>
}
>
<BatchResOverrideButton hostSettings={hostSettings} shortcuts={shortcuts} />
<BatchResOverrideButton hostSettings={hostSettings} shortcuts={shortcuts} settingsManager={settingsManager} />
</Field>
</DialogControlsSection>;
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/serverproxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
};

Expand Down
Loading

0 comments on commit de903e5

Please sign in to comment.