Merge remote-tracking branch 'origin/main' into feat/add-select-folder-modal-in-game-installation

This commit is contained in:
José Luís 2024-04-20 17:03:29 -03:00
commit af715aa110
30 changed files with 416 additions and 275 deletions

View File

@ -57,6 +57,8 @@ jobs:
STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }} STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }} SENTRY_DSN: ${{ vars.SENTRY_DSN }}
ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
- name: Create artifact - name: Create artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@ -89,7 +89,6 @@
"lottie-react": "^2.4.0", "lottie-react": "^2.4.0",
"parse-torrent": "9.1.5", "parse-torrent": "9.1.5",
"ps-list": "^8.1.1", "ps-list": "^8.1.1",
"qs": "^6.12.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",

View File

@ -19,7 +19,17 @@ if (process.platform !== "darwin") {
} }
if (process.env.SENTRY_DSN) { if (process.env.SENTRY_DSN) {
init({ dsn: process.env.SENTRY_DSN }); init({
dsn: process.env.SENTRY_DSN,
beforeSend: async (event) => {
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
if (userPreferences?.telemetryEnabled) return event;
return null;
},
});
} }
i18n.init({ i18n.init({

View File

@ -121,7 +121,9 @@
"change": "Update", "change": "Update",
"notifications": "Notifications", "notifications": "Notifications",
"enable_download_notifications": "When a download is complete", "enable_download_notifications": "When a download is complete",
"enable_repack_list_notifications": "When a new repack is added" "enable_repack_list_notifications": "When a new repack is added",
"telemetry": "Telemetry",
"telemetry_description": "Enable anonymous usage statistics"
}, },
"notifications": { "notifications": {
"download_complete": "Download complete", "download_complete": "Download complete",

View File

@ -25,7 +25,7 @@
"downloads": "Descargas", "downloads": "Descargas",
"search_results": "Resultados de búsqueda", "search_results": "Resultados de búsqueda",
"settings": "Ajustes", "settings": "Ajustes",
"home": "Hogar" "home": "Início"
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "Sin descargas en progreso", "no_downloads_in_progress": "Sin descargas en progreso",
@ -111,7 +111,9 @@
"change": "Cambiar", "change": "Cambiar",
"notifications": "Notificaciones", "notifications": "Notificaciones",
"enable_download_notifications": "Cuando se completa una descarga", "enable_download_notifications": "Cuando se completa una descarga",
"enable_repack_list_notifications": "Cuando se añade un repack nuevo" "enable_repack_list_notifications": "Cuando se añade un repack nuevo",
"telemetry": "Telemetria",
"telemetry_description": "Habilitar estadísticas de uso anónimas"
}, },
"notifications": { "notifications": {
"download_complete": "Descarga completada", "download_complete": "Descarga completada",

View File

@ -16,7 +16,7 @@
"paused": "{{title}} (En pause)", "paused": "{{title}} (En pause)",
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)", "downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
"filter": "Filtrer la bibliothèque", "filter": "Filtrer la bibliothèque",
"home": "Maison", "home": "Page daccueil",
"follow_us": "Suivez-nous" "follow_us": "Suivez-nous"
}, },
"header": { "header": {
@ -111,7 +111,9 @@
"change": "Mettre à jour", "change": "Mettre à jour",
"notifications": "Notifications", "notifications": "Notifications",
"enable_download_notifications": "Quand un téléchargement est terminé", "enable_download_notifications": "Quand un téléchargement est terminé",
"enable_repack_list_notifications": "Quand une nouvelle réduction est ajoutée" "enable_repack_list_notifications": "Quand une nouvelle réduction est ajoutée",
"telemetry": "Télémétrie",
"telemetry_description": "Activer les statistiques d'utilisation anonymes"
}, },
"notifications": { "notifications": {
"download_complete": "Téléchargement terminé", "download_complete": "Téléchargement terminé",

View File

@ -117,7 +117,9 @@
"change": "Mudar", "change": "Mudar",
"notifications": "Notificações", "notifications": "Notificações",
"enable_download_notifications": "Quando um download for concluído", "enable_download_notifications": "Quando um download for concluído",
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada" "enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
"telemetry": "Telemetria",
"telemetry_description": "Habilitar estatísticas de uso anônimas"
}, },
"notifications": { "notifications": {
"download_complete": "Download concluído", "download_complete": "Download concluído",

View File

@ -23,6 +23,9 @@ export class UserPreferences {
@Column("boolean", { default: false }) @Column("boolean", { default: false })
repackUpdatesNotificationsEnabled: boolean; repackUpdatesNotificationsEnabled: boolean;
@Column("boolean", { default: true })
telemetryEnabled: boolean;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;

View File

@ -1,29 +1,39 @@
import type { CatalogueEntry } from "@types"; import type { CatalogueEntry, GameShop } from "@types";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { searchGames } from "../helpers/search-games"; import { searchRepacks } from "../helpers/search-games";
import slice from "lodash/slice"; import { stateManager } from "@main/state-manager";
import { getSteamAppAsset } from "@main/helpers";
const steamGames = stateManager.getValue("steamGames");
const getGames = async ( const getGames = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
take?: number, take?: number,
prevCursor = 0 cursor = 0
): Promise<{ results: CatalogueEntry[]; cursor: number }> => { ): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
let results: CatalogueEntry[] = []; const results: CatalogueEntry[] = [];
let i = 0;
const batchSize = 100; let i = 0 + cursor;
while (results.length < take) { while (results.length < take) {
const games = await searchGames({ const game = steamGames[i];
take: batchSize, const repacks = searchRepacks(game.name);
skip: (i + prevCursor) * batchSize,
}); if (repacks.length) {
results = [...results, ...games.filter((game) => game.repacks.length)]; results.push({
objectID: String(game.id),
title: game.name,
shop: "steam" as GameShop,
cover: getSteamAppAsset("library", String(game.id)),
repacks,
});
}
i++; i++;
} }
return { results: slice(results, 0, take), cursor: prevCursor + i }; return { results, cursor: i };
}; };
registerEvent(getGames, { registerEvent(getGames, {

View File

@ -5,14 +5,13 @@ import type { GameRepack, GameShop, CatalogueEntry } from "@types";
import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers"; import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
import { stateManager } from "@main/state-manager"; import { stateManager } from "@main/state-manager";
import { steamGameRepository } from "@main/repository";
import { FindManyOptions, Like } from "typeorm";
import { SteamGame } from "@main/entity";
const { Index } = flexSearch; const { Index } = flexSearch;
const repacksIndex = new Index(); const repacksIndex = new Index();
const steamGamesIndex = new Index({ tokenize: "reverse" });
const repacks = stateManager.getValue("repacks"); const repacks = stateManager.getValue("repacks");
const steamGames = stateManager.getValue("steamGames");
for (let i = 0; i < repacks.length; i++) { for (let i = 0; i < repacks.length; i++) {
const repack = repacks[i]; const repack = repacks[i];
@ -22,9 +21,12 @@ for (let i = 0; i < repacks.length; i++) {
repacksIndex.add(i, formatName(formatter(repack.title))); repacksIndex.add(i, formatName(formatter(repack.title)));
} }
export const searchRepacks = (title: string): GameRepack[] => { for (let i = 0; i < steamGames.length; i++) {
const repacks = stateManager.getValue("repacks"); const steamGame = steamGames[i];
steamGamesIndex.add(i, formatName(steamGame.name));
}
export const searchRepacks = (title: string): GameRepack[] => {
return orderBy( return orderBy(
repacksIndex repacksIndex
.search(formatName(title)) .search(formatName(title))
@ -45,34 +47,21 @@ export const searchGames = async ({
take, take,
skip, skip,
}: SearchGamesArgs): Promise<CatalogueEntry[]> => { }: SearchGamesArgs): Promise<CatalogueEntry[]> => {
const options: FindManyOptions<SteamGame> = {}; const results = steamGamesIndex
.search(formatName(query || ""), { limit: take, offset: skip })
.map((index) => {
const result = steamGames.at(index as number)!;
if (query) { return {
options.where = { objectID: String(result.id),
name: query ? Like(`%${formatName(query)}%`) : undefined, title: result.name,
}; shop: "steam" as GameShop,
} cover: getSteamAppAsset("library", String(result.id)),
repacks: searchRepacks(result.name),
};
});
const steamResults = await steamGameRepository.find({ return Promise.all(results).then((resultsWithRepacks) =>
...options,
take,
skip,
order: { name: "ASC" },
});
const results = steamResults.map((result) => ({
objectID: String(result.id),
title: result.name,
shop: "steam" as GameShop,
cover: getSteamAppAsset("library", String(result.id)),
}));
return Promise.all(
results.map(async (result) => ({
...result,
repacks: searchRepacks(result.title),
}))
).then((resultsWithRepacks) =>
orderBy( orderBy(
resultsWithRepacks, resultsWithRepacks,
[({ repacks }) => repacks.length, "repacks"], [({ repacks }) => repacks.length, "repacks"],

View File

@ -10,7 +10,8 @@ const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
objectID: string, objectID: string,
title: string, title: string,
gameShop: GameShop gameShop: GameShop,
executablePath: string
) => { ) => {
const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID)); const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID));
@ -19,6 +20,7 @@ const addGameToLibrary = async (
iconUrl, iconUrl,
objectID, objectID,
shop: gameShop, shop: gameShop,
executablePath,
}); });
}; };

View File

@ -5,7 +5,7 @@ import {
getNewRepacksFromCPG, getNewRepacksFromCPG,
getNewRepacksFromUser, getNewRepacksFromUser,
getNewRepacksFromXatab, getNewRepacksFromXatab,
getNewRepacksFromOnlineFix, // getNewRepacksFromOnlineFix,
readPipe, readPipe,
startProcessWatcher, startProcessWatcher,
writePipe, writePipe,
@ -14,6 +14,7 @@ import {
gameRepository, gameRepository,
repackRepository, repackRepository,
repackerFriendlyNameRepository, repackerFriendlyNameRepository,
steamGameRepository,
userPreferencesRepository, userPreferencesRepository,
} from "./repository"; } from "./repository";
import { TorrentClient } from "./services/torrent-client"; import { TorrentClient } from "./services/torrent-client";
@ -78,9 +79,9 @@ const checkForNewRepacks = async () => {
getNewRepacksFromCPG( getNewRepacksFromCPG(
existingRepacks.filter((repack) => repack.repacker === "CPG") existingRepacks.filter((repack) => repack.repacker === "CPG")
), ),
getNewRepacksFromOnlineFix( // getNewRepacksFromOnlineFix(
existingRepacks.filter((repack) => repack.repacker === "onlinefix") // existingRepacks.filter((repack) => repack.repacker === "onlinefix")
), // ),
track1337xUsers(existingRepacks), track1337xUsers(existingRepacks),
]).then(() => { ]).then(() => {
repackRepository.count().then((count) => { repackRepository.count().then((count) => {
@ -104,17 +105,23 @@ const checkForNewRepacks = async () => {
}; };
const loadState = async () => { const loadState = async () => {
const [friendlyNames, repacks] = await Promise.all([ const [friendlyNames, repacks, steamGames] = await Promise.all([
repackerFriendlyNameRepository.find(), repackerFriendlyNameRepository.find(),
repackRepository.find({ repackRepository.find({
order: { order: {
createdAt: "desc", createdAt: "desc",
}, },
}), }),
steamGameRepository.find({
order: {
name: "asc",
},
}),
]); ]);
stateManager.setValue("repackersFriendlyNames", friendlyNames); stateManager.setValue("repackersFriendlyNames", friendlyNames);
stateManager.setValue("repacks", repacks); stateManager.setValue("repacks", repacks);
stateManager.setValue("steamGames", steamGames);
import("./events"); import("./events");
}; };

View File

@ -3,7 +3,6 @@ import path from "node:path";
import { IsNull, Not } from "typeorm"; import { IsNull, Not } from "typeorm";
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { GameStatus } from "@main/constants";
import { getProcesses } from "@main/helpers"; import { getProcesses } from "@main/helpers";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
@ -18,7 +17,6 @@ export const startProcessWatcher = async () => {
const games = await gameRepository.find({ const games = await gameRepository.find({
where: { where: {
executablePath: Not(IsNull()), executablePath: Not(IsNull()),
status: GameStatus.Seeding,
}, },
}); });
@ -54,15 +52,16 @@ export const startProcessWatcher = async () => {
playTimeInMilliseconds: game.playTimeInMilliseconds + delta, playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
}); });
gameRepository.update(game.id, {
lastTimePlayed: new Date().toUTCString(),
});
gamesPlaytime.set(game.id, performance.now()); gamesPlaytime.set(game.id, performance.now());
await sleep(sleepTime); await sleep(sleepTime);
continue; continue;
} }
gamesPlaytime.set(game.id, performance.now()); gamesPlaytime.set(game.id, performance.now());
gameRepository.update(game.id, {
lastTimePlayed: new Date().toUTCString(),
});
await sleep(sleepTime); await sleep(sleepTime);
continue; continue;

View File

@ -2,4 +2,4 @@ export * from "./1337x";
export * from "./xatab"; export * from "./xatab";
export * from "./cpg-repacks"; export * from "./cpg-repacks";
export * from "./gog"; export * from "./gog";
export * from "./online-fix"; // export * from "./online-fix";

View File

@ -2,7 +2,6 @@ import { Repack } from "@main/entity";
import { savePage } from "./helpers"; import { savePage } from "./helpers";
import type { GameRepackInput } from "./helpers"; import type { GameRepackInput } from "./helpers";
import { logger } from "../logger"; import { logger } from "../logger";
import { stringify } from "qs";
import parseTorrent, { import parseTorrent, {
toMagnetURI, toMagnetURI,
Instance as TorrentInstance, Instance as TorrentInstance,
@ -58,8 +57,12 @@ export const getNewRepacksFromOnlineFix = async (
if (!preLogin.field || !preLogin.value) return; if (!preLogin.field || !preLogin.value) return;
const tokenField = preLogin.field; const params = new URLSearchParams({
const tokenValue = preLogin.value; login_name: process.env.ONLINEFIX_USERNAME,
login_password: process.env.ONLINEFIX_PASSWORD,
login: "submit",
[preLogin.field]: preLogin.value,
});
await http await http
.post("https://online-fix.me/", { .post("https://online-fix.me/", {
@ -69,12 +72,7 @@ export const getNewRepacksFromOnlineFix = async (
Origin: "https://online-fix.me", Origin: "https://online-fix.me",
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
body: stringify({ body: params.toString(),
login_name: process.env.ONLINEFIX_USERNAME,
login_password: process.env.ONLINEFIX_PASSWORD,
login: "submit",
[tokenField]: tokenValue,
}),
}) })
.text(); .text();
} }

View File

@ -1,14 +1,16 @@
import type { Repack, RepackerFriendlyName } from "@main/entity"; import type { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
interface State { interface State {
repacks: Repack[]; repacks: Repack[];
repackersFriendlyNames: RepackerFriendlyName[]; repackersFriendlyNames: RepackerFriendlyName[];
steamGames: SteamGame[];
eventResults: Map<[string, any[]], any>; eventResults: Map<[string, any[]], any>;
} }
const initialState: State = { const initialState: State = {
repacks: [], repacks: [],
repackersFriendlyNames: [], repackersFriendlyNames: [],
steamGames: [],
eventResults: new Map(), eventResults: new Map(),
}; };

View File

@ -59,15 +59,26 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("updateUserPreferences", preferences), ipcRenderer.invoke("updateUserPreferences", preferences),
/* Library */ /* Library */
addGameToLibrary: (objectID: string, title: string, shop: GameShop) => addGameToLibrary: (
ipcRenderer.invoke("addGameToLibrary", objectID, title, shop), objectID: string,
title: string,
shop: GameShop,
executablePath: string
) =>
ipcRenderer.invoke(
"addGameToLibrary",
objectID,
title,
shop,
executablePath
),
getLibrary: () => ipcRenderer.invoke("getLibrary"), getLibrary: () => ipcRenderer.invoke("getLibrary"),
getRepackersFriendlyNames: () => getRepackersFriendlyNames: () =>
ipcRenderer.invoke("getRepackersFriendlyNames"), ipcRenderer.invoke("getRepackersFriendlyNames"),
openGameInstaller: (gameId: number) => openGameInstaller: (gameId: number) =>
ipcRenderer.invoke("openGameInstaller", gameId), ipcRenderer.invoke("openGameInstaller", gameId),
openGame: (gameId: number, path: string) => openGame: (gameId: number, executablePath: string) =>
ipcRenderer.invoke("openGame", gameId, path), ipcRenderer.invoke("openGame", gameId, executablePath),
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId), closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId), removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId),
deleteGameFolder: (gameId: number) => deleteGameFolder: (gameId: number) =>

View File

@ -203,7 +203,7 @@ export function Sidebar() {
className={styles.menuItem({ className={styles.menuItem({
active: active:
location.pathname === `/game/${game.shop}/${game.objectID}`, location.pathname === `/game/${game.shop}/${game.objectID}`,
muted: game.status === null || game.status === "cancelled", muted: game.status === "cancelled",
})} })}
> >
<button <button

View File

@ -55,12 +55,13 @@ declare global {
addGameToLibrary: ( addGameToLibrary: (
objectID: string, objectID: string,
title: string, title: string,
shop: GameShop shop: GameShop,
executablePath: string
) => Promise<void>; ) => Promise<void>;
getLibrary: () => Promise<Game[]>; getLibrary: () => Promise<Game[]>;
getRepackersFriendlyNames: () => Promise<Record<string, string>>; getRepackersFriendlyNames: () => Promise<Record<string, string>>;
openGameInstaller: (gameId: number) => Promise<boolean>; openGameInstaller: (gameId: number) => Promise<boolean>;
openGame: (gameId: number, path: string) => Promise<void>; openGame: (gameId: number, executablePath: string) => Promise<void>;
closeGame: (gameId: number) => Promise<boolean>; closeGame: (gameId: number) => Promise<boolean>;
removeGame: (gameId: number) => Promise<void>; removeGame: (gameId: number) => Promise<void>;
deleteGameFolder: (gameId: number) => Promise<unknown>; deleteGameFolder: (gameId: number) => Promise<unknown>;

View File

@ -32,7 +32,18 @@ import { store } from "./store";
import * as resources from "@locales"; import * as resources from "@locales";
if (process.env.SENTRY_DSN) { if (process.env.SENTRY_DSN) {
init({ dsn: process.env.SENTRY_DSN }, reactInit); init(
{
dsn: process.env.SENTRY_DSN,
beforeSend: async (event) => {
const userPreferences = await window.electron.getUserPreferences();
if (userPreferences?.telemetryEnabled) return event;
return null;
},
},
reactInit
);
} }
const router = createHashRouter([ const router = createHashRouter([

View File

@ -71,6 +71,7 @@ export function Catalogue() {
display: "flex", display: "flex",
width: "100%", width: "100%",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center",
borderBottom: `1px solid ${vars.color.borderColor}`, borderBottom: `1px solid ${vars.color.borderColor}`,
}} }}
> >
@ -103,7 +104,6 @@ export function Catalogue() {
key={game.objectID} key={game.objectID}
game={game} game={game}
onClick={() => handleGameClick(game)} onClick={() => handleGameClick(game)}
disabled={!game.repacks.length}
/> />
))} ))}
</> </>

View File

@ -83,7 +83,9 @@ export function GameDetails() {
}, [getGame, gameDownloading?.id]); }, [getGame, gameDownloading?.id]);
useEffect(() => { useEffect(() => {
setGame(null);
setIsLoading(true); setIsLoading(true);
setIsGamePlaying(false);
dispatch(setHeaderTitle("")); dispatch(setHeaderTitle(""));
getRandomGame(); getRandomGame();

View File

@ -0,0 +1,212 @@
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
import { Button } from "@renderer/components";
import { useDownload, useLibrary } from "@renderer/hooks";
import type { Game, ShopDetails } from "@types";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export interface HeroPanelActionsProps {
game: Game | null;
gameDetails: ShopDetails | null;
isGamePlaying: boolean;
isGameDownloading: boolean;
openRepacksModal: () => void;
openBinaryNotFoundModal: () => void;
getGame: () => void;
}
export function HeroPanelActions({
game,
gameDetails,
isGamePlaying,
isGameDownloading,
openRepacksModal,
openBinaryNotFoundModal,
getGame,
}: HeroPanelActionsProps) {
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
useState(false);
const {
resumeDownload,
pauseDownload,
cancelDownload,
removeGame,
isGameDeleting,
} = useDownload();
const { updateLibrary } = useLibrary();
const { t } = useTranslation("game_details");
const selectGameExecutable = async () => {
return window.electron
.showOpenDialog({
properties: ["openFile"],
filters: [
{
name: "Game executable",
extensions: window.electron.platform === "win32" ? ["exe"] : [],
},
],
})
.then(({ filePaths }) => {
if (filePaths && filePaths.length > 0) {
return filePaths[0];
}
});
};
const toggleGameOnLibrary = async () => {
setToggleLibraryGameDisabled(true);
try {
if (game) {
await removeGame(game.id);
} else {
const gameExecutablePath = await selectGameExecutable();
await window.electron.addGameToLibrary(
gameDetails.objectID,
gameDetails.name,
"steam",
gameExecutablePath
);
}
updateLibrary();
getGame();
} finally {
setToggleLibraryGameDisabled(false);
}
};
const openGameInstaller = () => {
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
if (!isBinaryInPath) openBinaryNotFoundModal();
updateLibrary();
});
};
const openGame = async () => {
if (game.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
if (game?.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
const gameExecutablePath = await selectGameExecutable();
window.electron.openGame(game.id, gameExecutablePath);
};
const closeGame = () => window.electron.closeGame(game.id);
const deleting = isGameDeleting(game?.id);
const toggleGameOnLibraryButton = (
<Button
theme="outline"
disabled={!gameDetails || toggleLibraryGameDisabled}
onClick={toggleGameOnLibrary}
>
{game ? <NoEntryIcon /> : <PlusCircleIcon />}
{game ? t("remove_from_library") : t("add_to_library")}
</Button>
);
if (isGameDownloading) {
return (
<>
<Button onClick={() => pauseDownload(game.id)} theme="outline">
{t("pause")}
</Button>
<Button onClick={() => cancelDownload(game.id)} theme="outline">
{t("cancel")}
</Button>
</>
);
}
if (game?.status === "paused") {
return (
<>
<Button onClick={() => resumeDownload(game.id)} theme="outline">
{t("resume")}
</Button>
<Button
onClick={() => cancelDownload(game.id).then(getGame)}
theme="outline"
>
{t("cancel")}
</Button>
</>
);
}
if (game?.status === "seeding" || (game && !game.status)) {
return (
<>
{game?.status === "seeding" ? (
<Button
onClick={openGameInstaller}
theme="outline"
disabled={deleting || isGamePlaying}
>
{t("install")}
</Button>
) : (
toggleGameOnLibraryButton
)}
{isGamePlaying ? (
<Button onClick={closeGame} theme="outline" disabled={deleting}>
{t("close")}
</Button>
) : (
<Button
onClick={openGame}
theme="outline"
disabled={deleting || isGamePlaying}
>
{t("play")}
</Button>
)}
</>
);
}
if (game?.status === "cancelled") {
return (
<>
<Button onClick={openRepacksModal} theme="outline" disabled={deleting}>
{t("open_download_options")}
</Button>
<Button
onClick={() => removeGame(game.id).then(getGame)}
theme="outline"
disabled={deleting}
>
{t("remove_from_list")}
</Button>
</>
);
}
if (gameDetails && gameDetails.repacks.length) {
return (
<>
{toggleGameOnLibraryButton}
<Button onClick={openRepacksModal} theme="outline">
{t("open_download_options")}
</Button>
</>
);
}
return toggleGameOnLibraryButton;
}

View File

@ -2,16 +2,15 @@ import { format } from "date-fns";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "@renderer/components"; import { useDownload } from "@renderer/hooks";
import { useDownload, useLibrary } from "@renderer/hooks";
import type { Game, ShopDetails } from "@types"; import type { Game, ShopDetails } from "@types";
import { formatDownloadProgress } from "@renderer/helpers"; import { formatDownloadProgress } from "@renderer/helpers";
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal"; import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css"; import * as styles from "./hero-panel.css";
import { useDate } from "@renderer/hooks/use-date"; import { useDate } from "@renderer/hooks/use-date";
import { formatBytes } from "@renderer/utils"; import { formatBytes } from "@renderer/utils";
import { HeroPanelActions } from "./hero-panel-actions";
export interface HeroPanelProps { export interface HeroPanelProps {
game: Game | null; game: Game | null;
@ -44,21 +43,8 @@ export function HeroPanel({
eta, eta,
numPeers, numPeers,
numSeeds, numSeeds,
resumeDownload,
pauseDownload,
cancelDownload,
removeGame,
isGameDeleting, isGameDeleting,
} = useDownload(); } = useDownload();
const { updateLibrary, library } = useLibrary();
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
useState(false);
const gameOnLibrary = library.find(
({ objectID }) => objectID === gameDetails?.objectID
);
const isGameDownloading = isDownloading && gameDownloading?.id === game?.id; const isGameDownloading = isDownloading && gameDownloading?.id === game?.id;
const updateLastTimePlayed = useCallback(() => { const updateLastTimePlayed = useCallback(() => {
@ -83,41 +69,6 @@ export function HeroPanel({
} }
}, [game?.lastTimePlayed, updateLastTimePlayed]); }, [game?.lastTimePlayed, updateLastTimePlayed]);
const openGameInstaller = () => {
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
updateLibrary();
});
};
const openGame = () => {
if (game.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
if (game?.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
window.electron
.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Game executable (.exe)", extensions: ["exe"] }],
})
.then(({ filePaths }) => {
if (filePaths && filePaths.length > 0) {
const path = filePaths[0];
window.electron.openGame(game.id, path);
}
});
};
const closeGame = () => {
window.electron.closeGame(game.id);
};
const finalDownloadSize = useMemo(() => { const finalDownloadSize = useMemo(() => {
if (!game) return "N/A"; if (!game) return "N/A";
if (game.fileSize) return formatBytes(game.fileSize); if (game.fileSize) return formatBytes(game.fileSize);
@ -128,26 +79,6 @@ export function HeroPanel({
return game.repack?.fileSize ?? "N/A"; return game.repack?.fileSize ?? "N/A";
}, [game, isGameDownloading, gameDownloading]); }, [game, isGameDownloading, gameDownloading]);
const toggleLibraryGame = async () => {
setToggleLibraryGameDisabled(true);
try {
if (gameOnLibrary) {
await window.electron.removeGame(gameOnLibrary.id);
} else {
await window.electron.addGameToLibrary(
gameDetails.objectID,
gameDetails.name,
"steam"
);
}
await updateLibrary();
} finally {
setToggleLibraryGameDisabled(false);
}
};
const getInfo = () => { const getInfo = () => {
if (!gameDetails) return null; if (!gameDetails) return null;
@ -196,7 +127,7 @@ export function HeroPanel({
); );
} }
if (game?.status === "seeding") { if (game?.status === "seeding" || (game && !game.status)) {
if (!game.lastTimePlayed) { if (!game.lastTimePlayed) {
return <p>{t("not_played_yet", { title: game.title })}</p>; return <p>{t("not_played_yet", { title: game.title })}</p>;
} }
@ -239,121 +170,26 @@ export function HeroPanel({
return <p>{t("no_downloads")}</p>; return <p>{t("no_downloads")}</p>;
}; };
const getActions = () => {
const deleting = isGameDeleting(game?.id);
const toggleGameOnLibraryButton = (
<Button
theme="outline"
disabled={!gameDetails || toggleLibraryGameDisabled}
onClick={toggleLibraryGame}
>
{gameOnLibrary ? <NoEntryIcon /> : <PlusCircleIcon />}
{gameOnLibrary ? t("remove_from_library") : t("add_to_library")}
</Button>
);
if (isGameDownloading) {
return (
<>
<Button onClick={() => pauseDownload(game.id)} theme="outline">
{t("pause")}
</Button>
<Button onClick={() => cancelDownload(game.id)} theme="outline">
{t("cancel")}
</Button>
</>
);
}
if (game?.status === "paused") {
return (
<>
<Button onClick={() => resumeDownload(game.id)} theme="outline">
{t("resume")}
</Button>
<Button
onClick={() => cancelDownload(game.id).then(getGame)}
theme="outline"
>
{t("cancel")}
</Button>
</>
);
}
if (game?.status === "seeding") {
return (
<>
<Button
onClick={openGameInstaller}
theme="outline"
disabled={deleting || isGamePlaying}
>
{t("install")}
</Button>
{isGamePlaying ? (
<Button onClick={closeGame} theme="outline" disabled={deleting}>
{t("close")}
</Button>
) : (
<Button
onClick={openGame}
theme="outline"
disabled={deleting || isGamePlaying}
>
{t("play")}
</Button>
)}
</>
);
}
if (game?.status === "cancelled") {
return (
<>
<Button
onClick={openRepacksModal}
theme="outline"
disabled={deleting}
>
{t("open_download_options")}
</Button>
<Button
onClick={() => removeGame(game.id).then(getGame)}
theme="outline"
disabled={deleting}
>
{t("remove_from_list")}
</Button>
</>
);
}
if (gameDetails && gameDetails.repacks.length) {
return (
<>
{toggleGameOnLibraryButton}
<Button onClick={openRepacksModal} theme="outline">
{t("open_download_options")}
</Button>
</>
);
}
return toggleGameOnLibraryButton;
};
return ( return (
<> <>
<BinaryNotFoundModal <BinaryNotFoundModal
visible={showBinaryNotFoundModal} visible={showBinaryNotFoundModal}
onClose={() => setShowBinaryNotFoundModal(false)} onClose={() => setShowBinaryNotFoundModal(false)}
/> />
<div style={{ backgroundColor: color }} className={styles.panel}> <div style={{ backgroundColor: color }} className={styles.panel}>
<div className={styles.content}>{getInfo()}</div> <div className={styles.content}>{getInfo()}</div>
<div className={styles.actions}>{getActions()}</div> <div className={styles.actions}>
<HeroPanelActions
game={game}
gameDetails={gameDetails}
getGame={getGame}
openRepacksModal={openRepacksModal}
openBinaryNotFoundModal={() => setShowBinaryNotFoundModal(true)}
isGamePlaying={isGamePlaying}
isGameDownloading={isGameDownloading}
/>
</div>
</div> </div>
</> </>
); );

View File

@ -10,6 +10,7 @@ import type { DiskSpace } from "check-disk-space";
import { format } from "date-fns"; import { format } from "date-fns";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { formatBytes } from "@renderer/utils"; import { formatBytes } from "@renderer/utils";
import { useAppSelector } from "@renderer/hooks";
import { SelectFolderModal } from "./select-folder-modal"; import { SelectFolderModal } from "./select-folder-modal";
export interface RepacksModalProps { export interface RepacksModalProps {
@ -30,6 +31,10 @@ export function RepacksModal({
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]); const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack>(null); const [repack, setRepack] = useState<GameRepack>(null);
const repackersFriendlyNames = useAppSelector(
(state) => state.repackersFriendlyNames.value
);
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
useEffect(() => { useEffect(() => {
@ -91,7 +96,7 @@ export function RepacksModal({
> >
<p style={{ color: "#DADBE1" }}>{repack.title}</p> <p style={{ color: "#DADBE1" }}>{repack.title}</p>
<p style={{ fontSize: "12px" }}> <p style={{ fontSize: "12px" }}>
{repack.fileSize} - {repack.repacker} -{" "} {repack.fileSize} - {repackersFriendlyNames[repack.repacker]} -{" "}
{format(repack.uploadDate, "dd/MM/yyyy")} {format(repack.uploadDate, "dd/MM/yyyy")}
</p> </p>
</Button> </Button>

View File

@ -67,7 +67,6 @@ export function SearchResults() {
key={game.objectID} key={game.objectID}
game={game} game={game}
onClick={() => handleGameClick(game)} onClick={() => handleGameClick(game)}
disabled={!game.repacks.length}
/> />
))} ))}
</> </>

View File

@ -10,6 +10,7 @@ export function Settings() {
downloadsPath: "", downloadsPath: "",
downloadNotificationsEnabled: false, downloadNotificationsEnabled: false,
repackUpdatesNotificationsEnabled: false, repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false,
}); });
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
@ -25,6 +26,7 @@ export function Settings() {
userPreferences?.downloadNotificationsEnabled, userPreferences?.downloadNotificationsEnabled,
repackUpdatesNotificationsEnabled: repackUpdatesNotificationsEnabled:
userPreferences?.repackUpdatesNotificationsEnabled, userPreferences?.repackUpdatesNotificationsEnabled,
telemetryEnabled: userPreferences?.telemetryEnabled,
}); });
}); });
}, []); }, []);
@ -95,6 +97,16 @@ export function Settings() {
) )
} }
/> />
<h3>{t("telemetry")}</h3>
<CheckboxField
label={t("telemetry_description")}
checked={form.telemetryEnabled}
onChange={() =>
updateUserPreferences("telemetryEnabled", !form.telemetryEnabled)
}
/>
</div> </div>
</section> </section>
); );

