fix: fixing add game to library

This commit is contained in:
Hydra 2024-05-12 10:56:31 +01:00
commit 3bd8662b18
No known key found for this signature in database
34 changed files with 555 additions and 221 deletions

View File

@ -239,6 +239,13 @@ yarn build:linux
<sub><b>Null</b></sub> <sub><b>Null</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/Mkdantas">
<img src="https://avatars.githubusercontent.com/u/50972667?v=4" width="100;" alt="Mkdantas"/>
<br />
<sub><b>Matheus Dantas</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/Hachi-R"> <a href="https://github.com/Hachi-R">
<img src="https://avatars.githubusercontent.com/u/58823742?v=4" width="100;" alt="Hachi-R"/> <img src="https://avatars.githubusercontent.com/u/58823742?v=4" width="100;" alt="Hachi-R"/>
@ -259,15 +266,15 @@ yarn build:linux
<br /> <br />
<sub><b>FeriVOQ</b></sub> <sub><b>FeriVOQ</b></sub>
</a> </a>
</td> </td></tr>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/xbozo"> <a href="https://github.com/xbozo">
<img src="https://avatars.githubusercontent.com/u/119091492?v=4" width="100;" alt="xbozo"/> <img src="https://avatars.githubusercontent.com/u/119091492?v=4" width="100;" alt="xbozo"/>
<br /> <br />
<sub><b>Guilherme Viana</b></sub> <sub><b>Guilherme Viana</b></sub>
</a> </a>
</td></tr> </td>
<tr>
<td align="center"> <td align="center">
<a href="https://github.com/eltociear"> <a href="https://github.com/eltociear">
<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/> <img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/>
@ -283,10 +290,10 @@ yarn build:linux
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/FerNikoMF"> <a href="https://github.com/vnumex">
<img src="https://avatars.githubusercontent.com/u/76095334?v=4" width="100;" alt="FerNikoMF"/> <img src="https://avatars.githubusercontent.com/u/10434535?v=4" width="100;" alt="vnumex"/>
<br /> <br />
<sub><b>Firdavs</b></sub> <sub><b>Vnumex</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
@ -296,6 +303,21 @@ yarn build:linux
<sub><b>Ruslan</b></sub> <sub><b>Ruslan</b></sub>
</a> </a>
</td> </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>
<tr>
<td align="center">
<a href="https://github.com/PCTroller">
<img src="https://avatars.githubusercontent.com/u/146987801?v=4" width="100;" alt="PCTroller"/>
<br />
<sub><b>Null</b></sub>
</a>
</td>
<td align="center"> <td align="center">
<a href="https://github.com/Chr1s0Blood"> <a href="https://github.com/Chr1s0Blood">
<img src="https://avatars.githubusercontent.com/u/166660500?v=4" width="100;" alt="Chr1s0Blood"/> <img src="https://avatars.githubusercontent.com/u/166660500?v=4" width="100;" alt="Chr1s0Blood"/>

View File

