mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-02 16:23:48 +03:00
feat: updating play label on hero panel
This commit is contained in:
parent
91b1341271
commit
96e11e6be9
@ -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);
|
||||
|
||||
const steamResults = await searchSteamGame(formattedName);
|
||||
|
||||
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);
|
||||
export interface SearchGamesArgs {
|
||||
query?: string;
|
||||
take?: number;
|
||||
skip?: number;
|
||||
}
|
||||
|
||||
const filteredResults = gamesIndex
|
||||
.search(query)
|
||||
.map((index) => results[index as number]);
|
||||
export const searchGames = async ({
|
||||
query,
|
||||
take,
|
||||
skip,
|
||||
}: SearchGamesArgs): Promise<CatalogueEntry[]> => {
|
||||
const options: FindManyOptions<SteamGame> = {};
|
||||
|
||||
if (query) {
|
||||
options.where = {
|
||||
name: query ? Like(`%${formatName(query)}%`) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
|
||||
}
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
@ -68,8 +70,10 @@ export const startProcessWatcher = async () => {
|
||||
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
gamesPlaytime.delete(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 getTrendingGames = async () => {
|
||||
const response = await requestSteam250("/365day").catch((err) => {
|
||||
logger.error(err.response, { method: "getTrendingGames" });
|
||||
throw new Error(err);
|
||||
});
|
||||
|
||||
const { window } = new JSDOM(response);
|
||||
export const requestSteam250 = async (path: string) => {
|
||||
return axios.get(`https://steam250.com${path}`).then((response) => {
|
||||
const { window } = new JSDOM(response.data);
|
||||
const { document } = window;
|
||||
|
||||
return Array.from(document.querySelectorAll(".appline .title a")).map(
|
||||
($title) => $title.textContent!
|
||||
($title: HTMLAnchorElement) => {
|
||||
const steamGameUrl = $title.href;
|
||||
if (!steamGameUrl) return null;
|
||||
|
||||
return {
|
||||
title: $title.textContent,
|
||||
objectID: steamGameUrl.split("/").pop(),
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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,9 +116,12 @@ 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([
|
||||
const [updateRepacks, updateSteamGames, updateRepackerFriendlyNames] =
|
||||
await Promise.all([
|
||||
updateRepackRepository.find(),
|
||||
updateSteamGameRepository.find(),
|
||||
updateRepackerFriendlyNameRepository.find(),
|
||||
]);
|
||||
|
||||
@ -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)}
|
||||
style={{ width: 16 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
<Hero />
|
||||
|
||||
<section className={styles.catalogueHeader}>
|
||||
<div className={styles.catalogueCategories}>
|
||||
{categories.map((category) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={currentCategory === category ? "primary" : "outline"}
|
||||
onClick={() => handleSelectCategory(category)}
|
||||
<section
|
||||
style={{
|
||||
padding: `16px 32px`,
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: `1px solid ${vars.color.borderColor}`,
|
||||
}}
|
||||
>
|
||||
{t(category)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleRandomizerClick}
|
||||
onClick={() => navigate(-1)}
|
||||
theme="outline"
|
||||
disabled={isLoadingRandomGame}
|
||||
disabled={cursor === 0 || isLoading}
|
||||
>
|
||||
<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")}
|
||||
<ArrowLeftIcon />
|
||||
{t("previous_page")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleNextPage} theme="outline" disabled={isLoading}>
|
||||
{t("next_page")}
|
||||
<ArrowRightIcon />
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<h2>{t(currentCategory)}</h2>
|
||||
|
||||
<section className={styles.cards({})}>
|
||||
{isLoading
|
||||
? Array.from({ length: 12 }).map((_, index) => (
|
||||
<section ref={contentRef} className={styles.content}>
|
||||
<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>
|
||||
|
||||
{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,8 +23,7 @@ export const content = style({
|
||||
overflowY: "auto",
|
||||
});
|
||||
|
||||
export const cards = recipe({
|
||||
base: {
|
||||
export const cards = style({
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(1, 1fr)",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
@ -41,15 +39,6 @@ export const cards = recipe({
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
searching: {
|
||||
true: {
|
||||
pointerEvents: "none",
|
||||
opacity: vars.opacity.disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const cardSkeleton = style({
|
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…
Reference in New Issue
Block a user