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

This commit is contained in:
Hydra 2024-04-20 17:03:29 -03:00 committed by shadowtosser
commit 86cdc121e8
30 changed files with 416 additions and 275 deletions

View File

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

View File

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

View File

@ -19,7 +19,17 @@ if (process.platform !== "darwin") {
}
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({

View File

@ -121,7 +121,9 @@
"change": "Update",
"notifications": "Notifications",
"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": {
"download_complete": "Download complete",

View File

@ -25,7 +25,7 @@
"downloads": "Descargas",
"search_results": "Resultados de búsqueda",
"settings": "Ajustes",
"home": "Hogar"
"home": "Início"
},
"bottom_panel": {
"no_downloads_in_progress": "Sin descargas en progreso",
@ -111,7 +111,9 @@
"change": "Cambiar",
"notifications": "Notificaciones",
"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": {
"download_complete": "Descarga completada",

View File

@ -16,7 +16,7 @@
"paused": "{{title}} (En pause)",
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
"filter": "Filtrer la bibliothèque",
"home": "Maison",
"home": "Page daccueil",
"follow_us": "Suivez-nous"
},
"header": {
@ -111,7 +111,9 @@
"change": "Mettre à jour",
"notifications": "Notifications",
"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": {
"download_complete": "Téléchargement terminé",

View File

@ -117,7 +117,9 @@
"change": "Mudar",
"notifications": "Notificações",
"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": {
"download_complete": "Download concluído",

View File

@ -23,6 +23,9 @@ export class UserPreferences {
@Column("boolean", { default: false })
repackUpdatesNotificationsEnabled: boolean;
@Column("boolean", { default: true })
telemetryEnabled: boolean;
@CreateDateColumn()
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 { searchGames } from "../helpers/search-games";
import slice from "lodash/slice";
import { searchRepacks } from "../helpers/search-games";
import { stateManager } from "@main/state-manager";
import { getSteamAppAsset } from "@main/helpers";
const steamGames = stateManager.getValue("steamGames");
const getGames = async (
_event: Electron.IpcMainInvokeEvent,
take?: number,
prevCursor = 0
cursor = 0
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
let results: CatalogueEntry[] = [];
let i = 0;
const results: CatalogueEntry[] = [];
const batchSize = 100;
let i = 0 + cursor;
while (results.length < take) {
const games = await searchGames({
take: batchSize,
skip: (i + prevCursor) * batchSize,
});
results = [...results, ...games.filter((game) => game.repacks.length)];
const game = steamGames[i];
const repacks = searchRepacks(game.name);
if (repacks.length) {
results.push({
objectID: String(game.id),
title: game.name,
shop: "steam" as GameShop,
cover: getSteamAppAsset("library", String(game.id)),
repacks,
});
}
i++;
}
return { results: slice(results, 0, take), cursor: prevCursor + i };
return { results, cursor: i };
};
registerEvent(getGames, {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,18 @@ import { store } from "./store";
import * as resources from "@locales";
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([

View File

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

View File

@ -83,7 +83,9 @@ export function GameDetails() {
}, [getGame, gameDownloading?.id]);
useEffect(() => {
setGame(null);
setIsLoading(true);
setIsGamePlaying(false);
dispatch(setHeaderTitle(""));
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 { useTranslation } from "react-i18next";
import { Button } from "@renderer/components";
import { useDownload, useLibrary } from "@renderer/hooks";
import { useDownload } from "@renderer/hooks";
import type { Game, ShopDetails } from "@types";
import { formatDownloadProgress } from "@renderer/helpers";
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css";
import { useDate } from "@renderer/hooks/use-date";
import { formatBytes } from "@renderer/utils";
import { HeroPanelActions } from "./hero-panel-actions";
export interface HeroPanelProps {
game: Game | null;
@ -44,21 +43,8 @@ export function HeroPanel({
eta,
numPeers,
numSeeds,
resumeDownload,
pauseDownload,
cancelDownload,
removeGame,
isGameDeleting,
} = 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 updateLastTimePlayed = useCallback(() => {
@ -83,41 +69,6 @@ export function HeroPanel({
}
}, [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(() => {
if (!game) return "N/A";
if (game.fileSize) return formatBytes(game.fileSize);
@ -128,26 +79,6 @@ export function HeroPanel({
return game.repack?.fileSize ?? "N/A";
}, [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 = () => {
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) {
return <p>{t("not_played_yet", { title: game.title })}</p>;
}
@ -239,121 +170,26 @@ export function HeroPanel({
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 (
<>
<BinaryNotFoundModal
visible={showBinaryNotFoundModal}
onClose={() => setShowBinaryNotFoundModal(false)}
/>
<div style={{ backgroundColor: color }} className={styles.panel}>
<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>
</>
);

View File

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

View File

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

View File

@ -10,6 +10,7 @@ export function Settings() {
downloadsPath: "",
downloadNotificationsEnabled: false,
repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false,
});
const { t } = useTranslation("settings");
@ -25,6 +26,7 @@ export function Settings() {
userPreferences?.downloadNotificationsEnabled,
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>
</section>
);

View File

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

View File

@ -12,5 +12,9 @@ setup(
version="0.1",
description="Hydra Torrent Client",
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:
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:
version "2.2.0"
resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz"
@ -9896,7 +9889,16 @@ stream-transform@^2.1.3:
dependencies:
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"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -9974,7 +9976,14 @@ string_decoder@~1.1.1:
dependencies:
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"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
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"
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"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==