Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 289 additions & 10 deletions py_modules/proton_launch/plugin.py

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions src/components/BadgeIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import { IconType } from "react-icons";

interface BadgeIconProps {
icon: IconType;
color: string;
size?: number;
}

export const BadgeIcon: React.FC<BadgeIconProps> = ({
icon: Icon,
color,
size = 9,
}) => (
<span
style={{
background: "rgba(0,0,0,0.65)",
borderRadius: "3px",
padding: "2px 3px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color,
}}
>
<Icon size={size} />
</span>
);
55 changes: 39 additions & 16 deletions src/components/GameCover.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
import React, { useState, useEffect } from "react";
import { call } from "@decky/api";
import { FaSteam } from "react-icons/fa";
import { SteamGame } from "../data/types";

const COVER_URL = (appid: number) =>
`https://cdn.cloudflare.steamstatic.com/steam/apps/${appid}/header.jpg`;
import { getCachedCover, setCachedCover } from "../utils/coverCache";

interface GameCoverProps {
game: SteamGame;
}

export const GameCover: React.FC<GameCoverProps> = ({ game }) => {
const [shortcutCover, setShortcutCover] = useState<string | null>(null);
const [cover, setCover] = useState<string | null>(null);

useEffect(() => {
if (!game.is_shortcut) return;
call<[number], string>("get_shortcut_cover", game.appid).then((url) => {
if (url) setShortcutCover(url);
const cached = getCachedCover(game.appid);
if (cached !== undefined) {
if (cached) setCover(cached);
return;
}
call<[number], string>("get_game_cover", game.appid).then((url) => {
if (url) {
setCachedCover(game.appid, url);
setCover(url);
}
});
}, [game.appid, game.is_shortcut]);
}, [game.appid]);

const src = game.is_shortcut ? shortcutCover : COVER_URL(game.appid);
if (!src) return null;
if (!cover) return null;

return (
<div style={{ padding: "0 16px 12px" }}>
<img
src={src}
alt=""
style={{ width: "100%", borderRadius: "6px", display: "block" }}
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = "none"; }}
/>
<div style={{ position: "relative" }}>
<img
src={cover}
alt=""
style={{
width: "100%",
aspectRatio: "460 / 215",
objectFit: "cover",
borderRadius: "6px",
display: "block",
}}
/>
{!game.is_shortcut && (
<div style={{ position: "absolute", top: 6, left: 6 }}>
<FaSteam
size={14}
style={{
color: "rgba(255,255,255,0.35)",
filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.6))",
}}
/>
</div>
)}
</div>
</div>
);
};
142 changes: 66 additions & 76 deletions src/components/GameRow.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React, { useState, useEffect } from "react";
import { DialogButton } from "@decky/ui";
import { call } from "@decky/api";
import { FaCog, FaSteam } from "react-icons/fa";
import { SteamGame } from "../data/types";
import { BadgeIcon } from "./BadgeIcon";
import { getCachedCover, setCachedCover } from "../utils/coverCache";

export const GAME_ROW_STYLES = `
.plch-game-row:focus {
border: 2px solid #dcdedf !important;
outline: 2px solid #dcdedf !important;
outline-offset: 0px !important;
background: #2a3a4a !important;
}
.plch-val-btn:focus {
Expand All @@ -14,28 +18,38 @@ export const GAME_ROW_STYLES = `
}
`;

const COVER_URL = (appid: number) =>
`https://cdn.cloudflare.steamstatic.com/steam/apps/${appid}/header.jpg`;

interface GameRowProps {
game: SteamGame;
hasProfile: boolean;
isRunning: boolean;
nowPlaying?: boolean;
profileStatus?: "configured" | "ready";
onClick: () => void;
}

