diff --git a/README.md b/README.md index 67085e1f..a47be042 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,13 @@ yarn build:linux Null + + + Mkdantas +
+ Matheus Dantas +
+ Hachi-R @@ -259,15 +266,15 @@ yarn build:linux
FeriVOQ
- + + xbozo
Guilherme Viana
- - + eltociear @@ -283,10 +290,10 @@ yarn build:linux - - FerNikoMF + + vnumex
- Firdavs + Vnumex
@@ -296,6 +303,21 @@ yarn build:linux Ruslan + + + FerNikoMF +
+ Firdavs +
+ + + + + PCTroller +
+ Null +
+ Chr1s0Blood diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a766d58b..805eb83b 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -23,7 +23,7 @@ "github": "Contribute on GitHub" }, "header": { - "search": "Search", + "search": "Search games", "home": "Home", "catalogue": "Catalogue", "downloads": "Downloads", @@ -86,8 +86,7 @@ "change": "Change", "repacks_modal_description": "Choose the repack you want to download", "downloads_path": "Downloads path", - "select_folder_hint": "To change the default folder, access the", - "settings": "Settings", + "select_folder_hint": "To change the default folder, go to the <0>Settings", "download_now": "Download now", "installation_instructions": "Installation Instructions", "installation_instructions_description": "Additional steps are required to install this game", @@ -144,7 +143,9 @@ "launch_with_system": "Launch Hydra on system start-up", "general": "General", "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" }, "notifications": { "download_complete": "Download complete", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index dfc9a5d2..f180cebf 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -64,12 +64,14 @@ "copied_link_to_clipboard": "Enlace copiado", "hours": "horas", "minutes": "minutos", + "amount_hours": "{{amount}} horas", + "amount_minutes": "{{amount}} minutos", "accuracy": "{{accuracy}}% precisión", "add_to_library": "Agregar a la biblioteca", "remove_from_library": "Eliminar de la biblioteca", "no_downloads": "No hay descargas disponibles", "next_suggestion": "Siguiente sugerencia", - "play_time": "Jugado {{amount}}", + "play_time": "Jugado por {{amount}}", "install": "Instalar", "play": "Jugar", "not_played_yet": "Aún no has jugado a {{title}}", diff --git a/src/locales/index.ts b/src/locales/index.ts index 66f5a05f..dee8edce 100644 --- a/src/locales/index.ts +++ b/src/locales/index.ts @@ -6,3 +6,4 @@ export { default as hu } from "./hu/translation.json"; export { default as it } from "./it/translation.json"; export { default as pl } from "./pl/translation.json"; export { default as ru } from "./ru/translation.json"; +export { default as tr } from "./tr/translation.json"; diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index c5e66c02..c8a84227 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -23,7 +23,7 @@ "github": "Contribua no GitHub" }, "header": { - "search": "Buscar", + "search": "Buscar jogos", "catalogue": "Catálogo", "downloads": "Downloads", "search_results": "Resultados da busca", @@ -82,8 +82,7 @@ "change": "Mudar", "repacks_modal_description": "Escolha o repack do jogo que deseja baixar", "downloads_path": "Diretório do download", - "select_folder_hint": "Para trocar a pasta padrão, acesse as ", - "settings": "Configurações do Hydra", + "select_folder_hint": "Para trocar a pasta padrão, acesse a <0>Tela de Configurações", "download_now": "Baixe agora", "installation_instructions": "Instruções de Instalação", "installation_instructions_description": "Passos adicionais são necessários para instalar esse jogo", diff --git a/src/locales/tr/translation.json b/src/locales/tr/translation.json new file mode 100644 index 00000000..50edf6cc --- /dev/null +++ b/src/locales/tr/translation.json @@ -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" + } + } + diff --git a/src/main/events/catalogue/get-catalogue.ts b/src/main/events/catalogue/get-catalogue.ts index 3e802c92..cc93abda 100644 --- a/src/main/events/catalogue/get-catalogue.ts +++ b/src/main/events/catalogue/get-catalogue.ts @@ -8,42 +8,35 @@ import { requestSteam250 } from "@main/services"; const repacks = stateManager.getValue("repacks"); -interface GetStringForLookup { - (index: number): string; -} +const getStringForLookup = (index: number): string => { + const repack = repacks[index]; + const formatter = + repackerFormatter[repack.repacker as keyof typeof repackerFormatter]; + + return formatName(formatter(repack.title)); +}; + +const resultSize = 12; const getCatalogue = async ( _event: Electron.IpcMainInvokeEvent, category: CatalogueCategory ) => { - const getStringForLookup = (index: number): string => { - const repack = repacks[index]; - const formatter = - repackerFormatter[repack.repacker as keyof typeof repackerFormatter]; - - return formatName(formatter(repack.title)); - }; - if (!repacks.length) return []; - const resultSize = 12; - if (category === "trending") { return getTrendingCatalogue(resultSize); - } else { - return getRecentlyAddedCatalogue( - resultSize, - resultSize, - getStringForLookup - ); } + + return getRecentlyAddedCatalogue(resultSize); }; const getTrendingCatalogue = async ( resultSize: number ): Promise => { const results: CatalogueEntry[] = []; - const trendingGames = await requestSteam250("/30day"); + const trendingGames = await requestSteam250("/90day"); + for ( let i = 0; i < trendingGames.length && results.length < resultSize; @@ -51,7 +44,7 @@ const getTrendingCatalogue = async ( ) { if (!trendingGames[i]) continue; - const { title, objectID } = trendingGames[i]; + const { title, objectID } = trendingGames[i]!; const repacks = searchRepacks(title); if (title && repacks.length) { @@ -69,11 +62,8 @@ const getTrendingCatalogue = async ( }; const getRecentlyAddedCatalogue = async ( - resultSize: number, - requestSize: number, - getStringForLookup: GetStringForLookup + resultSize: number ): Promise => { - let lookupRequest = []; const results: CatalogueEntry[] = []; for (let i = 0; results.length < resultSize; i++) { @@ -84,15 +74,7 @@ const getRecentlyAddedCatalogue = async ( continue; } - lookupRequest.push(searchGames({ query: stringForLookup })); - - if (lookupRequest.length < requestSize) { - continue; - } - - const games = (await Promise.all(lookupRequest)).map((value) => - value.at(0) - ); + const games = searchGames({ query: stringForLookup }); for (const game of games) { const isAlreadyIncluded = results.some( @@ -105,7 +87,6 @@ const getRecentlyAddedCatalogue = async ( results.push(game); } - lookupRequest = []; } return results.slice(0, resultSize); diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 2644d183..49f42d5f 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -16,7 +16,6 @@ const addGameToLibrary = async ( const game = await gameRepository.findOne({ where: { objectID, - isDeleted: false, }, }); diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts index 66f37b28..264a652a 100644 --- a/src/main/events/library/delete-game-folder.ts +++ b/src/main/events/library/delete-game-folder.ts @@ -11,7 +11,7 @@ import { registerEvent } from "../register-event"; const deleteGameFolder = async ( _event: Electron.IpcMainInvokeEvent, gameId: number -) => { +): Promise => { const game = await gameRepository.findOne({ where: { id: gameId, @@ -38,7 +38,8 @@ const deleteGameFolder = async ( logger.error(error); reject(); } - resolve(null); + + resolve(); } ); }); diff --git a/src/main/services/steam-grid.ts b/src/main/services/steam-grid.ts index 9e2ce9d8..9cb51d73 100644 --- a/src/main/services/steam-grid.ts +++ b/src/main/services/steam-grid.ts @@ -1,3 +1,4 @@ +import axios from "axios"; import { getSteamAppAsset } from "@main/helpers"; export interface SteamGridResponse { @@ -27,33 +28,35 @@ export const getSteamGridData = async ( ): Promise => { 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()}`, { - method: "GET", headers: { Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`, }, } ); - return response.json(); + return response.data; }; export const getSteamGridGameById = async ( id: number ): Promise => { - const response = await fetch( + const response = await axios.get( `https://www.steamgriddb.com/api/public/game/${id}`, { - method: "GET", headers: { Referer: "https://www.steamgriddb.com/", }, } ); - return response.json(); + return response.data; }; export const getSteamGameIconUrl = async (objectID: string) => { diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 80dadcb7..266cf97c 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -32,7 +32,7 @@ export function App({ children }: AppProps) { const contentRef = useRef(null); const { updateLibrary } = useLibrary(); - const { clearDownload, addPacket } = useDownload(); + const { clearDownload, setLastPacket } = useDownload(); const dispatch = useAppDispatch(); @@ -64,14 +64,14 @@ export function App({ children }: AppProps) { return; } - addPacket(downloadProgress); + setLastPacket(downloadProgress); } ); return () => { unsubscribe(); }; - }, [clearDownload, addPacket, updateLibrary]); + }, [clearDownload, setLastPacket, updateLibrary]); const handleSearch = useCallback( (query: string) => { diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index 44d125cd..6cce070e 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -64,7 +64,7 @@ export function BottomPanel() { {status} - + v{version} "{VERSION_CODENAME}" diff --git a/src/renderer/src/components/button/button.css.ts b/src/renderer/src/components/button/button.css.ts index 2cc19776..de808ad8 100644 --- a/src/renderer/src/components/button/button.css.ts +++ b/src/renderer/src/components/button/button.css.ts @@ -19,6 +19,7 @@ const base = style({ ":disabled": { opacity: vars.opacity.disabled, pointerEvents: "none", + cursor: "not-allowed", }, }); diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index 2de22327..70777fea 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -7,3 +7,4 @@ export * from "./modal/modal"; export * from "./sidebar/sidebar"; export * from "./text-field/text-field"; export * from "./checkbox-field/checkbox-field"; +export * from "./link/link"; diff --git a/src/renderer/src/components/link/link.css.ts b/src/renderer/src/components/link/link.css.ts new file mode 100644 index 00000000..4f0e4c41 --- /dev/null +++ b/src/renderer/src/components/link/link.css.ts @@ -0,0 +1,9 @@ +import { style } from "@vanilla-extract/css"; + +export const link = style({ + textDecoration: "none", + color: "#C0C1C7", + ":hover": { + textDecoration: "underline", + }, +}); diff --git a/src/renderer/src/components/link/link.tsx b/src/renderer/src/components/link/link.tsx new file mode 100644 index 00000000..ffd5f89c --- /dev/null +++ b/src/renderer/src/components/link/link.tsx @@ -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 ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/src/renderer/src/components/text-field/text-field.css.ts b/src/renderer/src/components/text-field/text-field.css.ts index f5469fa1..90dfdbe6 100644 --- a/src/renderer/src/components/text-field/text-field.css.ts +++ b/src/renderer/src/components/text-field/text-field.css.ts @@ -2,6 +2,13 @@ import { SPACING_UNIT, vars } from "../../theme.css"; import { style } from "@vanilla-extract/css"; import { recipe } from "@vanilla-extract/recipes"; +export const textFieldContainer = style({ + flex: "1", + gap: `${SPACING_UNIT}px`, + display: "flex", + flexDirection: "column", +}); + export const textField = recipe({ base: { display: "inline-flex", @@ -50,9 +57,3 @@ export const textFieldInput = style({ cursor: "text", }, }); - -export const label = style({ - marginBottom: `${SPACING_UNIT}px`, - display: "block", - color: vars.color.bodyText, -}); diff --git a/src/renderer/src/components/text-field/text-field.tsx b/src/renderer/src/components/text-field/text-field.tsx index 9a7c21d1..b085f53e 100644 --- a/src/renderer/src/components/text-field/text-field.tsx +++ b/src/renderer/src/components/text-field/text-field.tsx @@ -9,28 +9,31 @@ export interface TextFieldProps > { theme?: NonNullable>["theme"]; label?: string | React.ReactNode; + hint?: string | React.ReactNode; textFieldProps?: React.DetailedHTMLProps< React.HTMLAttributes, HTMLDivElement >; + containerProps?: React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >; } export function TextField({ theme = "primary", label, + hint, textFieldProps, + containerProps, ...props }: TextFieldProps) { const [isFocused, setIsFocused] = useState(false); const id = useId(); return ( -
- {label && ( - - )} +
+ {label && }
+ + {hint && {hint}}
); } diff --git a/src/renderer/src/features/download-slice.ts b/src/renderer/src/features/download-slice.ts index 23ba9c7e..474258ba 100644 --- a/src/renderer/src/features/download-slice.ts +++ b/src/renderer/src/features/download-slice.ts @@ -3,13 +3,13 @@ import type { PayloadAction } from "@reduxjs/toolkit"; import type { TorrentProgress } from "@types"; interface DownloadState { - packets: TorrentProgress[]; + lastPacket: TorrentProgress | null; gameId: number | null; gamesWithDeletionInProgress: number[]; } const initialState: DownloadState = { - packets: [], + lastPacket: null, gameId: null, gamesWithDeletionInProgress: [], }; @@ -18,12 +18,12 @@ export const downloadSlice = createSlice({ name: "download", initialState, reducers: { - addPacket: (state, action: PayloadAction) => { - state.packets = [...state.packets, action.payload]; + setLastPacket: (state, action: PayloadAction) => { + state.lastPacket = action.payload; if (!state.gameId) state.gameId = action.payload.game.id; }, clearDownload: (state) => { - state.packets = []; + state.lastPacket = null; state.gameId = null; }, setGameDeleting: (state, action: PayloadAction) => { @@ -42,7 +42,7 @@ export const downloadSlice = createSlice({ }); export const { - addPacket, + setLastPacket, clearDownload, setGameDeleting, removeGameFromDeleting, diff --git a/src/renderer/src/hooks/use-date.ts b/src/renderer/src/hooks/use-date.ts index 2ffcb583..32bbedcb 100644 --- a/src/renderer/src/hooks/use-date.ts +++ b/src/renderer/src/hooks/use-date.ts @@ -1,6 +1,6 @@ import { formatDistance } 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"; export function useDate() { @@ -10,6 +10,11 @@ export function useDate() { if (i18n.language.startsWith("pt")) return ptBR; if (i18n.language.startsWith("es")) return es; 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; }; diff --git a/src/renderer/src/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts index ecc3e0a5..f5033738 100644 --- a/src/renderer/src/hooks/use-download.ts +++ b/src/renderer/src/hooks/use-download.ts @@ -4,7 +4,7 @@ import { formatDownloadProgress } from "@renderer/helpers"; import { useLibrary } from "./use-library"; import { useAppDispatch, useAppSelector } from "./redux"; import { - addPacket, + setLastPacket, clearDownload, setGameDeleting, removeGameFromDeleting, @@ -18,13 +18,11 @@ export function useDownload() { const { updateLibrary } = useLibrary(); const { formatDistance } = useDate(); - const { packets, gamesWithDeletionInProgress } = useAppSelector( + const { lastPacket, gamesWithDeletionInProgress } = useAppSelector( (state) => state.download ); const dispatch = useAppDispatch(); - const lastPacket = packets.at(-1); - const startDownload = ( repackId: number, objectID: string, @@ -128,6 +126,6 @@ export function useDownload() { deleteGame, isGameDeleting, clearDownload: () => dispatch(clearDownload()), - addPacket: (packet: TorrentProgress) => dispatch(addPacket(packet)), + setLastPacket: (packet: TorrentProgress) => dispatch(setLastPacket(packet)), }; } diff --git a/src/renderer/src/pages/downloads/downloads.css.ts b/src/renderer/src/pages/downloads/downloads.css.ts index c825924c..dcccb16a 100644 --- a/src/renderer/src/pages/downloads/downloads.css.ts +++ b/src/renderer/src/pages/downloads/downloads.css.ts @@ -29,6 +29,7 @@ export const downloaderName = style({ borderRadius: "4px", display: "flex", alignItems: "center", + alignSelf: "flex-start", }); export const downloads = style({ diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index a9e78385..d8a4aec2 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -266,12 +266,11 @@ export function Downloads() { > {game.title} - - - {downloaderName[game?.downloader]} -
+ + {downloaderName[game?.downloader]} + {getGameInfo(game)} diff --git a/src/renderer/src/pages/game-details/gallery-slider.tsx b/src/renderer/src/pages/game-details/gallery-slider.tsx index cb8ad052..a371421b 100644 --- a/src/renderer/src/pages/game-details/gallery-slider.tsx +++ b/src/renderer/src/pages/game-details/gallery-slider.tsx @@ -20,6 +20,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) { return gameDetails.screenshots.length; } } + return 0; }); diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index 068b9c10..7638f53e 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -50,7 +50,7 @@ export function HeroPanelActions({ filters: [ { name: "Game executable", - extensions: window.electron.platform === "win32" ? ["exe"] : [], + extensions: ["exe"], }, ], }) diff --git a/src/renderer/src/pages/game-details/repacks-modal.tsx b/src/renderer/src/pages/game-details/repacks-modal.tsx index aebcb071..0e441520 100644 --- a/src/renderer/src/pages/game-details/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/repacks-modal.tsx @@ -36,7 +36,7 @@ export function RepacksModal({ useEffect(() => { setFilteredRepacks(gameDetails.repacks); - }, [gameDetails.repacks]); + }, [gameDetails.repacks, visible]); const handleRepackClick = (repack: GameRepack) => { setRepack(repack); diff --git a/src/renderer/src/pages/game-details/select-folder-modal.css.tsx b/src/renderer/src/pages/game-details/select-folder-modal.css.tsx index d9d0d540..21bbdfea 100644 --- a/src/renderer/src/pages/game-details/select-folder-modal.css.tsx +++ b/src/renderer/src/pages/game-details/select-folder-modal.css.tsx @@ -17,11 +17,3 @@ export const hintText = style({ fontSize: "12px", color: vars.color.bodyText, }); - -export const settingsLink = style({ - textDecoration: "none", - color: "#C0C1C7", - ":hover": { - textDecoration: "underline", - }, -}); diff --git a/src/renderer/src/pages/game-details/select-folder-modal.tsx b/src/renderer/src/pages/game-details/select-folder-modal.tsx index 266b48c5..0e3ae000 100644 --- a/src/renderer/src/pages/game-details/select-folder-modal.tsx +++ b/src/renderer/src/pages/game-details/select-folder-modal.tsx @@ -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 { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { formatBytes } from "@renderer/utils"; import { DiskSpace } from "check-disk-space"; -import { Link } from "react-router-dom"; import * as styles from "./select-folder-modal.css"; import { DownloadIcon } from "@primer/octicons-react"; @@ -100,10 +99,9 @@ export function SelectFolderModal({

- {t("select_folder_hint")}{" "} - - {t("settings")} - + + +

+ + +

{t("notifications")}

+ + + updateUserPreferences({ + downloadNotificationsEnabled: !form.downloadNotificationsEnabled, + }) + } + /> + + + updateUserPreferences({ + repackUpdatesNotificationsEnabled: + !form.repackUpdatesNotificationsEnabled, + }) + } + /> + +

{t("telemetry")}

+ + + updateUserPreferences({ + telemetryEnabled: !form.telemetryEnabled, + }) + } + /> + + ); +} diff --git a/src/renderer/src/pages/settings/settings-real-debrid.css.ts b/src/renderer/src/pages/settings/settings-real-debrid.css.ts new file mode 100644 index 00000000..73763780 --- /dev/null +++ b/src/renderer/src/pages/settings/settings-real-debrid.css.ts @@ -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`, +}); diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-real-debrid.tsx new file mode 100644 index 00000000..3df11d18 --- /dev/null +++ b/src/renderer/src/pages/settings/settings-real-debrid.tsx @@ -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) => 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 = (event) => { + event.preventDefault(); + updateUserPreferences({ realDebridApiToken: form.realDebridApiToken }); + }; + + const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken; + + return ( +
+ + setForm((prev) => ({ + ...prev, + useRealDebrid: !form.useRealDebrid, + })) + } + /> + + {form.useRealDebrid && ( + + setForm({ ...form, realDebridApiToken: event.target.value }) + } + placeholder="API Token" + containerProps={{ style: { marginTop: `${SPACING_UNIT}px` } }} + hint={ + + + + } + /> + )} + + + + ); +} diff --git a/src/renderer/src/pages/settings/settings.css.ts b/src/renderer/src/pages/settings/settings.css.ts index 52f5ff1e..b73ba786 100644 --- a/src/renderer/src/pages/settings/settings.css.ts +++ b/src/renderer/src/pages/settings/settings.css.ts @@ -20,11 +20,6 @@ export const content = style({ flexDirection: "column", }); -export const downloadsPathField = style({ - display: "flex", - gap: `${SPACING_UNIT}px`, -}); - export const settingsCategories = style({ display: "flex", gap: `${SPACING_UNIT}px`, diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index 5a5a6ab2..69409eda 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -1,138 +1,46 @@ 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 { useTranslation } from "react-i18next"; import { UserPreferences } from "@types"; +import { SettingsRealDebrid } from "./settings-real-debrid"; +import { SettingsGeneral } from "./settings-general"; const categories = ["general", "behavior", "real_debrid"]; export function Settings() { const [currentCategory, setCurrentCategory] = useState(categories.at(0)!); - - const [form, setForm] = useState({ - downloadsPath: "", - downloadNotificationsEnabled: false, - repackUpdatesNotificationsEnabled: false, - telemetryEnabled: false, - realDebridApiToken: null as string | null, - preferQuitInsteadOfHiding: false, - runAtStartup: false, - }); + const [userPreferences, setUserPreferences] = + useState(null); const { t } = useTranslation("settings"); useEffect(() => { - Promise.all([ - window.electron.getDefaultDownloadsPath(), - 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, - }); + window.electron.getUserPreferences().then((userPreferences) => { + setUserPreferences(userPreferences); }); }, []); - const updateUserPreferences = ( - field: T, - 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 handleUpdateUserPreferences = (values: Partial) => { + window.electron.updateUserPreferences(values); }; const renderCategory = () => { if (currentCategory === "general") { return ( - <> -
- - - -
- -

{t("notifications")}

- - - updateUserPreferences( - "downloadNotificationsEnabled", - !form.downloadNotificationsEnabled - ) - } - /> - - - updateUserPreferences( - "repackUpdatesNotificationsEnabled", - !form.repackUpdatesNotificationsEnabled - ) - } - /> - -

{t("telemetry")}

- - - updateUserPreferences("telemetryEnabled", !form.telemetryEnabled) - } - /> - + ); } if (currentCategory === "real_debrid") { return ( - { - updateUserPreferences("realDebridApiToken", event.target.value); - }} - placeholder="API Token" + ); } @@ -177,7 +85,7 @@ export function Settings() { ))} -

{t(currentCategory)}

+

{t(currentCategory)}

{renderCategory()}