mirror of
				https://github.com/hydralauncher/hydra.git
				synced 2025-03-09 15:40:26 +00:00 
			
		
		
		
	feat: updating play label on hero panel
This commit is contained in:
		
							parent
							
								
									91b1341271
								
							
						
					
					
						commit
						96e11e6be9
					
				
					 40 changed files with 2049 additions and 745 deletions
				
			
		| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "hydra",
 | 
			
		||||
  "productName": "Hydra",
 | 
			
		||||
  "version": "1.0.1",
 | 
			
		||||
  "version": "1.1.0",
 | 
			
		||||
  "description": "No bullshit. Just play.",
 | 
			
		||||
  "main": ".webpack/main",
 | 
			
		||||
  "repository": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
{
 | 
			
		||||
  "catalogue": {
 | 
			
		||||
  "home": {
 | 
			
		||||
    "featured": "Featured",
 | 
			
		||||
    "recently_added": "Recently added",
 | 
			
		||||
    "trending": "Trending",
 | 
			
		||||
| 
						 | 
				
			
			@ -7,6 +7,7 @@
 | 
			
		|||
    "no_results": "No results found"
 | 
			
		||||
  },
 | 
			
		||||
  "sidebar": {
 | 
			
		||||
    "home": "Home",
 | 
			
		||||
    "catalogue": "Catalogue",
 | 
			
		||||
    "downloads": "Downloads",
 | 
			
		||||
    "settings": "Settings",
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +20,7 @@
 | 
			
		|||
  },
 | 
			
		||||
  "header": {
 | 
			
		||||
    "search": "Search",
 | 
			
		||||
    "home": "Home",
 | 
			
		||||
    "catalogue": "Catalogue",
 | 
			
		||||
    "downloads": "Downloads",
 | 
			
		||||
    "search_results": "Search results",
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +32,10 @@
 | 
			
		|||
    "checking_files": "Checking {{title}} files… ({{percentage}} complete)",
 | 
			
		||||
    "downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}"
 | 
			
		||||
  },
 | 
			
		||||
  "catalogue": {
 | 
			
		||||
    "next_page": "Next page",
 | 
			
		||||
    "previous_page": "Previous page"
 | 
			
		||||
  },
 | 
			
		||||
  "game_details": {
 | 
			
		||||
    "open_download_options": "Open download options",
 | 
			
		||||
    "download_options_zero": "No download option",
 | 
			
		||||
| 
						 | 
				
			
			@ -64,12 +70,13 @@
 | 
			
		|||
    "remove_from_library": "Remove from library",
 | 
			
		||||
    "no_downloads": "No downloads available",
 | 
			
		||||
    "play_time": "Played for {{amount}}",
 | 
			
		||||
    "last_time_played": "Played for the last time {{period}}",
 | 
			
		||||
    "last_time_played": "Last played {{period}}",
 | 
			
		||||
    "not_played_yet": "You haven't played {{title}} yet",
 | 
			
		||||
    "next_suggestion": "Next suggestion",
 | 
			
		||||
    "play": "Play",
 | 
			
		||||
    "deleting": "Deleting installer…",
 | 
			
		||||
    "close": "Close"
 | 
			
		||||
    "close": "Close",
 | 
			
		||||
    "playing_now": "Playing now"
 | 
			
		||||
  },
 | 
			
		||||
  "activation": {
 | 
			
		||||
    "title": "Activate Hydra",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
{
 | 
			
		||||
  "catalogue": {
 | 
			
		||||
  "home": {
 | 
			
		||||
    "featured": "Destacado",
 | 
			
		||||
    "recently_added": "Recién Añadidos",
 | 
			
		||||
    "trending": "Tendencias",
 | 
			
		||||
| 
						 | 
				
			
			@ -15,14 +15,16 @@
 | 
			
		|||
    "checking_files": "{{title}} ({{percentage}} - Analizando archivos…)",
 | 
			
		||||
    "paused": "{{title}} (Pausado)",
 | 
			
		||||
    "downloading": "{{title}} ({{percentage}} - Descargando…)",
 | 
			
		||||
    "filter": "Filtrar biblioteca"
 | 
			
		||||
    "filter": "Filtrar biblioteca",
 | 
			
		||||
    "home": "Hogar"
 | 
			
		||||
  },
 | 
			
		||||
  "header": {
 | 
			
		||||
    "search": "Buscar",
 | 
			
		||||
    "catalogue": "Catálogo",
 | 
			
		||||
    "downloads": "Descargas",
 | 
			
		||||
    "search_results": "Resultados de búsqueda",
 | 
			
		||||
    "settings": "Ajustes"
 | 
			
		||||
    "settings": "Ajustes",
 | 
			
		||||
    "home": "Hogar"
 | 
			
		||||
  },
 | 
			
		||||
  "bottom_panel": {
 | 
			
		||||
    "no_downloads_in_progress": "Sin descargas en progreso",
 | 
			
		||||
| 
						 | 
				
			
			@ -65,11 +67,12 @@
 | 
			
		|||
    "next_suggestion": "Siguiente sugerencia",
 | 
			
		||||
    "play_time": "Jugado por {{cantidad}}",
 | 
			
		||||
    "install": "Instalar",
 | 
			
		||||
    "last_time_played": "Jugado por última vez {{period}}",
 | 
			
		||||
    "play": "Jugar",
 | 
			
		||||
    "not_played_yet": "Aún no has jugado a {{title}}",
 | 
			
		||||
    "close": "Cerca",
 | 
			
		||||
    "deleting": "Eliminando instalador…"
 | 
			
		||||
    "deleting": "Eliminando instalador…",
 | 
			
		||||
    "playing_now": "Jugando ahora",
 | 
			
		||||
    "last_time_played": "Jugado por última vez {{period}}"
 | 
			
		||||
  },
 | 
			
		||||
  "activation": {
 | 
			
		||||
    "title": "Activar Hydra",
 | 
			
		||||
| 
						 | 
				
			
			@ -127,5 +130,9 @@
 | 
			
		|||
    "title": "Programas no instalados",
 | 
			
		||||
    "description": "Los ejecutables de Wine o Lutris no se encontraron en su sistema",
 | 
			
		||||
    "instructions": "Comprueba la forma correcta de instalar cualquiera de ellos en tu distro Linux para que el juego pueda ejecutarse con normalidad"
 | 
			
		||||
  },
 | 
			
		||||
  "catalogue": {
 | 
			
		||||
    "next_page": "Siguiente página",
 | 
			
		||||
    "previous_page": "Pagina anterior"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
{
 | 
			
		||||
  "catalogue": {
 | 
			
		||||
  "home": {
 | 
			
		||||
    "featured": "En vedette",
 | 
			
		||||
    "recently_added": "Récemment ajouté",
 | 
			
		||||
    "trending": "Tendance",
 | 
			
		||||
| 
						 | 
				
			
			@ -15,14 +15,16 @@
 | 
			
		|||
    "checking_files": "{{title}} ({{percentage}} - Vérification des fichiers…)",
 | 
			
		||||
    "paused": "{{title}} (En pause)",
 | 
			
		||||
    "downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
 | 
			
		||||
    "filter": "Filtrer la bibliothèque"
 | 
			
		||||
    "filter": "Filtrer la bibliothèque",
 | 
			
		||||
    "home": "Maison"
 | 
			
		||||
  },
 | 
			
		||||
  "header": {
 | 
			
		||||
    "search": "Recherche",
 | 
			
		||||
    "catalogue": "Catalogue",
 | 
			
		||||
    "downloads": "Téléchargements",
 | 
			
		||||
    "search_results": "Résultats de la recherche",
 | 
			
		||||
    "settings": "Paramètres"
 | 
			
		||||
    "settings": "Paramètres",
 | 
			
		||||
    "home": "Maison"
 | 
			
		||||
  },
 | 
			
		||||
  "bottom_panel": {
 | 
			
		||||
    "no_downloads_in_progress": "Aucun téléchargement en cours",
 | 
			
		||||
| 
						 | 
				
			
			@ -65,11 +67,12 @@
 | 
			
		|||
    "next_suggestion": "Suggestion suivante",
 | 
			
		||||
    "play_time": "Joué pour {{montant}}",
 | 
			
		||||
    "install": "Installer",
 | 
			
		||||
    "last_time_played": "Joué pour la dernière fois {{période}}",
 | 
			
		||||
    "play": "Jouer",
 | 
			
		||||
    "not_played_yet": "Vous n'avez pas encore joué à {{title}}",
 | 
			
		||||
    "close": "Fermer",
 | 
			
		||||
    "deleting": "Suppression du programme d'installation…"
 | 
			
		||||
    "deleting": "Suppression du programme d'installation…",
 | 
			
		||||
    "playing_now": "Je joue maintenant",
 | 
			
		||||
    "last_time_played": "Dernière lecture {{période}}"
 | 
			
		||||
  },
 | 
			
		||||
  "activation": {
 | 
			
		||||
    "title": "Activer Hydra",
 | 
			
		||||
| 
						 | 
				
			
			@ -127,5 +130,9 @@
 | 
			
		|||
    "description": "Les exécutables Wine ou Lutris sont introuvables sur votre système",
 | 
			
		||||
    "instructions": "Vérifiez la bonne façon d'installer l'un d'entre eux sur votre distribution Linux afin que le jeu puisse fonctionner normalement",
 | 
			
		||||
    "title": "Programmes non installés"
 | 
			
		||||
  },
 | 
			
		||||
  "catalogue": {
 | 
			
		||||
    "next_page": "Page suivante",
 | 
			
		||||
    "previous_page": "Page précédente"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
{
 | 
			
		||||
  "catalogue": {
 | 
			
		||||
  "home": {
 | 
			
		||||
    "featured": "Destaque",
 | 
			
		||||
    "recently_added": "Novidades",
 | 
			
		||||
    "trending": "Populares",
 | 
			
		||||
| 
						 | 
				
			
			@ -15,14 +15,16 @@
 | 
			
		|||
    "checking_files": "{{title}} ({{percentage}} - Verificando arquivos…)",
 | 
			
		||||
    "paused": "{{title}} (Pausado)",
 | 
			
		||||
    "downloading": "{{title}} ({{percentage}} - Baixando…)",
 | 
			
		||||
    "filter": "Filtrar biblioteca"
 | 
			
		||||
    "filter": "Filtrar biblioteca",
 | 
			
		||||
    "home": "Início"
 | 
			
		||||
  },
 | 
			
		||||
  "header": {
 | 
			
		||||
    "search": "Buscar",
 | 
			
		||||
    "catalogue": "Catálogo",
 | 
			
		||||
    "downloads": "Downloads",
 | 
			
		||||
    "search_results": "Resultados da busca",
 | 
			
		||||
    "settings": "Configurações"
 | 
			
		||||
    "settings": "Configurações",
 | 
			
		||||
    "home": "Início"
 | 
			
		||||
  },
 | 
			
		||||
  "bottom_panel": {
 | 
			
		||||
    "no_downloads_in_progress": "Sem downloads em andamento",
 | 
			
		||||
| 
						 | 
				
			
			@ -65,11 +67,12 @@
 | 
			
		|||
    "play_time": "Jogado por {{amount}}",
 | 
			
		||||
    "next_suggestion": "Próxima sugestão",
 | 
			
		||||
    "install": "Instalar",
 | 
			
		||||
    "last_time_played": "Jogado pela última vez {{period}}",
 | 
			
		||||
    "last_time_played": "Jogou por último {{period}}",
 | 
			
		||||
    "play": "Jogar",
 | 
			
		||||
    "not_played_yet": "Você ainda não jogou {{title}}",
 | 
			
		||||
    "close": "Fechar",
 | 
			
		||||
    "deleting": "Excluindo instalador…"
 | 
			
		||||
    "deleting": "Excluindo instalador…",
 | 
			
		||||
    "playing_now": "Jogando agora"
 | 
			
		||||
  },
 | 
			
		||||
  "activation": {
 | 
			
		||||
    "title": "Ativação",
 | 
			
		||||
| 
						 | 
				
			
			@ -127,5 +130,9 @@
 | 
			
		|||
    "title": "Programas não instalados",
 | 
			
		||||
    "description": "Não foram encontrados no seu sistema os executáveis do Wine ou Lutris",
 | 
			
		||||
    "instructions": "Verifique a forma correta de instalar algum deles na sua distro Linux para que o jogo possa ser executado normalmente"
 | 
			
		||||
  },
 | 
			
		||||
  "catalogue": {
 | 
			
		||||
    "next_page": "Próxima página",
 | 
			
		||||
    "previous_page": "Página anterior"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import {
 | 
			
		|||
  RepackerFriendlyName,
 | 
			
		||||
  UserPreferences,
 | 
			
		||||
  MigrationScript,
 | 
			
		||||
  SteamGame,
 | 
			
		||||
} from "@main/entity";
 | 
			
		||||
import type { SqliteConnectionOptions } from "typeorm/driver/sqlite/SqliteConnectionOptions";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +25,7 @@ export const createDataSource = (options: Partial<SqliteConnectionOptions>) =>
 | 
			
		|||
      UserPreferences,
 | 
			
		||||
      GameShopCache,
 | 
			
		||||
      MigrationScript,
 | 
			
		||||
      SteamGame,
 | 
			
		||||
    ],
 | 
			
		||||
    ...options,
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,3 +5,4 @@ export * from "./repacker-friendly-name.entity";
 | 
			
		|||
export * from "./user-preferences.entity";
 | 
			
		||||
export * from "./game-shop-cache.entity";
 | 
			
		||||
export * from "./migration-script.entity";
 | 
			
		||||
export * from "./steam-game.entity";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										10
									
								
								src/main/entity/steam-game.entity.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/main/entity/steam-game.entity.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
import { Column, Entity, PrimaryColumn } from "typeorm";
 | 
			
		||||
 | 
			
		||||
@Entity("steam_game")
 | 
			
		||||
export class SteamGame {
 | 
			
		||||
  @PrimaryColumn()
 | 
			
		||||
  id: number;
 | 
			
		||||
 | 
			
		||||
  @Column()
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,25 +1,22 @@
 | 
			
		|||
import { formatName, repackerFormatter } from "@main/helpers";
 | 
			
		||||
import { getTrendingGames } from "@main/services";
 | 
			
		||||
import type { CatalogueCategory, CatalogueEntry } from "@types";
 | 
			
		||||
import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
 | 
			
		||||
import type { CatalogueCategory, CatalogueEntry, GameShop } from "@types";
 | 
			
		||||
 | 
			
		||||
import { stateManager } from "@main/state-manager";
 | 
			
		||||
import { searchGames } from "../helpers/search-games";
 | 
			
		||||
import { searchGames, searchRepacks } from "../helpers/search-games";
 | 
			
		||||
import { registerEvent } from "../register-event";
 | 
			
		||||
import { requestSteam250 } from "@main/services";
 | 
			
		||||
 | 
			
		||||
const repacks = stateManager.getValue("repacks");
 | 
			
		||||
 | 
			
		||||
interface GetStringForLookup {
 | 
			
		||||
  (index: number): string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getCatalogue = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  category: CatalogueCategory
 | 
			
		||||
) => {
 | 
			
		||||
  const trendingGames = await getTrendingGames();
 | 
			
		||||
 | 
			
		||||
  let i = 0;
 | 
			
		||||
  const results: CatalogueEntry[] = [];
 | 
			
		||||
 | 
			
		||||
  const getStringForLookup = (index: number) => {
 | 
			
		||||
    if (category === "trending") return trendingGames[index];
 | 
			
		||||
 | 
			
		||||
  const getStringForLookup = (index: number): string => {
 | 
			
		||||
    const repack = repacks[index];
 | 
			
		||||
    const formatter =
 | 
			
		||||
      repackerFormatter[repack.repacker as keyof typeof repackerFormatter];
 | 
			
		||||
| 
						 | 
				
			
			@ -30,10 +27,56 @@ const getCatalogue = async (
 | 
			
		|||
  if (!repacks.length) return [];
 | 
			
		||||
 | 
			
		||||
  const resultSize = 12;
 | 
			
		||||
  const requestSize = resultSize * 2;
 | 
			
		||||
  let lookupRequest = [];
 | 
			
		||||
 | 
			
		||||
  while (results.length < resultSize) {
 | 
			
		||||
  if (category === "trending") {
 | 
			
		||||
    return getTrendingCatalogue(resultSize);
 | 
			
		||||
  } else {
 | 
			
		||||
    return getRecentlyAddedCatalogue(
 | 
			
		||||
      resultSize,
 | 
			
		||||
      resultSize,
 | 
			
		||||
      getStringForLookup
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getTrendingCatalogue = async (
 | 
			
		||||
  resultSize: number
 | 
			
		||||
): Promise<CatalogueEntry[]> => {
 | 
			
		||||
  const results: CatalogueEntry[] = [];
 | 
			
		||||
  const trendingGames = await requestSteam250("/30day");
 | 
			
		||||
  for (
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    i < trendingGames.length && results.length < resultSize;
 | 
			
		||||
    i++
 | 
			
		||||
  ) {
 | 
			
		||||
    if (!trendingGames[i]) continue;
 | 
			
		||||
 | 
			
		||||
    const { title, objectID } = trendingGames[i];
 | 
			
		||||
    const repacks = searchRepacks(title);
 | 
			
		||||
 | 
			
		||||
    if (title && repacks.length) {
 | 
			
		||||
      const catalogueEntry = {
 | 
			
		||||
        objectID,
 | 
			
		||||
        title,
 | 
			
		||||
        shop: "steam" as GameShop,
 | 
			
		||||
        cover: getSteamAppAsset("library", objectID),
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      results.push({ ...catalogueEntry, repacks });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return results;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getRecentlyAddedCatalogue = async (
 | 
			
		||||
  resultSize: number,
 | 
			
		||||
  requestSize: number,
 | 
			
		||||
  getStringForLookup: GetStringForLookup
 | 
			
		||||
): Promise<CatalogueEntry[]> => {
 | 
			
		||||
  let lookupRequest = [];
 | 
			
		||||
  const results: CatalogueEntry[] = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; results.length < resultSize; i++) {
 | 
			
		||||
    const stringForLookup = getStringForLookup(i);
 | 
			
		||||
 | 
			
		||||
    if (!stringForLookup) {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,9 +84,7 @@ const getCatalogue = async (
 | 
			
		|||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    lookupRequest.push(searchGames(stringForLookup));
 | 
			
		||||
 | 
			
		||||
    i++;
 | 
			
		||||
    lookupRequest.push(searchGames({ query: stringForLookup }));
 | 
			
		||||
 | 
			
		||||
    if (lookupRequest.length < requestSize) {
 | 
			
		||||
      continue;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										32
									
								
								src/main/events/catalogue/get-games.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/main/events/catalogue/get-games.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
import type { CatalogueEntry } from "@types";
 | 
			
		||||
 | 
			
		||||
import { registerEvent } from "../register-event";
 | 
			
		||||
import { searchGames } from "../helpers/search-games";
 | 
			
		||||
import slice from "lodash/slice";
 | 
			
		||||
 | 
			
		||||
const getGames = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  take?: number,
 | 
			
		||||
  prevCursor = 0
 | 
			
		||||
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
 | 
			
		||||
  let results: CatalogueEntry[] = [];
 | 
			
		||||
  let i = 0;
 | 
			
		||||
 | 
			
		||||
  const batchSize = 100;
 | 
			
		||||
 | 
			
		||||
  while (results.length < take) {
 | 
			
		||||
    const games = await searchGames({
 | 
			
		||||
      take: batchSize,
 | 
			
		||||
      skip: (i + prevCursor) * batchSize,
 | 
			
		||||
    });
 | 
			
		||||
    results = [...results, ...games.filter((game) => game.repacks.length)];
 | 
			
		||||
    i++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { results: slice(results, 0, take), cursor: prevCursor + i };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
registerEvent(getGames, {
 | 
			
		||||
  name: "getGames",
 | 
			
		||||
  memoize: true,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -11,10 +11,10 @@ const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
 | 
			
		|||
    const shuffledList = shuffle(games);
 | 
			
		||||
 | 
			
		||||
    for (const game of shuffledList) {
 | 
			
		||||
      const repacks = searchRepacks(formatName(game));
 | 
			
		||||
      const repacks = searchRepacks(formatName(game.title));
 | 
			
		||||
 | 
			
		||||
      if (repacks.length) {
 | 
			
		||||
        const results = await searchGames(game);
 | 
			
		||||
        const results = await searchGames({ query: game.title });
 | 
			
		||||
 | 
			
		||||
        if (results.length) {
 | 
			
		||||
          return results[0].objectID;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,8 @@ import { registerEvent } from "../register-event";
 | 
			
		|||
import { searchGames } from "../helpers/search-games";
 | 
			
		||||
 | 
			
		||||
registerEvent(
 | 
			
		||||
  (_event: Electron.IpcMainInvokeEvent, query: string) => searchGames(query),
 | 
			
		||||
  (_event: Electron.IpcMainInvokeEvent, query: string) =>
 | 
			
		||||
    searchGames({ query, take: 12 }),
 | 
			
		||||
  {
 | 
			
		||||
    name: "searchGames",
 | 
			
		||||
    memoize: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,8 +4,10 @@ import orderBy from "lodash/orderBy";
 | 
			
		|||
import type { GameRepack, GameShop, CatalogueEntry } from "@types";
 | 
			
		||||
 | 
			
		||||
import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
 | 
			
		||||
import { searchSteamGame } from "@main/services";
 | 
			
		||||
import { stateManager } from "@main/state-manager";
 | 
			
		||||
import { steamGameRepository } from "@main/repository";
 | 
			
		||||
import { FindManyOptions, Like } from "typeorm";
 | 
			
		||||
import { SteamGame } from "@main/entity";
 | 
			
		||||
 | 
			
		||||
const { Index } = flexSearch;
 | 
			
		||||
const repacksIndex = new Index();
 | 
			
		||||
| 
						 | 
				
			
			@ -32,33 +34,41 @@ export const searchRepacks = (title: string): GameRepack[] => {
 | 
			
		|||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const searchGames = async (query: string): Promise<CatalogueEntry[]> => {
 | 
			
		||||
  const formattedName = formatName(query);
 | 
			
		||||
export interface SearchGamesArgs {
 | 
			
		||||
  query?: string;
 | 
			
		||||
  take?: number;
 | 
			
		||||
  skip?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  const steamResults = await searchSteamGame(formattedName);
 | 
			
		||||
export const searchGames = async ({
 | 
			
		||||
  query,
 | 
			
		||||
  take,
 | 
			
		||||
  skip,
 | 
			
		||||
}: SearchGamesArgs): Promise<CatalogueEntry[]> => {
 | 
			
		||||
  const options: FindManyOptions<SteamGame> = {};
 | 
			
		||||
 | 
			
		||||
  const results = steamResults.map((result) => ({
 | 
			
		||||
    objectID: result.objectID,
 | 
			
		||||
    title: result.name,
 | 
			
		||||
    shop: "steam" as GameShop,
 | 
			
		||||
    cover: getSteamAppAsset("library", result.objectID),
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  const gamesIndex = new Index({
 | 
			
		||||
    tokenize: "full",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < results.length; i++) {
 | 
			
		||||
    const game = results[i];
 | 
			
		||||
    gamesIndex.add(i, game.title);
 | 
			
		||||
  if (query) {
 | 
			
		||||
    options.where = {
 | 
			
		||||
      name: query ? Like(`%${formatName(query)}%`) : undefined,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const filteredResults = gamesIndex
 | 
			
		||||
    .search(query)
 | 
			
		||||
    .map((index) => results[index as number]);
 | 
			
		||||
  const steamResults = await steamGameRepository.find({
 | 
			
		||||
    ...options,
 | 
			
		||||
    take,
 | 
			
		||||
    skip,
 | 
			
		||||
    order: { name: "ASC" },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const results = steamResults.map((result) => ({
 | 
			
		||||
    objectID: String(result.id),
 | 
			
		||||
    title: result.name,
 | 
			
		||||
    shop: "steam" as GameShop,
 | 
			
		||||
    cover: getSteamAppAsset("library", String(result.id)),
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  return Promise.all(
 | 
			
		||||
    filteredResults.map(async (result) => ({
 | 
			
		||||
    results.map(async (result) => ({
 | 
			
		||||
      ...result,
 | 
			
		||||
      repacks: searchRepacks(result.title),
 | 
			
		||||
    }))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ import "./library/remove-game";
 | 
			
		|||
import "./library/delete-game-folder";
 | 
			
		||||
import "./catalogue/get-random-game";
 | 
			
		||||
import "./catalogue/get-how-long-to-beat";
 | 
			
		||||
import "./catalogue/get-games";
 | 
			
		||||
 | 
			
		||||
ipcMain.handle("ping", () => "pong");
 | 
			
		||||
ipcMain.handle("getVersion", () => app.getVersion());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import {
 | 
			
		|||
  RepackerFriendlyName,
 | 
			
		||||
  UserPreferences,
 | 
			
		||||
  MigrationScript,
 | 
			
		||||
  SteamGame,
 | 
			
		||||
} from "@main/entity";
 | 
			
		||||
 | 
			
		||||
export const gameRepository = dataSource.getRepository(Game);
 | 
			
		||||
| 
						 | 
				
			
			@ -25,3 +26,5 @@ export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
 | 
			
		|||
 | 
			
		||||
export const migrationScriptRepository =
 | 
			
		||||
  dataSource.getRepository(MigrationScript);
 | 
			
		||||
 | 
			
		||||
export const steamGameRepository = dataSource.getRepository(SteamGame);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,7 +46,9 @@ export const startProcessWatcher = async () => {
 | 
			
		|||
          const zero = gamesPlaytime.get(game.id);
 | 
			
		||||
          const delta = performance.now() - zero;
 | 
			
		||||
 | 
			
		||||
          WindowManager.mainWindow.webContents.send("on-playtime", game.id);
 | 
			
		||||
          if (WindowManager.mainWindow) {
 | 
			
		||||
            WindowManager.mainWindow.webContents.send("on-playtime", game.id);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          await gameRepository.update(game.id, {
 | 
			
		||||
            playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +70,9 @@ export const startProcessWatcher = async () => {
 | 
			
		|||
 | 
			
		||||
      if (gamesPlaytime.has(game.id)) {
 | 
			
		||||
        gamesPlaytime.delete(game.id);
 | 
			
		||||
        WindowManager.mainWindow.webContents.send("on-game-close", game.id);
 | 
			
		||||
        if (WindowManager.mainWindow) {
 | 
			
		||||
          WindowManager.mainWindow.webContents.send("on-game-close", game.id);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await sleep(sleepTime);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,24 @@
 | 
			
		|||
import axios from "axios";
 | 
			
		||||
import { JSDOM } from "jsdom";
 | 
			
		||||
import shuffle from "lodash/shuffle";
 | 
			
		||||
import { logger } from "./logger";
 | 
			
		||||
 | 
			
		||||
const requestSteam250 = async (path: string) => {
 | 
			
		||||
  return axios
 | 
			
		||||
    .get(`https://steam250.com${path}`)
 | 
			
		||||
    .then((response) => response.data);
 | 
			
		||||
};
 | 
			
		||||
export const requestSteam250 = async (path: string) => {
 | 
			
		||||
  return axios.get(`https://steam250.com${path}`).then((response) => {
 | 
			
		||||
    const { window } = new JSDOM(response.data);
 | 
			
		||||
    const { document } = window;
 | 
			
		||||
 | 
			
		||||
export const getTrendingGames = async () => {
 | 
			
		||||
  const response = await requestSteam250("/365day").catch((err) => {
 | 
			
		||||
    logger.error(err.response, { method: "getTrendingGames" });
 | 
			
		||||
    throw new Error(err);
 | 
			
		||||
    return Array.from(document.querySelectorAll(".appline .title a")).map(
 | 
			
		||||
      ($title: HTMLAnchorElement) => {
 | 
			
		||||
        const steamGameUrl = $title.href;
 | 
			
		||||
        if (!steamGameUrl) return null;
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          title: $title.textContent,
 | 
			
		||||
          objectID: steamGameUrl.split("/").pop(),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const { window } = new JSDOM(response);
 | 
			
		||||
  const { document } = window;
 | 
			
		||||
 | 
			
		||||
  return Array.from(document.querySelectorAll(".appline .title a")).map(
 | 
			
		||||
    ($title) => $title.textContent!
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const steam250Paths = [
 | 
			
		||||
| 
						 | 
				
			
			@ -32,15 +30,5 @@ const steam250Paths = [
 | 
			
		|||
 | 
			
		||||
export const getRandomSteam250List = async () => {
 | 
			
		||||
  const [path] = shuffle(steam250Paths);
 | 
			
		||||
  const response = await requestSteam250(path).catch((err) => {
 | 
			
		||||
    logger.error(err.response, { method: "getRandomSteam250List" });
 | 
			
		||||
    throw new Error(err);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const { window } = new JSDOM(response);
 | 
			
		||||
  const { document } = window;
 | 
			
		||||
 | 
			
		||||
  return Array.from(document.querySelectorAll(".appline .title a")).map(
 | 
			
		||||
    ($title) => $title.textContent!
 | 
			
		||||
  );
 | 
			
		||||
  return requestSteam250(path);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
import axios from "axios";
 | 
			
		||||
import { JSDOM } from "jsdom";
 | 
			
		||||
 | 
			
		||||
import type { SteamAppDetails } from "@types";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,45 +33,3 @@ export const getSteamAppDetails = async (
 | 
			
		|||
      throw new Error(err);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const searchSteamGame = async (term: string) => {
 | 
			
		||||
  const searchParams = new URLSearchParams({
 | 
			
		||||
    start: "0",
 | 
			
		||||
    count: "12",
 | 
			
		||||
    sort_by: "_ASC",
 | 
			
		||||
    /* Games only */
 | 
			
		||||
    category1: "998",
 | 
			
		||||
    term: term,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const response = await axios.get(
 | 
			
		||||
    `https://store.steampowered.com/search/results/?${searchParams.toString()}`
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const { window } = new JSDOM(response.data);
 | 
			
		||||
  const { document } = window;
 | 
			
		||||
 | 
			
		||||
  const $anchors = Array.from(
 | 
			
		||||
    document.querySelectorAll("#search_resultsRows a")
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return $anchors.reduce((prev, $a) => {
 | 
			
		||||
    const $title = $a.querySelector(".title");
 | 
			
		||||
    const objectIDs = $a.getAttribute("data-ds-appid");
 | 
			
		||||
 | 
			
		||||
    if (!objectIDs) return prev;
 | 
			
		||||
 | 
			
		||||
    const [objectID] = objectIDs.split(",");
 | 
			
		||||
 | 
			
		||||
    if (!objectID || prev.some((game) => game.objectID === objectID))
 | 
			
		||||
      return prev;
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
      ...prev,
 | 
			
		||||
      {
 | 
			
		||||
        name: $title.textContent,
 | 
			
		||||
        objectID,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
  }, []);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,11 +4,12 @@ import { app } from "electron";
 | 
			
		|||
import chunk from "lodash/chunk";
 | 
			
		||||
 | 
			
		||||
import { createDataSource, dataSource } from "@main/data-source";
 | 
			
		||||
import { Repack, RepackerFriendlyName } from "@main/entity";
 | 
			
		||||
import { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
 | 
			
		||||
import {
 | 
			
		||||
  migrationScriptRepository,
 | 
			
		||||
  repackRepository,
 | 
			
		||||
  repackerFriendlyNameRepository,
 | 
			
		||||
  steamGameRepository,
 | 
			
		||||
} from "@main/repository";
 | 
			
		||||
import { MigrationScript } from "@main/entity/migration-script.entity";
 | 
			
		||||
import { Like } from "typeorm";
 | 
			
		||||
| 
						 | 
				
			
			@ -115,11 +116,14 @@ export const resolveDatabaseUpdates = async () => {
 | 
			
		|||
    const updateRepackRepository = updateDataSource.getRepository(Repack);
 | 
			
		||||
    const updateRepackerFriendlyNameRepository =
 | 
			
		||||
      updateDataSource.getRepository(RepackerFriendlyName);
 | 
			
		||||
    const updateSteamGameRepository = updateDataSource.getRepository(SteamGame);
 | 
			
		||||
 | 
			
		||||
    const [updateRepacks, updateRepackerFriendlyNames] = await Promise.all([
 | 
			
		||||
      updateRepackRepository.find(),
 | 
			
		||||
      updateRepackerFriendlyNameRepository.find(),
 | 
			
		||||
    ]);
 | 
			
		||||
    const [updateRepacks, updateSteamGames, updateRepackerFriendlyNames] =
 | 
			
		||||
      await Promise.all([
 | 
			
		||||
        updateRepackRepository.find(),
 | 
			
		||||
        updateSteamGameRepository.find(),
 | 
			
		||||
        updateRepackerFriendlyNameRepository.find(),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
    await runMigrationScripts(updateRepacks);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -140,5 +144,16 @@ export const resolveDatabaseUpdates = async () => {
 | 
			
		|||
        .orIgnore()
 | 
			
		||||
        .execute();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const steamGamesChunks = chunk(updateSteamGames, 800);
 | 
			
		||||
 | 
			
		||||
    for (const chunk of steamGamesChunks) {
 | 
			
		||||
      await steamGameRepository
 | 
			
		||||
        .createQueryBuilder()
 | 
			
		||||
        .insert()
 | 
			
		||||
        .values(chunk)
 | 
			
		||||
        .orIgnore()
 | 
			
		||||
        .execute();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,6 +41,8 @@ contextBridge.exposeInMainWorld("electron", {
 | 
			
		|||
  getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
 | 
			
		||||
  getHowLongToBeat: (objectID: string, shop: GameShop, title: string) =>
 | 
			
		||||
    ipcRenderer.invoke("getHowLongToBeat", objectID, shop, title),
 | 
			
		||||
  getGames: (take?: number, prevCursor?: number) =>
 | 
			
		||||
    ipcRenderer.invoke("getGames", take, prevCursor),
 | 
			
		||||
 | 
			
		||||
  /* User preferences */
 | 
			
		||||
  getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								src/renderer/assets/lottie/settings.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/renderer/assets/lottie/settings.json
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -15,7 +15,8 @@ export interface HeaderProps {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
const pathTitle: Record<string, string> = {
 | 
			
		||||
  "/": "catalogue",
 | 
			
		||||
  "/": "home",
 | 
			
		||||
  "/catalogue": "catalogue",
 | 
			
		||||
  "/downloads": "downloads",
 | 
			
		||||
  "/settings": "settings",
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ export const heroMedia = style({
 | 
			
		|||
  transition: "all ease 0.2s",
 | 
			
		||||
  selectors: {
 | 
			
		||||
    [`${hero}:hover &`]: {
 | 
			
		||||
      transform: "scale(1.05)",
 | 
			
		||||
      transform: "scale(1.02)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
export const downloadIconWrapper = style({
 | 
			
		||||
  width: "16px",
 | 
			
		||||
  height: "12px",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadIcon = style({
 | 
			
		||||
  width: "24px",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  left: "-4px",
 | 
			
		||||
  top: "-9px",
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ import { useRef } from "react";
 | 
			
		|||
import Lottie from "lottie-react";
 | 
			
		||||
 | 
			
		||||
import downloadingAnimation from "@renderer/assets/lottie/downloading.json";
 | 
			
		||||
import * as styles from "./download-icon.css";
 | 
			
		||||
 | 
			
		||||
export interface DownloadIconProps {
 | 
			
		||||
  isDownloading: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -12,15 +11,12 @@ export function DownloadIcon({ isDownloading }: DownloadIconProps) {
 | 
			
		|||
  const lottieRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.downloadIconWrapper}>
 | 
			
		||||
      <Lottie
 | 
			
		||||
        lottieRef={lottieRef}
 | 
			
		||||
        animationData={downloadingAnimation}
 | 
			
		||||
        loop={isDownloading}
 | 
			
		||||
        autoplay={isDownloading}
 | 
			
		||||
        className={styles.downloadIcon}
 | 
			
		||||
        onDOMLoaded={() => lottieRef.current?.setSpeed(1.7)}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
    <Lottie
 | 
			
		||||
      lottieRef={lottieRef}
 | 
			
		||||
      animationData={downloadingAnimation}
 | 
			
		||||
      loop={isDownloading}
 | 
			
		||||
      autoplay={isDownloading}
 | 
			
		||||
      style={{ width: 16 }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,16 @@
 | 
			
		|||
import { GearIcon, ListUnorderedIcon } from "@primer/octicons-react";
 | 
			
		||||
import { AppsIcon, GearIcon, HomeIcon } from "@primer/octicons-react";
 | 
			
		||||
import { DownloadIcon } from "./download-icon";
 | 
			
		||||
 | 
			
		||||
export const routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: "/",
 | 
			
		||||
    nameKey: "home",
 | 
			
		||||
    render: () => <HomeIcon />,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/catalogue",
 | 
			
		||||
    nameKey: "catalogue",
 | 
			
		||||
    render: () => <ListUnorderedIcon />,
 | 
			
		||||
    render: () => <AppsIcon />,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/downloads",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								src/renderer/declaration.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/renderer/declaration.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -45,6 +45,10 @@ declare global {
 | 
			
		|||
      shop: GameShop,
 | 
			
		||||
      title: string
 | 
			
		||||
    ) => Promise<HowLongToBeatCategory[] | null>;
 | 
			
		||||
    getGames: (
 | 
			
		||||
      take?: number,
 | 
			
		||||
      prevCursor?: number
 | 
			
		||||
    ) => Promise<{ results: CatalogueEntry[]; cursor: number }>;
 | 
			
		||||
 | 
			
		||||
    /* Library */
 | 
			
		||||
    addGameToLibrary: (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,13 +3,11 @@ import type { PayloadAction } from "@reduxjs/toolkit";
 | 
			
		|||
 | 
			
		||||
interface WindowState {
 | 
			
		||||
  draggingDisabled: boolean;
 | 
			
		||||
  scrollingDisabled: boolean;
 | 
			
		||||
  headerTitle: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const initialState: WindowState = {
 | 
			
		||||
  draggingDisabled: false,
 | 
			
		||||
  scrollingDisabled: false,
 | 
			
		||||
  headerTitle: "",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,14 +18,10 @@ export const windowSlice = createSlice({
 | 
			
		|||
    toggleDragging: (state, action: PayloadAction<boolean>) => {
 | 
			
		||||
      state.draggingDisabled = action.payload;
 | 
			
		||||
    },
 | 
			
		||||
    toggleScrolling: (state, action: PayloadAction<boolean>) => {
 | 
			
		||||
      state.scrollingDisabled = action.payload;
 | 
			
		||||
    },
 | 
			
		||||
    setHeaderTitle: (state, action: PayloadAction<string>) => {
 | 
			
		||||
      state.headerTitle = action.payload;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const { toggleDragging, toggleScrolling, setHeaderTitle } =
 | 
			
		||||
  windowSlice.actions;
 | 
			
		||||
export const { toggleDragging, setHeaderTitle } = windowSlice.actions;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,11 +19,12 @@ import "react-loading-skeleton/dist/skeleton.css";
 | 
			
		|||
 | 
			
		||||
import { App } from "./app";
 | 
			
		||||
import {
 | 
			
		||||
  Catalogue,
 | 
			
		||||
  Home,
 | 
			
		||||
  Downloads,
 | 
			
		||||
  GameDetails,
 | 
			
		||||
  SearchResults,
 | 
			
		||||
  Settings,
 | 
			
		||||
  Catalogue,
 | 
			
		||||
} from "@renderer/pages";
 | 
			
		||||
 | 
			
		||||
import { store } from "./store";
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +42,10 @@ const router = createHashRouter([
 | 
			
		|||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: "/",
 | 
			
		||||
        Component: Home,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: "/catalogue",
 | 
			
		||||
        Component: Catalogue,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,141 +1,113 @@
 | 
			
		|||
import { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useNavigate, useSearchParams } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
import { Button, GameCard } from "@renderer/components";
 | 
			
		||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
import { Button, GameCard, Hero } from "@renderer/components";
 | 
			
		||||
import type { CatalogueCategory, CatalogueEntry } from "@types";
 | 
			
		||||
import type { CatalogueEntry } from "@types";
 | 
			
		||||
 | 
			
		||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./catalogue.css";
 | 
			
		||||
import { clearSearch } from "@renderer/features";
 | 
			
		||||
import { useAppDispatch } from "@renderer/hooks";
 | 
			
		||||
import { vars } from "@renderer/theme.css";
 | 
			
		||||
import Lottie from "lottie-react";
 | 
			
		||||
 | 
			
		||||
const categories: CatalogueCategory[] = ["trending", "recently_added"];
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import { useNavigate, useSearchParams } from "react-router-dom";
 | 
			
		||||
import * as styles from "../home/home.css";
 | 
			
		||||
import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react";
 | 
			
		||||
 | 
			
		||||
export function Catalogue() {
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation("catalogue");
 | 
			
		||||
 | 
			
		||||
  const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]);
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const contentRef = useRef<HTMLElement>(null);
 | 
			
		||||
 | 
			
		||||
  const cursorRef = useRef<number>(0);
 | 
			
		||||
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
 | 
			
		||||
  const randomGameObjectID = useRef<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  const [searchParams] = useSearchParams();
 | 
			
		||||
  const cursor = Number(searchParams.get("cursor") ?? 0);
 | 
			
		||||
 | 
			
		||||
  const [catalogue, setCatalogue] = useState<
 | 
			
		||||
    Record<CatalogueCategory, CatalogueEntry[]>
 | 
			
		||||
  >({
 | 
			
		||||
    trending: [],
 | 
			
		||||
    recently_added: [],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const getCatalogue = useCallback((category: CatalogueCategory) => {
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
 | 
			
		||||
    window.electron
 | 
			
		||||
      .getCatalogue(category)
 | 
			
		||||
      .then((catalogue) => {
 | 
			
		||||
        setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(() => {})
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        setIsLoading(false);
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const currentCategory = searchParams.get("category") || categories[0];
 | 
			
		||||
 | 
			
		||||
  const handleSelectCategory = (category: CatalogueCategory) => {
 | 
			
		||||
    if (category !== currentCategory) {
 | 
			
		||||
      getCatalogue(category);
 | 
			
		||||
      navigate(`/?category=${category}`, { replace: true });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getRandomGame = useCallback(() => {
 | 
			
		||||
    setIsLoadingRandomGame(true);
 | 
			
		||||
 | 
			
		||||
    window.electron
 | 
			
		||||
      .getRandomGame()
 | 
			
		||||
      .then((objectID) => {
 | 
			
		||||
        randomGameObjectID.current = objectID;
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        setIsLoadingRandomGame(false);
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleRandomizerClick = () => {
 | 
			
		||||
    const searchParams = new URLSearchParams({
 | 
			
		||||
      fromRandomizer: "1",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    navigate(
 | 
			
		||||
      `/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
 | 
			
		||||
    );
 | 
			
		||||
  const handleGameClick = (game: CatalogueEntry) => {
 | 
			
		||||
    dispatch(clearSearch());
 | 
			
		||||
    navigate(`/game/${game.shop}/${game.objectID}`);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (contentRef.current) contentRef.current.scrollTop = 0;
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    getCatalogue(currentCategory as CatalogueCategory);
 | 
			
		||||
    getRandomGame();
 | 
			
		||||
  }, [getCatalogue, currentCategory, getRandomGame]);
 | 
			
		||||
    setSearchResults([]);
 | 
			
		||||
 | 
			
		||||
    window.electron
 | 
			
		||||
      .getGames(24, cursor)
 | 
			
		||||
      .then(({ results, cursor }) => {
 | 
			
		||||
        return new Promise((resolve) => {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            cursorRef.current = cursor;
 | 
			
		||||
            setSearchResults(results);
 | 
			
		||||
            resolve(null);
 | 
			
		||||
          }, 500);
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        setIsLoading(false);
 | 
			
		||||
      });
 | 
			
		||||
  }, [dispatch, cursor, searchParams]);
 | 
			
		||||
 | 
			
		||||
  const handleNextPage = () => {
 | 
			
		||||
    const params = new URLSearchParams({
 | 
			
		||||
      cursor: cursorRef.current.toString(),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    navigate(`/catalogue?${params.toString()}`);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
 | 
			
		||||
      <section className={styles.content}>
 | 
			
		||||
        <h2>{t("featured")}</h2>
 | 
			
		||||
      <section
 | 
			
		||||
        style={{
 | 
			
		||||
          padding: `16px 32px`,
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          width: "100%",
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          borderBottom: `1px solid ${vars.color.borderColor}`,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => navigate(-1)}
 | 
			
		||||
          theme="outline"
 | 
			
		||||
          disabled={cursor === 0 || isLoading}
 | 
			
		||||
        >
 | 
			
		||||
          <ArrowLeftIcon />
 | 
			
		||||
          {t("previous_page")}
 | 
			
		||||
        </Button>
 | 
			
		||||
 | 
			
		||||
        <Hero />
 | 
			
		||||
        <Button onClick={handleNextPage} theme="outline" disabled={isLoading}>
 | 
			
		||||
          {t("next_page")}
 | 
			
		||||
          <ArrowRightIcon />
 | 
			
		||||
        </Button>
 | 
			
		||||
      </section>
 | 
			
		||||
 | 
			
		||||
        <section className={styles.catalogueHeader}>
 | 
			
		||||
          <div className={styles.catalogueCategories}>
 | 
			
		||||
            {categories.map((category) => (
 | 
			
		||||
              <Button
 | 
			
		||||
                key={category}
 | 
			
		||||
                theme={currentCategory === category ? "primary" : "outline"}
 | 
			
		||||
                onClick={() => handleSelectCategory(category)}
 | 
			
		||||
              >
 | 
			
		||||
                {t(category)}
 | 
			
		||||
              </Button>
 | 
			
		||||
      <section ref={contentRef} className={styles.content}>
 | 
			
		||||
        <section className={styles.cards}>
 | 
			
		||||
          {isLoading &&
 | 
			
		||||
            Array.from({ length: 12 }).map((_, index) => (
 | 
			
		||||
              <Skeleton key={index} className={styles.cardSkeleton} />
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={handleRandomizerClick}
 | 
			
		||||
            theme="outline"
 | 
			
		||||
            disabled={isLoadingRandomGame}
 | 
			
		||||
          >
 | 
			
		||||
            <div style={{ width: 16, height: 16, position: "relative" }}>
 | 
			
		||||
              <Lottie
 | 
			
		||||
                animationData={starsAnimation}
 | 
			
		||||
                style={{ width: 70, position: "absolute", top: -28, left: -27 }}
 | 
			
		||||
                loop
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            {t("surprise_me")}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
        <h2>{t(currentCategory)}</h2>
 | 
			
		||||
 | 
			
		||||
        <section className={styles.cards({})}>
 | 
			
		||||
          {isLoading
 | 
			
		||||
            ? Array.from({ length: 12 }).map((_, index) => (
 | 
			
		||||
                <Skeleton key={index} className={styles.cardSkeleton} />
 | 
			
		||||
              ))
 | 
			
		||||
            : catalogue[currentCategory as CatalogueCategory].map((result) => (
 | 
			
		||||
          {!isLoading && searchResults.length > 0 && (
 | 
			
		||||
            <>
 | 
			
		||||
              {searchResults.map((game) => (
 | 
			
		||||
                <GameCard
 | 
			
		||||
                  key={result.objectID}
 | 
			
		||||
                  game={result}
 | 
			
		||||
                  onClick={() =>
 | 
			
		||||
                    navigate(`/game/${result.shop}/${result.objectID}`)
 | 
			
		||||
                  }
 | 
			
		||||
                  key={game.objectID}
 | 
			
		||||
                  game={game}
 | 
			
		||||
                  onClick={() => handleGameClick(game)}
 | 
			
		||||
                  disabled={!game.repacks.length}
 | 
			
		||||
                />
 | 
			
		||||
              ))}
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </section>
 | 
			
		||||
      </section>
 | 
			
		||||
    </SkeletonTheme>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -217,16 +217,19 @@ export const howLongToBeatCategorySkeleton = style({
 | 
			
		|||
 | 
			
		||||
export const randomizerButton = style({
 | 
			
		||||
  animationName: slideIn,
 | 
			
		||||
  animationDuration: "0.4s",
 | 
			
		||||
  animationDuration: "0.2s",
 | 
			
		||||
  position: "fixed",
 | 
			
		||||
  bottom: 26 + 16,
 | 
			
		||||
  /* Bottom panel height + spacing */
 | 
			
		||||
  bottom: `${26 + SPACING_UNIT * 2}px`,
 | 
			
		||||
  /* Scroll bar + spacing */
 | 
			
		||||
  right: `${9 + SPACING_UNIT * 2}px`,
 | 
			
		||||
  boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px",
 | 
			
		||||
  border: `solid 1px ${vars.color.borderColor}`,
 | 
			
		||||
  backgroundColor: vars.color.darkBackground,
 | 
			
		||||
  border: `solid 2px ${vars.color.borderColor}`,
 | 
			
		||||
  backgroundColor: vars.color.background,
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    backgroundColor: vars.color.background,
 | 
			
		||||
    boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 15px 5px",
 | 
			
		||||
    opacity: 1,
 | 
			
		||||
    opacity: "1",
 | 
			
		||||
  },
 | 
			
		||||
  ":active": {
 | 
			
		||||
    transform: "scale(0.98)",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -104,7 +104,9 @@ export function HeroPanel({
 | 
			
		|||
    window.electron
 | 
			
		||||
      .showOpenDialog({
 | 
			
		||||
        properties: ["openFile"],
 | 
			
		||||
        filters: [{ name: "Game executable (.exe)", extensions: ["exe"] }],
 | 
			
		||||
        filters: [
 | 
			
		||||
          { name: "Game executable (.exe)", extensions: ["exe", "app"] },
 | 
			
		||||
        ],
 | 
			
		||||
      })
 | 
			
		||||
      .then(({ filePaths }) => {
 | 
			
		||||
        if (filePaths && filePaths.length > 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -209,11 +211,15 @@ export function HeroPanel({
 | 
			
		|||
            })}
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
          <p>
 | 
			
		||||
            {t("last_time_played", {
 | 
			
		||||
              period: lastTimePlayed,
 | 
			
		||||
            })}
 | 
			
		||||
          </p>
 | 
			
		||||
          {isGamePlaying ? (
 | 
			
		||||
            <p>{t("playing_now")}</p>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <p>
 | 
			
		||||
              {t("last_time_played", {
 | 
			
		||||
                period: lastTimePlayed,
 | 
			
		||||
              })}
 | 
			
		||||
            </p>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
import { SPACING_UNIT } from "@renderer/theme.css";
 | 
			
		||||
 | 
			
		||||
export const catalogueCategories = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
| 
						 | 
				
			
			@ -23,12 +23,4 @@ export const cards = recipe({
 | 
			
		|||
    gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    searching: {
 | 
			
		||||
      true: {
 | 
			
		||||
        pointerEvents: "none",
 | 
			
		||||
        opacity: vars.opacity.disabled,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +1,12 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
 | 
			
		||||
 | 
			
		||||
export const catalogueCategories = style({
 | 
			
		||||
export const homeCategories = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const catalogueHeader = style({
 | 
			
		||||
export const homeHeader = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  justifyContent: "space-between",
 | 
			
		||||
| 
						 | 
				
			
			@ -24,30 +23,20 @@ export const content = style({
 | 
			
		|||
  overflowY: "auto",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const cards = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    display: "grid",
 | 
			
		||||
    gridTemplateColumns: "repeat(1, 1fr)",
 | 
			
		||||
    gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    "@media": {
 | 
			
		||||
      "(min-width: 768px)": {
 | 
			
		||||
        gridTemplateColumns: "repeat(2, 1fr)",
 | 
			
		||||
      },
 | 
			
		||||
      "(min-width: 1250px)": {
 | 
			
		||||
        gridTemplateColumns: "repeat(3, 1fr)",
 | 
			
		||||
      },
 | 
			
		||||
      "(min-width: 1600px)": {
 | 
			
		||||
        gridTemplateColumns: "repeat(4, 1fr)",
 | 
			
		||||
      },
 | 
			
		||||
export const cards = style({
 | 
			
		||||
  display: "grid",
 | 
			
		||||
  gridTemplateColumns: "repeat(1, 1fr)",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  "@media": {
 | 
			
		||||
    "(min-width: 768px)": {
 | 
			
		||||
      gridTemplateColumns: "repeat(2, 1fr)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    searching: {
 | 
			
		||||
      true: {
 | 
			
		||||
        pointerEvents: "none",
 | 
			
		||||
        opacity: vars.opacity.disabled,
 | 
			
		||||
      },
 | 
			
		||||
    "(min-width: 1250px)": {
 | 
			
		||||
      gridTemplateColumns: "repeat(3, 1fr)",
 | 
			
		||||
    },
 | 
			
		||||
    "(min-width: 1600px)": {
 | 
			
		||||
      gridTemplateColumns: "repeat(4, 1fr)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										143
									
								
								src/renderer/pages/home/home.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/renderer/pages/home/home.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,143 @@
 | 
			
		|||
import { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useNavigate, useSearchParams } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
 | 
			
		||||
 | 
			
		||||
import { Button, GameCard, Hero } from "@renderer/components";
 | 
			
		||||
import type { CatalogueCategory, CatalogueEntry } from "@types";
 | 
			
		||||
 | 
			
		||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./home.css";
 | 
			
		||||
import { vars } from "@renderer/theme.css";
 | 
			
		||||
import Lottie from "lottie-react";
 | 
			
		||||
 | 
			
		||||
const categories: CatalogueCategory[] = ["trending", "recently_added"];
 | 
			
		||||
 | 
			
		||||
export function Home() {
 | 
			
		||||
  const { t } = useTranslation("home");
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
 | 
			
		||||
  const randomGameObjectID = useRef<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  const [searchParams] = useSearchParams();
 | 
			
		||||
 | 
			
		||||
  const [catalogue, setCatalogue] = useState<
 | 
			
		||||
    Record<CatalogueCategory, CatalogueEntry[]>
 | 
			
		||||
  >({
 | 
			
		||||
    trending: [],
 | 
			
		||||
    recently_added: [],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const getCatalogue = useCallback((category: CatalogueCategory) => {
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
 | 
			
		||||
    window.electron
 | 
			
		||||
      .getCatalogue(category)
 | 
			
		||||
      .then((catalogue) => {
 | 
			
		||||
        setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
 | 
			
		||||
      })
 | 
			
		||||
      .catch(() => {})
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        setIsLoading(false);
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const currentCategory = searchParams.get("category") || categories[0];
 | 
			
		||||
 | 
			
		||||
  const handleSelectCategory = (category: CatalogueCategory) => {
 | 
			
		||||
    if (category !== currentCategory) {
 | 
			
		||||
      getCatalogue(category);
 | 
			
		||||
      navigate(`/?category=${category}`, { replace: true });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getRandomGame = useCallback(() => {
 | 
			
		||||
    setIsLoadingRandomGame(true);
 | 
			
		||||
 | 
			
		||||
    window.electron
 | 
			
		||||
      .getRandomGame()
 | 
			
		||||
      .then((objectID) => {
 | 
			
		||||
        randomGameObjectID.current = objectID;
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        setIsLoadingRandomGame(false);
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleRandomizerClick = () => {
 | 
			
		||||
    const searchParams = new URLSearchParams({
 | 
			
		||||
      fromRandomizer: "1",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    navigate(
 | 
			
		||||
      `/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    getCatalogue(currentCategory as CatalogueCategory);
 | 
			
		||||
    getRandomGame();
 | 
			
		||||
  }, [getCatalogue, currentCategory, getRandomGame]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
 | 
			
		||||
      <section className={styles.content}>
 | 
			
		||||
        <h2>{t("featured")}</h2>
 | 
			
		||||
 | 
			
		||||
        <Hero />
 | 
			
		||||
 | 
			
		||||
        <section className={styles.homeHeader}>
 | 
			
		||||
          <div className={styles.homeCategories}>
 | 
			
		||||
            {categories.map((category) => (
 | 
			
		||||
              <Button
 | 
			
		||||
                key={category}
 | 
			
		||||
                theme={currentCategory === category ? "primary" : "outline"}
 | 
			
		||||
                onClick={() => handleSelectCategory(category)}
 | 
			
		||||
              >
 | 
			
		||||
                {t(category)}
 | 
			
		||||
              </Button>
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={handleRandomizerClick}
 | 
			
		||||
            theme="outline"
 | 
			
		||||
            disabled={isLoadingRandomGame}
 | 
			
		||||
          >
 | 
			
		||||
            <div style={{ width: 16, height: 16, position: "relative" }}>
 | 
			
		||||
              <Lottie
 | 
			
		||||
                animationData={starsAnimation}
 | 
			
		||||
                style={{ width: 70, position: "absolute", top: -28, left: -27 }}
 | 
			
		||||
                loop
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            {t("surprise_me")}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
        <h2>{t(currentCategory)}</h2>
 | 
			
		||||
 | 
			
		||||
        <section className={styles.cards}>
 | 
			
		||||
          {isLoading
 | 
			
		||||
            ? Array.from({ length: 12 }).map((_, index) => (
 | 
			
		||||
                <Skeleton key={index} className={styles.cardSkeleton} />
 | 
			
		||||
              ))
 | 
			
		||||
            : catalogue[currentCategory as CatalogueCategory].map((result) => (
 | 
			
		||||
                <GameCard
 | 
			
		||||
                  key={result.objectID}
 | 
			
		||||
                  game={result}
 | 
			
		||||
                  onClick={() =>
 | 
			
		||||
                    navigate(`/game/${result.shop}/${result.objectID}`)
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
              ))}
 | 
			
		||||
        </section>
 | 
			
		||||
      </section>
 | 
			
		||||
    </SkeletonTheme>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,12 +13,12 @@ import { vars } from "@renderer/theme.css";
 | 
			
		|||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useNavigate, useSearchParams } from "react-router-dom";
 | 
			
		||||
import * as styles from "./catalogue.css";
 | 
			
		||||
import * as styles from "./home.css";
 | 
			
		||||
 | 
			
		||||
export function SearchResults() {
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation("catalogue");
 | 
			
		||||
  const { t } = useTranslation("home");
 | 
			
		||||
  const [searchParams] = useSearchParams();
 | 
			
		||||
 | 
			
		||||
  const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]);
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +54,7 @@ export function SearchResults() {
 | 
			
		|||
  return (
 | 
			
		||||
    <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
 | 
			
		||||
      <section className={styles.content}>
 | 
			
		||||
        <section className={styles.cards({ searching: false })}>
 | 
			
		||||
        <section className={styles.cards}>
 | 
			
		||||
          {isLoading &&
 | 
			
		||||
            Array.from({ length: 12 }).map((_, index) => (
 | 
			
		||||
              <Skeleton key={index} className={styles.cardSkeleton} />
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
export * from "./catalogue/catalogue";
 | 
			
		||||
export * from "./home/home";
 | 
			
		||||
export * from "./game-details/game-details";
 | 
			
		||||
export * from "./downloads/downloads";
 | 
			
		||||
export * from "./catalogue/search-results";
 | 
			
		||||
export * from "./home/search-results";
 | 
			
		||||
export * from "./settings/settings";
 | 
			
		||||
export * from "./catalogue/catalogue";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue