mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
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:
commit
64449910c5
38
README.md
38
README.md
@ -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
BIN
build/installerSidebar.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 151 KiB |
@ -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:
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 { 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"),
|
||||||
|
@ -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");
|
||||||
|
@ -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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@ -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());
|
||||||
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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" && (
|
||||||
|
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({
|
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",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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)",
|
||||||
},
|
},
|
||||||
|
@ -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)",
|
||||||
},
|
},
|
||||||
|
@ -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({
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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": {
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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")
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
|
@ -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}`,
|
||||||
|
});
|
@ -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>
|
||||||
</>
|
</>
|
@ -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({
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
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 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";
|
||||||
|
|
||||||
|
@ -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 {
|
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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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`,
|
||||||
});
|
});
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user