mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-02 16:23:48 +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 }}
|
||||
path: |
|
||||
dist/win-unpacked/**
|
||||
dist/*-portable.exe
|
||||
dist/*.zip
|
||||
dist/*.dmg
|
||||
dist/*.deb
|
||||
|
@ -18,6 +18,9 @@ asarUnpack:
|
||||
win:
|
||||
executableName: Hydra
|
||||
requestedExecutionLevel: requireAdministrator
|
||||
target:
|
||||
- nsis
|
||||
- portable
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
@ -25,6 +28,9 @@ nsis:
|
||||
createDesktopShortcut: always
|
||||
oneClick: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
portable:
|
||||
artifactName: ${name}-${version}-portable.${ext}
|
||||
requestExecutionLevel: admin
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
|
@ -48,6 +48,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"color": "^4.2.3",
|
||||
"color.js": "^1.2.0",
|
||||
"create-desktop-shortcuts": "^1.11.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"electron-log": "^5.1.4",
|
||||
"electron-updater": "^6.1.8",
|
||||
|
File diff suppressed because one or more lines are too long
@ -15,7 +15,8 @@
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filter library",
|
||||
"home": "Home",
|
||||
"queued": "{{title}} (Queued)"
|
||||
"queued": "{{title}} (Queued)",
|
||||
"game_has_no_executable": "Game has no executable selected"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search games",
|
||||
@ -100,7 +101,25 @@
|
||||
"screenshot": "Screenshot {{number}}",
|
||||
"open_screenshot": "Open screenshot {{number}}",
|
||||
"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": {
|
||||
"title": "Activate Hydra",
|
||||
|
@ -16,7 +16,8 @@
|
||||
"filter": "Filtrar biblioteca",
|
||||
"home": "Início",
|
||||
"follow_us": "Acompanhe-nos",
|
||||
"queued": "{{title}} (Na fila)"
|
||||
"queued": "{{title}} (Na fila)",
|
||||
"game_has_no_executable": "Jogo não possui executável selecionado"
|
||||
},
|
||||
"header": {
|
||||
"search": "Buscar jogos",
|
||||
@ -97,7 +98,25 @@
|
||||
"screenshot": "Captura de tela {{number}}",
|
||||
"open_screenshot": "Ver captura de tela {{number}}",
|
||||
"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": {
|
||||
"title": "Ativação",
|
||||
|
@ -10,12 +10,16 @@ import "./catalogue/search-games";
|
||||
import "./catalogue/search-game-repacks";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/create-game-shortcut";
|
||||
import "./library/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
import "./library/get-game-by-object-id";
|
||||
import "./library/get-library";
|
||||
import "./library/open-game";
|
||||
import "./library/open-game-executable-path";
|
||||
import "./library/open-game-installer";
|
||||
import "./library/open-game-installer-path";
|
||||
import "./library/update-executable-path";
|
||||
import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
import "./misc/open-external";
|
||||
|
@ -11,8 +11,7 @@ const addGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectID: string,
|
||||
title: string,
|
||||
shop: GameShop,
|
||||
executablePath: string | null
|
||||
shop: GameShop
|
||||
) => {
|
||||
return gameRepository
|
||||
.update(
|
||||
@ -22,7 +21,6 @@ const addGameToLibrary = async (
|
||||
{
|
||||
shop,
|
||||
status: null,
|
||||
executablePath,
|
||||
isDeleted: false,
|
||||
}
|
||||
)
|
||||
@ -42,7 +40,6 @@ const addGameToLibrary = async (
|
||||
iconUrl,
|
||||
objectID,
|
||||
shop,
|
||||
executablePath,
|
||||
})
|
||||
.then(() => {
|
||||
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,
|
||||
gameId: number
|
||||
) => {
|
||||
gameRepository.update({ id: gameId }, { isDeleted: true });
|
||||
gameRepository.update(
|
||||
{ id: gameId },
|
||||
{ isDeleted: true, executablePath: null }
|
||||
);
|
||||
};
|
||||
|
||||
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"),
|
||||
|
||||
/* Library */
|
||||
addGameToLibrary: (
|
||||
objectID: string,
|
||||
title: string,
|
||||
shop: GameShop,
|
||||
executablePath: string
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"addGameToLibrary",
|
||||
objectID,
|
||||
title,
|
||||
shop,
|
||||
executablePath
|
||||
),
|
||||
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("addGameToLibrary", objectID, title, shop),
|
||||
createGameShortcut: (id: number) =>
|
||||
ipcRenderer.invoke("createGameShortcut", id),
|
||||
updateExecutablePath: (id: number, executablePath: string) =>
|
||||
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
||||
getLibrary: () => ipcRenderer.invoke("getLibrary"),
|
||||
openGameInstaller: (gameId: number) =>
|
||||
ipcRenderer.invoke("openGameInstaller", gameId),
|
||||
openGameInstallerPath: (gameId: number) =>
|
||||
ipcRenderer.invoke("openGameInstallerPath", gameId),
|
||||
openGameExecutablePath: (gameId: number) =>
|
||||
ipcRenderer.invoke("openGameExecutablePath", gameId),
|
||||
openGame: (gameId: number, executablePath: string) =>
|
||||
ipcRenderer.invoke("openGame", gameId, executablePath),
|
||||
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
||||
|
@ -55,4 +55,15 @@ export const button = styleVariants({
|
||||
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}>
|
||||
<img
|
||||
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
|
||||
src={steamUrlBuilder.libraryHero(FEATURED_GAME_ID)}
|
||||
alt={FEATURED_GAME_TITLE}
|
||||
className={styles.heroMedia}
|
||||
/>
|
||||
|
@ -2,24 +2,25 @@ import { keyframes, style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const fadeIn = keyframes({
|
||||
"0%": { opacity: 0 },
|
||||
export const scaleFadeIn = keyframes({
|
||||
"0%": { opacity: "0", scale: "0.5" },
|
||||
"100%": {
|
||||
opacity: 1,
|
||||
opacity: "1",
|
||||
scale: "1",
|
||||
},
|
||||
});
|
||||
|
||||
export const fadeOut = keyframes({
|
||||
"0%": { opacity: 1 },
|
||||
export const scaleFadeOut = keyframes({
|
||||
"0%": { opacity: "1", scale: "1" },
|
||||
"100%": {
|
||||
opacity: 0,
|
||||
opacity: "0",
|
||||
scale: "0.5",
|
||||
},
|
||||
});
|
||||
|
||||
export const modal = recipe({
|
||||
base: {
|
||||
animationName: fadeIn,
|
||||
animationDuration: "0.3s",
|
||||
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: "4px",
|
||||
maxWidth: "600px",
|
||||
@ -33,8 +34,14 @@ export const modal = recipe({
|
||||
variants: {
|
||||
closing: {
|
||||
true: {
|
||||
animationName: fadeOut,
|
||||
opacity: 0,
|
||||
animationName: scaleFadeOut,
|
||||
opacity: "0",
|
||||
},
|
||||
},
|
||||
large: {
|
||||
true: {
|
||||
width: "800px",
|
||||
maxWidth: "800px",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -12,6 +12,7 @@ export interface ModalProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
onClose: () => void;
|
||||
large?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@ -20,6 +21,7 @@ export function Modal({
|
||||
title,
|
||||
description,
|
||||
onClose,
|
||||
large,
|
||||
children,
|
||||
}: ModalProps) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
@ -88,7 +90,7 @@ export function Modal({
|
||||
return createPortal(
|
||||
<Backdrop isClosing={isClosing}>
|
||||
<div
|
||||
className={styles.modal({ closing: isClosing })}
|
||||
className={styles.modal({ closing: isClosing, large })}
|
||||
role="dialog"
|
||||
aria-labelledby={title}
|
||||
aria-describedby={description}
|
||||
|
@ -5,7 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import type { LibraryGame } from "@types";
|
||||
|
||||
import { TextField } from "@renderer/components";
|
||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
||||
|
||||
import { routes } from "./routes";
|
||||
|
||||
@ -36,6 +36,8 @@ export function Sidebar() {
|
||||
|
||||
const { lastPacket, progress } = useDownload();
|
||||
|
||||
const { showWarningToast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
@ -183,9 +203,7 @@ export function Sidebar() {
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={() =>
|
||||
handleSidebarItemClick(buildGameDetailsPath(game))
|
||||
}
|
||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
|
@ -81,3 +81,7 @@ export const successIcon = style({
|
||||
export const errorIcon = style({
|
||||
color: vars.color.danger,
|
||||
});
|
||||
|
||||
export const warningIcon = style({
|
||||
color: vars.color.warning,
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
AlertIcon,
|
||||
CheckCircleFillIcon,
|
||||
XCircleFillIcon,
|
||||
XIcon,
|
||||
@ -11,7 +12,7 @@ import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
export interface ToastProps {
|
||||
visible: boolean;
|
||||
message: string;
|
||||
type: "success" | "error";
|
||||
type: "success" | "error" | "warning";
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@ -84,6 +85,8 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||
)}
|
||||
|
||||
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
|
||||
|
||||
{type === "warning" && <AlertIcon className={styles.warningIcon} />}
|
||||
<span style={{ fontWeight: "bold" }}>{message}</span>
|
||||
</div>
|
||||
|
||||
|
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@ -56,11 +56,14 @@ declare global {
|
||||
addGameToLibrary: (
|
||||
objectID: string,
|
||||
title: string,
|
||||
shop: GameShop,
|
||||
executablePath: string | null
|
||||
shop: GameShop
|
||||
) => Promise<void>;
|
||||
createGameShortcut: (id: number) => Promise<boolean>;
|
||||
updateExecutablePath: (id: number, executablePath: string) => Promise<void>;
|
||||
getLibrary: () => Promise<LibraryGame[]>;
|
||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||
openGameInstallerPath: (gameId: number) => Promise<boolean>;
|
||||
openGameExecutablePath: (gameId: number) => Promise<void>;
|
||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
||||
closeGame: (gameId: number) => Promise<boolean>;
|
||||
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
||||
|
@ -29,5 +29,17 @@ export function useToast() {
|
||||
[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 { 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";
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
RepacksModal,
|
||||
} from "./modals";
|
||||
import { Downloader } from "@shared";
|
||||
import { GameOptionsModal } from "./modals/game-options-modal";
|
||||
|
||||
export interface GameDetailsContext {
|
||||
game: Game | null;
|
||||
@ -29,6 +30,8 @@ export interface GameDetailsContext {
|
||||
gameColor: string;
|
||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
openRepacksModal: () => void;
|
||||
openGameOptionsModal: () => void;
|
||||
selectGameExecutable: () => Promise<string | null>;
|
||||
updateGame: () => Promise<void>;
|
||||
}
|
||||
|
||||
@ -44,6 +47,8 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||
gameColor: "",
|
||||
setGameColor: () => {},
|
||||
openRepacksModal: () => {},
|
||||
openGameOptionsModal: () => {},
|
||||
selectGameExecutable: async () => null,
|
||||
updateGame: async () => {},
|
||||
});
|
||||
|
||||
@ -70,6 +75,7 @@ export function GameDetailsContextProvider({
|
||||
>(null);
|
||||
const [isGameRunning, setisGameRunning] = useState(false);
|
||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@ -81,6 +87,10 @@ export function GameDetailsContextProvider({
|
||||
|
||||
const { startDownload, lastPacket } = useDownload();
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const updateGame = useCallback(async () => {
|
||||
return window.electron
|
||||
.getGameByObjectID(objectID!)
|
||||
@ -158,6 +168,7 @@ export function GameDetailsContextProvider({
|
||||
|
||||
await updateGame();
|
||||
setShowRepacksModal(false);
|
||||
setShowGameOptionsModal(false);
|
||||
|
||||
if (
|
||||
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 openGameOptionsModal = () => setShowGameOptionsModal(true);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
@ -188,6 +228,8 @@ export function GameDetailsContextProvider({
|
||||
gameColor,
|
||||
setGameColor,
|
||||
openRepacksModal,
|
||||
openGameOptionsModal,
|
||||
selectGameExecutable,
|
||||
updateGame,
|
||||
}}
|
||||
>
|
||||
@ -208,6 +250,16 @@ export function GameDetailsContextProvider({
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
{game && (
|
||||
<GameOptionsModal
|
||||
visible={showGameOptionsModal}
|
||||
game={game}
|
||||
onClose={() => {
|
||||
setShowGameOptionsModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</>
|
||||
</Provider>
|
||||
|
@ -1,7 +1,18 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { vars } from "../../../theme.css";
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const heroPanelAction = style({
|
||||
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 { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
|
||||
|
||||
import { GearIcon, PlayIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||
import { Button } from "@renderer/components";
|
||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./hero-panel-actions.css";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
import { Downloader } from "@shared";
|
||||
|
||||
export function HeroPanelActions() {
|
||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||
useState(false);
|
||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||
|
||||
const {
|
||||
resumeDownload,
|
||||
pauseDownload,
|
||||
cancelDownload,
|
||||
removeGameFromLibrary,
|
||||
isGameDeleting,
|
||||
} = useDownload();
|
||||
const { isGameDeleting } = useDownload();
|
||||
|
||||
const {
|
||||
game,
|
||||
@ -31,61 +19,20 @@ export function HeroPanelActions() {
|
||||
objectID,
|
||||
gameTitle,
|
||||
openRepacksModal,
|
||||
openGameOptionsModal,
|
||||
updateGame,
|
||||
selectGameExecutable,
|
||||
} = useContext(gameDetailsContext);
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
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 toggleGameOnLibrary = async () => {
|
||||
const addGameToLibrary = async () => {
|
||||
setToggleLibraryGameDisabled(true);
|
||||
|
||||
try {
|
||||
if (game) {
|
||||
await removeGameFromLibrary(game.id);
|
||||
} else {
|
||||
const gameExecutablePath = await selectGameExecutable();
|
||||
|
||||
await window.electron.addGameToLibrary(
|
||||
objectID!,
|
||||
gameTitle,
|
||||
"steam",
|
||||
gameExecutablePath
|
||||
);
|
||||
}
|
||||
await window.electron.addGameToLibrary(objectID!, gameTitle, "steam");
|
||||
|
||||
updateLibrary();
|
||||
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 () => {
|
||||
if (game) {
|
||||
if (game.executablePath) {
|
||||
@ -122,15 +60,15 @@ export function HeroPanelActions() {
|
||||
|
||||
const deleting = game ? isGameDeleting(game?.id) : false;
|
||||
|
||||
const toggleGameOnLibraryButton = (
|
||||
const addGameToLibraryButton = (
|
||||
<Button
|
||||
theme="outline"
|
||||
disabled={toggleLibraryGameDisabled}
|
||||
onClick={toggleGameOnLibrary}
|
||||
onClick={addGameToLibrary}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{game ? <NoEntryIcon /> : <PlusCircleIcon />}
|
||||
{game ? t("remove_from_library") : t("add_to_library")}
|
||||
<PlusCircleIcon />
|
||||
{t("add_to_library")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -145,109 +83,18 @@ export function HeroPanelActions() {
|
||||
</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) {
|
||||
return (
|
||||
<>
|
||||
{toggleGameOnLibraryButton}
|
||||
<Button
|
||||
onClick={openRepacksModal}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
{addGameToLibraryButton}
|
||||
{showDownloadOptionsButton}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game) {
|
||||
return (
|
||||
<>
|
||||
{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}
|
||||
|
||||
<div className={styles.actions}>
|
||||
{isGameRunning ? (
|
||||
<Button
|
||||
onClick={closeGame}
|
||||
@ -264,12 +111,25 @@ export function HeroPanelActions() {
|
||||
disabled={deleting || isGameRunning}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
<PlayIcon />
|
||||
{t("play")}
|
||||
</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 { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { useDate, useDownload } from "@renderer/hooks";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
import { Link } from "@renderer/components";
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
@ -13,6 +15,8 @@ export function HeroPanelPlaytime() {
|
||||
|
||||
const { i18n, t } = useTranslation("game_details");
|
||||
|
||||
const { progress, lastPacket } = useDownload();
|
||||
|
||||
const { formatDistance } = useDate();
|
||||
|
||||
useEffect(() => {
|
||||
@ -46,8 +50,45 @@ export function HeroPanelPlaytime() {
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
};
|
||||
|
||||
if (!game?.lastTimePlayed) {
|
||||
return <p>{t("not_played_yet", { title: game?.title })}</p>;
|
||||
if (!game) return null;
|
||||
|
||||
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 (
|
||||
@ -58,8 +99,8 @@ export function HeroPanelPlaytime() {
|
||||
})}
|
||||
</p>
|
||||
|
||||
{isGameRunning ? (
|
||||
<p>{t("playing_now")}</p>
|
||||
{hasDownload ? (
|
||||
downloadInProgressInfo
|
||||
) : (
|
||||
<p>
|
||||
{t("last_time_played", {
|
||||
|
@ -28,9 +28,15 @@ export const actions = style({
|
||||
});
|
||||
|
||||
export const downloadDetailsRow = style({
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
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({
|
||||
|
@ -1,14 +1,9 @@
|
||||
import { format } from "date-fns";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Color from "color";
|
||||
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { HeroPanelActions } from "./hero-panel-actions";
|
||||
import { Downloader, formatBytes } from "@shared";
|
||||
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
@ -18,82 +13,13 @@ export function HeroPanel() {
|
||||
|
||||
const { game, repacks, gameColor } = useContext(gameDetailsContext);
|
||||
|
||||
const { progress, eta, lastPacket, isGameDeleting } = 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 { lastPacket } = useDownload();
|
||||
|
||||
const isGameDownloading =
|
||||
game?.status === "active" && lastPacket?.game.id === game?.id;
|
||||
|
||||
const getInfo = () => {
|
||||
if (isGameDeleting(game?.id ?? -1)) return <p>{t("deleting")}</p>;
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!game) {
|
||||
const [latestRepack] = repacks;
|
||||
|
||||
if (latestRepack) {
|
||||
@ -109,6 +35,9 @@ export function HeroPanel() {
|
||||
}
|
||||
|
||||
return <p>{t("no_downloads")}</p>;
|
||||
}
|
||||
|
||||
return <HeroPanelPlaytime />;
|
||||
};
|
||||
|
||||
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 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 * as styles from "./repacks-modal.css";
|
||||
@ -31,13 +32,22 @@ export function RepacksModal({
|
||||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||
|
||||
const { repacks } = useContext(gameDetailsContext);
|
||||
const [infoHash, setInfoHash] = useState("");
|
||||
|
||||
const { repacks, game } = useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const getInfoHash = useCallback(async () => {
|
||||
const torrent = await parseTorrent(game?.uri ?? "");
|
||||
setInfoHash(torrent.infoHash ?? "");
|
||||
}, [game]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredRepacks(repacks);
|
||||
}, [repacks, visible]);
|
||||
|
||||
if (game?.uri) getInfoHash();
|
||||
}, [repacks, visible, game, getInfoHash]);
|
||||
|
||||
const handleRepackClick = (repack: GameRepack) => {
|
||||
setRepack(repack);
|
||||
@ -89,6 +99,11 @@ export function RepacksModal({
|
||||
<p style={{ color: "#DADBE1", wordBreak: "break-word" }}>
|
||||
{repack.title}
|
||||
</p>
|
||||
|
||||
{repack.magnet.toLowerCase().includes(infoHash) && (
|
||||
<Badge>{t("last_downloaded_option")}</Badge>
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: "12px" }}>
|
||||
{repack.fileSize} - {repack.repacker} -{" "}
|
||||
{repack.uploadDate
|
||||
|
@ -11,6 +11,7 @@ export const [themeClass, vars] = createTheme({
|
||||
border: "#424244",
|
||||
success: "#1c9749",
|
||||
danger: "#e11d48",
|
||||
warning: "#ffc107",
|
||||
},
|
||||
opacity: {
|
||||
disabled: "0.5",
|
||||
|
@ -106,6 +106,7 @@ export interface Game {
|
||||
downloader: Downloader;
|
||||
executablePath: string | null;
|
||||
lastTimePlayed: Date | null;
|
||||
uri: string | null;
|
||||
fileSize: number;
|
||||
objectID: string;
|
||||
shop: GameShop;
|
||||
|
@ -2403,6 +2403,13 @@ crc@^3.8.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "1.0.3"
|
||||
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"
|
||||
has-tostringtag "^1.0.2"
|
||||
|
||||
which@^2.0.1:
|
||||
which@2.0.2, which@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
||||
|
Loading…
Reference in New Issue
Block a user