Merge branch 'main' into feature/clearpaths

This commit is contained in:
Chubby Granny Chaser 2024-12-02 18:47:19 +00:00 committed by GitHub
commit 5f9397f6db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 92 additions and 244 deletions

View File

@ -1,4 +1,3 @@
MAIN_VITE_API_URL=API_URL MAIN_VITE_API_URL=API_URL
MAIN_VITE_AUTH_URL=AUTH_URL MAIN_VITE_AUTH_URL=AUTH_URL
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID

View File

@ -22,16 +22,6 @@ jobs:
with: with:
node-version: 20.18.0 node-version: 20.18.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Push build to R2
run: aws s3 sync ./docs s3://${{ vars.BUILDS_BUCKET_NAME }}
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
@ -58,6 +48,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }} MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }} RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows - name: Build Windows
@ -69,6 +60,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }} MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }} RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact - name: Create artifact

View File

@ -47,6 +47,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }} MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }} RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows - name: Build Windows
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
@ -57,6 +58,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }} MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }} RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact - name: Create artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@ -1,23 +1,21 @@
import type { HowLongToBeatCategory } from "@types"; import type { GameShop, HowLongToBeatCategory } from "@types";
import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { formatName } from "@shared"; import { HydraApi } from "@main/services";
const getHowLongToBeat = async ( const getHowLongToBeat = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
title: string objectId: string,
shop: GameShop
): Promise<HowLongToBeatCategory[] | null> => { ): Promise<HowLongToBeatCategory[] | null> => {
const response = await searchHowLongToBeat(title); const params = new URLSearchParams({
objectId,
const game = response.data.find((game) => { shop,
return formatName(game.game_name) === formatName(title);
}); });
if (!game) return null; return HydraApi.get(`/games/how-long-to-beat?${params.toString()}`, null, {
const howLongToBeat = await getHowLongToBeatGame(String(game.game_id)); needsAuth: false,
});
return howLongToBeat;
}; };
registerEvent("getHowLongToBeat", getHowLongToBeat); registerEvent("getHowLongToBeat", getHowLongToBeat);

View File

@ -1,4 +1,4 @@
import { appVersion, defaultDownloadsPath } from "@main/constants"; import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import "./catalogue/get-catalogue"; import "./catalogue/get-catalogue";
@ -72,5 +72,6 @@ import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion); ipcMain.handle("getVersion", () => appVersion);
ipcMain.handle("isStaging", () => isStaging);
ipcMain.handle("isPortableVersion", () => isPortableVersion()); ipcMain.handle("isPortableVersion", () => isPortableVersion());
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath); ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);

View File

@ -1,5 +1,4 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import parseTorrent from "parse-torrent";
import type { StartGameDownloadPayload } from "@types"; import type { StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi, logger } from "@main/services"; import { DownloadManager, HydraApi, logger } from "@main/services";
@ -9,7 +8,6 @@ import { createGame } from "@main/services/library-sync";
import { steamUrlBuilder } from "@shared"; import { steamUrlBuilder } from "@shared";
import { dataSource } from "@main/data-source"; import { dataSource } from "@main/data-source";
import { DownloadQueue, Game } from "@main/entity"; import { DownloadQueue, Game } from "@main/entity";
import { HydraAnalytics } from "@main/services/hydra-analytics";
const startGameDownload = async ( const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -91,17 +89,6 @@ const startGameDownload = async (
logger.error("Failed to create game download", err); logger.error("Failed to create game download", err);
}); });
if (uri.startsWith("magnet:")) {
try {
const { infoHash } = await parseTorrent(payload.uri);
if (infoHash) {
HydraAnalytics.postDownload(infoHash).catch(() => {});
}
} catch (err) {
logger.error("Failed to parse torrent", err);
}
}
await DownloadManager.cancelDownload(updatedGame!.id); await DownloadManager.cancelDownload(updatedGame!.id);
await DownloadManager.startDownload(updatedGame!); await DownloadManager.startDownload(updatedGame!);

View File