export const GameRow: React.FC<GameRowProps> = ({ game, hasProfile, isRunning, nowPlaying, onClick }) => {
const [shortcutCover, setShortcutCover] = useState<string | null>(null);
export const GameRow: React.FC<GameRowProps> = ({
game,
hasProfile,
nowPlaying,
profileStatus,
onClick,
}) => {
const [cover, setCover] = useState<string | null>(null);

useEffect(() => {
if (!game.is_shortcut) return;
call<[number], string>("get_shortcut_cover", game.appid).then((url) => {
if (url) setShortcutCover(url);
const cached = getCachedCover(game.appid);
if (cached !== undefined) {
if (cached) setCover(cached);
return;
}
call<[number], string>("get_game_cover", game.appid).then((url) => {
if (url) {
setCachedCover(game.appid, url);
setCover(url);
}
});
}, [game.appid, game.is_shortcut]);
}, [game.appid]);

const border = nowPlaying ? "2px solid #4caf50" : hasProfile ? "2px solid #f5a623" : "2px solid transparent";
const border = nowPlaying ? "1px solid #4caf50" : "2px solid transparent";
const background = nowPlaying ? "#0d1f0d" : "#1a1a2e";

return (
Expand All @@ -51,87 +65,63 @@ export const GameRow: React.FC<GameRowProps> = ({ game, hasProfile, isRunning, n
background,
display: "flex",
flexDirection: "row",
alignItems: "center",
alignItems: "stretch",
width: "100%",
}}
>
<div style={{ position: "relative", width: 80, height: 37, flexShrink: 0 }}>
{game.is_shortcut && shortcutCover ? (
<div
style={{
position: "relative",
width: 80,
minHeight: 37,
flexShrink: 0,
overflow: "hidden",
}}
>
{cover ? (
<img
src={shortcutCover}
src={cover}
alt=""
style={{ width: 80, height: 37, objectFit: "cover", display: "block" }}
/>
) : game.is_shortcut ? (
<div
style={{
width: 80,
height: 37,
background: "#2a2a3e",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 9,
color: "#666",
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
}}
>
Non-Steam
</div>
/>
) : (
<img
src={COVER_URL(game.appid)}
alt=""
style={{ width: 80, height: 37, objectFit: "cover", display: "block" }}
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
<div
style={{ position: "absolute", inset: 0, background: "#2a2a3e" }}
/>
)}
{(hasProfile || isRunning) && (
<div
style={{
position: "absolute",
bottom: 3,
left: 3,
display: "flex",
gap: "3px",
}}
>
{hasProfile && (
<span
style={{
background: "rgba(0,0,0,0.65)",
borderRadius: "3px",
padding: "1px 3px",
fontSize: 10,
lineHeight: 1,
color: "#f5a623",
}}
>
</span>
)}
{isRunning && (
<span
style={{
background: "rgba(0,0,0,0.65)",
borderRadius: "3px",
padding: "1px 3px",
fontSize: 10,
lineHeight: 1,
color: "#4caf50",
}}
>
</span>
)}
{!game.is_shortcut && (
<div style={{ position: "absolute", top: 3, left: 3 }}>
<BadgeIcon
icon={FaSteam}
color="rgba(255,255,255,0.4)"
size={8}
/>
</div>
)}
{hasProfile && (
<div style={{ position: "absolute", bottom: 3, left: 3 }}>
<BadgeIcon
icon={FaCog}
color={profileStatus === "ready" ? "#4caf50" : "#f5a623"}
/>
</div>
)}
</div>
<div
style={{
padding: "4px 8px",
fontSize: 12,
padding: "0 8px",
fontSize: 11,
color: "#fff",
flex: 1,
display: "flex",
alignItems: "center",
}}
>
<span
Expand Down
22 changes: 20 additions & 2 deletions src/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,28 @@ export const NavBar: React.FC<NavBarProps> = ({
</div>
<div style={{ flex: 1 }} />
{scriptStatus === "current" && (
<span style={{ color: "#4caf50", fontSize: 14, lineHeight: 1, paddingRight: 2 }}>✓</span>
<span
style={{
color: "#4caf50",
fontSize: 14,
lineHeight: 1,
paddingRight: 2,
}}
>
</span>
)}
{scriptStatus === "outdated" && (
<span style={{ color: "#f5a623", fontSize: 14, lineHeight: 1, paddingRight: 2 }}>⚠</span>
<span
style={{
color: "#f5a623",
fontSize: 14,
lineHeight: 1,
paddingRight: 2,
}}
>
</span>
)}
<ActionButton onClick={onSettings}>
<FiSettings size={16} />
Expand Down
6 changes: 4 additions & 2 deletions src/components/NowPlayingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ interface NowPlayingCardProps {
onSelect: () => void;
}

export const NowPlayingCard: React.FC<NowPlayingCardProps> = ({ game, onSelect }) => (
export const NowPlayingCard: React.FC<NowPlayingCardProps> = ({
game,
onSelect,
}) => (
<PanelSectionCustom>
<GameRow
game={game}
hasProfile={false}
isRunning={true}
nowPlaying={true}
onClick={onSelect}
/>
Expand Down
10 changes: 9 additions & 1 deletion src/components/PanelSectionCustom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ const PanelSectionCustom: React.FC<{
children: React.ReactNode;
style?: React.CSSProperties;
}> = ({ children, style }) => (
<div style={{ padding: "4px 16px 1em 16px", ...style }}>
<div
style={{
paddingTop: "4px",
paddingBottom: "1em",
...style,
paddingRight: "16px",
paddingLeft: "16px",
}}
>
{children}
</div>
);
Expand Down
32 changes: 32 additions & 0 deletions src/components/SearchField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import { TextField } from "@decky/ui";
import { useTranslation } from "react-i18next";
import PanelSectionCustom from "./PanelSectionCustom";

interface SearchFieldProps {
value: string;
onChange: (value: string) => void;
}

export const SearchField: React.FC<SearchFieldProps> = ({
value,
onChange,
}) => {
const { t } = useTranslation();
return (
<PanelSectionCustom
style={{ paddingBottom: "0", paddingTop: "0", marginBottom: "0" }}
>
<TextField
value={value}
onChange={(e) => onChange(e.target.value)}
label={t("search")}
style={{
width: "100%",
marginBottom: "0!important",
padding: "4px 10px",
}}
/>
</PanelSectionCustom>
);
};
Loading
Loading