Merge branch 'main' into feat/format-playtime-in-hours

# Conflicts:
#	src/renderer/src/pages/game-details/hero/hero-panel.tsx
This commit is contained in:
Zamitto 2024-05-03 09:21:02 -03:00
commit 64449910c5
56 changed files with 1324 additions and 596 deletions

View File

@ -112,21 +112,35 @@ yarn make
<sub><b>Null</b></sub> <sub><b>Null</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/fhilipecrash">
<img src="https://avatars.githubusercontent.com/u/36455575?v=4" width="100;" alt="fhilipecrash"/>
<br />
<sub><b>Fhilipe Coelho</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/Magrid0"> <a href="https://github.com/Magrid0">
<img src="https://avatars.githubusercontent.com/u/73496008?v=4" width="100;" alt="Magrid0"/> <img src="https://avatars.githubusercontent.com/u/73496008?v=4" width="100;" alt="Magrid0"/>
<br /> <br />
<sub><b>Magrid</b></sub> <sub><b>Magrid</b></sub>
</a> </a>
</td>
<td align="center">
<a href="https://github.com/fhilipecrash">
<img src="https://avatars.githubusercontent.com/u/36455575?v=4" width="100;" alt="fhilipecrash"/>
<br />
<sub><b>Fhilipe Coelho</b></sub>
</a>
</td></tr> </td></tr>
<tr> <tr>
<td align="center">
<a href="https://github.com/jps14">
<img src="https://avatars.githubusercontent.com/u/168477146?v=4" width="100;" alt="jps14"/>
<br />
<sub><b>José Luís</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/shadowtosser">
<img src="https://avatars.githubusercontent.com/u/168544958?v=4" width="100;" alt="shadowtosser"/>
<br />
<sub><b>Null</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/ferivoq"> <a href="https://github.com/ferivoq">
<img src="https://avatars.githubusercontent.com/u/36544651?v=4" width="100;" alt="ferivoq"/> <img src="https://avatars.githubusercontent.com/u/36544651?v=4" width="100;" alt="ferivoq"/>
@ -154,13 +168,21 @@ yarn make
<br /> <br />
<sub><b>Ikko Eltociear Ashimine</b></sub> <sub><b>Ikko Eltociear Ashimine</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/Netflixyapp"> <a href="https://github.com/Netflixyapp">
<img src="https://avatars.githubusercontent.com/u/91623880?v=4" width="100;" alt="Netflixyapp"/> <img src="https://avatars.githubusercontent.com/u/91623880?v=4" width="100;" alt="Netflixyapp"/>
<br /> <br />
<sub><b>Netflixy</b></sub> <sub><b>Netflixy</b></sub>
</a> </a>
</td>
<td align="center">
<a href="https://github.com/FerNikoMF">
<img src="https://avatars.githubusercontent.com/u/76095334?v=4" width="100;" alt="FerNikoMF"/>
<br />
<sub><b>Firdavs</b></sub>
</a>
</td></tr> </td></tr>
</table> </table>
<!-- readme: contributors -end --> <!-- readme: contributors -end -->

BIN
build/installerSidebar.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -23,6 +23,7 @@ nsis:
shortcutName: ${productName} shortcutName: ${productName}
uninstallDisplayName: ${productName} uninstallDisplayName: ${productName}
createDesktopShortcut: always createDesktopShortcut: always
oneClick: false
mac: mac:
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
extendInfo: extendInfo:

View File

@ -58,6 +58,7 @@
"react-router-dom": "^6.22.3", "react-router-dom": "^6.22.3",
"tough-cookie": "^4.1.3", "tough-cookie": "^4.1.3",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"user-agents": "^1.1.193",
"windows-1251": "^3.0.4", "windows-1251": "^3.0.4",
"winston": "^3.13.0", "winston": "^3.13.0",
"yaml": "^2.4.1" "yaml": "^2.4.1"

View File

@ -17,7 +17,10 @@
"downloading": "{{title}} ({{percentage}} - Downloading…)", "downloading": "{{title}} ({{percentage}} - Downloading…)",
"filter": "Filter library", "filter": "Filter library",
"follow_us": "Follow us", "follow_us": "Follow us",
"home": "Home" "home": "Home",
"discord": "Join our Discord",
"x": "Follow on X",
"github": "Contribute on GitHub"
}, },
"header": { "header": {
"search": "Search", "search": "Search",
@ -82,8 +85,16 @@
"repacks_modal_description": "Choose the repack you want to download", "repacks_modal_description": "Choose the repack you want to download",
"downloads_path": "Downloads path", "downloads_path": "Downloads path",
"select_folder_hint": "To change the default folder, access the", "select_folder_hint": "To change the default folder, access the",
"settings": "Hydra settings", "settings": "Settings",
"download_now": "Download now" "download_now": "Download now",
"installation_instructions": "Installation Instructions",
"installation_instructions_description": "Additional steps are required to install this game",
"online_fix_instruction": "OnlineFix games requires a password to be extracted. When required, use the following password:",
"dodi_installation_instruction": "When you open DODI installer, press your keyboard up key <0 /> to start the installation process:",
"dont_show_it_again": "Don't show it again",
"copy_to_clipboard": "Copy",
"copied_to_clipboard": "Copied",
"got_it": "Got it"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",
@ -143,5 +154,8 @@
"title": "Programs not installed", "title": "Programs not installed",
"description": "Wine or Lutris executables were not found on your system", "description": "Wine or Lutris executables were not found on your system",
"instructions": "Check the correct way to install any of them on your Linux distro so that the game can run normally" "instructions": "Check the correct way to install any of them on your Linux distro so that the game can run normally"
},
"modal": {
"close": "Close button"
} }
} }

View File

@ -4,3 +4,4 @@ export { default as es } from "./es/translation.json";
export { default as fr } from "./fr/translation.json"; export { default as fr } from "./fr/translation.json";
export { default as hu } from "./hu/translation.json"; export { default as hu } from "./hu/translation.json";
export { default as it } from "./it/translation.json"; export { default as it } from "./it/translation.json";
export { default as ru } from "./ru/translation.json";

View File

@ -17,7 +17,10 @@
"downloading": "{{title}} ({{percentage}} - Download…)", "downloading": "{{title}} ({{percentage}} - Download…)",
"filter": "Filtra libreria", "filter": "Filtra libreria",
"follow_us": "Seguici", "follow_us": "Seguici",
"home": "Home" "home": "Home",
"discord": "Unisciti al nostro Discord",
"x": "Segui su X",
"github": "Contribuisci su GitHub"
}, },
"header": { "header": {
"search": "Cerca", "search": "Cerca",
@ -77,7 +80,21 @@
"play": "Gioca", "play": "Gioca",
"deleting": "Eliminazione dell'installer…", "deleting": "Eliminazione dell'installer…",
"close": "Chiudi", "close": "Chiudi",
"playing_now": "Stai giocando adesso" "playing_now": "Stai giocando adesso",
"change": "Aggiorna",
"repacks_modal_description": "Scegli il repack che vuoi scaricare",
"downloads_path": "Percorso dei download",
"select_folder_hint": "Per cambiare la cartella predefinita, accedi alle",
"settings": "Impostazioni",
"download_now": "Scarica ora",
"installation_instructions": "Istruzioni di installazione",
"installation_instructions_description": "Sono necessari passaggi aggiuntivi per installare questo gioco",
"online_fix_instruction": "I giochi OnlineFix richiedono una password per essere estratti. Quando richiesto, utilizza la seguente password:",
"dodi_installation_instruction": "Quando apri l'installatore di DODI, premi il tasto su della tua tastiera <0 /> per avviare il processo di installazione:",
"dont_show_it_again": "Non mostrarlo più",
"copy_to_clipboard": "Copia",
"copied_to_clipboard": "Copiato",
"got_it": "Capito"
}, },
"activation": { "activation": {
"title": "Attiva Hydra", "title": "Attiva Hydra",
@ -137,5 +154,8 @@
"title": "Programmi non installati", "title": "Programmi non installati",
"description": "Gli eseguibili di Wine o Lutris non sono stati trovati sul tuo sistema", "description": "Gli eseguibili di Wine o Lutris non sono stati trovati sul tuo sistema",
"instructions": "Verifica il modo corretto di installare uno di essi sulla tua distribuzione Linux in modo che il gioco possa funzionare normalmente" "instructions": "Verifica il modo corretto di installare uno di essi sulla tua distribuzione Linux in modo che il gioco possa funzionare normalmente"
},
"modal": {
"close": "Pulsante Chiudi"
} }
} }

View File

