feat: adding onlinefix credentials

This commit is contained in:
Hydra 2024-04-20 17:11:35 +01:00
parent 70fe495965
commit 5bb1f753a2
No known key found for this signature in database
25 changed files with 357 additions and 245 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

@ -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

@ -115,7 +115,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

@ -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

@ -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

@ -111,7 +111,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: false })
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

@ -11,6 +11,7 @@ const addGameToLibrary = async (
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

@ -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";
@ -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

@ -42,6 +42,8 @@ const getXatabRepack = async (url: string) => {
if (!$downloadButton) throw new Error("Download button not found"); if (!$downloadButton) throw new Error("Download button not found");
const torrentBuffer = await getTorrentBuffer($downloadButton.href); const torrentBuffer = await getTorrentBuffer($downloadButton.href);
console.log(url);
console.log(torrentBuffer.byteLength);
return { return {
fileSize: formatXatabDownloadSize($size.textContent).toUpperCase(), fileSize: formatXatabDownloadSize($size.textContent).toUpperCase(),

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

@ -50,15 +50,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

@ -54,12 +54,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,207 @@
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;
getGame: () => void;
}
export function HeroPanelActions({
game,
gameDetails,
isGamePlaying,
isGameDownloading,
openRepacksModal,
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 (.exe)", extensions: ["exe", "app"] },
],
})
.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) setShowBinaryNotFoundModal(true);
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,112 +170,6 @@ 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
@ -353,7 +178,16 @@ export function HeroPanel({
/> />
<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}
isGamePlaying={isGamePlaying}
isGameDownloading={isGameDownloading}
/>
</div>
</div> </div>
</> </>
); );

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 {