mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
Merge branch 'main' of https://github.com/hydralauncher/hydra
This commit is contained in:
commit
bdbcf830f3
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,29 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve. Write in English, please
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
|
||||
- OS: [Windows 11/Linux Distro/Steam Deck]
|
||||
- Hydra Version:
|
||||
- Additional information and context of your problem:
|
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,19 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for Hydra. Write in English, please
|
||||
title: "[REQUEST]"
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
@ -183,6 +183,13 @@ yarn make
|
||||
<br />
|
||||
<sub><b>Netflixy</b></sub>
|
||||
</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>
|
||||
</table>
|
||||
<!-- readme: contributors -end -->
|
||||
|
BIN
build/installerSidebar.bmp
Normal file
BIN
build/installerSidebar.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 151 KiB |
@ -23,6 +23,7 @@ nsis:
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
oneClick: false
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
|
@ -60,6 +60,7 @@
|
||||
"react-router-dom": "^6.22.3",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"typeorm": "^0.3.20",
|
||||
"user-agents": "^1.1.193",
|
||||
"windows-1251": "^3.0.4",
|
||||
"winston": "^3.13.0",
|
||||
"yaml": "^2.4.1"
|
||||
|
@ -17,7 +17,10 @@
|
||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||
"filter": "Filter library",
|
||||
"follow_us": "Follow us",
|
||||
"home": "Home"
|
||||
"home": "Home",
|
||||
"discord": "Join our Discord",
|
||||
"x": "Follow on X",
|
||||
"github": "Contribute on GitHub"
|
||||
},
|
||||
"header": {
|
||||
"search": "Search",
|
||||
@ -82,8 +85,16 @@
|
||||
"repacks_modal_description": "Choose the repack you want to download",
|
||||
"downloads_path": "Downloads path",
|
||||
"select_folder_hint": "To change the default folder, access the",
|
||||
"settings": "Hydra settings",
|
||||
"download_now": "Download now"
|
||||
"settings": "Settings",
|
||||
"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": {
|
||||
"title": "Activate Hydra",
|
||||
@ -144,5 +155,8 @@
|
||||
"title": "Programs not installed",
|
||||
"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"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Close button"
|
||||
}
|
||||
}
|
||||
|
@ -4,3 +4,4 @@ export { default as es } from "./es/translation.json";
|
||||
export { default as fr } from "./fr/translation.json";
|
||||
export { default as hu } from "./hu/translation.json";
|
||||
export { default as it } from "./it/translation.json";
|
||||
export { default as ru } from "./ru/translation.json";
|
||||
|
@ -17,7 +17,10 @@
|
||||
"downloading": "{{title}} ({{percentage}} - Download…)",
|
||||
"filter": "Filtra libreria",
|
||||
"follow_us": "Seguici",
|
||||
"home": "Home"
|
||||
"home": "Home",
|
||||
"discord": "Unisciti al nostro Discord",
|
||||
"x": "Segui su X",
|
||||
"github": "Contribuisci su GitHub"
|
||||
},
|
||||
"header": {
|
||||
"search": "Cerca",
|
||||
@ -77,7 +80,21 @@
|
||||
"play": "Gioca",
|
||||
"deleting": "Eliminazione dell'installer…",
|
||||
"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": {
|
||||
"title": "Attiva Hydra",
|
||||
@ -138,5 +155,8 @@
|
||||
"title": "Programmi non installati",
|
||||
"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"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Pulsante Chiudi"
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,10 @@
|
||||
"downloading": "{{title}} ({{percentage}} - Baixando…)",
|
||||
"filter": "Filtrar biblioteca",
|
||||
"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": {
|
||||
"search": "Buscar",
|
||||
@ -79,7 +82,15 @@
|
||||
"downloads_path": "Diretório do download",
|
||||
"select_folder_hint": "Para trocar a pasta padrão, acesse as ",
|
||||
"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": {
|
||||
"title": "Ativação",
|
||||
@ -144,5 +155,8 @@
|
||||
"catalogue": {
|
||||
"next_page": "Próxima página",
|
||||
"previous_page": "Página anterior"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Botão de fechar"
|
||||
}
|
||||
}
|
||||
|
147
src/locales/ru/translation.json
Normal file
147
src/locales/ru/translation.json
Normal 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, чтобы игра могла нормально работать"
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { app } from "electron";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export const repackersOn1337x = [
|
||||
@ -34,7 +33,8 @@ export const months = [
|
||||
"Dec",
|
||||
];
|
||||
|
||||
export const defaultDownloadsPath = path.join(os.homedir(), "downloads");
|
||||
|
||||
export const defaultDownloadsPath = app.getPath("downloads");
|
||||
|
||||
export const databasePath = path.join(
|
||||
app.getPath("appData"),
|
||||
|
@ -8,7 +8,7 @@ import { stateManager } from "@main/state-manager";
|
||||
|
||||
const { Index } = flexSearch;
|
||||
const repacksIndex = new Index();
|
||||
const steamGamesIndex = new Index({ tokenize: "reverse" });
|
||||
const steamGamesIndex = new Index();
|
||||
|
||||
const repacks = stateManager.getValue("repacks");
|
||||
const steamGames = stateManager.getValue("steamGames");
|
||||
|
@ -34,6 +34,21 @@ export const searchHowLongToBeat = async (gameName: string) => {
|
||||
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 (
|
||||
id: string
|
||||
): Promise<HowLongToBeatCategory[]> => {
|
||||
@ -43,18 +58,16 @@ export const getHowLongToBeatGame = async (
|
||||
const { document } = window;
|
||||
|
||||
const $ul = document.querySelector(".shadow_shadow ul");
|
||||
if (!$ul) return [];
|
||||
|
||||
const $lis = Array.from($ul.children);
|
||||
|
||||
return $lis.map(($li) => {
|
||||
const title = $li.querySelector("h4").textContent;
|
||||
const [, accuracyClassName] = Array.from(($li as HTMLElement).classList);
|
||||
const [$firstLi] = $lis;
|
||||
|
||||
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 {
|
||||
title,
|
||||
duration: $li.querySelector("h5").textContent,
|
||||
accuracy,
|
||||
};
|
||||
});
|
||||
return parseListItems($lis);
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import UserAgent from "user-agents";
|
||||
|
||||
import type { Repack } from "@main/entity";
|
||||
import { repackRepository } from "@main/repository";
|
||||
|
||||
@ -8,7 +10,13 @@ export const savePage = async (repacks: QueryDeepPartialEntity<Repack>[]) =>
|
||||
repacks.map((repack) => repackRepository.insert(repack).catch(() => {}))
|
||||
);
|
||||
|
||||
export const requestWebPage = async (url: string) =>
|
||||
fetch(url, {
|
||||
export const requestWebPage = async (url: string) => {
|
||||
const userAgent = new UserAgent();
|
||||
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"User-Agent": userAgent.toString(),
|
||||
},
|
||||
}).then((response) => response.text());
|
||||
};
|
||||
|
@ -6,10 +6,10 @@
|
||||
<title>Hydra</title>
|
||||
<meta
|
||||
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>
|
||||
<body style="background-color: #1c1c1">
|
||||
<body style="background-color: #1c1c1c">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
@ -26,6 +26,7 @@ globalStyle("body", {
|
||||
overflow: "hidden",
|
||||
userSelect: "none",
|
||||
fontFamily: "'Fira Mono', monospace",
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
background: vars.color.background,
|
||||
color: vars.color.bodyText,
|
||||
margin: "0",
|
||||
@ -36,13 +37,16 @@ globalStyle("button", {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
fontFamily: "inherit",
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
});
|
||||
|
||||
globalStyle("h1, h2, h3, h4, h5, h6, p", {
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
globalStyle("p", {
|
||||
lineHeight: "20px",
|
||||
});
|
||||
|
||||
globalStyle("#root, main", {
|
||||
display: "flex",
|
||||
});
|
||||
@ -103,5 +107,5 @@ export const titleBar = style({
|
||||
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||
WebkitAppRegion: "drag",
|
||||
zIndex: "2",
|
||||
borderBottom: `1px solid ${vars.color.borderColor}`,
|
||||
borderBottom: `1px solid ${vars.color.border}`,
|
||||
} as ComplexStyleRule);
|
||||
|
@ -18,11 +18,16 @@ import {
|
||||
clearSearch,
|
||||
setUserPreferences,
|
||||
setRepackersFriendlyNames,
|
||||
toggleDraggingDisabled,
|
||||
} from "@renderer/features";
|
||||
|
||||
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 { updateLibrary } = useLibrary();
|
||||
|
||||
@ -34,6 +39,9 @@ export function App({ children }: any) {
|
||||
const location = useLocation();
|
||||
|
||||
const search = useAppSelector((state) => state.search.value);
|
||||
const draggingDisabled = useAppSelector(
|
||||
(state) => state.window.draggingDisabled
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@ -93,6 +101,17 @@ export function App({ children }: any) {
|
||||
if (contentRef.current) contentRef.current.scrollTop = 0;
|
||||
}, [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 (
|
||||
<>
|
||||
{window.electron.platform === "win32" && (
|
||||
|
47
src/renderer/src/components/backdrop/backdrop.css.ts
Normal file
47
src/renderer/src/components/backdrop/backdrop.css.ts
Normal 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)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
12
src/renderer/src/components/backdrop/backdrop.tsx
Normal file
12
src/renderer/src/components/backdrop/backdrop.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -3,13 +3,12 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const bottomPanel = style({
|
||||
width: "100%",
|
||||
borderTop: `solid 1px ${vars.color.borderColor}`,
|
||||
borderTop: `solid 1px ${vars.color.border}`,
|
||||
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
transition: "all ease 0.2s",
|
||||
justifyContent: "space-between",
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
const base = style({
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
||||
backgroundColor: "#c0c1c7",
|
||||
backgroundColor: vars.color.muted,
|
||||
borderRadius: "8px",
|
||||
border: "solid 1px transparent",
|
||||
transition: "all ease 0.2s",
|
||||
@ -35,8 +35,8 @@ export const button = styleVariants({
|
||||
base,
|
||||
{
|
||||
backgroundColor: "transparent",
|
||||
border: "solid 1px #c0c1c7",
|
||||
color: "#c0c1c7",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
color: vars.color.muted,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
|
@ -19,7 +19,7 @@ export const checkbox = style({
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
transition: "all ease 0.2s",
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
":hover": {
|
||||
borderColor: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
|
@ -10,7 +10,7 @@ export const card = recipe({
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
transition: "all ease 0.2s",
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
cursor: "pointer",
|
||||
zIndex: "1",
|
||||
":active": {
|
||||
@ -103,7 +103,7 @@ export const specifics = style({
|
||||
export const specificsItem = style({
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
color: "#c0c1c7",
|
||||
color: vars.color.muted,
|
||||
fontSize: "12px",
|
||||
alignItems: "flex-end",
|
||||
});
|
||||
@ -112,7 +112,7 @@ export const titleContainer = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: "#c0c1c7",
|
||||
color: vars.color.muted,
|
||||
});
|
||||
|
||||
export const shopIcon = style({
|
||||
|
@ -29,8 +29,8 @@ export const header = recipe({
|
||||
WebkitAppRegion: "drag",
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||
color: "#c0c1c7",
|
||||
borderBottom: `solid 1px ${vars.color.borderColor}`,
|
||||
color: vars.color.muted,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
} as ComplexStyleRule,
|
||||
variants: {
|
||||
@ -55,7 +55,7 @@ export const search = recipe({
|
||||
width: "200px",
|
||||
alignItems: "center",
|
||||
borderRadius: "8px",
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
height: "40px",
|
||||
WebkitAppRegion: "no-drag",
|
||||
} as ComplexStyleRule,
|
||||
@ -83,7 +83,6 @@ export const searchInput = style({
|
||||
color: "#DADBE1",
|
||||
cursor: "default",
|
||||
fontFamily: "inherit",
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
textOverflow: "ellipsis",
|
||||
":focus": {
|
||||
cursor: "text",
|
||||
|
@ -11,7 +11,7 @@ export const hero = style({
|
||||
overflow: "hidden",
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
cursor: "pointer",
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
@ -33,7 +33,7 @@ export const heroMedia = style({
|
||||
export const backdrop = style({
|
||||
width: "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",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
@ -41,8 +41,7 @@ export const backdrop = style({
|
||||
|
||||
export const description = style({
|
||||
maxWidth: "700px",
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
color: "#c0c1c7",
|
||||
color: vars.color.muted,
|
||||
textAlign: "left",
|
||||
fontFamily: "'Fira Sans', sans-serif",
|
||||
lineHeight: "20px",
|
||||
|
@ -6,7 +6,7 @@ import { ShopDetails } from "@types";
|
||||
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const FEATURED_GAME_ID = "253230";
|
||||
const FEATURED_GAME_ID = "2420110";
|
||||
|
||||
export function Hero() {
|
||||
const [featuredGameDetails, setFeaturedGameDetails] =
|
||||
@ -36,7 +36,7 @@ export function Hero() {
|
||||
>
|
||||
<div className={styles.backdrop}>
|
||||
<AsyncImage
|
||||
src="https://cdn2.steamgriddb.com/hero/a6115ed32394915aac1e5502382eaaea.jpg"
|
||||
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
|
||||
alt={featuredGameDetails?.name}
|
||||
className={styles.heroMedia}
|
||||
/>
|
||||
|
@ -2,22 +2,6 @@ import { keyframes, style } from "@vanilla-extract/css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
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({
|
||||
"0%": { opacity: 0 },
|
||||
"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({
|
||||
base: {
|
||||
animationName: modalSlideIn,
|
||||
@ -69,7 +25,7 @@ export const modal = recipe({
|
||||
maxWidth: "600px",
|
||||
color: vars.color.bodyText,
|
||||
maxHeight: "100%",
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -94,13 +50,18 @@ export const modalHeader = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
borderBottom: `solid 1px ${vars.color.borderColor}`,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const closeModalButton = style({
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.2s",
|
||||
alignSelf: "flex-start",
|
||||
":hover": {
|
||||
opacity: "0.75",
|
||||
},
|
||||
});
|
||||
|
||||
export const closeModalButtonIcon = style({
|
||||
|
@ -3,13 +3,14 @@ import { createPortal } from "react-dom";
|
||||
import { XIcon } from "@primer/octicons-react";
|
||||
|
||||
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 {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@ -22,9 +23,10 @@ export function Modal({
|
||||
children,
|
||||
}: ModalProps) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
const modalContentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { t } = useTranslation("modal");
|
||||
|
||||
const handleCloseClick = useCallback(() => {
|
||||
setIsClosing(true);
|
||||
const zero = performance.now();
|
||||
@ -81,14 +83,10 @@ export function Modal({
|
||||
return () => {};
|
||||
}, [handleCloseClick, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(toggleDragging(visible));
|
||||
}, [dispatch, visible]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className={styles.backdrop({ closing: isClosing })}>
|
||||
<Backdrop isClosing={isClosing}>
|
||||
<div
|
||||
className={styles.modal({ closing: isClosing })}
|
||||
role="modal"
|
||||
@ -97,20 +95,21 @@ export function Modal({
|
||||
<div className={styles.modalHeader}>
|
||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
||||
<h3>{title}</h3>
|
||||
<p style={{ fontSize: 14 }}>{description}</p>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseClick}
|
||||
className={styles.closeModalButton}
|
||||
aria-label={t("close")}
|
||||
>
|
||||
<XIcon className={styles.closeModalButtonIcon} size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>{children}</div>
|
||||
</div>
|
||||
</div>,
|
||||
</Backdrop>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
@ -5,11 +5,11 @@ import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
export const sidebar = recipe({
|
||||
base: {
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
color: "#c0c1c7",
|
||||
color: vars.color.muted,
|
||||
flexDirection: "column",
|
||||
display: "flex",
|
||||
transition: "opacity ease 0.2s",
|
||||
borderRight: `solid 1px ${vars.color.borderColor}`,
|
||||
borderRight: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
},
|
||||
variants: {
|
||||
@ -65,7 +65,7 @@ export const menuItem = recipe({
|
||||
textWrap: "nowrap",
|
||||
display: "flex",
|
||||
opacity: "0.9",
|
||||
color: "#DADBE1",
|
||||
color: vars.color.muted,
|
||||
":hover": {
|
||||
opacity: "1",
|
||||
},
|
||||
@ -130,7 +130,7 @@ export const section = recipe({
|
||||
variants: {
|
||||
hasBorder: {
|
||||
true: {
|
||||
borderBottom: `solid 1px ${vars.color.borderColor}`,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -157,10 +157,10 @@ export const footerSocialsItem = style({
|
||||
height: "16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
transition: "all ease 0.15s",
|
||||
":hover": {
|
||||
opacity: 0.75,
|
||||
transition: "all ease 0.2s",
|
||||
cursor: "pointer",
|
||||
":hover": {
|
||||
opacity: "0.75",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -17,21 +17,6 @@ import XLogo from "@renderer/assets/x-icon.svg?react";
|
||||
import * as styles from "./sidebar.css";
|
||||
import { GameStatus } from "@globals";
|
||||
|
||||
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_INITIAL_WIDTH = 250;
|
||||
const SIDEBAR_MAX_WIDTH = 450;
|
||||
@ -50,6 +35,24 @@ export function Sidebar() {
|
||||
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 { game: gameDownloading, progress } = useDownload();
|
||||
@ -241,6 +244,8 @@ export function Sidebar() {
|
||||
key={item.url}
|
||||
className={styles.footerSocialsItem}
|
||||
onClick={() => window.electron.openExternal(item.url)}
|
||||
title={item.label}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</button>
|
||||
|
@ -9,7 +9,7 @@ export const textField = recipe({
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
borderRadius: "8px",
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
height: "40px",
|
||||
minHeight: "40px",
|
||||
},
|
||||
@ -44,7 +44,6 @@ export const textFieldInput = style({
|
||||
color: "#DADBE1",
|
||||
cursor: "default",
|
||||
fontFamily: "inherit",
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
textOverflow: "ellipsis",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
":focus": {
|
||||
|
@ -9,11 +9,16 @@ export interface TextFieldProps
|
||||
> {
|
||||
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
|
||||
label?: string;
|
||||
textFieldProps?: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
}
|
||||
|
||||
export function TextField({
|
||||
theme = "primary",
|
||||
label,
|
||||
textFieldProps,
|
||||
...props
|
||||
}: TextFieldProps) {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
@ -27,7 +32,10 @@ export function TextField({
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className={styles.textField({ focused: isFocused, theme })}>
|
||||
<div
|
||||
className={styles.textField({ focused: isFocused, theme })}
|
||||
{...textFieldProps}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
|
@ -15,7 +15,7 @@ export const windowSlice = createSlice({
|
||||
name: "window",
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleDragging: (state, action: PayloadAction<boolean>) => {
|
||||
toggleDraggingDisabled: (state, action: PayloadAction<boolean>) => {
|
||||
state.draggingDisabled = action.payload;
|
||||
},
|
||||
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;
|
||||
|
@ -21,5 +21,8 @@ export const getSteamLanguage = (language: string) => {
|
||||
if (language.startsWith("pt")) return "brazilian";
|
||||
if (language.startsWith("es")) return "spanish";
|
||||
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";
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import type { CatalogueEntry } from "@types";
|
||||
|
||||
import { clearSearch } from "@renderer/features";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { vars } from "../../theme.css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import * as styles from "../home/home.css";
|
||||
@ -67,12 +67,12 @@ export function Catalogue() {
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<section
|
||||
style={{
|
||||
padding: `16px 32px`,
|
||||
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 4}px`,
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderBottom: `1px solid ${vars.color.borderColor}`,
|
||||
borderBottom: `1px solid ${vars.color.border}`,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
|
@ -31,7 +31,7 @@ export const downloadCover = style({
|
||||
height: "auto",
|
||||
objectFit: "cover",
|
||||
objectPosition: "center",
|
||||
borderRight: `solid 1px ${vars.color.borderColor}`,
|
||||
borderRight: `solid 1px ${vars.color.border}`,
|
||||
});
|
||||
|
||||
export const download = recipe({
|
||||
@ -40,7 +40,7 @@ export const download = recipe({
|
||||
backgroundColor: vars.color.background,
|
||||
display: "flex",
|
||||
borderRadius: "8px",
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
overflow: "hidden",
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
transition: "all ease 0.2s",
|
||||
|
@ -15,25 +15,26 @@ export interface DescriptionHeaderProps {
|
||||
}
|
||||
|
||||
export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
||||
const [clipboardLock, setClipboardLock] = useState(false);
|
||||
const [clipboardLocked, setClipboardLocked] = useState(false);
|
||||
const { t, i18n } = useTranslation("game_details");
|
||||
|
||||
const { objectID, shop } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (!gameDetails) return setClipboardLock(true);
|
||||
setClipboardLock(false);
|
||||
if (!gameDetails) return setClipboardLocked(true);
|
||||
setClipboardLocked(false);
|
||||
}, [gameDetails]);
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
setClipboardLock(true);
|
||||
if (gameDetails) {
|
||||
setClipboardLocked(true);
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
p: btoa(
|
||||
JSON.stringify([
|
||||
objectID,
|
||||
shop,
|
||||
encodeURIComponent(gameDetails?.name),
|
||||
encodeURIComponent(gameDetails.name),
|
||||
i18n.language,
|
||||
])
|
||||
),
|
||||
@ -49,9 +50,10 @@ export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
||||
if (time - zero <= 3000) {
|
||||
requestAnimationFrame(holdLock);
|
||||
} else {
|
||||
setClipboardLock(false);
|
||||
setClipboardLocked(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -68,9 +70,9 @@ export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={handleCopyToClipboard}
|
||||
disabled={clipboardLock || !gameDetails}
|
||||
disabled={clipboardLocked || !gameDetails}
|
||||
>
|
||||
{clipboardLock ? (
|
||||
{clipboardLocked ? (
|
||||
t("copied_link_to_clipboard")
|
||||
) : (
|
||||
<>
|
||||
|
@ -80,7 +80,7 @@ export const descriptionContent = style({
|
||||
});
|
||||
|
||||
export const contentSidebar = style({
|
||||
borderLeft: `solid 1px ${vars.color.borderColor};`,
|
||||
borderLeft: `solid 1px ${vars.color.border};`,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"@media": {
|
||||
@ -105,7 +105,6 @@ export const contentSidebarTitle = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
borderBottom: `solid 1px ${vars.color.borderColor}`,
|
||||
});
|
||||
|
||||
export const requirementButtonContainer = style({
|
||||
@ -114,7 +113,7 @@ export const requirementButtonContainer = style({
|
||||
});
|
||||
|
||||
export const requirementButton = style({
|
||||
border: `solid 1px ${vars.color.borderColor};`,
|
||||
border: `solid 1px ${vars.color.border};`,
|
||||
borderLeft: "none",
|
||||
borderRight: "none",
|
||||
borderRadius: "0",
|
||||
@ -171,11 +170,11 @@ export const descriptionSkeleton = style({
|
||||
export const descriptionHeader = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
borderBottom: `solid 1px ${vars.color.borderColor}`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
borderBottom: `solid 1px ${vars.color.border}`,
|
||||
height: "72px",
|
||||
});
|
||||
|
||||
@ -183,7 +182,6 @@ export const descriptionHeaderInfo = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
});
|
||||
|
||||
export const howLongToBeatCategoriesList = style({
|
||||
@ -201,16 +199,15 @@ export const howLongToBeatCategory = style({
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: "8px",
|
||||
padding: `8px 16px`,
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
});
|
||||
|
||||
export const howLongToBeatCategoryLabel = style({
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
color: "#DADBE1",
|
||||
color: vars.color.muted,
|
||||
});
|
||||
|
||||
export const howLongToBeatCategorySkeleton = style({
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
borderRadius: "8px",
|
||||
height: "76px",
|
||||
});
|
||||
@ -224,7 +221,7 @@ export const randomizerButton = style({
|
||||
/* Scroll bar + spacing */
|
||||
right: `${9 + SPACING_UNIT * 2}px`,
|
||||
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,
|
||||
":hover": {
|
||||
backgroundColor: vars.color.background,
|
||||
|
@ -5,6 +5,7 @@ import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
import type {
|
||||
Game,
|
||||
GameRepack,
|
||||
GameShop,
|
||||
HowLongToBeatCategory,
|
||||
ShopDetails,
|
||||
@ -18,23 +19,30 @@ import { useAppDispatch, useDownload } from "@renderer/hooks";
|
||||
|
||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||
|
||||
import { vars } from "../../theme.css";
|
||||
import Lottie from "lottie-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import { DescriptionHeader } from "./description-header";
|
||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||
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 { 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() {
|
||||
const { objectID, shop } = useParams();
|
||||
|
||||
const [isLoading, setIsLoading] = 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 [howLongToBeat, setHowLongToBeat] = useState<{
|
||||
isLoading: boolean;
|
||||
@ -43,6 +51,10 @@ export function GameDetails() {
|
||||
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
const [isGamePlaying, setIsGamePlaying] = useState(false);
|
||||
const [showInstructionsModal, setShowInstructionsModal] = useState<
|
||||
null | "onlinefix" | "DODI"
|
||||
>(null);
|
||||
|
||||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
@ -52,7 +64,6 @@ export function GameDetails() {
|
||||
const { t, i18n } = useTranslation("game_details");
|
||||
|
||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -61,7 +72,8 @@ export function GameDetails() {
|
||||
const handleImageSettled = useCallback((url: string) => {
|
||||
average(url, { amount: 1, format: "hex" })
|
||||
.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(() => {});
|
||||
}, []);
|
||||
@ -112,7 +124,10 @@ export function GameDetails() {
|
||||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -134,12 +149,12 @@ export function GameDetails() {
|
||||
}, [game?.id, isGamePlaying, getGame]);
|
||||
|
||||
const handleStartDownload = async (
|
||||
repackId: number,
|
||||
repack: GameRepack,
|
||||
downloadPath: string
|
||||
) => {
|
||||
if (gameDetails) {
|
||||
return startDownload(
|
||||
repackId,
|
||||
repack.id,
|
||||
gameDetails.objectID,
|
||||
gameDetails.name,
|
||||
shop as GameShop,
|
||||
@ -147,7 +162,18 @@ export function GameDetails() {
|
||||
).then(() => {
|
||||
getGame();
|
||||
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}
|
||||
gameDetails={gameDetails}
|
||||
startDownload={handleStartDownload}
|
||||
showSelectFolderModal={showSelectFolderModal}
|
||||
setShowSelectFolderModal={setShowSelectFolderModal}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<OnlineFixInstallationGuide
|
||||
visible={showInstructionsModal === "onlinefix"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
<DODIInstallationGuide
|
||||
windowColor={color.light}
|
||||
visible={showInstructionsModal === "DODI"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<GameDetailsSkeleton />
|
||||
) : (
|
||||
@ -201,7 +236,7 @@ export function GameDetails() {
|
||||
|
||||
<HeroPanel
|
||||
game={game}
|
||||
color={color}
|
||||
color={color.dark}
|
||||
gameDetails={gameDetails}
|
||||
openRepacksModal={() => setShowRepacksModal(true)}
|
||||
getGame={getGame}
|
||||
|
@ -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}`,
|
||||
});
|
@ -7,6 +7,8 @@ import type { Game, ShopDetails } from "@types";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./hero-panel-actions.css";
|
||||
|
||||
export interface HeroPanelActionsProps {
|
||||
game: Game | null;
|
||||
gameDetails: ShopDetails | null;
|
||||
@ -56,6 +58,8 @@ export function HeroPanelActions({
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
return filePaths[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
@ -114,6 +118,7 @@ export function HeroPanelActions({
|
||||
theme="outline"
|
||||
disabled={!gameDetails || toggleLibraryGameDisabled}
|
||||
onClick={toggleGameOnLibrary}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{game ? <NoEntryIcon /> : <PlusCircleIcon />}
|
||||
{game ? t("remove_from_library") : t("add_to_library")}
|
||||
@ -123,10 +128,18 @@ export function HeroPanelActions({
|
||||
if (isGameDownloading) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => pauseDownload(game.id)} theme="outline">
|
||||
<Button
|
||||
onClick={() => pauseDownload(game.id)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
@ -136,12 +149,17 @@ export function HeroPanelActions({
|
||||
if (game?.status === GameStatus.Paused) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => resumeDownload(game.id)} theme="outline">
|
||||
<Button
|
||||
onClick={() => resumeDownload(game.id)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id).then(getGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
@ -157,6 +175,7 @@ export function HeroPanelActions({
|
||||
onClick={openGameInstaller}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("install")}
|
||||
</Button>
|
||||
@ -165,7 +184,12 @@ export function HeroPanelActions({
|
||||
)}
|
||||
|
||||
{isGamePlaying ? (
|
||||
<Button onClick={closeGame} theme="outline" disabled={deleting}>
|
||||
<Button
|
||||
onClick={closeGame}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("close")}
|
||||
</Button>
|
||||
) : (
|
||||
@ -173,6 +197,7 @@ export function HeroPanelActions({
|
||||
onClick={openGame}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("play")}
|
||||
</Button>
|
||||
@ -184,13 +209,19 @@ export function HeroPanelActions({
|
||||
if (game?.status === GameStatus.Cancelled) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={openRepacksModal} theme="outline" disabled={deleting}>
|
||||
<Button
|
||||
onClick={openRepacksModal}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => removeGameFromLibrary(game.id).then(getGame)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("remove_from_list")}
|
||||
</Button>
|
||||
@ -202,7 +233,11 @@ export function HeroPanelActions({
|
||||
return (
|
||||
<>
|
||||
{toggleGameOnLibraryButton}
|
||||
<Button onClick={openRepacksModal} theme="outline">
|
||||
<Button
|
||||
onClick={openRepacksModal}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
</>
|
@ -1,5 +1,5 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const panel = style({
|
||||
width: "100%",
|
||||
@ -9,7 +9,8 @@ export const panel = style({
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
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",
|
||||
});
|
||||
|
||||
@ -17,7 +18,6 @@ export const content = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
fontSize: vars.size.bodyFontSize,
|
||||
});
|
||||
|
||||
export const actions = style({
|
@ -6,13 +6,14 @@ import { useDownload } from "@renderer/hooks";
|
||||
import type { Game, ShopDetails } from "@types";
|
||||
|
||||
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 { formatBytes } from "@renderer/utils";
|
||||
import { HeroPanelActions } from "./hero-panel-actions";
|
||||
import { GameStatus } from "@globals";
|
||||
|
||||
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./hero-panel.css";
|
||||
|
||||
export interface HeroPanelProps {
|
||||
game: Game | null;
|
||||
gameDetails: ShopDetails | null;
|
||||
@ -68,6 +69,8 @@ export function HeroPanel({
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [game?.lastTimePlayed, updateLastTimePlayed]);
|
||||
|
||||
const finalDownloadSize = useMemo(() => {
|
||||
@ -83,7 +86,7 @@ export function HeroPanel({
|
||||
const getInfo = () => {
|
||||
if (!gameDetails) return null;
|
||||
|
||||
if (isGameDeleting(game?.id)) {
|
||||
if (isGameDeleting(game?.id ?? -1)) {
|
||||
return <p>{t("deleting")}</p>;
|
||||
}
|
||||
|
1
src/renderer/src/pages/game-details/hero/index.ts
Normal file
1
src/renderer/src/pages/game-details/hero/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./hero-panel";
|
@ -1,6 +1,6 @@
|
||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||
import type { HowLongToBeatCategory } from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { HowLongToBeatCategory } from "@types";
|
||||
import { vars } from "../../theme.css";
|
||||
import * as styles from "./game-details.css";
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
export const DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY =
|
||||
"dontShowOnlineFixInstructions";
|
||||
export const DONT_SHOW_DODI_INSTRUCTIONS_KEY = "dontShowDodiInstructions";
|
@ -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",
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export * from "./online-fix-installation-guide";
|
||||
export * from "./dodi-installation-guide";
|
||||
export * from "./constants";
|
@ -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`,
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
@ -14,22 +14,19 @@ import { SelectFolderModal } from "./select-folder-modal";
|
||||
export interface RepacksModalProps {
|
||||
visible: boolean;
|
||||
gameDetails: ShopDetails;
|
||||
showSelectFolderModal: boolean;
|
||||
setShowSelectFolderModal: (value: boolean) => void;
|
||||
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
|
||||
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RepacksModal({
|
||||
visible,
|
||||
gameDetails,
|
||||
showSelectFolderModal,
|
||||
setShowSelectFolderModal,
|
||||
startDownload,
|
||||
onClose,
|
||||
}: RepacksModalProps) {
|
||||
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
|
||||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||
|
||||
const repackersFriendlyNames = useAppSelector(
|
||||
(state) => state.repackersFriendlyNames.value
|
||||
@ -57,12 +54,7 @@ export function RepacksModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={`${gameDetails.name} Repacks`}
|
||||
description={t("repacks_modal_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<>
|
||||
<SelectFolderModal
|
||||
visible={showSelectFolderModal}
|
||||
onClose={() => setShowSelectFolderModal(false)}
|
||||
@ -70,6 +62,13 @@ export function RepacksModal({
|
||||
startDownload={startDownload}
|
||||
repack={repack}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={`${gameDetails.name} Repacks`}
|
||||
description={t("repacks_modal_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}>
|
||||
<TextField placeholder={t("filter")} onChange={handleFilter} />
|
||||
</div>
|
||||
@ -91,5 +90,6 @@ export function RepacksModal({
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -10,10 +10,18 @@ export const container = style({
|
||||
|
||||
export const downloadsPathField = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const hintText = style({
|
||||
fontSize: "12px",
|
||||
color: vars.color.bodyText,
|
||||
});
|
||||
|
||||
export const settingsLink = style({
|
||||
textDecoration: "none",
|
||||
color: "#C0C1C7",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
});
|
||||
|
@ -7,12 +7,13 @@ import { formatBytes } from "@renderer/utils";
|
||||
import { DiskSpace } from "check-disk-space";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as styles from "./select-folder-modal.css";
|
||||
import { DownloadIcon } from "@primer/octicons-react";
|
||||
|
||||
export interface SelectFolderModalProps {
|
||||
visible: boolean;
|
||||
gameDetails: ShopDetails;
|
||||
onClose: () => void;
|
||||
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
|
||||
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
|
||||
repack: GameRepack | null;
|
||||
}
|
||||
|
||||
@ -63,8 +64,10 @@ export function SelectFolderModal({
|
||||
const handleStartClick = () => {
|
||||
if (repack) {
|
||||
setDownloadStarting(true);
|
||||
startDownload(repack.id, selectedPath).finally(() => {
|
||||
|
||||
startDownload(repack, selectedPath).finally(() => {
|
||||
setDownloadStarting(false);
|
||||
onClose();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -98,17 +101,12 @@ export function SelectFolderModal({
|
||||
</div>
|
||||
<p className={styles.hintText}>
|
||||
{t("select_folder_hint")}{" "}
|
||||
<Link
|
||||
to="/settings"
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
color: "#C0C1C7",
|
||||
}}
|
||||
>
|
||||
<Link to="/settings" className={styles.settingsLink}>
|
||||
{t("settings")}
|
||||
</Link>
|
||||
</p>
|
||||
<Button onClick={handleStartClick} disabled={downloadStarting}>
|
||||
<DownloadIcon />
|
||||
{t("download_now")}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@ export const content = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
boxShadow: "0px 0px 15px 0px #000000",
|
||||
borderRadius: "8px",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
@ -22,5 +22,5 @@ export const content = style({
|
||||
|
||||
export const downloadsPathField = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
@ -6,8 +6,9 @@ export const [themeClass, vars] = createTheme({
|
||||
color: {
|
||||
background: "#1c1c1c",
|
||||
darkBackground: "#151515",
|
||||
muted: "#c0c1c7",
|
||||
bodyText: "#8e919b",
|
||||
borderColor: "rgba(255, 255, 255, 0.1)",
|
||||
border: "#424244",
|
||||
},
|
||||
opacity: {
|
||||
disabled: "0.5",
|
||||
|
22
yarn.lock
22
yarn.lock
@ -4311,6 +4311,21 @@ lodash-es@^4.17.21:
|
||||
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash.defaults@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz"
|
||||
integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
|
||||
|
||||
lodash.difference@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz"
|
||||
integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==
|
||||
|
||||
lodash.flatten@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz"
|
||||
integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz"
|
||||
@ -6041,6 +6056,13 @@ use-sync-external-store@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz"
|
||||
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
|
||||
|
||||
user-agents@^1.1.193:
|
||||
version "1.1.193"
|
||||
resolved "https://registry.yarnpkg.com/user-agents/-/user-agents-1.1.193.tgz#44858e607e8a6550603cd23a173b7bd1658dd638"
|
||||
integrity sha512-NKJzgR2UoVu09WmHkwlvM6+WuqJzpGCi/CukqC/ohOKCWJV5NuqZJecnox7eSWPQ2FhihuM/qTt/EQKHyrCuJw==
|
||||
dependencies:
|
||||
lodash.clonedeep "^4.5.0"
|
||||
|
||||
utf8-byte-length@^1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user