Compare commits

..

17 Commits

Author SHA1 Message Date
Chubby Granny Chaser
9b9b3f73d0 fix: fixing data: links on CSP
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
2024-12-02 20:37:43 +00:00
Chubby Granny Chaser
de36965017 fix: prevent loading external resources twice 2024-12-02 19:37:32 +00:00
Chubby Granny Chaser
e9fddd2456 Merge branch 'main' of github.com:hydralauncher/hydra 2024-12-02 19:13:41 +00:00
Chubby Granny Chaser
8b4791f1f4 chore: bump version 2024-12-02 19:12:39 +00:00
Chubby Granny Chaser
870e45991b Merge pull request #1268 from hydralauncher/fix/migrating-hltb
Fix/migrating hltb
2024-12-02 18:47:08 +00:00
Chubby Granny Chaser
38e94c92d7 fix: cache-busting external resources 2024-12-02 18:44:03 +00:00
Chubby Granny Chaser
93fbf7657d fix: returning hydra api call directly 2024-12-02 18:15:45 +00:00
Chubby Granny Chaser
0fc6d69851 fix: migrating hltb to api 2024-12-02 17:58:13 +00:00
Chubby Granny Chaser
7f600a0cbf feat: adding csp update 2024-12-02 17:10:13 +00:00
Zamitto
5bc424796a Merge pull request #1219 from hydralauncher/feat/intercom-user-id
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
feat: add intercom user id
2024-11-28 11:27:57 -03:00
Zamitto
7f33b63bed Merge branch 'main' into feat/intercom-user-id 2024-11-28 09:55:15 -03:00
Zamitto
57e0fb493f Merge pull request #1261 from hydralauncher/fix/correct-subscription-date-validation
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
fix: subscription date validation
2024-11-27 13:37:32 -03:00
Zamitto
730ea4f2b9 fix: subscription date validation 2024-11-27 13:27:13 -03:00
Zamitto
8cfe5b4d34 fix: add friend's pass to format name 2024-11-11 10:35:33 -03:00
Zamitto
a2e41b81a3 Merge branch 'main' into feat/intercom-user-id 2024-11-10 23:38:07 -03:00
Zamitto
6e6469d90f fix: format ignore case 2024-11-10 20:55:23 -03:00
Zamitto
2828640ed7 feat: add intercom user id 2024-11-09 02:54:23 -03:00
25 changed files with 96 additions and 428 deletions

View File

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

View File

@@ -22,16 +22,6 @@ jobs:
with:
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
run: yarn
@@ -58,6 +48,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_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_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
@@ -69,6 +60,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_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_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact

View File

@@ -47,6 +47,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_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_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -57,6 +58,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_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_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact
uses: actions/upload-artifact@v4

View File