@ -17,7 +17,10 @@
"downloading": "{{title}} ({{percentage}} - Baixando…)", "downloading": "{{title}} ({{percentage}} - Baixando…)",
"filter": "Filtrar biblioteca", "filter": "Filtrar biblioteca",
"home": "Início", "home": "Início",
"follow_us": "Acompanhe-nos" "follow_us": "Acompanhe-nos",
"discord": "Entre no nosso Discord",
"x": "Siga-nos no X",
"github": "Contribua no GitHub"
}, },
"header": { "header": {
"search": "Buscar", "search": "Buscar",
@ -81,7 +84,15 @@
"downloads_path": "Diretório do download", "downloads_path": "Diretório do download",
"select_folder_hint": "Para trocar a pasta padrão, acesse as ", "select_folder_hint": "Para trocar a pasta padrão, acesse as ",
"settings": "Configurações do Hydra", "settings": "Configurações do Hydra",
"download_now": "Baixe agora" "download_now": "Baixe agora",
"installation_instructions": "Instruções de Instalação",
"installation_instructions_description": "Passos adicionais são necessários para instalar esse jogo",
"online_fix_instruction": "Jogos OnlineFix precisam de uma senha para serem extraídos. Quando solicitado, utilize a seguinte senha:",
"dodi_installation_instruction": "Quando o instalador do DODI for aberto, pressione a seta para cima <0 /> do teclado para iniciar o processo de instalação:",
"dont_show_it_again": "Não mostrar novamente",
"copy_to_clipboard": "Copiar",
"copied_to_clipboard": "Copiado",
"got_it": "Entendi"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",
@ -145,5 +156,8 @@
"catalogue": { "catalogue": {
"next_page": "Próxima página", "next_page": "Próxima página",
"previous_page": "Página anterior" "previous_page": "Página anterior"
},
"modal": {
"close": "Botão de fechar"
} }
} }

View File

@ -0,0 +1,147 @@
{
"home": {
"featured": "Рекомендованное",
"recently_added": "Недавно добавленное",
"trending": "Тенденции",
"surprise_me": "Удиви меня",
"no_results": "Результатов не найдено"
},
"sidebar": {
"catalogue": "Каталог",
"downloads": "Загрузки",
"settings": "Настройки",
"my_library": "Моя библиотека",
"downloading_metadata": "{{title}} (Загрузка метаданных…)",
"checking_files": "{{title}} ({{percentage}} - Проверка файлов…)",
"paused": "{{title}} (Приостановлено)",
"downloading": "{{title}} ({{percentage}} - Загрузка…)",
"filter": "Фильтровать библиотеку",
"follow_us": "Подписывайтесь на нас",
"home": "Главная"
},
"header": {
"search": "Поиск",
"home": "Главная",
"catalogue": "Каталог",
"downloads": "Загрузки",
"search_results": "Результаты поиска",
"settings": "Настройки"
},
"bottom_panel": {
"no_downloads_in_progress": "Нет активных загрузок",
"downloading_metadata": "Загрузка метаданных {{title}}…",
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)",
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Следующая страница",
"previous_page": "Предыдущая страница"
},
"game_details": {
"open_download_options": "Открыть опции загрузки",
"download_options_zero": "Нет вариантов загрузки",
"download_options_one": "{{count}} вариант загрузки",
"download_options_other": "{{count}} вариантов загрузки",
"updated_at": "Обновлено {{updated_at}}",
"install": "Установить",
"resume": "Возобновить",
"pause": "Приостановить",
"cancel": "Отменить",
"remove": "Удалить",
"remove_from_list": "Удалить",
"space_left_on_disk": "{{space}} осталось на диске",
"eta": "Окончание {{eta}}",
"downloading_metadata": "Загрузка метаданных…",
"checking_files": "Проверка файлов…",
"filter": "Фильтр репаков",
"requirements": "Системные требования",
"minimum": "Минимальные",
"recommended": "Рекомендуемые",
"no_minimum_requirements": "{{title}} не предоставляет информации о минимальных требованиях",
"no_recommended_requirements": "{{title}} не предоставляет информации о рекомендуемых требованиях",
"paused_progress": "{{progress}} (Приостановлено)",
"release_date": "Выпущено в {{date}}",
"publisher": "Опубликовано {{publisher}}",
"copy_link_to_clipboard": "Скопировать ссылку",
"copied_link_to_clipboard": "Ссылка скопирована",
"hours": "часов",
"minutes": "минут",
"accuracy": "{{accuracy}}% точность",
"add_to_library": "Добавить в библиотеку",
"remove_from_library": "Удалить из библиотеки",
"no_downloads": "Нет доступных загрузок",
"play_time": "Сыграно {{amount}}",
"last_time_played": "Последний раз сыграно {{period}}",
"not_played_yet": "Вы еще не сыграли в {{title}}",
"next_suggestion": "Следующее предложение",
"play": "Играть",
"deleting": "Удаление установщика…",
"close": "Закрыть",
"playing_now": "Сейчас играет",
"change": "Изменить",
"repacks_modal_description": "Выберите репак, который хотите загрузить",
"downloads_path": "Путь загрузок",
"select_folder_hint": "Чтобы изменить папку по умолчанию, откройте",
"settings": "Настройки Hydra",
"download_now": "Загрузить сейчас"
},
"activation": {
"title": "Активировать Hydra",
"installation_id": "ID установки:",
"enter_activation_code": "Введите ваш активационный код",
"message": "Если вы не знаете, где его запросить, то не должны иметь это.",
"activate": "Активировать",
"loading": "Загрузка…"
},
"downloads": {
"resume": "Возобновить",
"pause": "Приостановить",
"eta": "Окончание {{eta}}",
"paused": "Приостановлено",
"verifying": "Проверка…",
"completed_at": "Завершено в {{date}}",
"completed": "Завершено",
"cancelled": "Отменено",
"download_again": "Загрузить снова",
"cancel": "Отменить",
"filter": "Фильтровать загруженные игры",
"remove": "Удалить",
"downloading_metadata": "Загрузка метаданных…",
"checking_files": "Проверка файлов…",
"starting_download": "Начало загрузки…",
"deleting": "Удаление установщика…",
"delete": "Удалить установщик",
"remove_from_list": "Удалить",
"delete_modal_title": "Вы уверены?",
"delete_modal_description": "Это удалит все установочные файлы с вашего компьютера",
"install": "Установить"
},
"settings": {
"downloads_path": "Путь загрузок",
"change": "Изменить путь",
"notifications": "Уведомления",
"enable_download_notifications": "По завершении загрузки",
"enable_repack_list_notifications": "При добавлении нового репака",
"telemetry": "Телеметрия",
"telemetry_description": "Включить анонимную статистику использования"
},
"notifications": {
"download_complete": "Загрузка завершена",
"game_ready_to_install": "{{title}} готова к установке",
"repack_list_updated": "Список репаков обновлен",
"repack_count_one": "{{count}} репак добавлен",
"repack_count_other": "{{count}} репаков добавлено"
},
"system_tray": {
"open": "Открыть Hydra",
"quit": "Выйти"
},
"game_card": {
"no_downloads": "Нет доступных загрузок"
},
"binary_not_found_modal": {
"title": "Программы не установлены",
"description": "Исполняемые файлы Wine или Lutris не найдены на вашей системе",
"instructions": "Узнайте правильный способ установить любой из них на вашем дистрибутиве Linux, чтобы игра могла нормально работать"
}
}

View File