View File

@ -104,6 +104,7 @@ export interface UserPreferences {
language: string; language: string;
downloadNotificationsEnabled: boolean; downloadNotificationsEnabled: boolean;
repackUpdatesNotificationsEnabled: boolean; repackUpdatesNotificationsEnabled: boolean;
telemetryEnabled: boolean;
} }
export interface HowLongToBeatCategory { export interface HowLongToBeatCategory {

View File

@ -12,5 +12,9 @@ setup(
version="0.1", version="0.1",
description="Hydra Torrent Client", description="Hydra Torrent Client",
options={"build_exe": build_exe_options}, options={"build_exe": build_exe_options},
executables=[Executable("torrent-client/main.py", target_name="hydra-download-manager")] executables=[Executable(
"torrent-client/main.py",
target_name="hydra-download-manager",
icon="images/icon.ico"
)]
) )

View File

@ -8894,13 +8894,6 @@ qs@6.11.0:
dependencies: dependencies:
side-channel "^1.0.4" side-channel "^1.0.4"
qs@^6.12.0:
version "6.12.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a"
integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==
dependencies:
side-channel "^1.0.6"
querystringify@^2.1.1: querystringify@^2.1.1:
version "2.2.0" version "2.2.0"
resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz"
@ -9896,7 +9889,16 @@ stream-transform@^2.1.3:
dependencies: dependencies:
mixme "^0.5.1" mixme "^0.5.1"
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: "string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -9974,7 +9976,14 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -11010,7 +11019,16 @@ word-wrap@^1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==