diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6ec93984..6da066af 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -26,4 +26,9 @@ module.exports = { }, ], }, + settings: { + react: { + version: "detect", + }, + }, }; diff --git a/package.json b/package.json index 848eac78..33f05e2b 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "electron-log": "^5.2.4", "electron-updater": "^6.3.9", "file-type": "^19.6.0", - "flexsearch": "^0.7.43", "i18next": "^23.11.2", "i18next-browser-languagedetector": "^7.2.1", "jsdom": "^24.0.0", @@ -64,11 +63,13 @@ "lodash-es": "^4.17.21", "parse-torrent": "^11.0.17", "piscina": "^4.7.0", + "rc-virtual-list": "^3.16.1", "react-hook-form": "^7.53.0", "react-i18next": "^14.1.0", "react-loading-skeleton": "^3.4.0", "react-redux": "^9.1.1", "react-router-dom": "^6.22.3", + "react-virtualized": "^9.22.5", "sound-play": "^1.1.0", "sudo-prompt": "^9.2.1", "tar": "^7.4.3", diff --git a/scripts/upload-build.cjs b/scripts/upload-build.cjs index 9f19ca89..c00b565a 100644 --- a/scripts/upload-build.cjs +++ b/scripts/upload-build.cjs @@ -20,7 +20,7 @@ const s3 = new S3Client({ const dist = path.resolve(__dirname, "..", "dist"); -const extensionsToUpload = [".deb", ".exe"]; +const extensionsToUpload = [".deb", ".exe", ".pacman"]; fs.readdir(dist, async (err, files) => { if (err) throw err; diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 74617a2e..64d1370f 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -284,8 +284,11 @@ "instructions": "Verifique a forma correta de instalar algum deles no seu distro Linux, garantindo assim a execução normal do jogo" }, "catalogue": { - "next_page": "Próxima página", - "previous_page": "Página anterior" + "search": "Pesquisar…", + "developers": "Desenvolvedores", + "genres": "Gêneros", + "tags": "Tags", + "download_sources": "Fontes de download" }, "modal": { "close": "Botão de fechar" diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 80a40f47..51c8522e 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -1,10 +1,8 @@ import { DataSource } from "typeorm"; import { DownloadQueue, - DownloadSource, Game, GameShopCache, - Repack, UserPreferences, UserAuth, GameAchievement, @@ -17,12 +15,10 @@ export const dataSource = new DataSource({ type: "better-sqlite3", entities: [ Game, - Repack, UserAuth, UserPreferences, UserSubscription, GameShopCache, - DownloadSource, DownloadQueue, GameAchievement, ], diff --git a/src/main/entity/download-source.entity.ts b/src/main/entity/download-source.entity.ts deleted file mode 100644 index dc59bac4..00000000 --- a/src/main/entity/download-source.entity.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToMany, -} from "typeorm"; -import type { Repack } from "./repack.entity"; - -import { DownloadSourceStatus } from "@shared"; - -@Entity("download_source") -export class DownloadSource { - @PrimaryGeneratedColumn() - id: number; - - @Column("text", { nullable: true, unique: true }) - url: string; - - @Column("text") - name: string; - - @Column("text", { nullable: true }) - etag: string | null; - - @Column("int", { default: 0 }) - downloadCount: number; - - @Column("text", { default: DownloadSourceStatus.UpToDate }) - status: DownloadSourceStatus; - - @OneToMany("Repack", "downloadSource", { cascade: true }) - repacks: Repack[]; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index baa6355c..8dfc4fae 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -5,9 +5,7 @@ import { CreateDateColumn, UpdateDateColumn, OneToOne, - JoinColumn, } from "typeorm"; -import { Repack } from "./repack.entity"; import type { GameShop, GameStatus } from "@types"; import { Downloader } from "@shared"; @@ -72,13 +70,6 @@ export class Game { @Column("text", { nullable: true }) uri: string | null; - /** - * @deprecated - */ - @OneToOne("Repack", "game", { nullable: true }) - @JoinColumn() - repack: Repack; - @OneToOne("DownloadQueue", "game") downloadQueue: DownloadQueue; diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index 5829e6a2..1625ac8a 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,10 +1,8 @@ export * from "./game.entity"; -export * from "./repack.entity"; export * from "./user-auth.entity"; export * from "./user-preferences.entity"; export * from "./user-subscription.entity"; export * from "./game-shop-cache.entity"; export * from "./game.entity"; export * from "./game-achievements.entity"; -export * from "./download-source.entity"; export * from "./download-queue.entity"; diff --git a/src/main/entity/repack.entity.ts b/src/main/entity/repack.entity.ts deleted file mode 100644 index 36de2a7c..00000000 --- a/src/main/entity/repack.entity.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, -} from "typeorm"; -import { DownloadSource } from "./download-source.entity"; - -@Entity("repack") -export class Repack { - @PrimaryGeneratedColumn() - id: number; - - @Column("text", { unique: true }) - title: string; - - /** - * @deprecated Use uris instead - */ - @Column("text", { unique: true }) - magnet: string; - - @Column("text") - repacker: string; - - @Column("text") - fileSize: string; - - @Column("datetime") - uploadDate: Date | string; - - @ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" }) - downloadSource: DownloadSource; - - @Column("text", { default: "[]" }) - uris: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/events/catalogue/get-catalogue.ts b/src/main/events/catalogue/get-catalogue.ts index a0542f25..2c2914aa 100644 --- a/src/main/events/catalogue/get-catalogue.ts +++ b/src/main/events/catalogue/get-catalogue.ts @@ -1,9 +1,6 @@ -import type { GameShop } from "@types"; - import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import { CatalogueCategory, steamUrlBuilder } from "@shared"; -import { steamGamesWorker } from "@main/workers"; +import { CatalogueCategory } from "@shared"; const getCatalogue = async ( _event: Electron.IpcMainInvokeEvent, @@ -14,26 +11,11 @@ const getCatalogue = async ( skip: "0", }); - const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>( + return HydraApi.get( `/catalogue/${category}?${params.toString()}`, {}, { needsAuth: false } ); - - return Promise.all( - response.map(async (game) => { - const steamGame = await steamGamesWorker.run(Number(game.objectId), { - name: "getById", - }); - - return { - title: steamGame.name, - shop: game.shop, - cover: steamUrlBuilder.library(game.objectId), - objectId: game.objectId, - }; - }) - ); }; registerEvent("getCatalogue", getCatalogue); diff --git a/src/main/events/catalogue/get-developers.ts b/src/main/events/catalogue/get-developers.ts new file mode 100644 index 00000000..76ae566b --- /dev/null +++ b/src/main/events/catalogue/get-developers.ts @@ -0,0 +1,10 @@ +import { HydraApi } from "@main/services"; +import { registerEvent } from "../register-event"; + +const getDevelopers = async (_event: Electron.IpcMainInvokeEvent) => { + return HydraApi.get(`/catalogue/developers`, null, { + needsAuth: false, + }); +}; + +registerEvent("getDevelopers", getDevelopers); diff --git a/src/main/events/catalogue/get-games.ts b/src/main/events/catalogue/get-games.ts deleted file mode 100644 index 3eb1f135..00000000 --- a/src/main/events/catalogue/get-games.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { CatalogueEntry } from "@types"; - -import { registerEvent } from "../register-event"; -import { HydraApi } from "@main/services"; -import { steamUrlBuilder } from "@shared"; - -const getGames = async ( - _event: Electron.IpcMainInvokeEvent, - take = 12, - skip = 0 -): Promise => { - const searchParams = new URLSearchParams({ - take: take.toString(), - skip: skip.toString(), - }); - - const games = await HydraApi.get( - `/games/catalogue?${searchParams.toString()}`, - undefined, - { needsAuth: false } - ); - - return games.map((game) => ({ - ...game, - cover: steamUrlBuilder.library(game.objectId), - })); -}; - -registerEvent("getGames", getGames); diff --git a/src/main/events/catalogue/get-publishers.ts b/src/main/events/catalogue/get-publishers.ts new file mode 100644 index 00000000..3b8fdc5f --- /dev/null +++ b/src/main/events/catalogue/get-publishers.ts @@ -0,0 +1,10 @@ +import { HydraApi } from "@main/services"; +import { registerEvent } from "../register-event"; + +const getPublishers = async (_event: Electron.IpcMainInvokeEvent) => { + return HydraApi.get(`/catalogue/publishers`, null, { + needsAuth: false, + }); +}; + +registerEvent("getPublishers", getPublishers); diff --git a/src/main/events/catalogue/search-games.ts b/src/main/events/catalogue/search-games.ts index 4ce42fd8..40b3b033 100644 --- a/src/main/events/catalogue/search-games.ts +++ b/src/main/events/catalogue/search-games.ts @@ -1,23 +1,16 @@ +import type { CatalogueSearchPayload } from "@types"; import { registerEvent } from "../register-event"; -import { convertSteamGameToCatalogueEntry } from "../helpers/search-games"; -import type { CatalogueEntry } from "@types"; import { HydraApi } from "@main/services"; -const searchGamesEvent = async ( +const searchGames = async ( _event: Electron.IpcMainInvokeEvent, - query: string -): Promise => { - const games = await HydraApi.get< - { objectId: string; title: string; shop: string }[] - >("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false }); - - return games.map((game) => { - return convertSteamGameToCatalogueEntry({ - id: Number(game.objectId), - name: game.title, - clientIcon: null, - }); - }); + payload: CatalogueSearchPayload +) => { + return HydraApi.post( + "/catalogue/search", + { ...payload, take: 24, skip: 0 }, + { needsAuth: false } + ); }; -registerEvent("searchGames", searchGamesEvent); +registerEvent("searchGames", searchGames); diff --git a/src/main/events/download-sources/delete-download-source.ts b/src/main/events/download-sources/delete-download-source.ts deleted file mode 100644 index abfbf661..00000000 --- a/src/main/events/download-sources/delete-download-source.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { registerEvent } from "../register-event"; -import { knexClient } from "@main/knex-client"; - -const deleteDownloadSource = async ( - _event: Electron.IpcMainInvokeEvent, - id: number -) => knexClient("download_source").where({ id }).delete(); - -registerEvent("deleteDownloadSource", deleteDownloadSource); diff --git a/src/main/events/download-sources/get-download-sources.ts b/src/main/events/download-sources/get-download-sources.ts deleted file mode 100644 index 97f8a6d8..00000000 --- a/src/main/events/download-sources/get-download-sources.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { registerEvent } from "../register-event"; -import { knexClient } from "@main/knex-client"; - -const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => - knexClient.select("*").from("download_source"); - -registerEvent("getDownloadSources", getDownloadSources); diff --git a/src/main/events/download-sources/put-download-source.ts b/src/main/events/download-sources/put-download-source.ts new file mode 100644 index 00000000..72297059 --- /dev/null +++ b/src/main/events/download-sources/put-download-source.ts @@ -0,0 +1,17 @@ +import { HydraApi } from "@main/services"; +import { registerEvent } from "../register-event"; + +const putDownloadSource = async ( + _event: Electron.IpcMainInvokeEvent, + objectIds: string[] +) => { + return HydraApi.put<{ fingerprint: string }>( + "/download-sources", + { + objectIds, + }, + { needsAuth: false } + ); +}; + +registerEvent("putDownloadSource", putDownloadSource); diff --git a/src/main/events/helpers/search-games.ts b/src/main/events/helpers/search-games.ts deleted file mode 100644 index 74e0b6a8..00000000 --- a/src/main/events/helpers/search-games.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { GameShop, CatalogueEntry, SteamGame } from "@types"; - -import { steamGamesWorker } from "@main/workers"; -import { steamUrlBuilder } from "@shared"; - -export interface SearchGamesArgs { - query?: string; - take?: number; - skip?: number; -} - -export const convertSteamGameToCatalogueEntry = ( - game: SteamGame -): CatalogueEntry => ({ - objectId: String(game.id), - title: game.name, - shop: "steam" as GameShop, - cover: steamUrlBuilder.library(String(game.id)), -}); - -export const getSteamGameById = async ( - objectId: string -): Promise => { - const steamGame = await steamGamesWorker.run(Number(objectId), { - name: "getById", - }); - - if (!steamGame) return null; - - return convertSteamGameToCatalogueEntry(steamGame); -}; diff --git a/src/main/events/index.ts b/src/main/events/index.ts index fd89ba30..86b14988 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -3,12 +3,13 @@ import { ipcMain } from "electron"; import "./catalogue/get-catalogue"; import "./catalogue/get-game-shop-details"; -import "./catalogue/get-games"; import "./catalogue/get-how-long-to-beat"; import "./catalogue/get-random-game"; import "./catalogue/search-games"; import "./catalogue/get-game-stats"; import "./catalogue/get-trending-games"; +import "./catalogue/get-publishers"; +import "./catalogue/get-developers"; import "./hardware/get-disk-free-space"; import "./library/add-game-to-library"; import "./library/create-game-shortcut"; @@ -40,8 +41,7 @@ import "./user-preferences/auto-launch"; import "./autoupdater/check-for-updates"; import "./autoupdater/restart-and-install-update"; import "./user-preferences/authenticate-real-debrid"; -import "./download-sources/delete-download-source"; -import "./download-sources/get-download-sources"; +import "./download-sources/put-download-source"; import "./auth/sign-out"; import "./auth/open-auth-window"; import "./auth/get-session-hash"; diff --git a/src/main/repository.ts b/src/main/repository.ts index cf3ab143..e0c4204e 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -1,10 +1,8 @@ import { dataSource } from "./data-source"; import { DownloadQueue, - DownloadSource, Game, GameShopCache, - Repack, UserPreferences, UserAuth, GameAchievement, @@ -13,16 +11,11 @@ import { export const gameRepository = dataSource.getRepository(Game); -export const repackRepository = dataSource.getRepository(Repack); - export const userPreferencesRepository = dataSource.getRepository(UserPreferences); export const gameShopCacheRepository = dataSource.getRepository(GameShopCache); -export const downloadSourceRepository = - dataSource.getRepository(DownloadSource); - export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); export const userAuthRepository = dataSource.getRepository(UserAuth); diff --git a/src/preload/index.ts b/src/preload/index.ts index 07c8c598..8548754b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,6 +11,7 @@ import type { GameRunning, FriendRequestAction, UpdateProfileRequest, + CatalogueSearchPayload, SeedingStatus, GameAchievement, } from "@types"; @@ -54,7 +55,8 @@ contextBridge.exposeInMainWorld("electron", { }, /* Catalogue */ - searchGames: (query: string) => ipcRenderer.invoke("searchGames", query), + searchGames: (payload: CatalogueSearchPayload) => + ipcRenderer.invoke("searchGames", payload), getCatalogue: (category: CatalogueCategory) => ipcRenderer.invoke("getCatalogue", category), getGameShopDetails: (objectId: string, shop: GameShop, language: string) => @@ -62,10 +64,6 @@ contextBridge.exposeInMainWorld("electron", { getRandomGame: () => ipcRenderer.invoke("getRandomGame"), getHowLongToBeat: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getHowLongToBeat", objectId, shop), - getGames: (take?: number, skip?: number) => - ipcRenderer.invoke("getGames", take, skip), - searchGameRepacks: (query: string) => - ipcRenderer.invoke("searchGameRepacks", query), getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"), @@ -85,6 +83,8 @@ contextBridge.exposeInMainWorld("electron", { listener ); }, + getPublishers: () => ipcRenderer.invoke("getPublishers"), + getDevelopers: () => ipcRenderer.invoke("getDevelopers"), /* User preferences */ getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"), @@ -96,9 +96,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("authenticateRealDebrid", apiToken), /* Download sources */ - getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"), - deleteDownloadSource: (id: number) => - ipcRenderer.invoke("deleteDownloadSource", id), + putDownloadSource: (objectIds: string[]) => + ipcRenderer.invoke("putDownloadSource", objectIds), /* Library */ addGameToLibrary: (objectId: string, title: string, shop: GameShop) => diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index cad14f55..a8d0ea06 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; @@ -7,6 +7,7 @@ import { useAppSelector, useDownload, useLibrary, + useRepacks, useToast, useUserDetails, } from "@renderer/hooks"; @@ -15,8 +16,6 @@ import * as styles from "./app.css"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { - setSearch, - clearSearch, setUserPreferences, toggleDraggingDisabled, closeToast, @@ -44,9 +43,9 @@ export function App() { const downloadSourceMigrationLock = useRef(false); - const { clearDownload, setLastPacket } = useDownload(); + const { updateRepacks } = useRepacks(); - const { indexRepacks } = useContext(repacksContext); + const { clearDownload, setLastPacket } = useDownload(); const { userDetails, @@ -69,8 +68,6 @@ export function App() { const navigate = useNavigate(); const location = useLocation(); - const search = useAppSelector((state) => state.search.value); - const draggingDisabled = useAppSelector( (state) => state.window.draggingDisabled ); @@ -197,31 +194,6 @@ export function App() { }; }, [onSignIn, updateLibrary, clearUserDetails]); - const handleSearch = useCallback( - (query: string) => { - dispatch(setSearch(query)); - - if (query === "") { - navigate(-1); - return; - } - - const searchParams = new URLSearchParams({ - query, - }); - - navigate(`/search?${searchParams.toString()}`, { - replace: location.pathname.startsWith("/search"), - }); - }, - [dispatch, location.pathname, navigate] - ); - - const handleClear = useCallback(() => { - dispatch(clearSearch()); - navigate(-1); - }, [dispatch, navigate]); - useEffect(() => { if (contentRef.current) contentRef.current.scrollTop = 0; }, [location.pathname, location.search]); @@ -242,49 +214,19 @@ export function App() { downloadSourceMigrationLock.current = true; - window.electron.getDownloadSources().then(async (downloadSources) => { - if (!downloadSources.length) { - const id = crypto.randomUUID(); - const channel = new BroadcastChannel(`download_sources:sync:${id}`); + updateRepacks(); - channel.onmessage = (event: MessageEvent) => { - const newRepacksCount = event.data; - window.electron.publishNewRepacksNotification(newRepacksCount); - }; + const id = crypto.randomUUID(); + const channel = new BroadcastChannel(`download_sources:sync:${id}`); - downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]); - } + channel.onmessage = (event: MessageEvent) => { + const newRepacksCount = event.data; + window.electron.publishNewRepacksNotification(newRepacksCount); + updateRepacks(); + }; - for (const downloadSource of downloadSources) { - logger.info("Migrating download source", downloadSource.url); - - const channel = new BroadcastChannel( - `download_sources:import:${downloadSource.url}` - ); - await new Promise((resolve) => { - downloadSourcesWorker.postMessage([ - "IMPORT_DOWNLOAD_SOURCE", - downloadSource.url, - ]); - - channel.onmessage = () => { - window.electron.deleteDownloadSource(downloadSource.id).then(() => { - resolve(true); - logger.info( - "Deleted download source from SQLite", - downloadSource.url - ); - }); - - indexRepacks(); - channel.close(); - }; - }).catch(() => channel.close()); - } - - downloadSourceMigrationLock.current = false; - }); - }, [indexRepacks]); + downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]); + }, [updateRepacks]); const handleToastClose = useCallback(() => { dispatch(closeToast()); @@ -328,11 +270,7 @@ export function App() {
-
+
diff --git a/src/renderer/src/components/badge/badge.tsx b/src/renderer/src/components/badge/badge.tsx index 752a33ba..c4819ae3 100644 --- a/src/renderer/src/components/badge/badge.tsx +++ b/src/renderer/src/components/badge/badge.tsx @@ -7,9 +7,5 @@ export interface BadgeProps { } export function Badge({ children }: BadgeProps) { - return ( -
- {children} -
- ); + return
{children}
; } diff --git a/src/renderer/src/components/checkbox-field/checkbox-field.css.ts b/src/renderer/src/components/checkbox-field/checkbox-field.css.ts index 606b226a..ce7aead8 100644 --- a/src/renderer/src/components/checkbox-field/checkbox-field.css.ts +++ b/src/renderer/src/components/checkbox-field/checkbox-field.css.ts @@ -1,6 +1,7 @@ import { style } from "@vanilla-extract/css"; import { SPACING_UNIT, vars } from "../../theme.css"; +import { recipe } from "@vanilla-extract/recipes"; export const checkboxField = style({ display: "flex", @@ -10,19 +11,31 @@ export const checkboxField = style({ cursor: "pointer", }); -export const checkbox = style({ - width: "20px", - height: "20px", - borderRadius: "4px", - backgroundColor: vars.color.darkBackground, - display: "flex", - justifyContent: "center", - alignItems: "center", - position: "relative", - transition: "all ease 0.2s", - border: `solid 1px ${vars.color.border}`, - ":hover": { - borderColor: "rgba(255, 255, 255, 0.5)", +export const checkbox = recipe({ + base: { + width: "20px", + height: "20px", + borderRadius: "4px", + backgroundColor: vars.color.darkBackground, + display: "flex", + justifyContent: "center", + alignItems: "center", + position: "relative", + transition: "all ease 0.2s", + border: `solid 1px ${vars.color.border}`, + minWidth: "20px", + minHeight: "20px", + color: vars.color.darkBackground, + ":hover": { + borderColor: "rgba(255, 255, 255, 0.5)", + }, + }, + variants: { + checked: { + true: { + backgroundColor: vars.color.muted, + }, + }, }, }); @@ -38,4 +51,7 @@ export const checkboxInput = style({ export const checkboxLabel = style({ cursor: "pointer", + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", }); diff --git a/src/renderer/src/components/checkbox-field/checkbox-field.tsx b/src/renderer/src/components/checkbox-field/checkbox-field.tsx index bb81a910..f40c05c2 100644 --- a/src/renderer/src/components/checkbox-field/checkbox-field.tsx +++ b/src/renderer/src/components/checkbox-field/checkbox-field.tsx @@ -15,7 +15,7 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) { return (
-
+
, HTMLButtonElement > { - game: CatalogueEntry; + game: any; } const shopIcon = { @@ -26,20 +26,12 @@ export function GameCard({ game, ...props }: GameCardProps) { const { t } = useTranslation("game_card"); const [stats, setStats] = useState(null); - const [repacks, setRepacks] = useState([]); - const { searchRepacks, isIndexingRepacks } = useContext(repacksContext); - - useEffect(() => { - if (!isIndexingRepacks) { - searchRepacks(game.title).then((repacks) => { - setRepacks(repacks); - }); - } - }, [game, isIndexingRepacks, searchRepacks]); + const { getRepacksForObjectId } = useRepacks(); + const repacks = getRepacksForObjectId(game.objectId); const uniqueRepackers = Array.from( - new Set(repacks.map(({ repacker }) => repacker)) + new Set(repacks.map((repack) => repack.repacker)) ); const handleHover = useCallback(() => { @@ -61,7 +53,7 @@ export function GameCard({ game, ...props }: GameCardProps) { >
{game.title} void; - onClear: () => void; - search?: string; -} +import { setSearch } from "@renderer/features"; const pathTitle: Record = { "/": "home", @@ -22,7 +16,7 @@ const pathTitle: Record = { "/settings": "settings", }; -export function Header({ onSearch, onClear, search }: HeaderProps) { +export function Header() { const inputRef = useRef(null); const navigate = useNavigate(); @@ -31,6 +25,11 @@ export function Header({ onSearch, onClear, search }: HeaderProps) { const { headerTitle, draggingDisabled } = useAppSelector( (state) => state.window ); + + const searchValue = useAppSelector( + (state) => state.catalogueSearch.value.title + ); + const dispatch = useAppDispatch(); const [isFocused, setIsFocused] = useState(false); @@ -46,12 +45,6 @@ export function Header({ onSearch, onClear, search }: HeaderProps) { return t(pathTitle[location.pathname]); }, [location.pathname, headerTitle, t]); - useEffect(() => { - if (search && !location.pathname.startsWith("/search")) { - dispatch(clearSearch()); - } - }, [location.pathname, search, dispatch]); - const focusInput = () => { setIsFocused(true); inputRef.current?.focus(); @@ -65,6 +58,14 @@ export function Header({ onSearch, onClear, search }: HeaderProps) { navigate(-1); }; + const handleSearch = (value: string) => { + dispatch(setSearch({ title: value })); + + if (!location.pathname.startsWith("/catalogue")) { + navigate("/catalogue"); + } + }; + return ( <>
onSearch(event.target.value)} + onChange={(event) => handleSearch(event.target.value)} onFocus={() => setIsFocused(true)} onBlur={handleBlur} /> - {search && ( + {searchValue && ( + +
+ + {/* */} +
+ +
+
- - {t("previous_page")} - - - -
- -
-
- {isLoading && - Array.from({ length: 12 }).map((_, index) => ( - - ))} - - {!isLoading && searchResults.length > 0 && ( - <> - {searchResults.map((game) => ( - handleGameClick(game)} + {isLoading ? ( + + {Array.from({ length: 24 }).map((_, i) => ( + ))} - + + ) : ( + gamesWithRepacks.map((game, i) => ( + + )) )} -
-
- + + {/*
+ + +
*/} + + +
+
+ dispatch(setSearch({ genres: [] }))} + color={filterCategoryColors.genres} + onSelect={(value) => { + if (filters.genres.includes(value)) { + dispatch( + setSearch({ + genres: filters.genres.filter((genre) => genre !== value), + }) + ); + } else { + dispatch(setSearch({ genres: [...filters.genres, value] })); + } + }} + items={[ + "Action", + "Strategy", + "RPG", + "Casual", + "Racing", + "Sports", + "Indie", + "Adventure", + "Simulation", + "Massively Multiplayer", + "Free to Play", + "Accounting", + "Animation & Modeling", + "Audio Production", + "Design & Illustration", + "Education", + "Photo Editing", + "Software Training", + "Utilities", + "Video Production", + "Web Publishing", + "Game Development", + "Early Access", + "Sexual Content", + "Nudity", + "Violent", + "Gore", + "Documentary", + "Tutorial", + ] + .sort() + .map((genre) => ({ + label: genre, + value: genre, + checked: filters.genres.includes(genre), + }))} + /> + + dispatch(setSearch({ tags: [] }))} + onSelect={(value) => { + if (filters.tags.includes(Number(value))) { + dispatch( + setSearch({ + tags: filters.tags.filter((tag) => tag !== Number(value)), + }) + ); + } else { + dispatch( + setSearch({ tags: [...filters.tags, Number(value)] }) + ); + } + }} + items={ + Object.entries(steamUserTags) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + .map(([key, value]) => ({ + label: key, + value: value, + checked: filters.tags.includes(value as number), + })) as any + } + /> + + + dispatch(setSearch({ downloadSourceFingerprints: [] })) + } + onSelect={(value) => { + if (filters.downloadSourceFingerprints.includes(value)) { + dispatch( + setSearch({ + downloadSourceFingerprints: + filters.downloadSourceFingerprints.filter( + (fingerprint) => fingerprint !== value + ), + }) + ); + } else { + dispatch( + setSearch({ + downloadSourceFingerprints: [ + ...filters.downloadSourceFingerprints, + value, + ], + }) + ); + } + }} + items={downloadSources.map((downloadSource) => ({ + label: downloadSource.name, + value: downloadSource.fingerprint, + checked: filters.downloadSourceFingerprints.includes( + downloadSource.fingerprint + ), + }))} + /> + + dispatch(setSearch({ developers: [] }))} + onSelect={(value) => { + if (filters.developers.includes(value)) { + dispatch( + setSearch({ + developers: filters.developers.filter( + (developer) => developer !== value + ), + }) + ); + } else { + dispatch( + setSearch({ developers: [...filters.developers, value] }) + ); + } + }} + items={developers.map((developer) => ({ + label: developer, + value: developer, + checked: filters.developers.includes(developer), + }))} + /> + + dispatch(setSearch({ publishers: [] }))} + onSelect={(value) => { + if (filters.publishers.includes(value)) { + dispatch( + setSearch({ + publishers: filters.publishers.filter( + (publisher) => publisher !== value + ), + }) + ); + } else { + dispatch( + setSearch({ publishers: [...filters.publishers, value] }) + ); + } + }} + items={publishers.map((publisher) => ({ + label: publisher, + value: publisher, + checked: filters.publishers.includes(publisher), + }))} + /> +
+
+ + ); } diff --git a/src/renderer/src/pages/catalogue/filter-section.tsx b/src/renderer/src/pages/catalogue/filter-section.tsx new file mode 100644 index 00000000..e82b9ded --- /dev/null +++ b/src/renderer/src/pages/catalogue/filter-section.tsx @@ -0,0 +1,126 @@ +import { CheckboxField, TextField } from "@renderer/components"; +import { useFormat } from "@renderer/hooks"; +import { useCallback, useMemo, useState } from "react"; + +import List from "rc-virtual-list"; + +export interface FilterSectionProps { + title: string; + items: { + label: string; + value: string; + checked: boolean; + }[]; + onSelect: (value: string) => void; + color: string; + onClear: () => void; +} + +export function FilterSection({ + title, + items, + color, + onSelect, + onClear, +}: FilterSectionProps) { + const [search, setSearch] = useState(""); + + const filteredItems = useMemo(() => { + if (search.length > 0) { + return items.filter((item) => + item.label.toLowerCase().includes(search.toLowerCase()) + ); + } + + return items; + }, [items, search]); + + const selectedItemsCount = useMemo(() => { + return items.filter((item) => item.checked).length; + }, [items]); + + const onSearch = useCallback((value: string) => { + setSearch(value); + }, []); + + const { formatNumber } = useFormat(); + + return ( +
+
+
+

+ {title} +

+
+ + {selectedItemsCount > 0 ? ( + + ) : ( + + {formatNumber(items.length)} disponíveis + + )} + + onSearch(e.target.value)} + value={search} + containerProps={{ style: { marginBottom: 16 } }} + theme="dark" + /> + + 10 ? 10 : filteredItems.length)} + itemHeight={28} + itemKey="value" + styles={{ + verticalScrollBar: { + backgroundColor: "rgba(255, 255, 255, 0.03)", + }, + verticalScrollBarThumb: { + backgroundColor: "rgba(255, 255, 255, 0.08)", + borderRadius: "24px", + }, + }} + > + {(item) => ( +
+ onSelect(item.value)} + /> +
+ )} +
+
+ ); +} diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index e04f53c8..abaaba84 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom"; import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; import { Button, GameCard, Hero } from "@renderer/components"; -import type { Steam250Game, CatalogueEntry } from "@types"; +import type { Steam250Game } from "@types"; import flameIconStatic from "@renderer/assets/icons/flame-static.png"; import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif"; @@ -15,14 +15,6 @@ import * as styles from "./home.css"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { buildGameDetailsPath } from "@renderer/helpers"; import { CatalogueCategory } from "@shared"; -import { catalogueCacheTable, db } from "@renderer/dexie"; -import { add } from "date-fns"; - -const categoryCacheDurationInSeconds = { - [CatalogueCategory.Hot]: 60 * 60 * 2, - [CatalogueCategory.Weekly]: 60 * 60 * 24, - [CatalogueCategory.Achievements]: 60 * 60 * 24, -}; export default function Home() { const { t } = useTranslation("home"); @@ -36,9 +28,7 @@ export default function Home() { CatalogueCategory.Hot ); - const [catalogue, setCatalogue] = useState< - Record - >({ + const [catalogue, setCatalogue] = useState>({ [CatalogueCategory.Hot]: [], [CatalogueCategory.Weekly]: [], [CatalogueCategory.Achievements]: [], @@ -46,37 +36,11 @@ export default function Home() { const getCatalogue = useCallback(async (category: CatalogueCategory) => { try { - const catalogueCache = await catalogueCacheTable - .where("expiresAt") - .above(new Date()) - .and((cache) => cache.category === category) - .first(); - setCurrentCatalogueCategory(category); setIsLoading(true); - if (catalogueCache) - return setCatalogue((prev) => ({ - ...prev, - [category]: catalogueCache.games, - })); - const catalogue = await window.electron.getCatalogue(category); - db.transaction("rw", catalogueCacheTable, async () => { - await catalogueCacheTable.where("category").equals(category).delete(); - - await catalogueCacheTable.add({ - category, - games: catalogue, - createdAt: new Date(), - updatedAt: new Date(), - expiresAt: add(new Date(), { - seconds: categoryCacheDurationInSeconds[category], - }), - }); - }); - setCatalogue((prev) => ({ ...prev, [category]: catalogue })); } finally { setIsLoading(false); diff --git a/src/renderer/src/pages/home/search-results.tsx b/src/renderer/src/pages/home/search-results.tsx deleted file mode 100644 index d86a362a..00000000 --- a/src/renderer/src/pages/home/search-results.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { GameCard } from "@renderer/components"; -import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; - -import type { CatalogueEntry } from "@types"; - -import type { DebouncedFunc } from "lodash"; -import { debounce } from "lodash"; - -import { InboxIcon, SearchIcon } from "@primer/octicons-react"; -import { clearSearch, setSearch } from "@renderer/features"; -import { useAppDispatch } from "@renderer/hooks"; -import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import * as styles from "./home.css"; -import { buildGameDetailsPath } from "@renderer/helpers"; - -import { vars } from "@renderer/theme.css"; - -export default function SearchResults() { - const dispatch = useAppDispatch(); - - const { t } = useTranslation("home"); - const [searchParams] = useSearchParams(); - - const [searchResults, setSearchResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [showTypingMessage, setShowTypingMessage] = useState(false); - - const debouncedFunc = useRef void> | null>(null); - const abortControllerRef = useRef(null); - - const navigate = useNavigate(); - - const handleGameClick = (game: CatalogueEntry) => { - dispatch(clearSearch()); - navigate(buildGameDetailsPath(game)); - }; - - useEffect(() => { - dispatch(setSearch(searchParams.get("query") ?? "")); - }, [dispatch, searchParams]); - - useEffect(() => { - setIsLoading(true); - if (debouncedFunc.current) debouncedFunc.current.cancel(); - if (abortControllerRef.current) abortControllerRef.current.abort(); - - const abortController = new AbortController(); - abortControllerRef.current = abortController; - - debouncedFunc.current = debounce(() => { - const query = searchParams.get("query") ?? ""; - - if (query.length < 3) { - setIsLoading(false); - setShowTypingMessage(true); - setSearchResults([]); - return; - } - - setShowTypingMessage(false); - window.electron - .searchGames(query) - .then((results) => { - if (abortController.signal.aborted) return; - - setSearchResults(results); - setIsLoading(false); - }) - .catch(() => { - setIsLoading(false); - }); - }, 500); - - debouncedFunc.current(); - }, [searchParams, dispatch]); - - const noResultsContent = () => { - if (isLoading) return null; - - if (showTypingMessage) { - return ( -
- - -

{t("start_typing")}

-
- ); - } - - if (searchResults.length === 0) { - return ( -
- - -

{t("no_results")}

-
- ); - } - - return null; - }; - - return ( - -
-
- {isLoading && - Array.from({ length: 12 }).map((_, index) => ( - - ))} - - {!isLoading && searchResults.length > 0 && ( - <> - {searchResults.map((game) => ( - handleGameClick(game)} - /> - ))} - - )} -
- - {noResultsContent()} -
-
- ); -} diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx index 5b05d5b8..c2b94b7f 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -100,17 +100,30 @@ export function AddDownloadSourceModal({ } }, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]); - const handleAddDownloadSource = async () => { - setIsLoading(true); + const putDownloadSource = async () => { + const downloadSource = await downloadSourcesTable.where({ url }).first(); + if (!downloadSource) return; + window.electron + .putDownloadSource(downloadSource.objectIds) + .then(({ fingerprint }) => { + downloadSourcesTable.update(downloadSource.id, { fingerprint }); + }); + }; + + const handleAddDownloadSource = async () => { if (validationResult) { + setIsLoading(true); + const channel = new BroadcastChannel(`download_sources:import:${url}`); downloadSourcesWorker.postMessage(["IMPORT_DOWNLOAD_SOURCE", url]); - channel.onmessage = () => { + channel.onmessage = async () => { setIsLoading(false); + putDownloadSource(); + onClose(); onAddDownloadSource(); channel.close(); diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index d2f45329..f5c01a21 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -7,12 +7,14 @@ import * as styles from "./settings-download-sources.css"; import type { DownloadSource } from "@types"; import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react"; import { AddDownloadSourceModal } from "./add-download-source-modal"; -import { useToast } from "@renderer/hooks"; +import { useAppDispatch, useRepacks, useToast } from "@renderer/hooks"; import { DownloadSourceStatus } from "@shared"; -import { SPACING_UNIT } from "@renderer/theme.css"; -import { repacksContext, settingsContext } from "@renderer/context"; +import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { settingsContext } from "@renderer/context"; import { downloadSourcesTable } from "@renderer/dexie"; import { downloadSourcesWorker } from "@renderer/workers"; +import { clearSearch, setSearch } from "@renderer/features"; +import { useNavigate } from "react-router-dom"; export function SettingsDownloadSources() { const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] = @@ -28,7 +30,11 @@ export function SettingsDownloadSources() { const { t } = useTranslation("settings"); const { showSuccessToast } = useToast(); - const { indexRepacks } = useContext(repacksContext); + const dispatch = useAppDispatch(); + + const navigate = useNavigate(); + + const { updateRepacks } = useRepacks(); const getDownloadSources = async () => { await downloadSourcesTable @@ -57,16 +63,16 @@ export function SettingsDownloadSources() { showSuccessToast(t("removed_download_source")); getDownloadSources(); - indexRepacks(); setIsRemovingDownloadSource(false); channel.close(); + updateRepacks(); }; }; const handleAddDownloadSource = async () => { - indexRepacks(); await getDownloadSources(); showSuccessToast(t("added_download_source")); + updateRepacks(); }; const syncDownloadSources = async () => { @@ -82,6 +88,7 @@ export function SettingsDownloadSources() { getDownloadSources(); setIsSyncingDownloadSources(false); channel.close(); + updateRepacks(); }; }; @@ -95,6 +102,13 @@ export function SettingsDownloadSources() { setShowAddDownloadSourceModal(false); }; + const navigateToCatalogue = (fingerprint: string) => { + dispatch(clearSearch()); + dispatch(setSearch({ downloadSourceFingerprints: [fingerprint] })); + + navigate("/catalogue"); + }; + return ( <> {statusTitle[downloadSource.status]}
-
navigateToCatalogue(downloadSource.fingerprint)} > {t("download_count", { @@ -160,7 +179,7 @@ export function SettingsDownloadSources() { downloadSource.downloadCount.toLocaleString(), })} -
+ name.replace("[DL]", ""), formatName); export const downloadSourceSchema = z.object({ name: z.string().max(255), @@ -22,6 +25,95 @@ type Payload = | ["VALIDATE_DOWNLOAD_SOURCE", string] | ["SYNC_DOWNLOAD_SOURCES", string]; +export type SteamGamesByLetter = Record; + +const addNewDownloads = async ( + downloadSource: { id: number; name: string }, + downloads: z.infer["downloads"], + steamGames: SteamGamesByLetter +) => { + const now = new Date(); + + const results = [] as (Omit & { + downloadSourceId: number; + })[]; + + const objectIdsOnSource = new Set(); + + for (const download of downloads) { + const formattedTitle = formatRepackName(download.title); + const [firstLetter] = formattedTitle; + const games = steamGames[firstLetter] || []; + + const gamesInSteam = games.filter((game) => + formattedTitle.startsWith(game.name) + ); + + if (gamesInSteam.length === 0) continue; + + for (const game of gamesInSteam) { + objectIdsOnSource.add(String(game.id)); + } + + results.push({ + objectIds: gamesInSteam.map((game) => String(game.id)), + title: download.title, + uris: download.uris, + fileSize: download.fileSize, + repacker: downloadSource.name, + uploadDate: download.uploadDate, + downloadSourceId: downloadSource.id, + createdAt: now, + updatedAt: now, + }); + } + + await repacksTable.bulkAdd(results); + + await downloadSourcesTable.update(downloadSource.id, { + objectIds: Array.from(objectIdsOnSource), + }); +}; + +const getSteamGames = async () => { + const response = await axios.get( + `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json` + ); + + return response.data; +}; + +const importDownloadSource = async (url: string) => { + const response = await axios.get>(url); + + const steamGames = await getSteamGames(); + + await db.transaction("rw", repacksTable, downloadSourcesTable, async () => { + const now = new Date(); + + const id = await downloadSourcesTable.add({ + url, + name: response.data.name, + etag: response.headers["etag"], + status: DownloadSourceStatus.UpToDate, + downloadCount: response.data.downloads.length, + createdAt: now, + updatedAt: now, + }); + + const downloadSource = await downloadSourcesTable.get(id); + + await addNewDownloads(downloadSource, response.data.downloads, steamGames); + }); +}; + +const deleteDownloadSource = async (id: number) => { + await db.transaction("rw", repacksTable, downloadSourcesTable, async () => { + await repacksTable.where({ downloadSourceId: id }).delete(); + await downloadSourcesTable.where({ id }).delete(); + }); +}; + self.onmessage = async (event: MessageEvent) => { const [type, data] = event.data; @@ -41,10 +133,7 @@ self.onmessage = async (event: MessageEvent) => { } if (type === "DELETE_DOWNLOAD_SOURCE") { - await db.transaction("rw", repacksTable, downloadSourcesTable, async () => { - await repacksTable.where({ downloadSourceId: data }).delete(); - await downloadSourcesTable.where({ id: data }).delete(); - }); + await deleteDownloadSource(data); const channel = new BroadcastChannel(`download_sources:delete:${data}`); @@ -52,37 +141,7 @@ self.onmessage = async (event: MessageEvent) => { } if (type === "IMPORT_DOWNLOAD_SOURCE") { - const response = - await axios.get>(data); - - await db.transaction("rw", repacksTable, downloadSourcesTable, async () => { - const now = new Date(); - - const id = await downloadSourcesTable.add({ - url: data, - name: response.data.name, - etag: response.headers["etag"], - status: DownloadSourceStatus.UpToDate, - downloadCount: response.data.downloads.length, - createdAt: now, - updatedAt: now, - }); - - const downloadSource = await downloadSourcesTable.get(id); - - const repacks = response.data.downloads.map((download) => ({ - title: download.title, - uris: download.uris, - fileSize: download.fileSize, - repacker: response.data.name, - uploadDate: download.uploadDate, - downloadSourceId: downloadSource!.id, - createdAt: now, - updatedAt: now, - })); - - await repacksTable.bulkAdd(repacks); - }); + await importDownloadSource(data); const channel = new BroadcastChannel(`download_sources:import:${data}`); channel.postMessage(true); @@ -97,6 +156,12 @@ self.onmessage = async (event: MessageEvent) => { const existingRepacks = await repacksTable.toArray(); for (const downloadSource of downloadSources) { + if (!downloadSource.fingerprint) { + await deleteDownloadSource(downloadSource.id); + await importDownloadSource(downloadSource.url); + continue; + } + const headers = new AxiosHeaders(); if (downloadSource.etag) { @@ -110,6 +175,8 @@ self.onmessage = async (event: MessageEvent) => { const source = downloadSourceSchema.parse(response.data); + const steamGames = await getSteamGames(); + await db.transaction( "rw", repacksTable, @@ -121,29 +188,16 @@ self.onmessage = async (event: MessageEvent) => { status: DownloadSourceStatus.UpToDate, }); - const now = new Date(); + const repacks = source.downloads.filter( + (download) => + !existingRepacks.some( + (repack) => repack.title === download.title + ) + ); - const repacks = source.downloads - .filter( - (download) => - !existingRepacks.some( - (repack) => repack.title === download.title - ) - ) - .map((download) => ({ - title: download.title, - uris: download.uris, - fileSize: download.fileSize, - repacker: source.name, - uploadDate: download.uploadDate, - downloadSourceId: downloadSource.id, - createdAt: now, - updatedAt: now, - })); + await addNewDownloads(downloadSource, repacks, steamGames); newRepacksCount += repacks.length; - - await repacksTable.bulkAdd(repacks); } ); } catch (err: unknown) { diff --git a/src/renderer/src/workers/index.ts b/src/renderer/src/workers/index.ts index b8141a8f..39367894 100644 --- a/src/renderer/src/workers/index.ts +++ b/src/renderer/src/workers/index.ts @@ -1,5 +1,3 @@ -import RepacksWorker from "./repacks.worker?worker"; import DownloadSourcesWorker from "./download-sources.worker?worker"; -export const repacksWorker = new RepacksWorker(); export const downloadSourcesWorker = new DownloadSourcesWorker(); diff --git a/src/renderer/src/workers/repacks.worker.ts b/src/renderer/src/workers/repacks.worker.ts deleted file mode 100644 index c2394510..00000000 --- a/src/renderer/src/workers/repacks.worker.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { repacksTable } from "@renderer/dexie"; -import { formatName } from "@shared"; -import type { GameRepack } from "@types"; -import flexSearch from "flexsearch"; - -interface SerializedGameRepack extends Omit { - uris: string; -} - -const state = { - repacks: [] as SerializedGameRepack[], - index: null as flexSearch.Index | null, -}; - -self.onmessage = async ( - event: MessageEvent<[string, string] | "INDEX_REPACKS"> -) => { - if (event.data === "INDEX_REPACKS") { - state.index = new flexSearch.Index(); - - repacksTable - .toCollection() - .sortBy("uploadDate") - .then((results) => { - state.repacks = results.reverse(); - - for (let i = 0; i < state.repacks.length; i++) { - const repack = state.repacks[i]; - const formattedTitle = formatName(repack.title); - state.index!.add(i, formattedTitle); - } - - self.postMessage("INDEXING_COMPLETE"); - }); - } else { - const [requestId, query] = event.data; - - const results = state.index!.search(formatName(query)).map((index) => { - const repack = state.repacks.at(index as number) as SerializedGameRepack; - - return { - ...repack, - uris: [...repack.uris, repack.magnet].filter(Boolean), - }; - }); - - const channel = new BroadcastChannel(`repacks:search:${requestId}`); - - channel.postMessage(results); - } -}; diff --git a/src/types/index.ts b/src/types/index.ts index 8bdace2f..54a3993e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,13 +17,10 @@ export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL"; export interface GameRepack { id: number; title: string; - /** - * @deprecated Use uris instead - */ - magnet: string; uris: string[]; repacker: string; fileSize: string | null; + objectIds: string[]; uploadDate: Date | string | null; createdAt: Date; updatedAt: Date; @@ -80,15 +77,6 @@ export interface TorrentFile { length: number; } -/* Used by the catalogue */ -export interface CatalogueEntry { - objectId: string; - shop: GameShop; - title: string; - /* Epic Games covers cannot be guessed with objectID */ - cover: string; -} - export interface UserGame { objectId: string; shop: GameShop; @@ -320,7 +308,9 @@ export interface DownloadSource { url: string; repackCount: number; status: DownloadSourceStatus; + objectIds: string[]; downloadCount: number; + fingerprint: string; etag: string | null; createdAt: Date; updatedAt: Date; @@ -405,6 +395,15 @@ export interface ComparedAchievements { }[]; } +export interface CatalogueSearchPayload { + title: string; + downloadSourceFingerprints: string[]; + tags: number[]; + publishers: string[]; + genres: string[]; + developers: string[]; +} + export * from "./steam.types"; export * from "./real-debrid.types"; export * from "./ludusavi.types"; diff --git a/yarn.lock b/yarn.lock index fb6bcfe8..e512d99d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -978,6 +978,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz" @@ -4101,7 +4108,7 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -classnames@^2.5.1: +classnames@^2.2.1, classnames@^2.2.6, classnames@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== @@ -4173,6 +4180,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +clsx@^1.0.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -4638,6 +4650,14 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-helpers@^5.1.3: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -5394,11 +5414,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -flexsearch@^0.7.43: - version "0.7.43" - resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.43.tgz#34f89b36278a466ce379c5bf6fb341965ed3f16c" - integrity sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg== - follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" @@ -5549,11 +5564,6 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" -get-nonce@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" - integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== - get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz#43dd3dd0e7b49b82b2dfcad10dc824bf7fc265d5" @@ -5570,6 +5580,11 @@ get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: hasown "^2.0.2" math-intrinsics "^1.0.0" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -7519,7 +7534,7 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -prop-types@^15.8.1: +prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7571,6 +7586,34 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +rc-resize-observer@^1.0.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz#4fd41fa561ba51362b5155a07c35d7c89a1ea569" + integrity sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ== + dependencies: + "@babel/runtime" "^7.20.7" + classnames "^2.2.1" + rc-util "^5.44.1" + resize-observer-polyfill "^1.5.1" + +rc-util@^5.36.0, rc-util@^5.44.1: + version "5.44.2" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.44.2.tgz#6bc5db0e96ebdb515eb5977a7371887e5413a6f8" + integrity sha512-uGSk3hpPBLa3/0QAcKhCjgl4SFnhQCJDLvvpoLdbR6KgDuXrujG+dQaUeUvBJr2ZWak1O/9n+cYbJiWmmk95EQ== + dependencies: + "@babel/runtime" "^7.18.3" + react-is "^18.2.0" + +rc-virtual-list@^3.16.1: + version "3.16.1" + resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.16.1.tgz#073d75cc0295497cdd9a35d6f5d1b71b4f35233e" + integrity sha512-algM5UsB7vrlPNr9lsZEH8s9KHkP8XbT/Y0qylyPkiM8mIOlSJLjBNADcmbYPEQCm4zW82mZRJuVHNzqqN0EAQ== + dependencies: + "@babel/runtime" "^7.20.0" + classnames "^2.2.6" + rc-resize-observer "^1.0.0" + rc-util "^5.36.0" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -7607,6 +7650,16 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-loading-skeleton@^3.4.0: version "3.5.0" resolved "https://registry.yarnpkg.com/react-loading-skeleton/-/react-loading-skeleton-3.5.0.tgz#da2090355b4dedcad5c53cb3f0ed364e3a76d6ca" @@ -7668,6 +7721,18 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" +react-virtualized@^9.22.5: + version "9.22.5" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.5.tgz#bfb96fed519de378b50d8c0064b92994b3b91620" + integrity sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ== + dependencies: + "@babel/runtime" "^7.7.2" + clsx "^1.0.4" + dom-helpers "^5.1.3" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.4" + react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -7784,6 +7849,11 @@ reselect@^5.1.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-alpn@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"