@ -1,5 +1,4 @@
import { app } from "electron"; import { app } from "electron";
import os from "node:os";
import path from "node:path"; import path from "node:path";
export const repackersOn1337x = [ export const repackersOn1337x = [
@ -43,7 +42,7 @@ export enum GameStatus {
Cancelled = "cancelled", Cancelled = "cancelled",
} }
export const defaultDownloadsPath = path.join(os.homedir(), "downloads"); export const defaultDownloadsPath = app.getPath("downloads");
export const databasePath = path.join( export const databasePath = path.join(
app.getPath("appData"), app.getPath("appData"),

View File

@ -8,7 +8,7 @@ import { stateManager } from "@main/state-manager";
const { Index } = flexSearch; const { Index } = flexSearch;
const repacksIndex = new Index(); const repacksIndex = new Index();
const steamGamesIndex = new Index({ tokenize: "reverse" }); const steamGamesIndex = new Index();
const repacks = stateManager.getValue("repacks"); const repacks = stateManager.getValue("repacks");
const steamGames = stateManager.getValue("steamGames"); const steamGames = stateManager.getValue("steamGames");

View File

@ -34,6 +34,21 @@ export const searchHowLongToBeat = async (gameName: string) => {
return response.data as HowLongToBeatSearchResponse; return response.data as HowLongToBeatSearchResponse;
}; };
const parseListItems = ($lis: Element[]) => {
return $lis.map(($li) => {
const title = $li.querySelector("h4")?.textContent;
const [, accuracyClassName] = Array.from(($li as HTMLElement).classList);
const accuracy = accuracyClassName.split("time_").at(1);
return {
title: title ?? "",
duration: $li.querySelector("h5")?.textContent ?? "",
accuracy: accuracy ?? "",
};
});
};
export const getHowLongToBeatGame = async ( export const getHowLongToBeatGame = async (
id: string id: string
): Promise<HowLongToBeatCategory[]> => { ): Promise<HowLongToBeatCategory[]> => {
@ -43,18 +58,16 @@ export const getHowLongToBeatGame = async (
const { document } = window; const { document } = window;
const $ul = document.querySelector(".shadow_shadow ul"); const $ul = document.querySelector(".shadow_shadow ul");
if (!$ul) return [];
const $lis = Array.from($ul.children); const $lis = Array.from($ul.children);
return $lis.map(($li) => { const [$firstLi] = $lis;
const title = $li.querySelector("h4").textContent;
const [, accuracyClassName] = Array.from(($li as HTMLElement).classList);
const accuracy = accuracyClassName.split("time_").at(1); if ($firstLi.tagName === "DIV") {
const $pcData = $lis.find(($li) => $li.textContent?.includes("PC"));
return parseListItems(Array.from($pcData?.querySelectorAll("li") ?? []));
}
return { return parseListItems($lis);
title,
duration: $li.querySelector("h5").textContent,
accuracy,
};
});
}; };

View File

@ -1,3 +1,5 @@
import UserAgent from "user-agents";
import type { Repack } from "@main/entity"; import type { Repack } from "@main/entity";
import { repackRepository } from "@main/repository"; import { repackRepository } from "@main/repository";
@ -8,7 +10,13 @@ export const savePage = async (repacks: QueryDeepPartialEntity<Repack>[]) =>
repacks.map((repack) => repackRepository.insert(repack).catch(() => {})) repacks.map((repack) => repackRepository.insert(repack).catch(() => {}))
); );
export const requestWebPage = async (url: string) => export const requestWebPage = async (url: string) => {
fetch(url, { const userAgent = new UserAgent();
return fetch(url, {
method: "GET", method: "GET",
headers: {
"User-Agent": userAgent.toString(),
},
}).then((response) => response.text()); }).then((response) => response.text());
};

View File

@ -6,10 +6,10 @@
<title>Hydra</title> <title>Hydra</title>
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://cdn2.steamgriddb.com;"
/> />
</head> </head>
<body style="background-color: #1c1c1"> <body style="background-color: #1c1c1c">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

View File

@ -26,6 +26,7 @@ globalStyle("body", {
overflow: "hidden", overflow: "hidden",
userSelect: "none", userSelect: "none",
fontFamily: "'Fira Mono', monospace", fontFamily: "'Fira Mono', monospace",
fontSize: vars.size.bodyFontSize,
background: vars.color.background, background: vars.color.background,
color: vars.color.bodyText, color: vars.color.bodyText,
margin: "0", margin: "0",
@ -36,13 +37,16 @@ globalStyle("button", {
backgroundColor: "transparent", backgroundColor: "transparent",
border: "none", border: "none",
fontFamily: "inherit", fontFamily: "inherit",
fontSize: vars.size.bodyFontSize,
}); });
globalStyle("h1, h2, h3, h4, h5, h6, p", { globalStyle("h1, h2, h3, h4, h5, h6, p", {
margin: 0, margin: 0,
}); });
globalStyle("p", {
lineHeight: "20px",
});
globalStyle("#root, main", { globalStyle("#root, main", {
display: "flex", display: "flex",
}); });
@ -103,5 +107,5 @@ export const titleBar = style({
padding: `0 ${SPACING_UNIT * 2}px`, padding: `0 ${SPACING_UNIT * 2}px`,
WebkitAppRegion: "drag", WebkitAppRegion: "drag",
zIndex: "2", zIndex: "2",
borderBottom: `1px solid ${vars.color.borderColor}`, borderBottom: `1px solid ${vars.color.border}`,
} as ComplexStyleRule); } as ComplexStyleRule);

View File

