mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
Merge branch 'main' into feature/clearpaths
This commit is contained in:
commit
5f9397f6db
@ -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
|
|
||||||
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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!);
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
};
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) {
|
||||||
|
@ -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";
|
||||||
|
@ -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"),
|
||||||
|
@ -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>
|
||||||
|
@ -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, {
|
||||||
|
@ -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" }}>
|
||||||
|
@ -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>
|
||||||
|
@ -181,6 +181,7 @@ export function GameDetailsContextProvider({
|
|||||||
shop,
|
shop,
|
||||||
i18n.language,
|
i18n.language,
|
||||||
userDetails,
|
userDetails,
|
||||||
|
userPreferences,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
@ -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>;
|
||||||
|
@ -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 {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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({
|
||||||
|
@ -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,
|
||||||
]);
|
]);
|
||||||
|
2
src/renderer/src/vite-env.d.ts
vendored
2
src/renderer/src/vite-env.d.ts
vendored
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user