@ -1,108 +0,0 @@
import axios from "axios";
import { requestWebPage } from "@main/helpers";
import type {
HowLongToBeatCategory,
HowLongToBeatSearchResponse,
} from "@types";
import { formatName } from "@shared";
import { logger } from "./logger";
import UserAgent from "user-agents";
const state = {
apiKey: null as string | null,
};
const getHowLongToBeatSearchApiKey = async () => {
const userAgent = new UserAgent();
const document = await requestWebPage("https://howlongtobeat.com/");
const scripts = Array.from(document.querySelectorAll("script"));
const appScript = scripts.find((script) =>
script.src.startsWith("/_next/static/chunks/pages/_app")
);
if (!appScript) return null;
const response = await axios.get(
`https://howlongtobeat.com${appScript.src}`,
{
headers: {
"User-Agent": userAgent.toString(),
},
}
);
const results = /fetch\("\/api\/search\/"\.concat\("(.*?)"\)/gm.exec(
response.data
);
if (!results) return null;
return results[1];
};
export const searchHowLongToBeat = async (gameName: string) => {
state.apiKey = state.apiKey ?? (await getHowLongToBeatSearchApiKey());
if (!state.apiKey) return { data: [] };
const userAgent = new UserAgent();
const response = await axios
.post(
`https://howlongtobeat.com/api/search/${state.apiKey}`,
{
searchType: "games",
searchTerms: formatName(gameName).split(" "),
searchPage: 1,
size: 20,
},
{
headers: {
"User-Agent": userAgent.toString(),
Referer: "https://howlongtobeat.com/",
},
}
)
.catch((error) => {
logger.error("Error searching HowLongToBeat:", error?.response?.status);
return { data: { data: [] } };
});
return response.data as HowLongToBeatSearchResponse;
};
const parseListItems = ($lis: Element[]) => {
return $lis.map(($li) => {
const title = $li.querySelector("h4")?.textContent;
const [, accuracyClassName] = Array.from(($li as HTMLElement).classList);
const accuracy = accuracyClassName.split("time_").at(1);
return {
title: title ?? "",
duration: $li.querySelector("h5")?.textContent ?? "",
accuracy: accuracy ?? "",
};
});
};
export const getHowLongToBeatGame = async (
id: string
): Promise<HowLongToBeatCategory[]> => {
const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
const $ul = document.querySelector(".shadow_shadow ul");
if (!$ul) return [];
const $lis = Array.from($ul.children);
const [$firstLi] = $lis;
if ($firstLi.tagName === "DIV") {
const $pcData = $lis.find(($li) => $li.textContent?.includes("PC"));
return parseListItems(Array.from($pcData?.querySelectorAll("li") ?? []));
}
return parseListItems($lis);
};

View File

@ -1,34 +0,0 @@
import { userSubscriptionRepository } from "@main/repository";
import axios from "axios";
import { appVersion } from "@main/constants";
export class HydraAnalytics {
private static instance = axios.create({
baseURL: import.meta.env.MAIN_VITE_ANALYTICS_API_URL,
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
});
private static async hasActiveSubscription() {
const userSubscription = await userSubscriptionRepository.findOne({
where: { id: 1 },
});
return (
userSubscription?.expiresAt && userSubscription.expiresAt > new Date()
);
}
static async postDownload(hash: string) {
const hasSubscription = await this.hasActiveSubscription();
return this.instance
.post("/track", {
event: "download",
attributes: {
hash,
hasSubscription,
},
})
.then((response) => response.data);
}
}

View File

@ -12,6 +12,7 @@ import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
import { omit } from "lodash-es"; import { omit } from "lodash-es";
import { appVersion } from "@main/constants"; import { appVersion } from "@main/constants";
import { getUserData } from "./user/get-user-data"; import { getUserData } from "./user/get-user-data";
import { isFuture, isToday } from "date-fns";
interface HydraApiOptions { interface HydraApiOptions {
needsAuth?: boolean; needsAuth?: boolean;
@ -45,10 +46,8 @@ export class HydraApi {
} }
private static hasActiveSubscription() { private static hasActiveSubscription() {
return ( const expiresAt = this.userAuth.subscription?.expiresAt;
this.userAuth.subscription?.expiresAt && return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
this.userAuth.subscription.expiresAt > new Date()
);
} }
static async handleExternalAuth(uri: string) { static async handleExternalAuth(uri: string) {

View File

@ -4,7 +4,6 @@ export * from "./steam-250";
export * from "./steam-grid"; export * from "./steam-grid";
export * from "./window-manager"; export * from "./window-manager";
export * from "./download"; export * from "./download";
export * from "./how-long-to-beat";
export * from "./process-watcher"; export * from "./process-watcher";
export * from "./main-loop"; export * from "./main-loop";
export * from "./hydra-api"; export * from "./hydra-api";

View File

@ -42,8 +42,8 @@ contextBridge.exposeInMainWorld("electron", {
getGameShopDetails: (objectId: string, shop: GameShop, language: string) => getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
ipcRenderer.invoke("getGameShopDetails", objectId, shop, language), ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
getRandomGame: () => ipcRenderer.invoke("getRandomGame"), getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
getHowLongToBeat: (title: string) => getHowLongToBeat: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getHowLongToBeat", title), ipcRenderer.invoke("getHowLongToBeat", objectId, shop),
getGames: (take?: number, skip?: number) => getGames: (take?: number, skip?: number) =>
ipcRenderer.invoke("getGames", take, skip), ipcRenderer.invoke("getGames", take, skip),
searchGameRepacks: (query: string) => searchGameRepacks: (query: string) =>
@ -198,6 +198,7 @@ contextBridge.exposeInMainWorld("electron", {
ping: () => ipcRenderer.invoke("ping"), ping: () => ipcRenderer.invoke("ping"),
getVersion: () => ipcRenderer.invoke("getVersion"), getVersion: () => ipcRenderer.invoke("getVersion"),
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"), getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
isStaging: () => ipcRenderer.invoke("isStaging"),
isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"), isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"),
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
openCheckout: () => ipcRenderer.invoke("openCheckout"), openCheckout: () => ipcRenderer.invoke("openCheckout"),

View File

@ -6,7 +6,7 @@
<title>Hydra</title> <title>Hydra</title>
<meta <meta
http-equiv="Content-Security-Policy" 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' *;"
/> />
</head> </head>
<body> <body>

View File

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

View File

@ -107,6 +107,7 @@ export function Modal({
aria-labelledby={title} aria-labelledby={title}
aria-describedby={description} aria-describedby={description}
ref={modalContentRef} ref={modalContentRef}
data-hydra-dialog
> >
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}> <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 { sortBy } from "lodash-es";
import { CommentDiscussionIcon } from "@primer/octicons-react"; import { CommentDiscussionIcon } from "@primer/octicons-react";
import { show, update } from "@intercom/messenger-js-sdk";
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_INITIAL_WIDTH = 250;
const SIDEBAR_MAX_WIDTH = 450; const SIDEBAR_MAX_WIDTH = 450;
@ -50,20 +48,7 @@ export function Sidebar() {
return sortBy(library, (game) => game.title); return sortBy(library, (game) => game.title);
}, [library]); }, [library]);
const { userDetails, hasActiveSubscription } = useUserDetails(); const { 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 { lastPacket, progress } = useDownload(); const { lastPacket, progress } = useDownload();
@ -266,7 +251,11 @@ export function Sidebar() {
</div> </div>
{hasActiveSubscription && ( {hasActiveSubscription && (
<button type="button" className={styles.helpButton} onClick={show}> <button
type="button"
className={styles.helpButton}
data-open-support-chat
>
<div className={styles.helpButtonIcon}> <div className={styles.helpButtonIcon}>
<CommentDiscussionIcon size={14} /> <CommentDiscussionIcon size={14} />
</div> </div>

View File

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

View File

@ -60,7 +60,8 @@ declare global {
) => Promise<ShopDetails | null>; ) => Promise<ShopDetails | null>;
getRandomGame: () => Promise<Steam250Game>; getRandomGame: () => Promise<Steam250Game>;
getHowLongToBeat: ( getHowLongToBeat: (
title: string objectId: string,
shop: GameShop
) => Promise<HowLongToBeatCategory[] | null>; ) => Promise<HowLongToBeatCategory[] | null>;
getGames: (take?: number, skip?: number) => Promise<CatalogueEntry[]>; getGames: (take?: number, skip?: number) => Promise<CatalogueEntry[]>;
searchGameRepacks: (query: string) => Promise<GameRepack[]>; searchGameRepacks: (query: string) => Promise<GameRepack[]>;
@ -162,6 +163,7 @@ declare global {
openExternal: (src: string) => Promise<void>; openExternal: (src: string) => Promise<void>;
openCheckout: () => Promise<void>; openCheckout: () => Promise<void>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
isStaging: () => Promise<boolean>;
ping: () => string; ping: () => string;
getDefaultDownloadsPath: () => Promise<string>; getDefaultDownloadsPath: () => Promise<string>;
isPortableVersion: () => Promise<boolean>; isPortableVersion: () => Promise<boolean>;

View File

@ -14,6 +14,7 @@ import type {
UserDetails, UserDetails,
} from "@types"; } from "@types";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import { isFuture, isToday } from "date-fns";
export function useUserDetails() { export function useUserDetails() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -128,10 +129,8 @@ export function useUserDetails() {
const unblockUser = (userId: string) => window.electron.unblockUser(userId); const unblockUser = (userId: string) => window.electron.unblockUser(userId);
const hasActiveSubscription = useMemo(() => { const hasActiveSubscription = useMemo(() => {
return ( const expiresAt = userDetails?.subscription?.expiresAt;
userDetails?.subscription?.expiresAt && return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
new Date(userDetails.subscription.expiresAt) > new Date()
);
}, [userDetails]); }, [userDetails]);
return { return {

View File

@ -75,7 +75,7 @@ export function CloudSyncFilesModal({
showSuccessToast(t("custom_backup_location_set")); showSuccessToast(t("custom_backup_location_set"));
getGameBackupPreview(); getGameBackupPreview();
} }
}, [objectId, setValue, shop, showSuccessToast, getGameBackupPreview]); }, [objectId, setValue, shop, showSuccessToast, getGameBackupPreview, t]);
const handleFileMappingMethodClick = useCallback( const handleFileMappingMethodClick = useCallback(
(mappingOption: FileMappingMethod) => { (mappingOption: FileMappingMethod) => {

View File

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

View File

@ -45,22 +45,25 @@ export function ProfileContent() {
return userProfile?.relation?.status === "ACCEPTED"; return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]); }, [userProfile]);
const buildUserGameDetailsPath = (game: UserGame) => { const buildUserGameDetailsPath = useCallback(
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) { (game: UserGame) => {
return buildGameDetailsPath({ if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
...game, return buildGameDetailsPath({
objectId: game.objectId, ...game,
}); objectId: game.objectId,
} });
}
const userParams = userProfile const userParams = userProfile
? { ? {
userId: userProfile.id, userId: userProfile.id,
} }
: undefined; : undefined;
return buildGameAchievementPath({ ...game }, userParams); return buildGameAchievementPath({ ...game }, userParams);
}; },
[userProfile]
);
const formatPlayTime = useCallback( const formatPlayTime = useCallback(
(playTimeInSeconds = 0) => { (playTimeInSeconds = 0) => {
@ -259,6 +262,7 @@ export function ProfileContent() {
userStats, userStats,
numberFormatter, numberFormatter,
t, t,
buildUserGameDetailsPath,
formatPlayTime, formatPlayTime,
navigate, navigate,
]); ]);

View File

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

View File

@ -46,7 +46,7 @@ export const removeSymbolsFromName = (name: string) =>
export const removeSpecialEditionFromName = (name: string) => export const removeSpecialEditionFromName = (name: string) =>
name.replace( name.replace(
/(The |Digital )?(GOTY|Deluxe|Standard|Ultimate|Definitive|Enhanced|Collector's|Premium|Digital|Limited|Game of the Year|Reloaded|[0-9]{4}) Edition/g, /(The |Digital )?(GOTY|Deluxe|Standard|Ultimate|Definitive|Enhanced|Collector's|Premium|Digital|Limited|Game of the Year|Reloaded|[0-9]{4}) Edition/gi,
"" ""
); );
@ -73,7 +73,8 @@ export const formatName = pipe<string>(
replaceUnderscoreWithSpace, replaceUnderscoreWithSpace,
replaceDotsWithSpace, replaceDotsWithSpace,
replaceNbspWithSpace, replaceNbspWithSpace,
(str) => str.replace(/DIRECTOR'S CUT/g, ""), (str) => str.replace(/DIRECTOR'S CUT/gi, ""),
(str) => str.replace(/Friend's Pass/gi, ""),
removeSymbolsFromName, removeSymbolsFromName,
removeDuplicateSpaces, removeDuplicateSpaces,
(str) => str.trim() (str) => str.trim()