mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
Merge pull request #575 from hydralauncher/feature/game-options-modal
feat: game options modal
This commit is contained in:
commit
4b97639972
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@ -44,6 +44,7 @@ jobs:
|
|||||||
name: Build-${{ matrix.os }}
|
name: Build-${{ matrix.os }}
|
||||||
path: |
|
path: |
|
||||||
dist/win-unpacked/**
|
dist/win-unpacked/**
|
||||||
|
dist/*-portable.exe
|
||||||
dist/*.zip
|
dist/*.zip
|
||||||
dist/*.dmg
|
dist/*.dmg
|
||||||
dist/*.deb
|
dist/*.deb
|
||||||
|
@ -18,6 +18,9 @@ asarUnpack:
|
|||||||
win:
|
win:
|
||||||
executableName: Hydra
|
executableName: Hydra
|
||||||
requestedExecutionLevel: requireAdministrator
|
requestedExecutionLevel: requireAdministrator
|
||||||
|
target:
|
||||||
|
- nsis
|
||||||
|
- portable
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${name}-${version}-setup.${ext}
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
@ -25,6 +28,9 @@ nsis:
|
|||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
oneClick: false
|
oneClick: false
|
||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
|
portable:
|
||||||
|
artifactName: ${name}-${version}-portable.${ext}
|
||||||
|
requestExecutionLevel: admin
|
||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
extendInfo:
|
extendInfo:
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
|
"create-desktop-shortcuts": "^1.11.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"electron-log": "^5.1.4",
|
"electron-log": "^5.1.4",
|
||||||
"electron-updater": "^6.1.8",
|
"electron-updater": "^6.1.8",
|
||||||
|
File diff suppressed because one or more lines are too long
@ -15,7 +15,8 @@
|
|||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filter library",
|
"filter": "Filter library",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"queued": "{{title}} (Queued)"
|
"queued": "{{title}} (Queued)",
|
||||||
|
"game_has_no_executable": "Game has no executable selected"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search games",
|
"search": "Search games",
|
||||||
@ -100,7 +101,25 @@
|
|||||||
"screenshot": "Screenshot {{number}}",
|
"screenshot": "Screenshot {{number}}",
|
||||||
"open_screenshot": "Open screenshot {{number}}",
|
"open_screenshot": "Open screenshot {{number}}",
|
||||||
"download_settings": "Download settings",
|
"download_settings": "Download settings",
|
||||||
"downloader": "Downloader"
|
"downloader": "Downloader",
|
||||||
|
"select_executable": "Select",
|
||||||
|
"no_executable_selected": "No executable selected",
|
||||||
|
"open_folder": "Open folder",
|
||||||
|
"open_download_location": "See downloaded files",
|
||||||
|
"create_shortcut": "Create desktop shortcut",
|
||||||
|
"remove_files": "Remove files",
|
||||||
|
"remove_from_library_title": "Are you sure?",
|
||||||
|
"remove_from_library_description": "This will remove {{game}} from your library",
|
||||||
|
"options": "Options",
|
||||||
|
"executable_section_title": "Executable",
|
||||||
|
"executable_section_description": "Path of the file that will be executed when \"Play\" is clicked",
|
||||||
|
"downloads_secion_title": "Downloads",
|
||||||
|
"downloads_section_description": "Check out updates or other versions of this game",
|
||||||
|
"danger_zone_section_title": "Danger zone",
|
||||||
|
"danger_zone_section_description": "Remove this game from your library or the files downloaded by Hydra",
|
||||||
|
"download_in_progress": "Download in progress",
|
||||||
|
"download_paused": "Download paused",
|
||||||
|
"last_downloaded_option": "Last downloaded option"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"filter": "Filtrar biblioteca",
|
"filter": "Filtrar biblioteca",
|
||||||
"home": "Início",
|
"home": "Início",
|
||||||
"follow_us": "Acompanhe-nos",
|
"follow_us": "Acompanhe-nos",
|
||||||
"queued": "{{title}} (Na fila)"
|
"queued": "{{title}} (Na fila)",
|
||||||
|
"game_has_no_executable": "Jogo não possui executável selecionado"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
@ -97,7 +98,25 @@
|
|||||||
"screenshot": "Captura de tela {{number}}",
|
"screenshot": "Captura de tela {{number}}",
|
||||||
"open_screenshot": "Ver captura de tela {{number}}",
|
"open_screenshot": "Ver captura de tela {{number}}",
|
||||||
"download_settings": "Ajustes do download",
|
"download_settings": "Ajustes do download",
|
||||||
"downloader": "Downloader"
|
"downloader": "Downloader",
|
||||||
|
"select_executable": "Selecionar",
|
||||||
|
"no_executable_selected": "Nenhum executável selecionado",
|
||||||
|
"open_folder": "Abrir pasta",
|
||||||
|
"open_download_location": "Ver arquivos baixados",
|
||||||
|
"create_shortcut": "Criar atalho na área de trabalho",
|
||||||
|
"remove_files": "Remover arquivos",
|
||||||
|
"options": "Opções",
|
||||||
|
"remove_from_library_description": "Isso irá remover {{game}} da sua biblioteca",
|
||||||
|
"remove_from_library_title": "Tem certeza?",
|
||||||
|
"executable_section_title": "Executável",
|
||||||
|
"executable_section_description": "O caminho do arquivo que será executado ao clicar em \"Jogar\"",
|
||||||
|
"downloads_secion_title": "Downloads",
|
||||||
|
"downloads_section_description": "Confira atualizações ou versões diferentes para este mesmo título",
|
||||||
|
"danger_zone_section_title": "Zona de perigo",
|
||||||
|
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os arquivos que foram baixados pelo Hydra",
|
||||||
|
"download_in_progress": "Download em andamento",
|
||||||
|
"download_paused": "Download pausado",
|
||||||
|
"last_downloaded_option": "Última opção baixada"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -10,12 +10,16 @@ import "./catalogue/search-games";
|
|||||||
import "./catalogue/search-game-repacks";
|
import "./catalogue/search-game-repacks";
|
||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
|
import "./library/create-game-shortcut";
|
||||||
import "./library/close-game";
|
import "./library/close-game";
|
||||||
import "./library/delete-game-folder";
|
import "./library/delete-game-folder";
|
||||||
import "./library/get-game-by-object-id";
|
import "./library/get-game-by-object-id";
|
||||||
import "./library/get-library";
|
import "./library/get-library";
|
||||||
import "./library/open-game";
|
import "./library/open-game";
|
||||||
|
import "./library/open-game-executable-path";
|
||||||
import "./library/open-game-installer";
|
import "./library/open-game-installer";
|
||||||
|
import "./library/open-game-installer-path";
|
||||||
|
import "./library/update-executable-path";
|
||||||
import "./library/remove-game";
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
|
@ -11,8 +11,7 @@ const addGameToLibrary = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectID: string,
|
objectID: string,
|
||||||
title: string,
|
title: string,
|
||||||
shop: GameShop,
|
shop: GameShop
|
||||||
executablePath: string | null
|
|
||||||
) => {
|
) => {
|
||||||
return gameRepository
|
return gameRepository
|
||||||
.update(
|
.update(
|
||||||
@ -22,7 +21,6 @@ const addGameToLibrary = async (
|
|||||||
{
|
{
|
||||||
shop,
|
shop,
|
||||||
status: null,
|
status: null,
|
||||||
executablePath,
|
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -42,7 +40,6 @@ const addGameToLibrary = async (
|
|||||||
iconUrl,
|
iconUrl,
|
||||||
objectID,
|
objectID,
|
||||||
shop,
|
shop,
|
||||||
executablePath,
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (iconUrl) {
|
if (iconUrl) {
|
||||||
|
29
src/main/events/library/create-game-shortcut.ts
Normal file
29
src/main/events/library/create-game-shortcut.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { IsNull, Not } from "typeorm";
|
||||||
|
import createDesktopShortcut from "create-desktop-shortcuts";
|
||||||
|
|
||||||
|
const createGameShortcut = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
id: number
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const game = await gameRepository.findOne({
|
||||||
|
where: { id, executablePath: Not(IsNull()) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (game) {
|
||||||
|
const filePath = game.executablePath;
|
||||||
|
|
||||||
|
const options = { filePath, name: game.title };
|
||||||
|
|
||||||
|
return createDesktopShortcut({
|
||||||
|
windows: options,
|
||||||
|
linux: options,
|
||||||
|
osx: options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("createGameShortcut", createGameShortcut);
|
18
src/main/events/library/open-game-executable-path.ts
Normal file
18
src/main/events/library/open-game-executable-path.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { shell } from "electron";
|
||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const openGameExecutablePath = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
gameId: number
|
||||||
|
) => {
|
||||||
|
const game = await gameRepository.findOne({
|
||||||
|
where: { id: gameId, isDeleted: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!game || !game.executablePath) return;
|
||||||
|
|
||||||
|
shell.showItemInFolder(game.executablePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("openGameExecutablePath", openGameExecutablePath);
|
27
src/main/events/library/open-game-installer-path.ts
Normal file
27
src/main/events/library/open-game-installer-path.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { shell } from "electron";
|
||||||
|
import path from "node:path";
|
||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const openGameInstallerPath = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
gameId: number
|
||||||
|
) => {
|
||||||
|
const game = await gameRepository.findOne({
|
||||||
|
where: { id: gameId, isDeleted: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!game || !game.folderName || !game.downloadPath) return true;
|
||||||
|
|
||||||
|
const gamePath = path.join(
|
||||||
|
game.downloadPath ?? (await getDownloadsPath()),
|
||||||
|
game.folderName!
|
||||||
|
);
|
||||||
|
|
||||||
|
shell.showItemInFolder(gamePath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("openGameInstallerPath", openGameInstallerPath);
|
@ -5,7 +5,10 @@ const removeGameFromLibrary = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
gameId: number
|
||||||
) => {
|
) => {
|
||||||
gameRepository.update({ id: gameId }, { isDeleted: true });
|
gameRepository.update(
|
||||||
|
{ id: gameId },
|
||||||
|
{ isDeleted: true, executablePath: null }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("removeGameFromLibrary", removeGameFromLibrary);
|
registerEvent("removeGameFromLibrary", removeGameFromLibrary);
|
||||||
|
20
src/main/events/library/update-executable-path.ts
Normal file
20
src/main/events/library/update-executable-path.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const updateExecutablePath = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
id: number,
|
||||||
|
executablePath: string
|
||||||
|
) => {
|
||||||
|
return gameRepository.update(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
executablePath,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateExecutablePath", updateExecutablePath);
|
@ -61,22 +61,19 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
|
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
|
||||||
objectID: string,
|
ipcRenderer.invoke("addGameToLibrary", objectID, title, shop),
|
||||||
title: string,
|
createGameShortcut: (id: number) =>
|
||||||
shop: GameShop,
|
ipcRenderer.invoke("createGameShortcut", id),
|
||||||
executablePath: string
|
updateExecutablePath: (id: number, executablePath: string) =>
|
||||||
) =>
|
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
||||||
ipcRenderer.invoke(
|
|
||||||
"addGameToLibrary",
|
|
||||||
objectID,
|
|
||||||
title,
|
|
||||||
shop,
|
|
||||||
executablePath
|
|
||||||
),
|
|
||||||
getLibrary: () => ipcRenderer.invoke("getLibrary"),
|
getLibrary: () => ipcRenderer.invoke("getLibrary"),
|
||||||
openGameInstaller: (gameId: number) =>
|
openGameInstaller: (gameId: number) =>
|
||||||
ipcRenderer.invoke("openGameInstaller", gameId),
|
ipcRenderer.invoke("openGameInstaller", gameId),
|
||||||
|
openGameInstallerPath: (gameId: number) =>
|
||||||
|
ipcRenderer.invoke("openGameInstallerPath", gameId),
|
||||||
|
openGameExecutablePath: (gameId: number) =>
|
||||||
|
ipcRenderer.invoke("openGameExecutablePath", gameId),
|
||||||
openGame: (gameId: number, executablePath: string) =>
|
openGame: (gameId: number, executablePath: string) =>
|
||||||
ipcRenderer.invoke("openGame", gameId, executablePath),
|
ipcRenderer.invoke("openGame", gameId, executablePath),
|
||||||
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
||||||
|
@ -55,4 +55,15 @@ export const button = styleVariants({
|
|||||||
color: "#c0c1c7",
|
color: "#c0c1c7",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
danger: [
|
||||||
|
base,
|
||||||
|
{
|
||||||
|
border: `solid 1px #a31533`,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "white",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "#a31533",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
@ -54,7 +54,7 @@ export function Hero() {
|
|||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className={styles.backdrop}>
|
||||||
<img
|
<img
|
||||||
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
|
src={steamUrlBuilder.libraryHero(FEATURED_GAME_ID)}
|
||||||
alt={FEATURED_GAME_TITLE}
|
alt={FEATURED_GAME_TITLE}
|
||||||
className={styles.heroMedia}
|
className={styles.heroMedia}
|
||||||
/>
|
/>
|
||||||
|
@ -2,24 +2,25 @@ import { keyframes, style } from "@vanilla-extract/css";
|
|||||||
import { recipe } from "@vanilla-extract/recipes";
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
export const fadeIn = keyframes({
|
export const scaleFadeIn = keyframes({
|
||||||
"0%": { opacity: 0 },
|
"0%": { opacity: "0", scale: "0.5" },
|
||||||
"100%": {
|
"100%": {
|
||||||
opacity: 1,
|
opacity: "1",
|
||||||
|
scale: "1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fadeOut = keyframes({
|
export const scaleFadeOut = keyframes({
|
||||||
"0%": { opacity: 1 },
|
"0%": { opacity: "1", scale: "1" },
|
||||||
"100%": {
|
"100%": {
|
||||||
opacity: 0,
|
opacity: "0",
|
||||||
|
scale: "0.5",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const modal = recipe({
|
export const modal = recipe({
|
||||||
base: {
|
base: {
|
||||||
animationName: fadeIn,
|
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
|
||||||
animationDuration: "0.3s",
|
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
maxWidth: "600px",
|
maxWidth: "600px",
|
||||||
@ -33,8 +34,14 @@ export const modal = recipe({
|
|||||||
variants: {
|
variants: {
|
||||||
closing: {
|
closing: {
|
||||||
true: {
|
true: {
|
||||||
animationName: fadeOut,
|
animationName: scaleFadeOut,
|
||||||
opacity: 0,
|
opacity: "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
true: {
|
||||||
|
width: "800px",
|
||||||
|
maxWidth: "800px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -12,6 +12,7 @@ export interface ModalProps {
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
large?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ export function Modal({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
onClose,
|
onClose,
|
||||||
|
large,
|
||||||
children,
|
children,
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
@ -88,7 +90,7 @@ export function Modal({
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<Backdrop isClosing={isClosing}>
|
<Backdrop isClosing={isClosing}>
|
||||||
<div
|
<div
|
||||||
className={styles.modal({ closing: isClosing })}
|
className={styles.modal({ closing: isClosing, large })}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-labelledby={title}
|
aria-labelledby={title}
|
||||||
aria-describedby={description}
|
aria-describedby={description}
|
||||||
|
@ -5,7 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
|||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
|
|
||||||
import { TextField } from "@renderer/components";
|
import { TextField } from "@renderer/components";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
||||||
|
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
|
|
||||||
@ -36,6 +36,8 @@ export function Sidebar() {
|
|||||||
|
|
||||||
const { lastPacket, progress } = useDownload();
|
const { lastPacket, progress } = useDownload();
|
||||||
|
|
||||||
|
const { showWarningToast } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
}, [lastPacket?.game.id, updateLibrary]);
|
}, [lastPacket?.game.id, updateLibrary]);
|
||||||
@ -122,6 +124,24 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSidebarGameClick = (
|
||||||
|
event: React.MouseEvent,
|
||||||
|
game: LibraryGame
|
||||||
|
) => {
|
||||||
|
const path = buildGameDetailsPath(game);
|
||||||
|
if (path !== location.pathname) {
|
||||||
|
navigate(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.detail == 2) {
|
||||||
|
if (game.executablePath) {
|
||||||
|
window.electron.openGame(game.id, game.executablePath);
|
||||||
|
} else {
|
||||||
|
showWarningToast(t("game_has_no_executable"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
@ -183,9 +203,7 @@ export function Sidebar() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.menuItemButton}
|
className={styles.menuItemButton}
|
||||||
onClick={() =>
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||||
handleSidebarItemClick(buildGameDetailsPath(game))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{game.iconUrl ? (
|
{game.iconUrl ? (
|
||||||
<img
|
<img
|
||||||
|
@ -81,3 +81,7 @@ export const successIcon = style({
|
|||||||
export const errorIcon = style({
|
export const errorIcon = style({
|
||||||
color: vars.color.danger,
|
color: vars.color.danger,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const warningIcon = style({
|
||||||
|
color: vars.color.warning,
|
||||||
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
AlertIcon,
|
||||||
CheckCircleFillIcon,
|
CheckCircleFillIcon,
|
||||||
XCircleFillIcon,
|
XCircleFillIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
@ -11,7 +12,7 @@ import { SPACING_UNIT } from "@renderer/theme.css";
|
|||||||
export interface ToastProps {
|
export interface ToastProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
type: "success" | "error";
|
type: "success" | "error" | "warning";
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +85,8 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
|
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
|
||||||
|
|
||||||
|
{type === "warning" && <AlertIcon className={styles.warningIcon} />}
|
||||||
<span style={{ fontWeight: "bold" }}>{message}</span>
|
<span style={{ fontWeight: "bold" }}>{message}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@ -56,11 +56,14 @@ declare global {
|
|||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
objectID: string,
|
objectID: string,
|
||||||
title: string,
|
title: string,
|
||||||
shop: GameShop,
|
shop: GameShop
|
||||||
executablePath: string | null
|
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
createGameShortcut: (id: number) => Promise<boolean>;
|
||||||
|
updateExecutablePath: (id: number, executablePath: string) => Promise<void>;
|
||||||
getLibrary: () => Promise<LibraryGame[]>;
|
getLibrary: () => Promise<LibraryGame[]>;
|
||||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||||
|
openGameInstallerPath: (gameId: number) => Promise<boolean>;
|
||||||
|
openGameExecutablePath: (gameId: number) => Promise<void>;
|
||||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
||||||
closeGame: (gameId: number) => Promise<boolean>;
|
closeGame: (gameId: number) => Promise<boolean>;
|
||||||
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
||||||
|
@ -29,5 +29,17 @@ export function useToast() {
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { showSuccessToast, showErrorToast };
|
const showWarningToast = useCallback(
|
||||||
|
(message: string) => {
|
||||||
|
dispatch(
|
||||||
|
showToast({
|
||||||
|
message,
|
||||||
|
type: "warning",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { showSuccessToast, showErrorToast, showWarningToast };
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { useParams, useSearchParams } from "react-router-dom";
|
|||||||
|
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { getSteamLanguage } from "@renderer/helpers";
|
import { getSteamLanguage } from "@renderer/helpers";
|
||||||
import { useAppDispatch, useDownload } from "@renderer/hooks";
|
import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ import {
|
|||||||
RepacksModal,
|
RepacksModal,
|
||||||
} from "./modals";
|
} from "./modals";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
|
import { GameOptionsModal } from "./modals/game-options-modal";
|
||||||
|
|
||||||
export interface GameDetailsContext {
|
export interface GameDetailsContext {
|
||||||
game: Game | null;
|
game: Game | null;
|
||||||
@ -29,6 +30,8 @@ export interface GameDetailsContext {
|
|||||||
gameColor: string;
|
gameColor: string;
|
||||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||||
openRepacksModal: () => void;
|
openRepacksModal: () => void;
|
||||||
|
openGameOptionsModal: () => void;
|
||||||
|
selectGameExecutable: () => Promise<string | null>;
|
||||||
updateGame: () => Promise<void>;
|
updateGame: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +47,8 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
|||||||
gameColor: "",
|
gameColor: "",
|
||||||
setGameColor: () => {},
|
setGameColor: () => {},
|
||||||
openRepacksModal: () => {},
|
openRepacksModal: () => {},
|
||||||
|
openGameOptionsModal: () => {},
|
||||||
|
selectGameExecutable: async () => null,
|
||||||
updateGame: async () => {},
|
updateGame: async () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,6 +75,7 @@ export function GameDetailsContextProvider({
|
|||||||
>(null);
|
>(null);
|
||||||
const [isGameRunning, setisGameRunning] = useState(false);
|
const [isGameRunning, setisGameRunning] = useState(false);
|
||||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||||
|
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
@ -81,6 +87,10 @@ export function GameDetailsContextProvider({
|
|||||||
|
|
||||||
const { startDownload, lastPacket } = useDownload();
|
const { startDownload, lastPacket } = useDownload();
|
||||||
|
|
||||||
|
const userPreferences = useAppSelector(
|
||||||
|
(state) => state.userPreferences.value
|
||||||
|
);
|
||||||
|
|
||||||
const updateGame = useCallback(async () => {
|
const updateGame = useCallback(async () => {
|
||||||
return window.electron
|
return window.electron
|
||||||
.getGameByObjectID(objectID!)
|
.getGameByObjectID(objectID!)
|
||||||
@ -158,6 +168,7 @@ export function GameDetailsContextProvider({
|
|||||||
|
|
||||||
await updateGame();
|
await updateGame();
|
||||||
setShowRepacksModal(false);
|
setShowRepacksModal(false);
|
||||||
|
setShowGameOptionsModal(false);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
repack.repacker === "onlinefix" &&
|
repack.repacker === "onlinefix" &&
|
||||||
@ -172,7 +183,36 @@ export function GameDetailsContextProvider({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDownloadsPath = async () => {
|
||||||
|
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||||
|
return window.electron.getDefaultDownloadsPath();
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectGameExecutable = async () => {
|
||||||
|
const downloadsPath = await getDownloadsPath();
|
||||||
|
|
||||||
|
return window.electron
|
||||||
|
.showOpenDialog({
|
||||||
|
properties: ["openFile"],
|
||||||
|
defaultPath: downloadsPath,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "Game executable",
|
||||||
|
extensions: ["exe"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then(({ filePaths }) => {
|
||||||
|
if (filePaths && filePaths.length > 0) {
|
||||||
|
return filePaths[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const openRepacksModal = () => setShowRepacksModal(true);
|
const openRepacksModal = () => setShowRepacksModal(true);
|
||||||
|
const openGameOptionsModal = () => setShowGameOptionsModal(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider
|
<Provider
|
||||||
@ -188,6 +228,8 @@ export function GameDetailsContextProvider({
|
|||||||
gameColor,
|
gameColor,
|
||||||
setGameColor,
|
setGameColor,
|
||||||
openRepacksModal,
|
openRepacksModal,
|
||||||
|
openGameOptionsModal,
|
||||||
|
selectGameExecutable,
|
||||||
updateGame,
|
updateGame,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -208,6 +250,16 @@ export function GameDetailsContextProvider({
|
|||||||
onClose={() => setShowInstructionsModal(null)}
|
onClose={() => setShowInstructionsModal(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{game && (
|
||||||
|
<GameOptionsModal
|
||||||
|
visible={showGameOptionsModal}
|
||||||
|
game={game}
|
||||||
|
onClose={() => {
|
||||||
|
setShowGameOptionsModal(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { vars } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
|
|
||||||
export const heroPanelAction = style({
|
export const heroPanelAction = style({
|
||||||
border: `solid 1px ${vars.color.muted}`,
|
border: `solid 1px ${vars.color.muted}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actions = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const separator = style({
|
||||||
|
width: "1px",
|
||||||
|
backgroundColor: vars.color.muted,
|
||||||
|
opacity: "0.2",
|
||||||
|
});
|
||||||
|
@ -1,28 +1,16 @@
|
|||||||
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
import { GearIcon, PlayIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
|
|
||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./hero-panel-actions.css";
|
import * as styles from "./hero-panel-actions.css";
|
||||||
import { gameDetailsContext } from "../game-details.context";
|
import { gameDetailsContext } from "../game-details.context";
|
||||||
import { Downloader } from "@shared";
|
|
||||||
|
|
||||||
export function HeroPanelActions() {
|
export function HeroPanelActions() {
|
||||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
|
||||||
|
|
||||||
const {
|
const { isGameDeleting } = useDownload();
|
||||||
resumeDownload,
|
|
||||||
pauseDownload,
|
|
||||||
cancelDownload,
|
|
||||||
removeGameFromLibrary,
|
|
||||||
isGameDeleting,
|
|
||||||
} = useDownload();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
game,
|
game,
|
||||||
@ -31,61 +19,20 @@ export function HeroPanelActions() {
|
|||||||
objectID,
|
objectID,
|
||||||
gameTitle,
|
gameTitle,
|
||||||
openRepacksModal,
|
openRepacksModal,
|
||||||
|
openGameOptionsModal,
|
||||||
updateGame,
|
updateGame,
|
||||||
|
selectGameExecutable,
|
||||||
} = useContext(gameDetailsContext);
|
} = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const userPreferences = useAppSelector(
|
|
||||||
(state) => state.userPreferences.value
|
|
||||||
);
|
|
||||||
|
|
||||||
const { updateLibrary } = useLibrary();
|
const { updateLibrary } = useLibrary();
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const getDownloadsPath = async () => {
|
const addGameToLibrary = async () => {
|
||||||
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
|
||||||
return window.electron.getDefaultDownloadsPath();
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectGameExecutable = async () => {
|
|
||||||
const downloadsPath = await getDownloadsPath();
|
|
||||||
|
|
||||||
return window.electron
|
|
||||||
.showOpenDialog({
|
|
||||||
properties: ["openFile"],
|
|
||||||
defaultPath: downloadsPath,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "Game executable",
|
|
||||||
extensions: ["exe"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.then(({ filePaths }) => {
|
|
||||||
if (filePaths && filePaths.length > 0) {
|
|
||||||
return filePaths[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleGameOnLibrary = async () => {
|
|
||||||
setToggleLibraryGameDisabled(true);
|
setToggleLibraryGameDisabled(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (game) {
|
await window.electron.addGameToLibrary(objectID!, gameTitle, "steam");
|
||||||
await removeGameFromLibrary(game.id);
|
|
||||||
} else {
|
|
||||||
const gameExecutablePath = await selectGameExecutable();
|
|
||||||
|
|
||||||
await window.electron.addGameToLibrary(
|
|
||||||
objectID!,
|
|
||||||
gameTitle,
|
|
||||||
"steam",
|
|
||||||
gameExecutablePath
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
updateGame();
|
updateGame();
|
||||||
@ -94,15 +41,6 @@ export function HeroPanelActions() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openGameInstaller = () => {
|
|
||||||
if (game) {
|
|
||||||
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
|
|
||||||
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
|
||||||
updateLibrary();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openGame = async () => {
|
const openGame = async () => {
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.executablePath) {
|
if (game.executablePath) {
|
||||||
@ -122,15 +60,15 @@ export function HeroPanelActions() {
|
|||||||
|
|
||||||
const deleting = game ? isGameDeleting(game?.id) : false;
|
const deleting = game ? isGameDeleting(game?.id) : false;
|
||||||
|
|
||||||
const toggleGameOnLibraryButton = (
|
const addGameToLibraryButton = (
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={toggleLibraryGameDisabled}
|
disabled={toggleLibraryGameDisabled}
|
||||||
onClick={toggleGameOnLibrary}
|
onClick={addGameToLibrary}
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
>
|
>
|
||||||
{game ? <NoEntryIcon /> : <PlusCircleIcon />}
|
<PlusCircleIcon />
|
||||||
{game ? t("remove_from_library") : t("add_to_library")}
|
{t("add_to_library")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -145,109 +83,18 @@ export function HeroPanelActions() {
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (game?.status === "active" && game?.progress !== 1) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={() => pauseDownload(game.id).then(updateGame)}
|
|
||||||
theme="outline"
|
|
||||||
className={styles.heroPanelAction}
|
|
||||||
>
|
|
||||||
{t("pause")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => cancelDownload(game.id).then(updateGame)}
|
|
||||||
theme="outline"
|
|
||||||
className={styles.heroPanelAction}
|
|
||||||
>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game?.status === "paused") {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={() => resumeDownload(game.id).then(updateGame)}
|
|
||||||
theme="outline"
|
|
||||||
className={styles.heroPanelAction}
|
|
||||||
disabled={
|
|
||||||
game.downloader === Downloader.RealDebrid &&
|
|
||||||
!userPreferences?.realDebridApiToken
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("resume")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => cancelDownload(game.id).then(updateGame)}
|
|
||||||
theme="outline"
|
|
||||||
className={styles.heroPanelAction}
|
|
||||||
>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game?.status === "removed") {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{showDownloadOptionsButton}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => removeGameFromLibrary(game.id).then(updateGame)}
|
|
||||||
theme="outline"
|
|
||||||
disabled={deleting}
|
|
||||||
className={styles.heroPanelAction}
|
|
||||||
>
|
|
||||||
{t("remove_from_list")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (repacks.length && !game) {
|
if (repacks.length && !game) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{toggleGameOnLibraryButton}
|
{addGameToLibraryButton}
|
||||||
<Button
|
{showDownloadOptionsButton}
|
||||||
onClick={openRepacksModal}
|
|
||||||
theme="outline"
|
|
||||||
className={styles.heroPanelAction}
|
|
||||||
>
|
|
||||||
{t("open_download_options")}
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.actions}>
|
||||||
{game.progress === 1 && game.downloadPath && (
|
|
||||||
<>
|
|
||||||
<BinaryNotFoundModal
|
|
||||||
visible={showBinaryNotFoundModal}
|
|
||||||
onClose={() => setShowBinaryNotFoundModal(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={openGameInstaller}
|
|
||||||
theme="outline"
|
|
||||||
disabled={deleting || isGameRunning}
|
|
||||||
className={styles.heroPanelAction}
|
|
||||||
>
|
|
||||||
{t("install")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{game.progress === 1 && !game.downloadPath && showDownloadOptionsButton}
|
|
||||||
|
|
||||||
{game.progress !== 1 && toggleGameOnLibraryButton}
|
|
||||||
|
|
||||||
{isGameRunning ? (
|
{isGameRunning ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={closeGame}
|
onClick={closeGame}
|
||||||
@ -264,12 +111,25 @@ export function HeroPanelActions() {
|
|||||||
disabled={deleting || isGameRunning}
|
disabled={deleting || isGameRunning}
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
>
|
>
|
||||||
|
<PlayIcon />
|
||||||
{t("play")}
|
{t("play")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
|
<div className={styles.separator} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={openGameOptionsModal}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting || isGameRunning}
|
||||||
|
className={styles.heroPanelAction}
|
||||||
|
>
|
||||||
|
<GearIcon />
|
||||||
|
{t("options")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return toggleGameOnLibraryButton;
|
return addGameToLibraryButton;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import * as styles from "./hero-panel.css";
|
||||||
import { useDate } from "@renderer/hooks";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
|
import { useDate, useDownload } from "@renderer/hooks";
|
||||||
import { gameDetailsContext } from "../game-details.context";
|
import { gameDetailsContext } from "../game-details.context";
|
||||||
|
import { Link } from "@renderer/components";
|
||||||
|
|
||||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
@ -13,6 +15,8 @@ export function HeroPanelPlaytime() {
|
|||||||
|
|
||||||
const { i18n, t } = useTranslation("game_details");
|
const { i18n, t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const { progress, lastPacket } = useDownload();
|
||||||
|
|
||||||
const { formatDistance } = useDate();
|
const { formatDistance } = useDate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -46,8 +50,45 @@ export function HeroPanelPlaytime() {
|
|||||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!game?.lastTimePlayed) {
|
if (!game) return null;
|
||||||
return <p>{t("not_played_yet", { title: game?.title })}</p>;
|
|
||||||
|
const hasDownload =
|
||||||
|
["active", "paused"].includes(game.status) && game.progress !== 1;
|
||||||
|
|
||||||
|
const isGameDownloading =
|
||||||
|
game.status === "active" && lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
|
const downloadInProgressInfo = (
|
||||||
|
<div className={styles.downloadDetailsRow}>
|
||||||
|
<Link to="/downloads" className={styles.downloadsLink}>
|
||||||
|
{game.status === "active"
|
||||||
|
? t("download_in_progress")
|
||||||
|
: t("download_paused")}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<small>
|
||||||
|
{isGameDownloading ? progress : formatDownloadProgress(game.progress)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!game.lastTimePlayed) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{t("not_played_yet", { title: game?.title })}</p>
|
||||||
|
{hasDownload && downloadInProgressInfo}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGameRunning) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{t("playing_now")}</p>
|
||||||
|
|
||||||
|
{hasDownload && downloadInProgressInfo}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -58,8 +99,8 @@ export function HeroPanelPlaytime() {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isGameRunning ? (
|
{hasDownload ? (
|
||||||
<p>{t("playing_now")}</p>
|
downloadInProgressInfo
|
||||||
) : (
|
) : (
|
||||||
<p>
|
<p>
|
||||||
{t("last_time_played", {
|
{t("last_time_played", {
|
||||||
|
@ -28,9 +28,15 @@ export const actions = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const downloadDetailsRow = style({
|
export const downloadDetailsRow = style({
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-end",
|
color: vars.color.body,
|
||||||
|
alignItems: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadsLink = style({
|
||||||
|
color: vars.color.body,
|
||||||
|
textDecoration: "underline",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const progressBar = recipe({
|
export const progressBar = recipe({
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useContext, useMemo } from "react";
|
import { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
|
|
||||||
import { useDownload } from "@renderer/hooks";
|
import { useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
|
||||||
import { HeroPanelActions } from "./hero-panel-actions";
|
import { HeroPanelActions } from "./hero-panel-actions";
|
||||||
import { Downloader, formatBytes } from "@shared";
|
|
||||||
|
|
||||||
import * as styles from "./hero-panel.css";
|
import * as styles from "./hero-panel.css";
|
||||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||||
import { gameDetailsContext } from "../game-details.context";
|
import { gameDetailsContext } from "../game-details.context";
|
||||||
@ -18,82 +13,13 @@ export function HeroPanel() {
|
|||||||
|
|
||||||
const { game, repacks, gameColor } = useContext(gameDetailsContext);
|
const { game, repacks, gameColor } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { progress, eta, lastPacket, isGameDeleting } = useDownload();
|
const { lastPacket } = useDownload();
|
||||||
|
|
||||||
const finalDownloadSize = useMemo(() => {
|
|
||||||
if (game?.fileSize) return formatBytes(game.fileSize);
|
|
||||||
|
|
||||||
if (lastPacket?.game.fileSize && game?.status === "active")
|
|
||||||
return formatBytes(lastPacket?.game.fileSize);
|
|
||||||
|
|
||||||
return "N/A";
|
|
||||||
}, [game, lastPacket?.game]);
|
|
||||||
|
|
||||||
const isGameDownloading =
|
const isGameDownloading =
|
||||||
game?.status === "active" && lastPacket?.game.id === game?.id;
|
game?.status === "active" && lastPacket?.game.id === game?.id;
|
||||||
|
|
||||||
const getInfo = () => {
|
const getInfo = () => {
|
||||||
if (isGameDeleting(game?.id ?? -1)) return <p>{t("deleting")}</p>;
|
if (!game) {
|
||||||
|
|
||||||
if (game?.progress === 1) return <HeroPanelPlaytime />;
|
|
||||||
|
|
||||||
if (game?.status === "active") {
|
|
||||||
if (lastPacket?.isDownloadingMetadata && isGameDownloading) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>{progress}</p>
|
|
||||||
<p>{t("downloading_metadata")}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sizeDownloaded = formatBytes(
|
|
||||||
lastPacket?.game?.bytesDownloaded ?? game?.bytesDownloaded
|
|
||||||
);
|
|
||||||
|
|
||||||
const showPeers =
|
|
||||||
game?.downloader === Downloader.Torrent &&
|
|
||||||
lastPacket?.numPeers !== undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p className={styles.downloadDetailsRow}>
|
|
||||||
{isGameDownloading
|
|
||||||
? progress
|
|
||||||
: formatDownloadProgress(game?.progress)}
|
|
||||||
|
|
||||||
<small>{eta ? t("eta", { eta }) : t("calculating_eta")}</small>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className={styles.downloadDetailsRow}>
|
|
||||||
<span>
|
|
||||||
{sizeDownloaded} / {finalDownloadSize}
|
|
||||||
</span>
|
|
||||||
{showPeers && (
|
|
||||||
<small>
|
|
||||||
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game?.status === "paused") {
|
|
||||||
const formattedProgress = formatDownloadProgress(game.progress);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p className={styles.downloadDetailsRow}>
|
|
||||||
{formattedProgress} <small>{t("paused")}</small>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [latestRepack] = repacks;
|
const [latestRepack] = repacks;
|
||||||
|
|
||||||
if (latestRepack) {
|
if (latestRepack) {
|
||||||
@ -109,6 +35,9 @@ export function HeroPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <p>{t("no_downloads")}</p>;
|
return <p>{t("no_downloads")}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <HeroPanelPlaytime />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgroundColor = gameColor
|
const backgroundColor = gameColor
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
import { SPACING_UNIT } from "../../../theme.css";
|
||||||
|
|
||||||
|
export const optionsContainer = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
flexDirection: "column",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gameOptionHeader = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gameOptionHeaderDescription = style({
|
||||||
|
fontFamily: "'Fira Sans', sans-serif",
|
||||||
|
fontWeight: "400",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gameOptionRow = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
@ -0,0 +1,192 @@
|
|||||||
|
import { useContext, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
|
import type { Game } from "@types";
|
||||||
|
import * as styles from "./game-options-modal.css";
|
||||||
|
import { gameDetailsContext } from "../game-details.context";
|
||||||
|
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||||
|
import { useDownload } from "@renderer/hooks";
|
||||||
|
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||||
|
|
||||||
|
export interface GameOptionsModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
game: Game;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GameOptionsModal({
|
||||||
|
visible,
|
||||||
|
game,
|
||||||
|
onClose,
|
||||||
|
}: GameOptionsModalProps) {
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const { updateGame, openRepacksModal, selectGameExecutable } =
|
||||||
|
useContext(gameDetailsContext);
|
||||||
|
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
removeGameInstaller,
|
||||||
|
removeGameFromLibrary,
|
||||||
|
isGameDeleting,
|
||||||
|
cancelDownload,
|
||||||
|
} = useDownload();
|
||||||
|
|
||||||
|
const deleting = isGameDeleting(game.id);
|
||||||
|
|
||||||
|
const { lastPacket } = useDownload();
|
||||||
|
|
||||||
|
const isGameDownloading =
|
||||||
|
game.status === "active" && lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
|
const handleRemoveGameFromLibrary = async () => {
|
||||||
|
if (isGameDownloading) {
|
||||||
|
await cancelDownload(game.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await removeGameFromLibrary(game.id);
|
||||||
|
updateGame();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeExecutableLocation = async () => {
|
||||||
|
const path = await selectGameExecutable();
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
await window.electron.updateExecutablePath(game.id, path);
|
||||||
|
updateGame();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateShortcut = async () => {
|
||||||
|
await window.electron.createGameShortcut(game.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenDownloadFolder = async () => {
|
||||||
|
await window.electron.openGameInstallerPath(game.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteGame = async () => {
|
||||||
|
await removeGameInstaller(game.id);
|
||||||
|
updateGame();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenGameExecutablePath = async () => {
|
||||||
|
await window.electron.openGameExecutablePath(game.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteGameModal
|
||||||
|
visible={showDeleteModal}
|
||||||
|
onClose={() => setShowDeleteModal(false)}
|
||||||
|
deleteGame={handleDeleteGame}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RemoveGameFromLibraryModal
|
||||||
|
visible={showRemoveGameModal}
|
||||||
|
onClose={() => setShowRemoveGameModal(false)}
|
||||||
|
removeGameFromLibrary={handleRemoveGameFromLibrary}
|
||||||
|
game={game}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={game.title}
|
||||||
|
onClose={onClose}
|
||||||
|
large={true}
|
||||||
|
>
|
||||||
|
<div className={styles.optionsContainer}>
|
||||||
|
<div className={styles.gameOptionHeader}>
|
||||||
|
<h2>{t("executable_section_title")}</h2>
|
||||||
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
{t("executable_section_description")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className={styles.gameOptionRow}>
|
||||||
|
<TextField
|
||||||
|
value={game.executablePath || ""}
|
||||||
|
readOnly
|
||||||
|
theme="dark"
|
||||||
|
disabled
|
||||||
|
placeholder={t("no_executable_selected")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleChangeExecutableLocation}
|
||||||
|
>
|
||||||
|
{t("select_executable")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{game.executablePath && (
|
||||||
|
<div className={styles.gameOptionRow}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleOpenGameExecutablePath}
|
||||||
|
>
|
||||||
|
{t("open_folder")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreateShortcut} theme="outline">
|
||||||
|
{t("create_shortcut")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.gameOptionHeader}>
|
||||||
|
<h2>{t("downloads_secion_title")}</h2>
|
||||||
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
{t("downloads_section_description")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className={styles.gameOptionRow}>
|
||||||
|
<Button
|
||||||
|
onClick={openRepacksModal}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting || isGameDownloading}
|
||||||
|
>
|
||||||
|
{t("open_download_options")}
|
||||||
|
</Button>
|
||||||
|
{game.downloadPath && (
|
||||||
|
<Button
|
||||||
|
onClick={handleOpenDownloadFolder}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{t("open_download_location")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.gameOptionHeader}>
|
||||||
|
<h2>{t("danger_zone_section_title")}</h2>
|
||||||
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
{t("danger_zone_section_description")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className={styles.gameOptionRow}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowRemoveGameModal(true)}
|
||||||
|
theme="danger"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{t("remove_from_library")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
}}
|
||||||
|
theme="danger"
|
||||||
|
disabled={isGameDownloading || deleting || !game.downloadPath}
|
||||||
|
>
|
||||||
|
{t("remove_files")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { SPACING_UNIT } from "../../../theme.css";
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
export const deleteActionsButtonsCtn = style({
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "end",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, Modal } from "@renderer/components";
|
||||||
|
import * as styles from "./remove-from-library-modal.css";
|
||||||
|
import { Game } from "@types";
|
||||||
|
|
||||||
|
interface RemoveGameFromLibraryModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
game: Game;
|
||||||
|
onClose: () => void;
|
||||||
|
removeGameFromLibrary: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveGameFromLibraryModal({
|
||||||
|
onClose,
|
||||||
|
game,
|
||||||
|
visible,
|
||||||
|
removeGameFromLibrary,
|
||||||
|
}: RemoveGameFromLibraryModalProps) {
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const handleRemoveGame = async () => {
|
||||||
|
await removeGameFromLibrary();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("remove_from_library_title")}
|
||||||
|
description={t("remove_from_library_description", { game: game.title })}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className={styles.deleteActionsButtonsCtn}>
|
||||||
|
<Button onClick={handleRemoveGame} theme="outline">
|
||||||
|
{t("remove")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={onClose} theme="primary">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import parseTorrent from "parse-torrent";
|
||||||
|
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
import { Badge, Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
|
|
||||||
import * as styles from "./repacks-modal.css";
|
import * as styles from "./repacks-modal.css";
|
||||||
@ -31,13 +32,22 @@ export function RepacksModal({
|
|||||||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||||
|
|
||||||
const { repacks } = useContext(gameDetailsContext);
|
const [infoHash, setInfoHash] = useState("");
|
||||||
|
|
||||||
|
const { repacks, game } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const getInfoHash = useCallback(async () => {
|
||||||
|
const torrent = await parseTorrent(game?.uri ?? "");
|
||||||
|
setInfoHash(torrent.infoHash ?? "");
|
||||||
|
}, [game]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredRepacks(repacks);
|
setFilteredRepacks(repacks);
|
||||||
}, [repacks, visible]);
|
|
||||||
|
if (game?.uri) getInfoHash();
|
||||||
|
}, [repacks, visible, game, getInfoHash]);
|
||||||
|
|
||||||
const handleRepackClick = (repack: GameRepack) => {
|
const handleRepackClick = (repack: GameRepack) => {
|
||||||
setRepack(repack);
|
setRepack(repack);
|
||||||
@ -89,6 +99,11 @@ export function RepacksModal({
|
|||||||
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
|
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
|
||||||
{repack.title}
|
{repack.title}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{repack.magnet.toLowerCase().includes(infoHash) && (
|
||||||
|
<Badge>{t("last_downloaded_option")}</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
<p style={{ fontSize: "12px" }}>
|
<p style={{ fontSize: "12px" }}>
|
||||||
{repack.fileSize} - {repack.repacker} -{" "}
|
{repack.fileSize} - {repack.repacker} -{" "}
|
||||||
{repack.uploadDate
|
{repack.uploadDate
|
||||||
|
@ -11,6 +11,7 @@ export const [themeClass, vars] = createTheme({
|
|||||||
border: "#424244",
|
border: "#424244",
|
||||||
success: "#1c9749",
|
success: "#1c9749",
|
||||||
danger: "#e11d48",
|
danger: "#e11d48",
|
||||||
|
warning: "#ffc107",
|
||||||
},
|
},
|
||||||
opacity: {
|
opacity: {
|
||||||
disabled: "0.5",
|
disabled: "0.5",
|
||||||
|
@ -106,6 +106,7 @@ export interface Game {
|
|||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
executablePath: string | null;
|
executablePath: string | null;
|
||||||
lastTimePlayed: Date | null;
|
lastTimePlayed: Date | null;
|
||||||
|
uri: string | null;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
objectID: string;
|
objectID: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
|
@ -2403,6 +2403,13 @@ crc@^3.8.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
buffer "^5.1.0"
|
buffer "^5.1.0"
|
||||||
|
|
||||||
|
create-desktop-shortcuts@^1.11.0:
|
||||||
|
version "1.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/create-desktop-shortcuts/-/create-desktop-shortcuts-1.11.0.tgz#8eed89329e9bce70dece46d02a80573fe1f2536d"
|
||||||
|
integrity sha512-nmVtPVqNyMuAyMpDnd7l++hb2laqCWZXnHQaFhqGT1YEi2Ve3unu6QyuyIpGxAwIscNHcG1Ehnl+lFw6ygB2nQ==
|
||||||
|
dependencies:
|
||||||
|
which "2.0.2"
|
||||||
|
|
||||||
cross-fetch-ponyfill@^1.0.3:
|
cross-fetch-ponyfill@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz#5c5524e3bd3374e71d5016c2327e416369a57527"
|
resolved "https://registry.yarnpkg.com/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz#5c5524e3bd3374e71d5016c2327e416369a57527"
|
||||||
@ -6278,7 +6285,7 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9:
|
|||||||
gopd "^1.0.1"
|
gopd "^1.0.1"
|
||||||
has-tostringtag "^1.0.2"
|
has-tostringtag "^1.0.2"
|
||||||
|
|
||||||
which@^2.0.1:
|
which@2.0.2, which@^2.0.1:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||||
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
||||||
|
Loading…
Reference in New Issue
Block a user