Merge branch 'main' into feature/seed-completed-downloads

This commit is contained in:
Hachi-R
2024-12-22 06:58:03 -03:00
64 changed files with 2765 additions and 670 deletions

View File

@@ -6,7 +6,7 @@
<title>Hydra</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *; connect-src *; font-src *;"
content="default-src 'self' 'unsafe-inline' * data: local:;"
/>
</head>
<body>

View File

@@ -2,8 +2,6 @@ import { useCallback, useContext, useEffect, useRef } from "react";
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
import Intercom from "@intercom/messenger-js-sdk";
import {
useAppDispatch,
useAppSelector,
@@ -36,10 +34,6 @@ export interface AppProps {
children: React.ReactNode;
}
Intercom({
app_id: import.meta.env.RENDERER_VITE_INTERCOM_APP_ID,
});
export function App() {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary, library } = useLibrary();
@@ -128,12 +122,21 @@ export function App() {
dispatch(setProfileBackground(profileBackground));
}
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
}
});
fetchUserDetails()
.then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
}
})
.finally(() => {
if (document.getElementById("external-resources")) return;
const $script = document.createElement("script");
$script.id = "external-resources";
$script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`;
document.head.appendChild($script);
});
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
const onSignIn = useCallback(() => {
@@ -223,9 +226,7 @@ export function App() {
useEffect(() => {
new MutationObserver(() => {
const modal = document.body.querySelector(
"[role=dialog]:not([data-intercom-frame='true'])"
);
const modal = document.body.querySelector("[data-hydra-dialog]");
dispatch(toggleDraggingDisabled(Boolean(modal)));
}).observe(document.body, {

View File

@@ -107,6 +107,7 @@ export function Modal({
aria-labelledby={title}
aria-describedby={description}
ref={modalContentRef}
data-hydra-dialog
>
<div className={styles.modalHeader}>
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>

View File

@@ -22,8 +22,6 @@ import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es";
import { CommentDiscussionIcon } from "@primer/octicons-react";
import { show, update } from "@intercom/messenger-js-sdk";
const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250;
const SIDEBAR_MAX_WIDTH = 450;
@@ -50,20 +48,7 @@ export function Sidebar() {
return sortBy(library, (game) => game.title);
}, [library]);
const { userDetails, hasActiveSubscription } = useUserDetails();
useEffect(() => {
if (userDetails) {
update({
name: userDetails.displayName,
Username: userDetails.username,
email: userDetails.email ?? undefined,
Email: userDetails.email,
"Subscription expiration date": userDetails?.subscription?.expiresAt,
"Payment status": userDetails?.subscription?.status,
});
}
}, [userDetails, hasActiveSubscription]);
const { hasActiveSubscription } = useUserDetails();
const { lastPacket, progress } = useDownload();
@@ -266,7 +251,11 @@ export function Sidebar() {
</div>
{hasActiveSubscription && (
<button type="button" className={styles.helpButton} onClick={show}>
<button
type="button"
className={styles.helpButton}
data-open-support-chat
>
<div className={styles.helpButtonIcon}>
<CommentDiscussionIcon size={14} />
</div>

View File

@@ -181,6 +181,7 @@ export function GameDetailsContextProvider({
shop,
i18n.language,
userDetails,
userPreferences,
]);
useEffect(() => {

View File

@@ -0,0 +1,46 @@
export function addCookieInterceptor(isStaging: boolean) {
const cookieKey = isStaging ? "cookies-staging" : "cookies";
Object.defineProperty(document, "cookie", {
enumerable: true,
configurable: true,
get() {
return localStorage.getItem(cookieKey) || "";
},
set(cookieString) {
try {
const [cookieName, cookieValue] = cookieString.split(";")[0].split("=");
const currentCookies = localStorage.getItem(cookieKey) || "";
const cookiesObject = parseCookieStringsToObjects(currentCookies);
cookiesObject[cookieName] = cookieValue;
const newString = Object.entries(cookiesObject)
.map(([key, value]) => {
return key + "=" + value;
})
.join("; ");
localStorage.setItem(cookieKey, newString);
} catch (err) {
console.error(err);
}
},
});
}
const parseCookieStringsToObjects = (
cookieStrings: string
): { [key: string]: string } => {
const result = {};
if (cookieStrings === "") return result;
cookieStrings.split(";").forEach((cookieString) => {
const [name, value] = cookieString.split("=");
result[name.trim()] = value.trim();
});
return result;
};

View File

@@ -67,8 +67,8 @@ declare global {
) => Promise<ShopDetails | null>;
getRandomGame: () => Promise<Steam250Game>;
getHowLongToBeat: (
shop: GameShop,
objectId: string
objectId: string,
shop: GameShop
) => Promise<HowLongToBeatCategory[] | null>;
getGames: (take?: number, skip?: number) => Promise<CatalogueEntry[]>;
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
@@ -87,8 +87,14 @@ declare global {
shop: GameShop
) => Promise<void>;
createGameShortcut: (id: number) => Promise<boolean>;
updateExecutablePath: (id: number, executablePath: string) => Promise<void>;
selectGameWinePrefix: (id: number, winePrefixPath: string) => Promise<void>;
updateExecutablePath: (
id: number,
executablePath: string | null
) => Promise<void>;
selectGameWinePrefix: (
id: number,
winePrefixPath: string | null
) => Promise<void>;
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
getLibrary: () => Promise<LibraryGame[]>;
openGameInstaller: (gameId: number) => Promise<boolean>;
@@ -170,6 +176,7 @@ declare global {
openExternal: (src: string) => Promise<void>;
openCheckout: () => Promise<void>;
getVersion: () => Promise<string>;
isStaging: () => Promise<boolean>;
ping: () => string;
getDefaultDownloadsPath: () => Promise<string>;
isPortableVersion: () => Promise<boolean>;

View File

@@ -56,6 +56,8 @@ export function useUserDetails() {
clearUserDetails();
}
window["userDetails"] = userDetails;
return userDetails;
});
}, [clearUserDetails]);

View File

@@ -20,6 +20,8 @@ import resources from "@locales";
import { RepacksContextProvider } from "./context";
import { SuspenseWrapper } from "./components";
import { logger } from "./logger";
import { addCookieInterceptor } from "./cookies";
const Home = React.lazy(() => import("./pages/home/home"));
const GameDetails = React.lazy(
@@ -34,6 +36,11 @@ const Achievements = React.lazy(
() => import("./pages/achievements/achievements")
);
console.log = logger.log;
const isStaging = await window.electron.isStaging();
addCookieInterceptor(isStaging);
i18n
.use(LanguageDetector)
.use(initReactI18next)

View File

@@ -95,6 +95,11 @@ export function GameOptionsModal({
await window.electron.openGameExecutablePath(game.id);
};
const handleClearExecutablePath = async () => {
await window.electron.updateExecutablePath(game.id, null);
updateGame();
};
const handleChangeWinePrefixPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
properties: ["openDirectory"],
@@ -106,6 +111,11 @@ export function GameOptionsModal({
}
};
const handleClearWinePrefixPath = async () => {
await window.electron.selectGameWinePrefix(game.id, null);
updateGame();
};
const shouldShowWinePrefixConfiguration =
window.electron.platform === "linux";
@@ -145,14 +155,21 @@ export function GameOptionsModal({
disabled
placeholder={t("no_executable_selected")}
rightContent={
<Button
type="button"
theme="outline"
onClick={handleChangeExecutableLocation}
>
<FileIcon />
{t("select_executable")}
</Button>
<>
<Button
type="button"
theme="outline"
onClick={handleChangeExecutableLocation}
>
<FileIcon />
{t("select_executable")}
</Button>
{game.executablePath && (
<Button onClick={handleClearExecutablePath} theme="outline">
{t("clear")}
</Button>
)}
</>
}
/>
@@ -186,14 +203,24 @@ export function GameOptionsModal({
disabled
placeholder={t("no_directory_selected")}
rightContent={
<Button
type="button"
theme="outline"
onClick={handleChangeWinePrefixPath}
>
<FileDirectoryIcon />
{t("select_executable")}
</Button>
<>
<Button
type="button"
theme="outline"
onClick={handleChangeWinePrefixPath}
>
<FileDirectoryIcon />
{t("select_executable")}
</Button>
{game.winePrefixPath && (
<Button
onClick={handleClearWinePrefixPath}
theme="outline"
>
{t("clear")}
</Button>
)}
</>
}
/>
</div>

View File

@@ -98,8 +98,8 @@ export function Sidebar() {
} else {
try {
const howLongToBeat = await window.electron.getHowLongToBeat(
shop,
objectId
objectId,
shop
);
if (howLongToBeat) {

View File

@@ -20,6 +20,7 @@ export function SettingsBehavior() {
startMinimized: false,
disableNsfwAlert: false,
seedAfterDownloadComplete: false,
showHiddenAchievementsDescription: false,
});
const { t } = useTranslation("settings");
@@ -32,6 +33,8 @@ export function SettingsBehavior() {
startMinimized: userPreferences.startMinimized,
disableNsfwAlert: userPreferences.disableNsfwAlert,
seedAfterDownloadComplete: userPreferences.seedAfterDownloadComplete,
showHiddenAchievementsDescription:
userPreferences.showHiddenAchievementsDescription,
});
}
}, [userPreferences]);
@@ -108,6 +111,17 @@ export function SettingsBehavior() {
})
}
/>
<CheckboxField
label={t("show_hidden_achievement_description")}
checked={form.showHiddenAchievementsDescription}
onChange={() =>
handleChange({
showHiddenAchievementsDescription:
!form.showHiddenAchievementsDescription,
})
}
/>
</>
);
}

View File

@@ -2,7 +2,7 @@
/// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv {
readonly RENDERER_VITE_INTERCOM_APP_ID: string;
readonly RENDERER_VITE_EXTERNAL_RESOURCES_URL: string;
}
interface ImportMeta {