@ -18,11 +18,16 @@ import {
clearSearch, clearSearch,
setUserPreferences, setUserPreferences,
setRepackersFriendlyNames, setRepackersFriendlyNames,
toggleDraggingDisabled,
} from "@renderer/features"; } from "@renderer/features";
document.body.classList.add(themeClass); document.body.classList.add(themeClass);
export function App({ children }: any) { export interface AppProps {
children: React.ReactNode;
}
export function App({ children }: AppProps) {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary } = useLibrary(); const { updateLibrary } = useLibrary();
@ -34,6 +39,9 @@ export function App({ children }: any) {
const location = useLocation(); const location = useLocation();
const search = useAppSelector((state) => state.search.value); const search = useAppSelector((state) => state.search.value);
const draggingDisabled = useAppSelector(
(state) => state.window.draggingDisabled
);
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
@ -93,6 +101,17 @@ export function App({ children }: any) {
if (contentRef.current) contentRef.current.scrollTop = 0; if (contentRef.current) contentRef.current.scrollTop = 0;
}, [location.pathname, location.search]); }, [location.pathname, location.search]);
useEffect(() => {
new MutationObserver(() => {
const modal = document.body.querySelector("[role=modal]");
dispatch(toggleDraggingDisabled(Boolean(modal)));
}).observe(document.body, {
attributes: false,
childList: true,
});
}, [dispatch, draggingDisabled]);
return ( return (
<> <>
{window.electron.platform === "win32" && ( {window.electron.platform === "win32" && (

View File

@ -0,0 +1,47 @@
import { keyframes } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT } from "../../theme.css";
export const backdropFadeIn = keyframes({
"0%": { backdropFilter: "blur(0px)", backgroundColor: "rgba(0, 0, 0, 0.5)" },
"100%": {
backdropFilter: "blur(2px)",
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
});
export const backdropFadeOut = keyframes({
"0%": { backdropFilter: "blur(2px)", backgroundColor: "rgba(0, 0, 0, 0.7)" },
"100%": {
backdropFilter: "blur(0px)",
backgroundColor: "rgba(0, 0, 0, 0)",
},
});
export const backdrop = recipe({
base: {
animationName: backdropFadeIn,
animationDuration: "0.4s",
backgroundColor: "rgba(0, 0, 0, 0.7)",
position: "absolute",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1,
top: 0,
padding: `${SPACING_UNIT * 3}px`,
backdropFilter: "blur(2px)",
transition: "all ease 0.2s",
},
variants: {
closing: {
true: {
animationName: backdropFadeOut,
backdropFilter: "blur(0px)",
backgroundColor: "rgba(0, 0, 0, 0)",
},
},
},
});

View File

@ -0,0 +1,12 @@
import * as styles from "./backdrop.css";
export interface BackdropProps {
isClosing?: boolean;
children: React.ReactNode;
}
export function Backdrop({ isClosing = false, children }: BackdropProps) {
return (
<div className={styles.backdrop({ closing: isClosing })}>{children}</div>
);
}

View File

@ -3,13 +3,12 @@ import { SPACING_UNIT, vars } from "../../theme.css";
export const bottomPanel = style({ export const bottomPanel = style({
width: "100%", width: "100%",
borderTop: `solid 1px ${vars.color.borderColor}`, borderTop: `solid 1px ${vars.color.border}`,
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
transition: "all ease 0.2s", transition: "all ease 0.2s",
justifyContent: "space-between", justifyContent: "space-between",
fontSize: vars.size.bodyFontSize,
zIndex: "1", zIndex: "1",
}); });

View File

@ -3,7 +3,7 @@ import { SPACING_UNIT, vars } from "../../theme.css";
const base = style({ const base = style({
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
backgroundColor: "#c0c1c7", backgroundColor: vars.color.muted,
borderRadius: "8px", borderRadius: "8px",
border: "solid 1px transparent", border: "solid 1px transparent",
transition: "all ease 0.2s", transition: "all ease 0.2s",
@ -35,8 +35,8 @@ export const button = styleVariants({
base, base,
{ {
backgroundColor: "transparent", backgroundColor: "transparent",
border: "solid 1px #c0c1c7", border: `solid 1px ${vars.color.border}`,
color: "#c0c1c7", color: vars.color.muted,
":hover": { ":hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)", backgroundColor: "rgba(255, 255, 255, 0.1)",
}, },

View File

@ -19,7 +19,7 @@ export const checkbox = style({
alignItems: "center", alignItems: "center",
position: "relative", position: "relative",
transition: "all ease 0.2s", transition: "all ease 0.2s",
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
":hover": { ":hover": {
borderColor: "rgba(255, 255, 255, 0.5)", borderColor: "rgba(255, 255, 255, 0.5)",
}, },

View File

@ -10,7 +10,7 @@ export const card = recipe({
overflow: "hidden", overflow: "hidden",
borderRadius: "4px", borderRadius: "4px",
transition: "all ease 0.2s", transition: "all ease 0.2s",
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
cursor: "pointer", cursor: "pointer",
zIndex: "1", zIndex: "1",
":active": { ":active": {
@ -103,7 +103,7 @@ export const specifics = style({
export const specificsItem = style({ export const specificsItem = style({
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
display: "flex", display: "flex",
color: "#c0c1c7", color: vars.color.muted,
fontSize: "12px", fontSize: "12px",
alignItems: "flex-end", alignItems: "flex-end",
}); });
@ -112,7 +112,7 @@ export const titleContainer = style({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
color: "#c0c1c7", color: vars.color.muted,
}); });
export const shopIcon = style({ export const shopIcon = style({

View File

@ -29,8 +29,8 @@ export const header = recipe({
WebkitAppRegion: "drag", WebkitAppRegion: "drag",
width: "100%", width: "100%",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`, padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
color: "#c0c1c7", color: vars.color.muted,
borderBottom: `solid 1px ${vars.color.borderColor}`, borderBottom: `solid 1px ${vars.color.border}`,
backgroundColor: vars.color.darkBackground, backgroundColor: vars.color.darkBackground,
} as ComplexStyleRule, } as ComplexStyleRule,
variants: { variants: {
@ -55,7 +55,7 @@ export const search = recipe({
width: "200px", width: "200px",
alignItems: "center", alignItems: "center",
borderRadius: "8px", borderRadius: "8px",
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
height: "40px", height: "40px",
WebkitAppRegion: "no-drag", WebkitAppRegion: "no-drag",
} as ComplexStyleRule, } as ComplexStyleRule,
@ -83,7 +83,6 @@ export const searchInput = style({
color: "#DADBE1", color: "#DADBE1",
cursor: "default", cursor: "default",
fontFamily: "inherit", fontFamily: "inherit",
fontSize: vars.size.bodyFontSize,
textOverflow: "ellipsis", textOverflow: "ellipsis",
":focus": { ":focus": {
cursor: "text", cursor: "text",

View File

@ -11,7 +11,7 @@ export const hero = style({
overflow: "hidden", overflow: "hidden",
boxShadow: "0px 0px 15px 0px #000000", boxShadow: "0px 0px 15px 0px #000000",
cursor: "pointer", cursor: "pointer",
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
zIndex: "1", zIndex: "1",
}); });
@ -33,7 +33,7 @@ export const heroMedia = style({
export const backdrop = style({ export const backdrop = style({
width: "100%", width: "100%",
height: "100%", height: "100%",
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.6) 25%, transparent 100%)", background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%)",
position: "relative", position: "relative",
display: "flex", display: "flex",
overflow: "hidden", overflow: "hidden",
@ -41,8 +41,7 @@ export const backdrop = style({
export const description = style({ export const description = style({
maxWidth: "700px", maxWidth: "700px",
fontSize: vars.size.bodyFontSize, color: vars.color.muted,
color: "#c0c1c7",
textAlign: "left", textAlign: "left",
fontFamily: "'Fira Sans', sans-serif", fontFamily: "'Fira Sans', sans-serif",
lineHeight: "20px", lineHeight: "20px",

View File

@ -6,7 +6,7 @@ import { ShopDetails } from "@types";
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers"; import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const FEATURED_GAME_ID = "253230"; const FEATURED_GAME_ID = "2420110";
export function Hero() { export function Hero() {
const [featuredGameDetails, setFeaturedGameDetails] = const [featuredGameDetails, setFeaturedGameDetails] =
@ -36,7 +36,7 @@ export function Hero() {
> >
<div className={styles.backdrop}> <div className={styles.backdrop}>
<AsyncImage <AsyncImage
src="https://cdn2.steamgriddb.com/hero/a6115ed32394915aac1e5502382eaaea.jpg" src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
alt={featuredGameDetails?.name} alt={featuredGameDetails?.name}
className={styles.heroMedia} className={styles.heroMedia}
/> />

View File

@ -2,22 +2,6 @@ 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 backdropFadeIn = keyframes({
"0%": { backdropFilter: "blur(0px)", backgroundColor: "rgba(0, 0, 0, 0.5)" },
"100%": {
backdropFilter: "blur(2px)",
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
});
export const backdropFadeOut = keyframes({
"0%": { backdropFilter: "blur(2px)", backgroundColor: "rgba(0, 0, 0, 0.7)" },
"100%": {
backdropFilter: "blur(0px)",
backgroundColor: "rgba(0, 0, 0, 0)",
},
});
export const modalSlideIn = keyframes({ export const modalSlideIn = keyframes({
"0%": { opacity: 0 }, "0%": { opacity: 0 },
"100%": { "100%": {
@ -32,34 +16,6 @@ export const modalSlideOut = keyframes({
}, },
}); });
export const backdrop = recipe({
base: {
animationName: backdropFadeIn,
animationDuration: "0.4s",
backgroundColor: "rgba(0, 0, 0, 0.7)",
position: "absolute",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1,
top: 0,
padding: `${SPACING_UNIT * 3}px`,
backdropFilter: "blur(2px)",
transition: "all ease 0.2s",
},
variants: {
closing: {
true: {
animationName: backdropFadeOut,
backdropFilter: "blur(0px)",
backgroundColor: "rgba(0, 0, 0, 0)",
},
},
},
});
export const modal = recipe({ export const modal = recipe({
base: { base: {
animationName: modalSlideIn, animationName: modalSlideIn,
@ -69,7 +25,7 @@ export const modal = recipe({
maxWidth: "600px", maxWidth: "600px",
color: vars.color.bodyText, color: vars.color.bodyText,
maxHeight: "100%", maxHeight: "100%",
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
overflow: "hidden", overflow: "hidden",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -94,13 +50,18 @@ export const modalHeader = style({
display: "flex", display: "flex",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 2}px`,
borderBottom: `solid 1px ${vars.color.borderColor}`, borderBottom: `solid 1px ${vars.color.border}`,
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "flex-start", alignItems: "center",
}); });
export const closeModalButton = style({ export const closeModalButton = style({
cursor: "pointer", cursor: "pointer",
transition: "all ease 0.2s",
alignSelf: "flex-start",
":hover": {
opacity: "0.75",
},
}); });
export const closeModalButtonIcon = style({ export const closeModalButtonIcon = style({

View File

@ -3,13 +3,14 @@ import { createPortal } from "react-dom";
import { XIcon } from "@primer/octicons-react"; import { XIcon } from "@primer/octicons-react";
import * as styles from "./modal.css"; import * as styles from "./modal.css";
import { useAppDispatch } from "@renderer/hooks";
import { toggleDragging } from "@renderer/features"; import { Backdrop } from "../backdrop/backdrop";
import { useTranslation } from "react-i18next";
export interface ModalProps { export interface ModalProps {
visible: boolean; visible: boolean;
title: string; title: string;
description: string; description?: string;
onClose: () => void; onClose: () => void;
children: React.ReactNode; children: React.ReactNode;
} }
@ -22,9 +23,10 @@ export function Modal({
children, children,
}: ModalProps) { }: ModalProps) {
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
const dispatch = useAppDispatch();
const modalContentRef = useRef<HTMLDivElement | null>(null); const modalContentRef = useRef<HTMLDivElement | null>(null);
const { t } = useTranslation("modal");
const handleCloseClick = useCallback(() => { const handleCloseClick = useCallback(() => {
setIsClosing(true); setIsClosing(true);
const zero = performance.now(); const zero = performance.now();
@ -81,14 +83,10 @@ export function Modal({
return () => {}; return () => {};
}, [handleCloseClick, visible]); }, [handleCloseClick, visible]);
useEffect(() => {
dispatch(toggleDragging(visible));
}, [dispatch, visible]);
if (!visible) return null; if (!visible) return null;
return createPortal( return createPortal(
<div className={styles.backdrop({ closing: isClosing })}> <Backdrop isClosing={isClosing}>
<div <div
className={styles.modal({ closing: isClosing })} className={styles.modal({ closing: isClosing })}
role="modal" role="modal"
@ -97,20 +95,21 @@ export function Modal({
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}> <div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
<h3>{title}</h3> <h3>{title}</h3>
<p style={{ fontSize: 14 }}>{description}</p> {description && <p>{description}</p>}
</div> </div>
<button <button
type="button" type="button"
onClick={handleCloseClick} onClick={handleCloseClick}
className={styles.closeModalButton} className={styles.closeModalButton}
aria-label={t("close")}
> >
<XIcon className={styles.closeModalButtonIcon} size={24} /> <XIcon className={styles.closeModalButtonIcon} size={24} />
</button> </button>
</div> </div>
<div className={styles.modalContent}>{children}</div> <div className={styles.modalContent}>{children}</div>
</div> </div>
</div>, </Backdrop>,
document.body document.body
); );
} }

View File

@ -5,11 +5,11 @@ import { SPACING_UNIT, vars } from "../../theme.css";
export const sidebar = recipe({ export const sidebar = recipe({
base: { base: {
backgroundColor: vars.color.darkBackground, backgroundColor: vars.color.darkBackground,
color: "#c0c1c7", color: vars.color.muted,
flexDirection: "column", flexDirection: "column",
display: "flex", display: "flex",
transition: "opacity ease 0.2s", transition: "opacity ease 0.2s",
borderRight: `solid 1px ${vars.color.borderColor}`, borderRight: `solid 1px ${vars.color.border}`,
position: "relative", position: "relative",
}, },
variants: { variants: {
@ -65,7 +65,7 @@ export const menuItem = recipe({
textWrap: "nowrap", textWrap: "nowrap",
display: "flex", display: "flex",
opacity: "0.9", opacity: "0.9",
color: "#DADBE1", color: vars.color.muted,
":hover": { ":hover": {
opacity: "1", opacity: "1",
}, },
@ -130,7 +130,7 @@ export const section = recipe({
variants: { variants: {
hasBorder: { hasBorder: {
true: { true: {
borderBottom: `solid 1px ${vars.color.borderColor}`, borderBottom: `solid 1px ${vars.color.border}`,
}, },
}, },
}, },
@ -157,10 +157,10 @@ export const footerSocialsItem = style({
height: "16px", height: "16px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
transition: "all ease 0.15s", transition: "all ease 0.2s",
":hover": {
opacity: 0.75,
cursor: "pointer", cursor: "pointer",
":hover": {
opacity: "0.75",
}, },
}); });

View File

@ -16,21 +16,6 @@ import XLogo from "@renderer/assets/x-icon.svg?react";
import * as styles from "./sidebar.css"; import * as styles from "./sidebar.css";
const socials = [
{
url: "https://discord.gg/hydralauncher",
icon: <DiscordLogo />,
},
{
url: "https://twitter.com/hydralauncher",
icon: <XLogo />,
},
{
url: "https://github.com/hydralauncher/hydra",
icon: <MarkGithubIcon size={16} />,
},
];
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_INITIAL_WIDTH = 250;
const SIDEBAR_MAX_WIDTH = 450; const SIDEBAR_MAX_WIDTH = 450;
@ -49,6 +34,24 @@ export function Sidebar() {
initialSidebarWidth ? Number(initialSidebarWidth) : SIDEBAR_INITIAL_WIDTH initialSidebarWidth ? Number(initialSidebarWidth) : SIDEBAR_INITIAL_WIDTH
); );
const socials = [
{
url: "https://discord.gg/hydralauncher",
icon: <DiscordLogo />,
label: t("discord"),
},
{
url: "https://twitter.com/hydralauncher",
icon: <XLogo />,
label: t("x"),
},
{
url: "https://github.com/hydralauncher/hydra",
icon: <MarkGithubIcon size={16} />,
label: t("github"),
},
];
const location = useLocation(); const location = useLocation();
const { game: gameDownloading, progress } = useDownload(); const { game: gameDownloading, progress } = useDownload();
@ -243,6 +246,8 @@ export function Sidebar() {
key={item.url} key={item.url}
className={styles.footerSocialsItem} className={styles.footerSocialsItem}
onClick={() => window.electron.openExternal(item.url)} onClick={() => window.electron.openExternal(item.url)}
title={item.label}
aria-label={item.label}
> >
{item.icon} {item.icon}
</button> </button>

View File

@ -9,7 +9,7 @@ export const textField = recipe({
width: "100%", width: "100%",
alignItems: "center", alignItems: "center",
borderRadius: "8px", borderRadius: "8px",
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
height: "40px", height: "40px",
minHeight: "40px", minHeight: "40px",
}, },
@ -44,7 +44,6 @@ export const textFieldInput = style({
color: "#DADBE1", color: "#DADBE1",
cursor: "default", cursor: "default",
fontFamily: "inherit", fontFamily: "inherit",
fontSize: vars.size.bodyFontSize,
textOverflow: "ellipsis", textOverflow: "ellipsis",
padding: `${SPACING_UNIT}px`, padding: `${SPACING_UNIT}px`,
":focus": { ":focus": {

View File

@ -9,11 +9,16 @@ export interface TextFieldProps
> { > {
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"]; theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
label?: string; label?: string;
textFieldProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
} }
export function TextField({ export function TextField({
theme = "primary", theme = "primary",
label, label,
textFieldProps,
...props ...props
}: TextFieldProps) { }: TextFieldProps) {
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
@ -27,7 +32,10 @@ export function TextField({
</label> </label>
)} )}
<div className={styles.textField({ focused: isFocused, theme })}> <div
className={styles.textField({ focused: isFocused, theme })}
{...textFieldProps}
>
<input <input
id={id} id={id}
type="text" type="text"

View File

@ -15,7 +15,7 @@ export const windowSlice = createSlice({
name: "window", name: "window",
initialState, initialState,
reducers: { reducers: {
toggleDragging: (state, action: PayloadAction<boolean>) => { toggleDraggingDisabled: (state, action: PayloadAction<boolean>) => {
state.draggingDisabled = action.payload; state.draggingDisabled = action.payload;
}, },
setHeaderTitle: (state, action: PayloadAction<string>) => { setHeaderTitle: (state, action: PayloadAction<string>) => {
@ -24,4 +24,4 @@ export const windowSlice = createSlice({
}, },
}); });
export const { toggleDragging, setHeaderTitle } = windowSlice.actions; export const { toggleDraggingDisabled, setHeaderTitle } = windowSlice.actions;

View File

@ -21,5 +21,8 @@ export const getSteamLanguage = (language: string) => {
if (language.startsWith("pt")) return "brazilian"; if (language.startsWith("pt")) return "brazilian";
if (language.startsWith("es")) return "spanish"; if (language.startsWith("es")) return "spanish";
if (language.startsWith("fr")) return "french"; if (language.startsWith("fr")) return "french";
if (language.startsWith("ru")) return "russian";
if (language.startsWith("it")) return "italian";
if (language.startsWith("hu")) return "hungarian";
return "english"; return "english";
}; };

View File

@ -6,7 +6,7 @@ import type { CatalogueEntry } from "@types";
import { clearSearch } from "@renderer/features"; import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks"; import { useAppDispatch } from "@renderer/hooks";
import { vars } from "../../theme.css"; import { SPACING_UNIT, vars } from "../../theme.css";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "../home/home.css"; import * as styles from "../home/home.css";
@ -67,12 +67,12 @@ export function Catalogue() {
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444"> <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section <section
style={{ style={{
padding: `16px 32px`, padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 4}px`,
display: "flex", display: "flex",
width: "100%", width: "100%",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
borderBottom: `1px solid ${vars.color.borderColor}`, borderBottom: `1px solid ${vars.color.border}`,
}} }}
> >
<Button <Button

View File

@ -31,7 +31,7 @@ export const downloadCover = style({
height: "auto", height: "auto",
objectFit: "cover", objectFit: "cover",
objectPosition: "center", objectPosition: "center",
borderRight: `solid 1px ${vars.color.borderColor}`, borderRight: `solid 1px ${vars.color.border}`,
}); });
export const download = recipe({ export const download = recipe({
@ -40,7 +40,7 @@ export const download = recipe({
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
display: "flex", display: "flex",
borderRadius: "8px", borderRadius: "8px",
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
overflow: "hidden", overflow: "hidden",
boxShadow: "0px 0px 15px 0px #000000", boxShadow: "0px 0px 15px 0px #000000",
transition: "all ease 0.2s", transition: "all ease 0.2s",

View File

@ -15,25 +15,26 @@ export interface DescriptionHeaderProps {
} }
export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) { export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
const [clipboardLock, setClipboardLock] = useState(false); const [clipboardLocked, setClipboardLocked] = useState(false);
const { t, i18n } = useTranslation("game_details"); const { t, i18n } = useTranslation("game_details");
const { objectID, shop } = useParams(); const { objectID, shop } = useParams();
useEffect(() => { useEffect(() => {
if (!gameDetails) return setClipboardLock(true); if (!gameDetails) return setClipboardLocked(true);
setClipboardLock(false); setClipboardLocked(false);
}, [gameDetails]); }, [gameDetails]);
const handleCopyToClipboard = () => { const handleCopyToClipboard = () => {
setClipboardLock(true); if (gameDetails) {
setClipboardLocked(true);
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
p: btoa( p: btoa(
JSON.stringify([ JSON.stringify([
objectID, objectID,
shop, shop,
encodeURIComponent(gameDetails?.name), encodeURIComponent(gameDetails.name),
i18n.language, i18n.language,
]) ])
), ),
@ -49,9 +50,10 @@ export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
if (time - zero <= 3000) { if (time - zero <= 3000) {
requestAnimationFrame(holdLock); requestAnimationFrame(holdLock);
} else { } else {
setClipboardLock(false); setClipboardLocked(false);
} }
}); });
}
}; };
return ( return (
@ -68,9 +70,9 @@ export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
<Button <Button
theme="outline" theme="outline"
onClick={handleCopyToClipboard} onClick={handleCopyToClipboard}
disabled={clipboardLock || !gameDetails} disabled={clipboardLocked || !gameDetails}
> >
{clipboardLock ? ( {clipboardLocked ? (
t("copied_link_to_clipboard") t("copied_link_to_clipboard")
) : ( ) : (
<> <>

View File

@ -80,7 +80,7 @@ export const descriptionContent = style({
}); });
export const contentSidebar = style({ export const contentSidebar = style({
borderLeft: `solid 1px ${vars.color.borderColor};`, borderLeft: `solid 1px ${vars.color.border};`,
width: "100%", width: "100%",
height: "100%", height: "100%",
"@media": { "@media": {
@ -105,7 +105,6 @@ export const contentSidebarTitle = style({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
borderBottom: `solid 1px ${vars.color.borderColor}`,
}); });
export const requirementButtonContainer = style({ export const requirementButtonContainer = style({
@ -114,7 +113,7 @@ export const requirementButtonContainer = style({
}); });
export const requirementButton = style({ export const requirementButton = style({
border: `solid 1px ${vars.color.borderColor};`, border: `solid 1px ${vars.color.border};`,
borderLeft: "none", borderLeft: "none",
borderRight: "none", borderRight: "none",
borderRadius: "0", borderRadius: "0",
@ -171,11 +170,11 @@ export const descriptionSkeleton = style({
export const descriptionHeader = style({ export const descriptionHeader = style({
width: "100%", width: "100%",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
borderBottom: `solid 1px ${vars.color.borderColor}`,
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
borderBottom: `solid 1px ${vars.color.border}`,
height: "72px", height: "72px",
}); });
@ -183,7 +182,6 @@ export const descriptionHeaderInfo = style({
display: "flex", display: "flex",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
flexDirection: "column", flexDirection: "column",
fontSize: vars.size.bodyFontSize,
}); });
export const howLongToBeatCategoriesList = style({ export const howLongToBeatCategoriesList = style({
@ -201,16 +199,15 @@ export const howLongToBeatCategory = style({
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
borderRadius: "8px", borderRadius: "8px",
padding: `8px 16px`, padding: `8px 16px`,
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
}); });
export const howLongToBeatCategoryLabel = style({ export const howLongToBeatCategoryLabel = style({
fontSize: vars.size.bodyFontSize, color: vars.color.muted,
color: "#DADBE1",
}); });
export const howLongToBeatCategorySkeleton = style({ export const howLongToBeatCategorySkeleton = style({
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
borderRadius: "8px", borderRadius: "8px",
height: "76px", height: "76px",
}); });
@ -224,7 +221,7 @@ export const randomizerButton = style({
/* Scroll bar + spacing */ /* Scroll bar + spacing */
right: `${9 + SPACING_UNIT * 2}px`, right: `${9 + SPACING_UNIT * 2}px`,
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px", boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px",
border: `solid 2px ${vars.color.borderColor}`, border: `solid 2px ${vars.color.border}`,
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
":hover": { ":hover": {
backgroundColor: vars.color.background, backgroundColor: vars.color.background,

View File

@ -5,6 +5,7 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import type { import type {
Game, Game,
GameRepack,
GameShop, GameShop,
HowLongToBeatCategory, HowLongToBeatCategory,
ShopDetails, ShopDetails,
@ -18,23 +19,30 @@ import { useAppDispatch, useDownload } from "@renderer/hooks";
import starsAnimation from "@renderer/assets/lottie/stars.json"; import starsAnimation from "@renderer/assets/lottie/stars.json";
import { vars } from "../../theme.css";
import Lottie from "lottie-react"; import Lottie from "lottie-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SkeletonTheme } from "react-loading-skeleton"; import { SkeletonTheme } from "react-loading-skeleton";
import { DescriptionHeader } from "./description-header"; import { DescriptionHeader } from "./description-header";
import { GameDetailsSkeleton } from "./game-details-skeleton"; import { GameDetailsSkeleton } from "./game-details-skeleton";
import * as styles from "./game-details.css"; import * as styles from "./game-details.css";
import { HeroPanel } from "./hero-panel"; import { HeroPanel } from "./hero";
import { HowLongToBeatSection } from "./how-long-to-beat-section"; import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { RepacksModal } from "./repacks-modal"; import { RepacksModal } from "./repacks-modal";
import { vars } from "../../theme.css";
import {
DODIInstallationGuide,
DONT_SHOW_DODI_INSTRUCTIONS_KEY,
DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY,
OnlineFixInstallationGuide,
} from "./installation-guides";
export function GameDetails() { export function GameDetails() {
const { objectID, shop } = useParams(); const { objectID, shop } = useParams();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false); const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
const [color, setColor] = useState(""); const [color, setColor] = useState({ dark: "", light: "" });
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null); const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
const [howLongToBeat, setHowLongToBeat] = useState<{ const [howLongToBeat, setHowLongToBeat] = useState<{
isLoading: boolean; isLoading: boolean;
@ -43,6 +51,10 @@ export function GameDetails() {
const [game, setGame] = useState<Game | null>(null); const [game, setGame] = useState<Game | null>(null);
const [isGamePlaying, setIsGamePlaying] = useState(false); const [isGamePlaying, setIsGamePlaying] = useState(false);
const [showInstructionsModal, setShowInstructionsModal] = useState<
null | "onlinefix" | "DODI"
>(null);
const [activeRequirement, setActiveRequirement] = const [activeRequirement, setActiveRequirement] =
useState<keyof SteamAppDetails["pc_requirements"]>("minimum"); useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
@ -52,7 +64,6 @@ export function GameDetails() {
const { t, i18n } = useTranslation("game_details"); const { t, i18n } = useTranslation("game_details");
const [showRepacksModal, setShowRepacksModal] = useState(false); const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -61,7 +72,8 @@ export function GameDetails() {
const handleImageSettled = useCallback((url: string) => { const handleImageSettled = useCallback((url: string) => {
average(url, { amount: 1, format: "hex" }) average(url, { amount: 1, format: "hex" })
.then((color) => { .then((color) => {
setColor(new Color(color).darken(0.6).toString() as string); const darkColor = new Color(color).darken(0.6).toString() as string;
setColor({ light: color as string, dark: darkColor });
}) })
.catch(() => {}); .catch(() => {});
}, []); }, []);
@ -112,7 +124,10 @@ export function GameDetails() {
useEffect(() => { useEffect(() => {
if (isGameDownloading) if (isGameDownloading)
setGame((prev) => ({ ...prev, status: gameDownloading?.status })); setGame((prev) => {
if (prev === null || !gameDownloading?.status) return prev;
return { ...prev, status: gameDownloading?.status };
});
}, [isGameDownloading, gameDownloading?.status]); }, [isGameDownloading, gameDownloading?.status]);
useEffect(() => { useEffect(() => {
@ -134,12 +149,12 @@ export function GameDetails() {
}, [game?.id, isGamePlaying, getGame]); }, [game?.id, isGamePlaying, getGame]);
const handleStartDownload = async ( const handleStartDownload = async (
repackId: number, repack: GameRepack,
downloadPath: string downloadPath: string
) => { ) => {
if (gameDetails) { if (gameDetails) {
return startDownload( return startDownload(
repackId, repack.id,
gameDetails.objectID, gameDetails.objectID,
gameDetails.name, gameDetails.name,
shop as GameShop, shop as GameShop,
@ -147,7 +162,18 @@ export function GameDetails() {
).then(() => { ).then(() => {
getGame(); getGame();
setShowRepacksModal(false); setShowRepacksModal(false);
setShowSelectFolderModal(false);
if (
repack.repacker === "onlinefix" &&
!window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
) {
setShowInstructionsModal("onlinefix");
} else if (
repack.repacker === "DODI" &&
!window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
) {
setShowInstructionsModal("DODI");
}
}); });
} }
}; };
@ -172,12 +198,21 @@ export function GameDetails() {
visible={showRepacksModal} visible={showRepacksModal}
gameDetails={gameDetails} gameDetails={gameDetails}
startDownload={handleStartDownload} startDownload={handleStartDownload}
showSelectFolderModal={showSelectFolderModal}
setShowSelectFolderModal={setShowSelectFolderModal}
onClose={() => setShowRepacksModal(false)} onClose={() => setShowRepacksModal(false)}
/> />
)} )}
<OnlineFixInstallationGuide
visible={showInstructionsModal === "onlinefix"}
onClose={() => setShowInstructionsModal(null)}
/>
<DODIInstallationGuide
windowColor={color.light}
visible={showInstructionsModal === "DODI"}
onClose={() => setShowInstructionsModal(null)}
/>
{isLoading ? ( {isLoading ? (
<GameDetailsSkeleton /> <GameDetailsSkeleton />
) : ( ) : (
@ -201,7 +236,7 @@ export function GameDetails() {
<HeroPanel <HeroPanel
game={game} game={game}
color={color} color={color.dark}
gameDetails={gameDetails} gameDetails={gameDetails}
openRepacksModal={() => setShowRepacksModal(true)} openRepacksModal={() => setShowRepacksModal(true)}
getGame={getGame} getGame={getGame}

View File

@ -0,0 +1,7 @@
import { style } from "@vanilla-extract/css";
import { vars } from "../../../theme.css";
export const heroPanelAction = style({
border: `solid 1px ${vars.color.muted}`,
});

View File

@ -6,6 +6,8 @@ import type { Game, ShopDetails } from "@types";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./hero-panel-actions.css";
export interface HeroPanelActionsProps { export interface HeroPanelActionsProps {
game: Game | null; game: Game | null;
gameDetails: ShopDetails | null; gameDetails: ShopDetails | null;
@ -55,6 +57,8 @@ export function HeroPanelActions({
if (filePaths && filePaths.length > 0) { if (filePaths && filePaths.length > 0) {
return filePaths[0]; return filePaths[0];
} }
return null;
}); });
}; };
@ -113,6 +117,7 @@ export function HeroPanelActions({
theme="outline" theme="outline"
disabled={!gameDetails || toggleLibraryGameDisabled} disabled={!gameDetails || toggleLibraryGameDisabled}
onClick={toggleGameOnLibrary} onClick={toggleGameOnLibrary}
className={styles.heroPanelAction}
> >
{game ? <NoEntryIcon /> : <PlusCircleIcon />} {game ? <NoEntryIcon /> : <PlusCircleIcon />}
{game ? t("remove_from_library") : t("add_to_library")} {game ? t("remove_from_library") : t("add_to_library")}
@ -122,10 +127,18 @@ export function HeroPanelActions({
if (isGameDownloading) { if (isGameDownloading) {
return ( return (
<> <>
<Button onClick={() => pauseDownload(game.id)} theme="outline"> <Button
onClick={() => pauseDownload(game.id)}
theme="outline"
className={styles.heroPanelAction}
>
{t("pause")} {t("pause")}
</Button> </Button>
<Button onClick={() => cancelDownload(game.id)} theme="outline"> <Button
onClick={() => cancelDownload(game.id)}
theme="outline"
className={styles.heroPanelAction}
>
{t("cancel")} {t("cancel")}
</Button> </Button>
</> </>
@ -135,12 +148,17 @@ export function HeroPanelActions({
if (game?.status === "paused") { if (game?.status === "paused") {
return ( return (
<> <>
<Button onClick={() => resumeDownload(game.id)} theme="outline"> <Button
onClick={() => resumeDownload(game.id)}
theme="outline"
className={styles.heroPanelAction}
>
{t("resume")} {t("resume")}
</Button> </Button>
<Button <Button
onClick={() => cancelDownload(game.id).then(getGame)} onClick={() => cancelDownload(game.id).then(getGame)}
theme="outline" theme="outline"
className={styles.heroPanelAction}
> >
{t("cancel")} {t("cancel")}
</Button> </Button>
@ -156,6 +174,7 @@ export function HeroPanelActions({
onClick={openGameInstaller} onClick={openGameInstaller}
theme="outline" theme="outline"
disabled={deleting || isGamePlaying} disabled={deleting || isGamePlaying}
className={styles.heroPanelAction}
> >
{t("install")} {t("install")}
</Button> </Button>
@ -164,7 +183,12 @@ export function HeroPanelActions({
)} )}
{isGamePlaying ? ( {isGamePlaying ? (
<Button onClick={closeGame} theme="outline" disabled={deleting}> <Button
onClick={closeGame}
theme="outline"
disabled={deleting}
className={styles.heroPanelAction}
>
{t("close")} {t("close")}
</Button> </Button>
) : ( ) : (
@ -172,6 +196,7 @@ export function HeroPanelActions({
onClick={openGame} onClick={openGame}
theme="outline" theme="outline"
disabled={deleting || isGamePlaying} disabled={deleting || isGamePlaying}
className={styles.heroPanelAction}
> >
{t("play")} {t("play")}
</Button> </Button>
@ -183,13 +208,19 @@ export function HeroPanelActions({
if (game?.status === "cancelled") { if (game?.status === "cancelled") {
return ( return (
<> <>
<Button onClick={openRepacksModal} theme="outline" disabled={deleting}> <Button
onClick={openRepacksModal}
theme="outline"
disabled={deleting}
className={styles.heroPanelAction}
>
{t("open_download_options")} {t("open_download_options")}
</Button> </Button>
<Button <Button
onClick={() => removeGameFromLibrary(game.id).then(getGame)} onClick={() => removeGameFromLibrary(game.id).then(getGame)}
theme="outline" theme="outline"
disabled={deleting} disabled={deleting}
className={styles.heroPanelAction}
> >
{t("remove_from_list")} {t("remove_from_list")}
</Button> </Button>
@ -201,7 +232,11 @@ export function HeroPanelActions({
return ( return (
<> <>
{toggleGameOnLibraryButton} {toggleGameOnLibraryButton}
<Button onClick={openRepacksModal} theme="outline"> <Button
onClick={openRepacksModal}
theme="outline"
className={styles.heroPanelAction}
>
{t("open_download_options")} {t("open_download_options")}
</Button> </Button>
</> </>

View File

@ -1,5 +1,5 @@
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css"; import { SPACING_UNIT, vars } from "../../../theme.css";
export const panel = style({ export const panel = style({
width: "100%", width: "100%",
@ -9,7 +9,8 @@ export const panel = style({
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
transition: "all ease 0.2s", transition: "all ease 0.2s",
borderBottom: `solid 1px ${vars.color.borderColor}`, borderBottom: `solid 1px ${vars.color.border}`,
color: "#8e919b",
boxShadow: "0px 0px 15px 0px #000000", boxShadow: "0px 0px 15px 0px #000000",
}); });
@ -17,7 +18,6 @@ export const content = style({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
fontSize: vars.size.bodyFontSize,
}); });
export const actions = style({ export const actions = style({

View File

@ -6,12 +6,13 @@ import { useDownload } from "@renderer/hooks";
import type { Game, ShopDetails } from "@types"; import type { Game, ShopDetails } from "@types";
import { formatDownloadProgress } from "@renderer/helpers"; import { formatDownloadProgress } from "@renderer/helpers";
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css";
import { useDate } from "@renderer/hooks/use-date"; import { useDate } from "@renderer/hooks/use-date";
import { formatBytes } from "@renderer/utils"; import { formatBytes } from "@renderer/utils";
import { HeroPanelActions } from "./hero-panel-actions"; import { HeroPanelActions } from "./hero-panel-actions";
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
import * as styles from "./hero-panel.css";
export interface HeroPanelProps { export interface HeroPanelProps {
game: Game | null; game: Game | null;
gameDetails: ShopDetails | null; gameDetails: ShopDetails | null;
@ -89,7 +90,7 @@ export function HeroPanel({
const getInfo = () => { const getInfo = () => {
if (!gameDetails) return null; if (!gameDetails) return null;
if (isGameDeleting(game?.id)) { if (isGameDeleting(game?.id ?? -1)) {
return <p>{t("deleting")}</p>; return <p>{t("deleting")}</p>;
} }

View File

@ -0,0 +1 @@
export * from "./hero-panel";

View File

@ -1,6 +1,6 @@
import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import type { HowLongToBeatCategory } from "@types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { HowLongToBeatCategory } from "@types";
import { vars } from "../../theme.css"; import { vars } from "../../theme.css";
import * as styles from "./game-details.css"; import * as styles from "./game-details.css";

View File

@ -0,0 +1,3 @@
export const DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY =
"dontShowOnlineFixInstructions";
export const DONT_SHOW_DODI_INSTRUCTIONS_KEY = "dontShowDodiInstructions";

View File

@ -0,0 +1,31 @@
import { vars } from "../../../theme.css";
import { keyframes, style } from "@vanilla-extract/css";
export const slideIn = keyframes({
"0%": { transform: "translateY(0)" },
"40%": { transform: "translateY(0)" },
"70%": { transform: "translateY(-100%)" },
"100%": { transform: "translateY(-100%)" },
});
export const windowContainer = style({
width: "250px",
height: "150px",
alignSelf: "center",
borderRadius: "2px",
overflow: "hidden",
border: `solid 1px ${vars.color.border}`,
});
export const windowContent = style({
backgroundColor: vars.color.muted,
height: "90%",
animationName: slideIn,
animationDuration: "3s",
animationIterationCount: "infinite",
animationTimingFunction: "ease-out",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#1c1c1c",
});

View File

@ -0,0 +1,75 @@
import { useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Button, CheckboxField, Modal } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import * as styles from "./dodi-installation-guide.css";
import { ArrowUpIcon } from "@primer/octicons-react";
import { DONT_SHOW_DODI_INSTRUCTIONS_KEY } from "./constants";
export interface DODIInstallationGuideProps {
windowColor: string;
visible: boolean;
onClose: () => void;
}
export function DODIInstallationGuide({
windowColor,
visible,
onClose,
}: DODIInstallationGuideProps) {
const { t } = useTranslation("game_details");
const [dontShowAgain, setDontShowAgain] = useState(true);
const handleClose = () => {
if (dontShowAgain) {
window.localStorage.setItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY, "1");
}
onClose();
};
return (
<Modal
title={t("installation_instructions")}
description={t("installation_instructions_description")}
onClose={handleClose}
visible={visible}
>
<div
style={{
display: "flex",
gap: SPACING_UNIT * 2,
flexDirection: "column",
}}
>
<p style={{ fontFamily: "Fira Sans", marginBottom: 8 }}>
<Trans i18nKey="dodi_installation_instruction" ns="game_details">
<ArrowUpIcon size={16} />
</Trans>
</p>
<div
className={styles.windowContainer}
style={{ backgroundColor: windowColor }}
>
<div className={styles.windowContent}>
<ArrowUpIcon size={24} />
</div>
</div>
<CheckboxField
label={t("dont_show_it_again")}
onChange={() => setDontShowAgain(!dontShowAgain)}
checked={dontShowAgain}
/>
<Button style={{ alignSelf: "flex-end" }} onClick={handleClose}>
{t("got_it")}
</Button>
</div>
</Modal>
);
}

View File

@ -0,0 +1,3 @@
export * from "./online-fix-installation-guide";
export * from "./dodi-installation-guide";
export * from "./constants";

View File

@ -0,0 +1,7 @@
import { SPACING_UNIT } from "../../../theme.css";
import { style } from "@vanilla-extract/css";
export const passwordField = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});

View File

@ -0,0 +1,104 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, CheckboxField, Modal, TextField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import * as styles from "./online-fix-installation-guide.css";
import { CopyIcon } from "@primer/octicons-react";
import { DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY } from "./constants";
const ONLINE_FIX_PASSWORD = "online-fix.me";
export interface OnlineFixInstallationGuideProps {
visible: boolean;
onClose: () => void;
}
export function OnlineFixInstallationGuide({
visible,
onClose,
}: OnlineFixInstallationGuideProps) {
const [clipboardLocked, setClipboardLocked] = useState(false);
const { t } = useTranslation("game_details");
const [dontShowAgain, setDontShowAgain] = useState(true);
const handleCopyToClipboard = () => {
setClipboardLocked(true);
navigator.clipboard.writeText(ONLINE_FIX_PASSWORD);
const zero = performance.now();
requestAnimationFrame(function holdLock(time) {
if (time - zero <= 3000) {
requestAnimationFrame(holdLock);
} else {
setClipboardLocked(false);
}
});
};
const handleClose = () => {
if (dontShowAgain) {
window.localStorage.setItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY, "1");
}
onClose();
};
return (
<Modal
title={t("installation_instructions")}
description={t("installation_instructions_description")}
onClose={handleClose}
visible={visible}
>
<div
style={{
display: "flex",
gap: SPACING_UNIT * 2,
flexDirection: "column",
}}
>
<p style={{ fontFamily: "Fira Sans" }}>{t("online_fix_instruction")}</p>
<div className={styles.passwordField}>
<TextField
value={ONLINE_FIX_PASSWORD}
readOnly
disabled
style={{ fontSize: 16 }}
textFieldProps={{ style: { height: 45 } }}
/>
<Button
style={{ alignSelf: "flex-end", height: 45 }}
theme="outline"
onClick={handleCopyToClipboard}
disabled={clipboardLocked}
>
{clipboardLocked ? (
t("copied_to_clipboard")
) : (
<>
<CopyIcon />
{t("copy_to_clipboard")}
</>
)}
</Button>
</div>
<CheckboxField
label={t("dont_show_it_again")}
onChange={() => setDontShowAgain(!dontShowAgain)}
checked={dontShowAgain}
/>
<Button style={{ alignSelf: "flex-end" }} onClick={handleClose}>
{t("got_it")}
</Button>
</div>
</Modal>
);
}

View File

@ -14,22 +14,19 @@ import { SelectFolderModal } from "./select-folder-modal";
export interface RepacksModalProps { export interface RepacksModalProps {
visible: boolean; visible: boolean;
gameDetails: ShopDetails; gameDetails: ShopDetails;
showSelectFolderModal: boolean; startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
setShowSelectFolderModal: (value: boolean) => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
onClose: () => void; onClose: () => void;
} }
export function RepacksModal({ export function RepacksModal({
visible, visible,
gameDetails, gameDetails,
showSelectFolderModal,
setShowSelectFolderModal,
startDownload, startDownload,
onClose, onClose,
}: RepacksModalProps) { }: RepacksModalProps) {
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]); const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack | null>(null); const [repack, setRepack] = useState<GameRepack | null>(null);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const repackersFriendlyNames = useAppSelector( const repackersFriendlyNames = useAppSelector(
(state) => state.repackersFriendlyNames.value (state) => state.repackersFriendlyNames.value
@ -57,12 +54,7 @@ export function RepacksModal({
}; };
return ( return (
<Modal <>
visible={visible}
title={`${gameDetails.name} Repacks`}
description={t("repacks_modal_description")}
onClose={onClose}
>
<SelectFolderModal <SelectFolderModal
visible={showSelectFolderModal} visible={showSelectFolderModal}
onClose={() => setShowSelectFolderModal(false)} onClose={() => setShowSelectFolderModal(false)}
@ -70,6 +62,13 @@ export function RepacksModal({
startDownload={startDownload} startDownload={startDownload}
repack={repack} repack={repack}
/> />
<Modal
visible={visible}
title={`${gameDetails.name} Repacks`}
description={t("repacks_modal_description")}
onClose={onClose}
>
<div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}> <div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}>
<TextField placeholder={t("filter")} onChange={handleFilter} /> <TextField placeholder={t("filter")} onChange={handleFilter} />
</div> </div>
@ -91,5 +90,6 @@ export function RepacksModal({
))} ))}
</div> </div>
</Modal> </Modal>
</>
); );
} }

View File

@ -10,10 +10,18 @@ export const container = style({
export const downloadsPathField = style({ export const downloadsPathField = style({
display: "flex", display: "flex",
gap: `${SPACING_UNIT * 2}px`, gap: `${SPACING_UNIT}px`,
}); });
export const hintText = style({ export const hintText = style({
fontSize: "12px", fontSize: "12px",
color: vars.color.bodyText, color: vars.color.bodyText,
}); });
export const settingsLink = style({
textDecoration: "none",
color: "#C0C1C7",
":hover": {
textDecoration: "underline",
},
});

View File

@ -7,12 +7,13 @@ import { formatBytes } from "@renderer/utils";
import { DiskSpace } from "check-disk-space"; import { DiskSpace } from "check-disk-space";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as styles from "./select-folder-modal.css"; import * as styles from "./select-folder-modal.css";
import { DownloadIcon } from "@primer/octicons-react";
export interface SelectFolderModalProps { export interface SelectFolderModalProps {
visible: boolean; visible: boolean;
gameDetails: ShopDetails; gameDetails: ShopDetails;
onClose: () => void; onClose: () => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>; startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
repack: GameRepack | null; repack: GameRepack | null;
} }
@ -63,8 +64,10 @@ export function SelectFolderModal({
const handleStartClick = () => { const handleStartClick = () => {
if (repack) { if (repack) {
setDownloadStarting(true); setDownloadStarting(true);
startDownload(repack.id, selectedPath).finally(() => {
startDownload(repack, selectedPath).finally(() => {
setDownloadStarting(false); setDownloadStarting(false);
onClose();
}); });
} }
}; };
@ -98,17 +101,12 @@ export function SelectFolderModal({
</div> </div>
<p className={styles.hintText}> <p className={styles.hintText}>
{t("select_folder_hint")}{" "} {t("select_folder_hint")}{" "}
<Link <Link to="/settings" className={styles.settingsLink}>
to="/settings"
style={{
textDecoration: "none",
color: "#C0C1C7",
}}
>
{t("settings")} {t("settings")}
</Link> </Link>
</p> </p>
<Button onClick={handleStartClick} disabled={downloadStarting}> <Button onClick={handleStartClick} disabled={downloadStarting}>
<DownloadIcon />
{t("download_now")} {t("download_now")}
</Button> </Button>
</div> </div>

View File

@ -12,7 +12,7 @@ export const content = style({
width: "100%", width: "100%",
height: "100%", height: "100%",
padding: `${SPACING_UNIT * 3}px`, padding: `${SPACING_UNIT * 3}px`,
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px #000000", boxShadow: "0px 0px 15px 0px #000000",
borderRadius: "8px", borderRadius: "8px",
gap: `${SPACING_UNIT * 2}px`, gap: `${SPACING_UNIT * 2}px`,
@ -22,5 +22,5 @@ export const content = style({
export const downloadsPathField = style({ export const downloadsPathField = style({
display: "flex", display: "flex",
gap: `${SPACING_UNIT * 2}px`, gap: `${SPACING_UNIT}px`,
}); });

View File

@ -6,8 +6,9 @@ export const [themeClass, vars] = createTheme({
color: { color: {
background: "#1c1c1c", background: "#1c1c1c",
darkBackground: "#151515", darkBackground: "#151515",
muted: "#c0c1c7",
bodyText: "#8e919b", bodyText: "#8e919b",
borderColor: "rgba(255, 255, 255, 0.1)", border: "#424244",
}, },
opacity: { opacity: {
disabled: "0.5", disabled: "0.5",

816
yarn.lock

File diff suppressed because it is too large Load Diff