@ -23,7 +23,7 @@
"github": "Contribute on GitHub" "github": "Contribute on GitHub"
}, },
"header": { "header": {
"search": "Search", "search": "Search games",
"home": "Home", "home": "Home",
"catalogue": "Catalogue", "catalogue": "Catalogue",
"downloads": "Downloads", "downloads": "Downloads",
@ -86,8 +86,7 @@
"change": "Change", "change": "Change",
"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, go to the <0>Settings</0>",
"settings": "Settings",
"download_now": "Download now", "download_now": "Download now",
"installation_instructions": "Installation Instructions", "installation_instructions": "Installation Instructions",
"installation_instructions_description": "Additional steps are required to install this game", "installation_instructions_description": "Additional steps are required to install this game",
@ -144,7 +143,9 @@
"launch_with_system": "Launch Hydra on system start-up", "launch_with_system": "Launch Hydra on system start-up",
"general": "General", "general": "General",
"behavior": "Behavior", "behavior": "Behavior",
"real_debrid": "Real Debrid" "enable_real_debrid": "Enable Real Debrid",
"real_debrid": "Real Debrid",
"real_debrid_api_token_hint": "You can get your API key <0>here</0>"
}, },
"notifications": { "notifications": {
"download_complete": "Download complete", "download_complete": "Download complete",

View File

@ -64,12 +64,14 @@
"copied_link_to_clipboard": "Enlace copiado", "copied_link_to_clipboard": "Enlace copiado",
"hours": "horas", "hours": "horas",
"minutes": "minutos", "minutes": "minutos",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"accuracy": "{{accuracy}}% precisión", "accuracy": "{{accuracy}}% precisión",
"add_to_library": "Agregar a la biblioteca", "add_to_library": "Agregar a la biblioteca",
"remove_from_library": "Eliminar de la biblioteca", "remove_from_library": "Eliminar de la biblioteca",
"no_downloads": "No hay descargas disponibles", "no_downloads": "No hay descargas disponibles",
"next_suggestion": "Siguiente sugerencia", "next_suggestion": "Siguiente sugerencia",
"play_time": "Jugado {{amount}}", "play_time": "Jugado por {{amount}}",
"install": "Instalar", "install": "Instalar",
"play": "Jugar", "play": "Jugar",
"not_played_yet": "Aún no has jugado a {{title}}", "not_played_yet": "Aún no has jugado a {{title}}",

View File

@ -6,3 +6,4 @@ 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 pl } from "./pl/translation.json"; export { default as pl } from "./pl/translation.json";
export { default as ru } from "./ru/translation.json"; export { default as ru } from "./ru/translation.json";
export { default as tr } from "./tr/translation.json";

View File

@ -23,7 +23,7 @@
"github": "Contribua no GitHub" "github": "Contribua no GitHub"
}, },
"header": { "header": {
"search": "Buscar", "search": "Buscar jogos",
"catalogue": "Catálogo", "catalogue": "Catálogo",
"downloads": "Downloads", "downloads": "Downloads",
"search_results": "Resultados da busca", "search_results": "Resultados da busca",
@ -82,8 +82,7 @@
"change": "Mudar", "change": "Mudar",
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar", "repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
"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 a <0>Tela de Configurações</0>",
"settings": "Configurações do Hydra",
"download_now": "Baixe agora", "download_now": "Baixe agora",
"installation_instructions": "Instruções de Instalação", "installation_instructions": "Instruções de Instalação",
"installation_instructions_description": "Passos adicionais são necessários para instalar esse jogo", "installation_instructions_description": "Passos adicionais são necessários para instalar esse jogo",

View File

@ -0,0 +1,164 @@
{
"home": {
"featured": "Öne çıkan",
"recently_added": "Son eklenen",
"trending": "Popüler",
"surprise_me": "Şaşırt beni",
"no_results": "Sonuç bulunamadı"
},
"sidebar": {
"catalogue": "Katalog",
"downloads": "İndirmeler",
"settings": "Ayarlar",
"my_library": "Kütüphane",
"downloading_metadata": "{{title}} (Metadata indiriliyor…)",
"checking_files": "{{title}} ({{percentage}} - Dosyalar kontrol ediliyor…)",
"paused": "{{title}} (Duraklatıldı)",
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
"filter": "Kütüphaneyi filtrele",
"follow_us": "Bizi takip et",
"home": "Ana menü",
"discord": "Discord'umuza katıl",
"x": "X'te bizi takip et",
"github": "GitHub'da bize katkı yap"
},
"header": {
"search": "Ara",
"home": "Ana menü",
"catalogue": "Katalog",
"downloads": "İndirmeler",
"search_results": "Arama sonuçları",
"settings": "Ayarlar"
},
"bottom_panel": {
"no_downloads_in_progress": "İndirilen bir şey yok",
"downloading_metadata": "{{title}} metadatası indiriliyor…",
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)",
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Sonraki sayfa",
"previous_page": "Önceki sayfa"
},
"game_details": {
"open_download_options": "İndirme seçeneklerini aç",
"download_options_zero": "İndirme seçeneği yok",
"download_options_one": "{{count}} indirme seçeneği",
"download_options_other": "{{count}} indirme seçeneği",
"updated_at": "{{updated_at}} güncellendi",
"install": "İndir",
"resume": "Devam et",
"pause": "Duraklat",
"cancel": "İptal et",
"remove": "Sil",
"remove_from_list": "Sil",
"space_left_on_disk": "Diskte {{space}} yer kaldı",
"eta": "Bitiş {{eta}}",
"downloading_metadata": "Metadata indiriliyor…",
"checking_files": "Dosyalar kontrol ediliyor…",
"filter": "Repackleri filtrele",
"requirements": "Sistem gereksinimleri",
"minimum": "Minimum",
"recommended": "Önerilen",
"no_minimum_requirements": "{{title}} minimum sistem gereksinim bilgilerini karşılamıyor",
"no_recommended_requirements": "{{title}} önerilen sistem gereksinim bilgilerini karşılamıyor",
"paused_progress": "{{progress}} (Duraklatıldı)",
"release_date": "{{date}} tarihinde çıktı",
"publisher": "{{publisher}} tarihinde yayınlandı",
"copy_link_to_clipboard": "Link'i kopyala",
"copied_link_to_clipboard": "Link kopyalandı",
"hours": "saatler",
"minutes": "dakikalar",
"amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika",
"accuracy": "%{{accuracy}} doğruluk",
"add_to_library": "Kütüphaneye ekle",
"remove_from_library": "Kütüphaneden kaldır",
"no_downloads": "İndirme yok",
"play_time": "{{amount}} oynandı",
"last_time_played": "Son oynanan {{period}}",
"not_played_yet": "Bu {{title}} hiç oynanmadı",
"next_suggestion": "Sıradaki öneri",
"play": "Oyna",
"deleting": "Installer siliniyor…",
"close": "Kapat",
"playing_now": "Şimdi oynanıyor",
"change": "Değiştir",
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
"downloads_path": "İndirme yolu",
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
"settings": "Ayarlar",
"download_now": "Şimdi",
"installation_instructions": "Kurulum",
"installation_instructions_description": "Bu oyunu kurmak için ek adımlar gerekiyor",
"online_fix_instruction": "OnlineFix oyunlarını ayıklamak için parola gerekiyor. Gerekli olduğunda bu parolayı kullanın:",
"dodi_installation_instruction": "Dodi installerını açtığınızda, kurulumu başlatmak için bu tuşa basın <0 />:",
"dont_show_it_again": "Tekrar gösterme",
"copy_to_clipboard": "Kopyala",
"copied_to_clipboard": "Kopyalandı",
"got_it": "Tamam"
},
"activation": {
"title": "Hydra'yı aktif et",
"installation_id": "Kurulum ID'si:",
"enter_activation_code": "Aktifleştirme kodunuzu girin",
"message": "Bunu nerede soracağınızı bilmiyorsanız, buna sahip olmamanız gerekiyor.",
"activate": "Aktif et",
"loading": "Yükleniyor…"
},
"downloads": {
"resume": "Devam et",
"pause": "Duraklat",
"eta": "Bitiş {{eta}}",
"paused": "Duraklatıldı",
"verifying": "Doğrulanıyor…",
"completed_at": "{{date}} tarihinde tamamlanacak",
"completed": "Tamamlandı",
"cancelled": "İptal edildi",
"download_again": "Tekrar indir",
"cancel": "İptal et",
"filter": "Yüklü oyunları filtrele",
"remove": "Kaldır",
"downloading_metadata": "Metadata indiriliyor…",
"checking_files": "Dosyalar kontrol ediliyor…",
"starting_download": "İndirme başlatılıyor…",
"deleting": "Installer siliniyor…",
"delete": "Installer'ı sil",
"remove_from_list": "Kaldır",
"delete_modal_title": "Emin misiniz?",
"delete_modal_description": "Bu bilgisayarınızdan tüm kurulum dosyalarını silecek",
"install": "Kur"
},
"settings": {
"downloads_path": "İndirme yolu",
"change": "Güncelle",
"notifications": "Bildirimler",
"enable_download_notifications": "Bir indirme bittiğinde",
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
"telemetry": "Telemetri",
"telemetry_description": "Anonim kullanım istatistiklerini aktifleştir"
},
"notifications": {
"download_complete": "İndirme tamamlandı",
"game_ready_to_install": "{{title}} kuruluma hazır",
"repack_list_updated": "Repack listesi güncellendi",
"repack_count_one": "{{count}} yeni repack eklendi",
"repack_count_other": "{{count}} yeni repack eklendi"
},
"system_tray": {
"open": "Hydra'yı aç",
"quit": ık"
},
"game_card": {
"no_downloads": "İndirme mevcut değil"
},
"binary_not_found_modal": {
"title": "Programlar yüklü değil",
"description": "Sisteminizde Wine veya Lutris çalıştırılabiliri bulunamadı",
"instructions": "Oyunları düzgün şekilde çalıştırmak için Linux distronuza bunlardan birini nasıl yükleyebileceğinize bakın"
},
"modal": {
"close": "Kapat tuşu"
}
}

View File

@ -8,42 +8,35 @@ import { requestSteam250 } from "@main/services";
const repacks = stateManager.getValue("repacks"); const repacks = stateManager.getValue("repacks");
interface GetStringForLookup { const getStringForLookup = (index: number): string => {
(index: number): string;
}
const getCatalogue = async (
_event: Electron.IpcMainInvokeEvent,
category: CatalogueCategory
) => {
const getStringForLookup = (index: number): string => {
const repack = repacks[index]; const repack = repacks[index];
const formatter = const formatter =
repackerFormatter[repack.repacker as keyof typeof repackerFormatter]; repackerFormatter[repack.repacker as keyof typeof repackerFormatter];
return formatName(formatter(repack.title)); return formatName(formatter(repack.title));
}; };
const resultSize = 12;
const getCatalogue = async (
_event: Electron.IpcMainInvokeEvent,
category: CatalogueCategory
) => {
if (!repacks.length) return []; if (!repacks.length) return [];
const resultSize = 12;
if (category === "trending") { if (category === "trending") {
return getTrendingCatalogue(resultSize); return getTrendingCatalogue(resultSize);
} else {
return getRecentlyAddedCatalogue(
resultSize,
resultSize,
getStringForLookup
);
} }
return getRecentlyAddedCatalogue(resultSize);
}; };
const getTrendingCatalogue = async ( const getTrendingCatalogue = async (
resultSize: number resultSize: number
): Promise<CatalogueEntry[]> => { ): Promise<CatalogueEntry[]> => {
const results: CatalogueEntry[] = []; const results: CatalogueEntry[] = [];
const trendingGames = await requestSteam250("/30day"); const trendingGames = await requestSteam250("/90day");
for ( for (
let i = 0; let i = 0;
i < trendingGames.length && results.length < resultSize; i < trendingGames.length && results.length < resultSize;
@ -51,7 +44,7 @@ const getTrendingCatalogue = async (
) { ) {
if (!trendingGames[i]) continue; if (!trendingGames[i]) continue;
const { title, objectID } = trendingGames[i]; const { title, objectID } = trendingGames[i]!;
const repacks = searchRepacks(title); const repacks = searchRepacks(title);
if (title && repacks.length) { if (title && repacks.length) {
@ -69,11 +62,8 @@ const getTrendingCatalogue = async (
}; };
const getRecentlyAddedCatalogue = async ( const getRecentlyAddedCatalogue = async (
resultSize: number, resultSize: number
requestSize: number,
getStringForLookup: GetStringForLookup
): Promise<CatalogueEntry[]> => { ): Promise<CatalogueEntry[]> => {
let lookupRequest = [];
const results: CatalogueEntry[] = []; const results: CatalogueEntry[] = [];
for (let i = 0; results.length < resultSize; i++) { for (let i = 0; results.length < resultSize; i++) {
@ -84,15 +74,7 @@ const getRecentlyAddedCatalogue = async (
continue; continue;
} }
lookupRequest.push(searchGames({ query: stringForLookup })); const games = searchGames({ query: stringForLookup });
if (lookupRequest.length < requestSize) {
continue;
}
const games = (await Promise.all(lookupRequest)).map((value) =>
value.at(0)
);
for (const game of games) { for (const game of games) {
const isAlreadyIncluded = results.some( const isAlreadyIncluded = results.some(
@ -105,7 +87,6 @@ const getRecentlyAddedCatalogue = async (
results.push(game); results.push(game);
} }
lookupRequest = [];
} }
return results.slice(0, resultSize); return results.slice(0, resultSize);

View File

@ -16,7 +16,6 @@ const addGameToLibrary = async (
const game = await gameRepository.findOne({ const game = await gameRepository.findOne({
where: { where: {
objectID, objectID,
isDeleted: false,
}, },
}); });

View File

@ -11,7 +11,7 @@ import { registerEvent } from "../register-event";
const deleteGameFolder = async ( const deleteGameFolder = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
gameId: number gameId: number
) => { ): Promise<void> => {
const game = await gameRepository.findOne({ const game = await gameRepository.findOne({
where: { where: {
id: gameId, id: gameId,
@ -38,7 +38,8 @@ const deleteGameFolder = async (
logger.error(error); logger.error(error);
reject(); reject();
} }
resolve(null);
resolve();
} }
); );
}); });

View File

@ -1,3 +1,4 @@
import axios from "axios";
import { getSteamAppAsset } from "@main/helpers"; import { getSteamAppAsset } from "@main/helpers";
export interface SteamGridResponse { export interface SteamGridResponse {
@ -27,33 +28,35 @@ export const getSteamGridData = async (
): Promise<SteamGridResponse> => { ): Promise<SteamGridResponse> => {
const searchParams = new URLSearchParams(params); const searchParams = new URLSearchParams(params);
const response = await fetch( if (!import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY) {
throw new Error("STEAMGRIDDB_API_KEY is not set");
}
const response = await axios.get(
`https://www.steamgriddb.com/api/v2/${path}/${shop}/${objectID}?${searchParams.toString()}`, `https://www.steamgriddb.com/api/v2/${path}/${shop}/${objectID}?${searchParams.toString()}`,
{ {
method: "GET",
headers: { headers: {
Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`, Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`,
}, },
} }
); );
return response.json(); return response.data;
}; };
export const getSteamGridGameById = async ( export const getSteamGridGameById = async (
id: number id: number
): Promise<SteamGridGameResponse> => { ): Promise<SteamGridGameResponse> => {
const response = await fetch( const response = await axios.get(
`https://www.steamgriddb.com/api/public/game/${id}`, `https://www.steamgriddb.com/api/public/game/${id}`,
{ {
method: "GET",
headers: { headers: {
Referer: "https://www.steamgriddb.com/", Referer: "https://www.steamgriddb.com/",
}, },
} }
); );
return response.json(); return response.data;
}; };
export const getSteamGameIconUrl = async (objectID: string) => { export const getSteamGameIconUrl = async (objectID: string) => {

View File

@ -32,7 +32,7 @@ export function App({ children }: AppProps) {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary } = useLibrary(); const { updateLibrary } = useLibrary();
const { clearDownload, addPacket } = useDownload(); const { clearDownload, setLastPacket } = useDownload();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -64,14 +64,14 @@ export function App({ children }: AppProps) {
return; return;
} }
addPacket(downloadProgress); setLastPacket(downloadProgress);
} }
); );
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [clearDownload, addPacket, updateLibrary]); }, [clearDownload, setLastPacket, updateLibrary]);
const handleSearch = useCallback( const handleSearch = useCallback(
(query: string) => { (query: string) => {

View File

@ -64,7 +64,7 @@ export function BottomPanel() {
<small>{status}</small> <small>{status}</small>
</button> </button>
<small> <small tabIndex={0}>
v{version} &quot;{VERSION_CODENAME}&quot; v{version} &quot;{VERSION_CODENAME}&quot;
</small> </small>
</footer> </footer>

View File

@ -19,6 +19,7 @@ const base = style({
":disabled": { ":disabled": {
opacity: vars.opacity.disabled, opacity: vars.opacity.disabled,
pointerEvents: "none", pointerEvents: "none",
cursor: "not-allowed",
}, },
}); });

View File

@ -7,3 +7,4 @@ export * from "./modal/modal";
export * from "./sidebar/sidebar"; export * from "./sidebar/sidebar";
export * from "./text-field/text-field"; export * from "./text-field/text-field";
export * from "./checkbox-field/checkbox-field"; export * from "./checkbox-field/checkbox-field";
export * from "./link/link";

View File

@ -0,0 +1,9 @@
import { style } from "@vanilla-extract/css";
export const link = style({
textDecoration: "none",
color: "#C0C1C7",
":hover": {
textDecoration: "underline",
},
});

View File

@ -0,0 +1,33 @@
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
import cn from "classnames";
import * as styles from "./link.css";
export function Link({ children, to, className, ...props }: LinkProps) {
const openExternal = (event: React.MouseEvent) => {
event.preventDefault();
window.electron.openExternal(to as string);
};
if (typeof to === "string" && to.startsWith("http")) {
return (
<a
href={to}
className={cn(styles.link, className)}
onClick={openExternal}
{...props}
>
{children}
</a>
);
}
return (
<ReactRouterDomLink
className={cn(styles.link, className)}
to={to}
{...props}
>
{children}
</ReactRouterDomLink>
);
}

View File

@ -2,6 +2,13 @@ import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
export const textFieldContainer = style({
flex: "1",
gap: `${SPACING_UNIT}px`,
display: "flex",
flexDirection: "column",
});
export const textField = recipe({ export const textField = recipe({
base: { base: {
display: "inline-flex", display: "inline-flex",
@ -50,9 +57,3 @@ export const textFieldInput = style({
cursor: "text", cursor: "text",
}, },
}); });
export const label = style({
marginBottom: `${SPACING_UNIT}px`,
display: "block",
color: vars.color.bodyText,
});

View File

@ -9,28 +9,31 @@ export interface TextFieldProps
> { > {
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"]; theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
label?: string | React.ReactNode; label?: string | React.ReactNode;
hint?: string | React.ReactNode;
textFieldProps?: React.DetailedHTMLProps< textFieldProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>, React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
>; >;
containerProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
} }
export function TextField({ export function TextField({
theme = "primary", theme = "primary",
label, label,
hint,
textFieldProps, textFieldProps,
containerProps,
...props ...props
}: TextFieldProps) { }: TextFieldProps) {
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const id = useId(); const id = useId();
return ( return (
<div style={{ flex: 1 }}> <div className={styles.textFieldContainer} {...containerProps}>
{label && ( {label && <label tabIndex={0}>{label}</label>}
<label htmlFor={id} className={styles.label} tabIndex={0}>
{label}
</label>
)}
<div <div
className={styles.textField({ focused: isFocused, theme })} className={styles.textField({ focused: isFocused, theme })}
@ -45,6 +48,8 @@ export function TextField({
{...props} {...props}
/> />
</div> </div>
{hint && <small tabIndex={0}>{hint}</small>}
</div> </div>
); );
} }

View File

@ -3,13 +3,13 @@ import type { PayloadAction } from "@reduxjs/toolkit";
import type { TorrentProgress } from "@types"; import type { TorrentProgress } from "@types";
interface DownloadState { interface DownloadState {
packets: TorrentProgress[]; lastPacket: TorrentProgress | null;
gameId: number | null; gameId: number | null;
gamesWithDeletionInProgress: number[]; gamesWithDeletionInProgress: number[];
} }
const initialState: DownloadState = { const initialState: DownloadState = {
packets: [], lastPacket: null,
gameId: null, gameId: null,
gamesWithDeletionInProgress: [], gamesWithDeletionInProgress: [],
}; };
@ -18,12 +18,12 @@ export const downloadSlice = createSlice({
name: "download", name: "download",
initialState, initialState,
reducers: { reducers: {
addPacket: (state, action: PayloadAction<TorrentProgress>) => { setLastPacket: (state, action: PayloadAction<TorrentProgress>) => {
state.packets = [...state.packets, action.payload]; state.lastPacket = action.payload;
if (!state.gameId) state.gameId = action.payload.game.id; if (!state.gameId) state.gameId = action.payload.game.id;
}, },
clearDownload: (state) => { clearDownload: (state) => {
state.packets = []; state.lastPacket = null;
state.gameId = null; state.gameId = null;
}, },
setGameDeleting: (state, action: PayloadAction<number>) => { setGameDeleting: (state, action: PayloadAction<number>) => {
@ -42,7 +42,7 @@ export const downloadSlice = createSlice({
}); });
export const { export const {
addPacket, setLastPacket,
clearDownload, clearDownload,
setGameDeleting, setGameDeleting,
removeGameFromDeleting, removeGameFromDeleting,

View File

@ -1,6 +1,6 @@
import { formatDistance } from "date-fns"; import { formatDistance } from "date-fns";
import type { FormatDistanceOptions } from "date-fns"; import type { FormatDistanceOptions } from "date-fns";
import { ptBR, enUS, es, fr } from "date-fns/locale"; import { ptBR, enUS, es, fr, pl, hu, tr, ru, it } from "date-fns/locale";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export function useDate() { export function useDate() {
@ -10,6 +10,11 @@ export function useDate() {
if (i18n.language.startsWith("pt")) return ptBR; if (i18n.language.startsWith("pt")) return ptBR;
if (i18n.language.startsWith("es")) return es; if (i18n.language.startsWith("es")) return es;
if (i18n.language.startsWith("fr")) return fr; if (i18n.language.startsWith("fr")) return fr;
if (i18n.language.startsWith("hu")) return hu;
if (i18n.language.startsWith("pl")) return pl;
if (i18n.language.startsWith("tr")) return tr;
if (i18n.language.startsWith("ru")) return ru;
if (i18n.language.startsWith("it")) return it;
return enUS; return enUS;
}; };

View File

@ -4,7 +4,7 @@ import { formatDownloadProgress } from "@renderer/helpers";
import { useLibrary } from "./use-library"; import { useLibrary } from "./use-library";
import { useAppDispatch, useAppSelector } from "./redux"; import { useAppDispatch, useAppSelector } from "./redux";
import { import {
addPacket, setLastPacket,
clearDownload, clearDownload,
setGameDeleting, setGameDeleting,
removeGameFromDeleting, removeGameFromDeleting,
@ -18,13 +18,11 @@ export function useDownload() {
const { updateLibrary } = useLibrary(); const { updateLibrary } = useLibrary();
const { formatDistance } = useDate(); const { formatDistance } = useDate();
const { packets, gamesWithDeletionInProgress } = useAppSelector( const { lastPacket, gamesWithDeletionInProgress } = useAppSelector(
(state) => state.download (state) => state.download
); );
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const lastPacket = packets.at(-1);
const startDownload = ( const startDownload = (
repackId: number, repackId: number,
objectID: string, objectID: string,
@ -128,6 +126,6 @@ export function useDownload() {
deleteGame, deleteGame,
isGameDeleting, isGameDeleting,
clearDownload: () => dispatch(clearDownload()), clearDownload: () => dispatch(clearDownload()),
addPacket: (packet: TorrentProgress) => dispatch(addPacket(packet)), setLastPacket: (packet: TorrentProgress) => dispatch(setLastPacket(packet)),
}; };
} }

View File

@ -29,6 +29,7 @@ export const downloaderName = style({
borderRadius: "4px", borderRadius: "4px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
alignSelf: "flex-start",
}); });
export const downloads = style({ export const downloads = style({

View File

@ -266,12 +266,11 @@ export function Downloads() {
> >
{game.title} {game.title}
</button> </button>
</div>
<small className={styles.downloaderName}> <small className={styles.downloaderName}>
{downloaderName[game?.downloader]} {downloaderName[game?.downloader]}
</small> </small>
</div>
{getGameInfo(game)} {getGameInfo(game)}
</div> </div>

View File

@ -20,6 +20,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
return gameDetails.screenshots.length; return gameDetails.screenshots.length;
} }
} }
return 0; return 0;
}); });

View File

@ -50,7 +50,7 @@ export function HeroPanelActions({
filters: [ filters: [
{ {
name: "Game executable", name: "Game executable",
extensions: window.electron.platform === "win32" ? ["exe"] : [], extensions: ["exe"],
}, },
], ],
}) })

View File

@ -36,7 +36,7 @@ export function RepacksModal({
useEffect(() => { useEffect(() => {
setFilteredRepacks(gameDetails.repacks); setFilteredRepacks(gameDetails.repacks);
}, [gameDetails.repacks]); }, [gameDetails.repacks, visible]);
const handleRepackClick = (repack: GameRepack) => { const handleRepackClick = (repack: GameRepack) => {
setRepack(repack); setRepack(repack);

View File

@ -17,11 +17,3 @@ export const hintText = style({
fontSize: "12px", fontSize: "12px",
color: vars.color.bodyText, color: vars.color.bodyText,
}); });
export const settingsLink = style({
textDecoration: "none",
color: "#C0C1C7",
":hover": {
textDecoration: "underline",
},
});

View File

@ -1,11 +1,10 @@
import { Button, Modal, TextField } from "@renderer/components"; import { Button, Link, Modal, TextField } from "@renderer/components";
import { GameRepack, ShopDetails } from "@types"; import { GameRepack, ShopDetails } from "@types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { formatBytes } from "@renderer/utils"; import { formatBytes } from "@renderer/utils";
import { DiskSpace } from "check-disk-space"; import { DiskSpace } from "check-disk-space";
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"; import { DownloadIcon } from "@primer/octicons-react";
@ -100,10 +99,9 @@ export function SelectFolderModal({
</Button> </Button>
</div> </div>
<p className={styles.hintText}> <p className={styles.hintText}>
{t("select_folder_hint")}{" "} <Trans i18nKey="select_folder_hint" ns="game_details">
<Link to="/settings" className={styles.settingsLink}> <Link to="/settings" />
{t("settings")} </Trans>
</Link>
</p> </p>
<Button onClick={handleStartClick} disabled={downloadStarting}> <Button onClick={handleStartClick} disabled={downloadStarting}>
<DownloadIcon /> <DownloadIcon />

View File

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

View File

@ -0,0 +1,115 @@
import { useEffect, useState } from "react";
import { TextField, Button, CheckboxField } from "@renderer/components";
import { useTranslation } from "react-i18next";
import * as styles from "./settings-general.css";
import type { UserPreferences } from "@types";
export interface SettingsGeneralProps {
userPreferences: UserPreferences | null;
updateUserPreferences: (values: Partial<UserPreferences>) => void;
}
export function SettingsGeneral({
userPreferences,
updateUserPreferences,
}: SettingsGeneralProps) {
const [form, setForm] = useState({
downloadsPath: "",
downloadNotificationsEnabled: false,
repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false,
});
useEffect(() => {
if (userPreferences) {
const {
downloadsPath,
downloadNotificationsEnabled,
repackUpdatesNotificationsEnabled,
telemetryEnabled,
} = userPreferences;
window.electron.getDefaultDownloadsPath().then((defaultDownloadsPath) => {
setForm((prev) => ({
...prev,
downloadsPath: downloadsPath ?? defaultDownloadsPath,
downloadNotificationsEnabled,
repackUpdatesNotificationsEnabled,
telemetryEnabled,
}));
});
}
}, [userPreferences]);
const { t } = useTranslation("settings");
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
defaultPath: form.downloadsPath,
properties: ["openDirectory"],
});
if (filePaths && filePaths.length > 0) {
const path = filePaths[0];
updateUserPreferences({ downloadsPath: path });
}
};
return (
<>
<div className={styles.downloadsPathField}>
<TextField
label={t("downloads_path")}
value={form.downloadsPath}
readOnly
disabled
/>
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
>
{t("change")}
</Button>
</div>
<h3>{t("notifications")}</h3>
<CheckboxField
label={t("enable_download_notifications")}
checked={form.downloadNotificationsEnabled}
onChange={() =>
updateUserPreferences({
downloadNotificationsEnabled: !form.downloadNotificationsEnabled,
})
}
/>
<CheckboxField
label={t("enable_repack_list_notifications")}
checked={form.repackUpdatesNotificationsEnabled}
onChange={() =>
updateUserPreferences({
repackUpdatesNotificationsEnabled:
!form.repackUpdatesNotificationsEnabled,
})
}
/>
<h3>{t("telemetry")}</h3>
<CheckboxField
label={t("telemetry_description")}
checked={form.telemetryEnabled}
onChange={() =>
updateUserPreferences({
telemetryEnabled: !form.telemetryEnabled,
})
}
/>
</>
);
}

View File

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

View File

@ -0,0 +1,83 @@
import { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Button, CheckboxField, Link, TextField } from "@renderer/components";
import * as styles from "./settings-real-debrid.css";
import type { UserPreferences } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
export interface SettingsRealDebridProps {
userPreferences: UserPreferences | null;
updateUserPreferences: (values: Partial<UserPreferences>) => void;
}
export function SettingsRealDebrid({
userPreferences,
updateUserPreferences,
}: SettingsRealDebridProps) {
const [form, setForm] = useState({
useRealDebrid: false,
realDebridApiToken: null as string | null,
});
const { t } = useTranslation("settings");
useEffect(() => {
if (userPreferences) {
setForm({
useRealDebrid: Boolean(userPreferences.realDebridApiToken),
realDebridApiToken: userPreferences.realDebridApiToken ?? null,
});
}
}, [userPreferences]);
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
updateUserPreferences({ realDebridApiToken: form.realDebridApiToken });
};
const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken;
return (
<form className={styles.form} onSubmit={handleFormSubmit}>
<CheckboxField
label={t("enable_real_debrid")}
checked={form.useRealDebrid}
onChange={() =>
setForm((prev) => ({
...prev,
useRealDebrid: !form.useRealDebrid,
}))
}
/>
{form.useRealDebrid && (
<TextField
label={t("real_debrid_api_token_description")}
value={form.realDebridApiToken ?? ""}
type="password"
onChange={(event) =>
setForm({ ...form, realDebridApiToken: event.target.value })
}
placeholder="API Token"
containerProps={{ style: { marginTop: `${SPACING_UNIT}px` } }}
hint={
<Trans i18nKey="real_debrid_api_token_hint" ns="settings">
<Link to={REAL_DEBRID_API_TOKEN_URL} />
</Trans>
}
/>
)}
<Button
type="submit"
style={{ alignSelf: "flex-end" }}
disabled={isButtonDisabled}
>
Save changes
</Button>
</form>
);
}

View File

@ -20,11 +20,6 @@ export const content = style({
flexDirection: "column", flexDirection: "column",
}); });
export const downloadsPathField = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});
export const settingsCategories = style({ export const settingsCategories = style({
display: "flex", display: "flex",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,

View File

@ -1,138 +1,46 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button, CheckboxField, TextField } from "@renderer/components"; import { Button, CheckboxField } from "@renderer/components";
import * as styles from "./settings.css"; import * as styles from "./settings.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UserPreferences } from "@types"; import { UserPreferences } from "@types";
import { SettingsRealDebrid } from "./settings-real-debrid";
import { SettingsGeneral } from "./settings-general";
const categories = ["general", "behavior", "real_debrid"]; const categories = ["general", "behavior", "real_debrid"];
export function Settings() { export function Settings() {
const [currentCategory, setCurrentCategory] = useState(categories.at(0)!); const [currentCategory, setCurrentCategory] = useState(categories.at(0)!);
const [userPreferences, setUserPreferences] =
const [form, setForm] = useState({ useState<UserPreferences | null>(null);
downloadsPath: "",
downloadNotificationsEnabled: false,
repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false,
realDebridApiToken: null as string | null,
preferQuitInsteadOfHiding: false,
runAtStartup: false,
});
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
useEffect(() => { useEffect(() => {
Promise.all([ window.electron.getUserPreferences().then((userPreferences) => {
window.electron.getDefaultDownloadsPath(), setUserPreferences(userPreferences);
window.electron.getUserPreferences(),
]).then(([path, userPreferences]) => {
setForm({
downloadsPath: userPreferences?.downloadsPath || path,
downloadNotificationsEnabled:
userPreferences?.downloadNotificationsEnabled ?? false,
repackUpdatesNotificationsEnabled:
userPreferences?.repackUpdatesNotificationsEnabled ?? false,
telemetryEnabled: userPreferences?.telemetryEnabled ?? false,
realDebridApiToken: userPreferences?.realDebridApiToken ?? null,
preferQuitInsteadOfHiding:
userPreferences?.preferQuitInsteadOfHiding ?? false,
runAtStartup: userPreferences?.runAtStartup ?? false,
});
}); });
}, []); }, []);
const updateUserPreferences = <T extends keyof UserPreferences>( const handleUpdateUserPreferences = (values: Partial<UserPreferences>) => {
field: T, window.electron.updateUserPreferences(values);
value: UserPreferences[T]
) => {
setForm((prev) => ({ ...prev, [field]: value }));
window.electron.updateUserPreferences({
[field]: value,
});
};
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
defaultPath: form.downloadsPath,
properties: ["openDirectory"],
});
if (filePaths && filePaths.length > 0) {
const path = filePaths[0];
updateUserPreferences("downloadsPath", path);
}
}; };
const renderCategory = () => { const renderCategory = () => {
if (currentCategory === "general") { if (currentCategory === "general") {
return ( return (
<> <SettingsGeneral
<div className={styles.downloadsPathField}> userPreferences={userPreferences}
<TextField updateUserPreferences={handleUpdateUserPreferences}
label={t("downloads_path")}
value={form.downloadsPath}
readOnly
disabled
/> />
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
>
{t("change")}
</Button>
</div>
<h3>{t("notifications")}</h3>
<CheckboxField
label={t("enable_download_notifications")}
checked={form.downloadNotificationsEnabled}
onChange={() =>
updateUserPreferences(
"downloadNotificationsEnabled",
!form.downloadNotificationsEnabled
)
}
/>
<CheckboxField
label={t("enable_repack_list_notifications")}
checked={form.repackUpdatesNotificationsEnabled}
onChange={() =>
updateUserPreferences(
"repackUpdatesNotificationsEnabled",
!form.repackUpdatesNotificationsEnabled
)
}
/>
<h3>{t("telemetry")}</h3>
<CheckboxField
label={t("telemetry_description")}
checked={form.telemetryEnabled}
onChange={() =>
updateUserPreferences("telemetryEnabled", !form.telemetryEnabled)
}
/>
</>
); );
} }
if (currentCategory === "real_debrid") { if (currentCategory === "real_debrid") {
return ( return (
<TextField <SettingsRealDebrid
label={t("real_debrid_api_token_description")} userPreferences={userPreferences}
value={form.realDebridApiToken ?? ""} updateUserPreferences={handleUpdateUserPreferences}
type="password"
onChange={(event) => {
updateUserPreferences("realDebridApiToken", event.target.value);
}}
placeholder="API Token"
/> />
); );
} }
@ -177,7 +85,7 @@ export function Settings() {
))} ))}
</section> </section>
<h3>{t(currentCategory)}</h3> <h2>{t(currentCategory)}</h2>
{renderCategory()} {renderCategory()}
</div> </div>
</section> </section>