diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 63682eee..3b227e5d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -101,15 +101,23 @@ "open_screenshot": "Open screenshot {{number}}", "download_settings": "Download settings", "downloader": "Downloader", - "select_executable": "Select executable", + "select_executable": "Select", "no_executable_selected": "No executable selected", "open_folder": "Open folder", - "open_download_location": "Abrir local de download", - "create_shortcut": "Create shortcut", + "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" + "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" }, "activation": { "title": "Activate Hydra", diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index 77121bd5..174abacd 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -98,15 +98,23 @@ "open_screenshot": "Ver captura de tela {{number}}", "download_settings": "Ajustes do download", "downloader": "Downloader", - "select_executable": "Selecionar executável", + "select_executable": "Selecionar", "no_executable_selected": "Nenhum executável selecionado", "open_folder": "Abrir pasta", - "open_download_location": "Abrir local de download", - "create_shortcut": "Criar atalho", + "open_download_location": "Ver arquivos baixados", + "create_shortcut": "Criar atalho na área de trabalho", "remove_files": "Remover arquivos", - "remove_from_library_description": "Tem certeza que deseja remover {{game}} da sua biblioteca?", + "options": "Opções", + "remove_from_library_description": "Isso irá remover {{game}} da sua biblioteca", "remove_from_library_title": "Tem certeza?", - "options": "Opções" + "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" }, "activation": { "title": "Ativação", diff --git a/src/main/events/library/create-game-shortcut.ts b/src/main/events/library/create-game-shortcut.ts index 7ce37bee..17b7e42c 100644 --- a/src/main/events/library/create-game-shortcut.ts +++ b/src/main/events/library/create-game-shortcut.ts @@ -13,10 +13,13 @@ const createGameShortcut = async ( if (game) { const filePath = game.executablePath; + + const options = { filePath, name: game.title }; + return createDesktopShortcut({ - windows: { filePath }, - linux: { filePath }, - osx: { filePath }, + windows: options, + linux: options, + osx: options, }); } diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 2581a555..29a7a635 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -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); diff --git a/src/renderer/src/components/button/button.css.ts b/src/renderer/src/components/button/button.css.ts index e342b2e9..c730ecfd 100644 --- a/src/renderer/src/components/button/button.css.ts +++ b/src/renderer/src/components/button/button.css.ts @@ -55,4 +55,15 @@ export const button = styleVariants({ color: "#c0c1c7", }, ], + danger: [ + base, + { + border: `solid 1px #a31533`, + backgroundColor: "transparent", + color: "white", + ":hover": { + backgroundColor: "#a31533", + }, + }, + ], }); diff --git a/src/renderer/src/components/hero/hero.tsx b/src/renderer/src/components/hero/hero.tsx index f0d52029..0cfdbea5 100644 --- a/src/renderer/src/components/hero/hero.tsx +++ b/src/renderer/src/components/hero/hero.tsx @@ -54,7 +54,7 @@ export function Hero() { >
{FEATURED_GAME_TITLE} diff --git a/src/renderer/src/components/modal/modal.css.ts b/src/renderer/src/components/modal/modal.css.ts index fcac6f4b..110f16f8 100644 --- a/src/renderer/src/components/modal/modal.css.ts +++ b/src/renderer/src/components/modal/modal.css.ts @@ -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,8 @@ export const modal = recipe({ variants: { closing: { true: { - animationName: fadeOut, - opacity: 0, + animationName: scaleFadeOut, + opacity: "0", }, }, large: { diff --git a/src/renderer/src/pages/game-details/game-details.context.tsx b/src/renderer/src/pages/game-details/game-details.context.tsx index 2c3fd61a..e4889275 100644 --- a/src/renderer/src/pages/game-details/game-details.context.tsx +++ b/src/renderer/src/pages/game-details/game-details.context.tsx @@ -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>; openRepacksModal: () => void; + openGameOptionsModal: () => void; + selectGameExecutable: () => Promise; updateGame: () => Promise; } @@ -44,6 +47,8 @@ export const gameDetailsContext = createContext({ 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 ( @@ -208,6 +250,16 @@ export function GameDetailsContextProvider({ onClose={() => setShowInstructionsModal(null)} /> + {game && ( + { + setShowGameOptionsModal(false); + }} + /> + )} + {children} diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.css.ts b/src/renderer/src/pages/game-details/hero/hero-panel-actions.css.ts index 48f8105b..ac9e6df1 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.css.ts +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.css.ts @@ -13,5 +13,6 @@ export const actions = style({ export const separator = style({ width: "1px", - backgroundColor: vars.color.border, + backgroundColor: vars.color.muted, + opacity: "0.2", }); diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index fcb7fbdd..6c35db3b 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -1,16 +1,14 @@ 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 { GameOptionsModal } from "../modals/game-options-modal"; export function HeroPanelActions() { const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] = useState(false); - const [showGameOptionsModal, setShowGameOptionsModal] = useState(false); const { isGameDeleting } = useDownload(); @@ -21,45 +19,15 @@ 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 addGameToLibrary = async () => { setToggleLibraryGameDisabled(true); @@ -126,53 +94,40 @@ export function HeroPanelActions() { if (game) { return ( - <> - { - setShowGameOptionsModal(false); - }} - selectGameExecutable={selectGameExecutable} - /> - -
- {isGameRunning ? ( - - ) : ( - - )} - -
- +
+ {isGameRunning ? ( + ) : ( + -
- + )} + +
+ + +
); } diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx index c32f24b0..1991d53f 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx @@ -15,6 +15,8 @@ export function HeroPanelPlaytime() { const { i18n, t } = useTranslation("game_details"); + const { progress, lastPacket } = useDownload(); + const { formatDistance } = useDate(); useEffect(() => { @@ -48,40 +50,33 @@ export function HeroPanelPlaytime() { return t("amount_hours", { amount: numberFormatter.format(hours) }); }; - const { progress, lastPacket } = useDownload(); + if (!game) return null; + + const hasDownload = + ["active", "paused"].includes(game.status) && game.progress !== 1; const isGameDownloading = - game?.status === "active" && lastPacket?.game.id === game?.id; + game.status === "active" && lastPacket?.game.id === game.id; - if (!game) return; + const downloadInProgressInfo = ( +
+ + {game.status === "active" + ? t("download_in_progress") + : t("download_paused")} + - let downloadContent: JSX.Element | null = null; - - if (game.status === "active") { - if (lastPacket?.isDownloadingMetadata && isGameDownloading) { - downloadContent =

{t("downloading_metadata")}

; - } else if (game.progress !== 1) { - downloadContent = ( -
- - Download em andamento - - - - {isGameDownloading - ? progress - : formatDownloadProgress(game.progress)} - -
- ); - } - } + + {isGameDownloading ? progress : formatDownloadProgress(game.progress)} + +
+ ); if (!game.lastTimePlayed) { return ( <>

{t("not_played_yet", { title: game?.title })}

- {downloadContent} + {hasDownload && downloadInProgressInfo} ); } @@ -89,15 +84,9 @@ export function HeroPanelPlaytime() { if (isGameRunning) { return ( <> - {downloadContent || ( -

- {t("play_time", { - amount: formatPlayTime(), - })} -

- )} -

{t("playing_now")}

+ + {hasDownload && downloadInProgressInfo} ); } @@ -110,7 +99,9 @@ export function HeroPanelPlaytime() { })}

- {downloadContent || ( + {hasDownload ? ( + downloadInProgressInfo + ) : (

{t("last_time_played", { period: lastTimePlayed, diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts index 54297b0b..5dfed5d8 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts +++ b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts @@ -36,6 +36,7 @@ export const downloadDetailsRow = style({ export const downloadsLink = style({ color: vars.color.body, + textDecoration: "underline", }); export const progressBar = recipe({ diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts b/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts index a6a854df..b8c538f0 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts @@ -7,6 +7,17 @@ export const optionsContainer = style({ 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`, diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index 145e262c..b40d93ab 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -4,7 +4,6 @@ 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 { NoEntryIcon, TrashIcon } from "@primer/octicons-react"; import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal"; import { useDownload } from "@renderer/hooks"; import { RemoveGameFromLibraryModal } from "./remove-from-library-modal"; @@ -13,43 +12,50 @@ export interface GameOptionsModalProps { visible: boolean; game: Game; onClose: () => void; - selectGameExecutable: () => Promise; } export function GameOptionsModal({ visible, game, onClose, - selectGameExecutable, }: GameOptionsModalProps) { const { t } = useTranslation("game_details"); - const { updateGame, openRepacksModal } = useContext(gameDetailsContext); + const { updateGame, openRepacksModal, selectGameExecutable } = + useContext(gameDetailsContext); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showRemoveGameModal, setShowRemoveGameModal] = useState(false); - const { removeGameInstaller, removeGameFromLibrary, isGameDeleting } = - useDownload(); + const { + removeGameInstaller, + removeGameFromLibrary, + isGameDeleting, + cancelDownload, + } = useDownload(); - const deleting = game ? isGameDeleting(game?.id) : false; + const deleting = isGameDeleting(game.id); const { lastPacket } = useDownload(); const isGameDownloading = - game?.status === "active" && lastPacket?.game.id === game?.id; + 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 location = await selectGameExecutable(); + const path = await selectGameExecutable(); - if (location) { - await window.electron.updateExecutablePath(game.id, location); + if (path) { + await window.electron.updateExecutablePath(game.id, path); updateGame(); } }; @@ -73,54 +79,34 @@ export function GameOptionsModal({ return ( <> + setShowDeleteModal(false)} + deleteGame={handleDeleteGame} + /> + + setShowRemoveGameModal(false)} + removeGameFromLibrary={handleRemoveGameFromLibrary} + game={game} + /> + - setShowDeleteModal(false)} - deleteGame={handleDeleteGame} - /> - - setShowRemoveGameModal(false)} - removeGameFromLibrary={handleRemoveGameFromLibrary} - game={game} - /> -

-
- - - +
+

{t("executable_section_title")}

+

+ {t("executable_section_description")} +

{t("select_executable")} - +
+ + {game.executablePath && ( +
+ + +
+ )} + +
+

{t("downloads_secion_title")}

+

+ {t("downloads_section_description")} +

+ + {game.downloadPath && ( + + )} +
+
+

{t("danger_zone_section_title")}

+

+ {t("danger_zone_section_description")} +

+
+
+ - -