@@ -1,183 +0,0 @@
<br>
<div align="center">
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra è un game launcher con il proprio client bittorrent e autogestore di repacks.</strong>
</p>
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![pl](https://img.shields.io/badge/lang-pl-white)](README.pl.md)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
</div>
## Table of Contents
- [A proposito](#a-proposito)
- [Caratteristiche](#caratteristiche)
- [Installazione](#installazione)
- [Contribuire](#contribuire)
- [Uscisciti su Telegram](#unisciti-su-telegram)
- [Forka e Clona la tua repository](#forka-e-clona-la-tua-repository)
- [Modi in cui contribuire](#modi-in-cui-contribuire)
- [Struttura del Progetto](#struttura-del-progetto)
- [Compila il codice sorgente](#compila-il-codice-sorgente)
- [Installa Node.js](#installa-nodejs)
- [Installa Yarn](#installa-yarn)
- [Installa le Node Dependencies](#installa-node-dependencies)
- [Installa Python 3.9](#installa-python-39)
- [Installa le Python Dependencies](#installa-python-dependencies)
- [Variabili d'Ambiente](#variabili-di-ambiente)
- [Esecuzione](#esecuzione)
- [Compilazione](#compilazione)
- [Compilare il client bittorrent](#compilazione-client-bittorrent)
- [Compilare l'applicazione Electron](#compilazione-applicazione-electron)
- [Collaboratori](#collaboratori)
## A proposito
**Hydra** è un **Game Launcher** con il proprio **Client BitTorrent** e **autogestore di repack**.
<br>
Il launcher è scritto in TypeScript (Electron) and Python, che gestisce il sistema di torrenting appoggiandosi a libtorrent.
## Caratteristiche
- Motore di ricerca automatizzato sulle fonti di repack dal [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/")
- Client Bittorrent integrato
- Integrazione How Long To Beat (HLTB) nella pagina del gioco
- Percorso del download Personalizzato
- Notifiche di aggiornamenti sulla list dei repacks
- Supporto Windows e Linux
- Costantemente Aggiornato
- E molto altro ...
## Installazione
Segui i seguenti passi:
1. Scarica l'ultima versione di Hydra dalla pagina [Releases](https://github.com/hydralauncher/hydra/releases/latest).
- Scarica solo il file .exe per installare Hydra su Windows.
- Scarica il file .deb o .rpm o .zip per Linux. (Dipende dalla tua distro Linux)
2. Esegui il file scaricato.
3. Goditi Hydra!
## <a name="contribuire"> Contribuire
### <a name="unisciti-su-telegram"></a> Unisciti su Telegram
Puoi unirti alle nostre conversazioni sul canale [Telegram](https://t.me/hydralauncher).
### Forka e Clona la repository
1. Forka la repository [(clicca qui per forkare)](https://github.com/hydralauncher/hydra/fork)
2. Clona il tuo codice forkato `git clone https://github.com/your_username/hydra`
3. Crea un nuovo branch
4. Aggiungi le modifiche (push)
5. Invia la richiesta di pull
### Modi in cui contribuire
- Traduzione: Vogliamo rendere Hydra disponibile a più persone possibile. Sentiti libero di tradurre in altre lingue o aggiornare e migliorare quelle già disponibili su Hydra.
- Programmazione: Hydra è programmato in TypeScript, Electron e un po' di Python. Se intendi contribuire unisciti al nostro [Telegram](https://t.me/hydralauncher)!
### Struttura del Progetto
- client-torrent: Usiamo libtorrent, una libreria Python, per gestire i download dei torrent
- src/renderer: l'UI dell'applicazione
- src/main: tutta la logica qui.
## Compilazione
### Installa Node.js
Assicurati di avere Node.js installato sulla tua macchina. Scaricalo e installalo da [nodejs.org](https://nodejs.org/).
### Installa Yarn
Yarn è un gestore di pacchetti per Node.js. Se non hai ancora installato Yarn segui le istruzioni su [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
### Installa le dipendenze Node
Naviga alla cartella del progetto e installa le dipendenze Node con Yarn:
```bash
cd hydra
yarn
```
### Installa Python 3.9
Assicurati di avere Python 3.9 installato. Puoi scaricarlo da [python.org](https://www.python.org/downloads/release/python-3913/).
### Installa le Dipendenze Python
Installa le dipendenze con pip:
```bash
pip install -r requirements.txt
```
## Variabili d'ambiente
Avrai bisogno di una chiave API SteamGridDB per poter caricare le icone di gioco.
Se intendi avere onlinefix come repacker dovrai aggiungere le tue credenziali al file .env
Una volta ottenuta, puoi copiare e rinominare il file `.env.example` a `.env` e metterlo in `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
## Esecuzione
Una volta impostato tutto, puoi eseguire il seguente comando per avviare il processo Electron e il client bittorrent:
```bash
yarn dev
```
## Compilazione
### Compila il bittorrent
Usa il comando:
```bash
python torrent-client/setup.py build
```
### Compila l'applicazione Electron
Usa il comando:
Per Windows:
```bash
yarn build:win
```
Per Linux:
```bash
yarn build:linux
```
## Collaboratori
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## Licenza
Hydra è concesso in licenza secondo la [MIT License](LICENSE).

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.0.5",
"version": "3.0.6",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",

View File

@@ -1,23 +1,21 @@
import type { HowLongToBeatCategory } from "@types";
import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services";
import type { GameShop, HowLongToBeatCategory } from "@types";
import { registerEvent } from "../register-event";
import { formatName } from "@shared";
import { HydraApi } from "@main/services";
const getHowLongToBeat = async (
_event: Electron.IpcMainInvokeEvent,
title: string
objectId: string,
shop: GameShop
): Promise<HowLongToBeatCategory[] | null> => {
const response = await searchHowLongToBeat(title);
const game = response.data.find((game) => {
return formatName(game.game_name) === formatName(title);
const params = new URLSearchParams({
objectId,
shop,
});
if (!game) return null;
const howLongToBeat = await getHowLongToBeatGame(String(game.game_id));
return howLongToBeat;
return HydraApi.get(`/games/how-long-to-beat?${params.toString()}`, null, {
needsAuth: false,
});
};
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 "./catalogue/get-catalogue";
@@ -72,5 +72,6 @@ import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion);
ipcMain.handle("isStaging", () => isStaging);
ipcMain.handle("isPortableVersion", () => isPortableVersion());
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);

View File

@@ -1,5 +1,4 @@
import { registerEvent } from "../register-event";
import parseTorrent from "parse-torrent";
import type { StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi, logger } from "@main/services";
@@ -9,7 +8,6 @@ import { createGame } from "@main/services/library-sync";
import { steamUrlBuilder } from "@shared";
import { dataSource } from "@main/data-source";
import { DownloadQueue, Game } from "@main/entity";
import { HydraAnalytics } from "@main/services/hydra-analytics";
const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
@@ -91,17 +89,6 @@ const startGameDownload = async (
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.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 { appVersion } from "@main/constants";
import { getUserData } from "./user/get-user-data";
import { isFuture, isToday } from "date-fns";
interface HydraApiOptions {
needsAuth?: boolean;
@@ -45,10 +46,8 @@ export class HydraApi {
}
private static hasActiveSubscription() {
return (
this.userAuth.subscription?.expiresAt &&
this.userAuth.subscription.expiresAt > new Date()
);
const expiresAt = this.userAuth.subscription?.expiresAt;
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
}
static async handleExternalAuth(uri: string) {

View File

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

View File

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

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:;"
/>
</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();
@@ -120,12 +114,36 @@ export function App() {
dispatch(setProfileBackground(profileBackground));
}
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
}
});
fetchUserDetails()
.then((response) => {
if (response) {
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(() => {
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}?t=${Date.now()}`;
document.head.appendChild($script);
});
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
const onSignIn = useCallback(() => {
@@ -215,9 +233,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

@@ -60,7 +60,8 @@ declare global {
) => Promise<ShopDetails | null>;
getRandomGame: () => Promise<Steam250Game>;
getHowLongToBeat: (
title: string
objectId: string,
shop: GameShop
) => Promise<HowLongToBeatCategory[] | null>;
getGames: (take?: number, skip?: number) => Promise<CatalogueEntry[]>;
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
@@ -162,6 +163,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

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

View File

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

View File

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

View File

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

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 {

View File

@@ -46,7 +46,7 @@ export const removeSymbolsFromName = (name: string) =>
export const removeSpecialEditionFromName = (name: string) =>
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,
replaceDotsWithSpace,
replaceNbspWithSpace,
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
(str) => str.replace(/DIRECTOR'S CUT/gi, ""),
(str) => str.replace(/Friend's Pass/gi, ""),
removeSymbolsFromName,
removeDuplicateSpaces,
(str) => str.trim()