feat: using api download sources

This commit is contained in:
Chubby Granny Chaser
2025-10-21 04:18:11 +01:00
parent c2273dbf71
commit 48ce9a2476
45 changed files with 295 additions and 1625 deletions

View File

@@ -7,7 +7,6 @@ import {
useAppSelector,
useDownload,
useLibrary,
useRepacks,
useToast,
useUserDetails,
} from "@renderer/hooks";
@@ -20,7 +19,6 @@ import {
setUserDetails,
setProfileBackground,
setGameRunning,
setIsImportingSources,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
@@ -40,8 +38,6 @@ export function App() {
const { t } = useTranslation("app");
const { updateRepacks } = useRepacks();
const { clearDownload, setLastPacket } = useDownload();
const {
@@ -199,36 +195,6 @@ export function App() {
});
}, [dispatch, draggingDisabled]);
useEffect(() => {
(async () => {
dispatch(setIsImportingSources(true));
try {
// Initial repacks load
await updateRepacks();
// Sync all local sources (check for updates)
const newRepacksCount = await window.electron.syncDownloadSources();
if (newRepacksCount > 0) {
window.electron.publishNewRepacksNotification(newRepacksCount);
}
// Update fingerprints for sources that don't have them
await window.electron.updateMissingFingerprints();
// Update repacks AFTER all syncing and fingerprint updates are complete
await updateRepacks();
} catch (error) {
console.error("Error syncing download sources:", error);
// Still update repacks even if sync fails
await updateRepacks();
} finally {
dispatch(setIsImportingSources(false));
}
})();
}, [updateRepacks, dispatch]);
const loadAndApplyTheme = useCallback(async () => {
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.code) {

View File

@@ -1,5 +1,5 @@
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
import type { GameStats } from "@types";
import type { GameStats, ShopAssets } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
@@ -8,15 +8,15 @@ import "./game-card.scss";
import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge";
import { StarRating } from "../star-rating/star-rating";
import { useCallback, useState, useMemo } from "react";
import { useFormat, useRepacks } from "@renderer/hooks";
import { useCallback, useState } from "react";
import { useFormat } from "@renderer/hooks";
export interface GameCardProps
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
game: any;
game: ShopAssets;
}
const shopIcon = {
@@ -28,13 +28,6 @@ export function GameCard({ game, ...props }: GameCardProps) {
const [stats, setStats] = useState<GameStats | null>(null);
const { getRepacksForObjectId } = useRepacks();
const repacks = getRepacksForObjectId(game.objectId);
const uniqueRepackers = Array.from(
new Set(repacks.map((repack) => repack.repacker))
);
const handleHover = useCallback(() => {
if (!stats) {
window.electron.getGameStats(game.objectId, game.shop).then((stats) => {
@@ -45,14 +38,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
const { numberFormatter } = useFormat();
const firstThreeRepackers = useMemo(
() => uniqueRepackers.slice(0, 3),
[uniqueRepackers]
);
const remainingCount = useMemo(
() => uniqueRepackers.length - 3,
[uniqueRepackers]
);
console.log("game", game);
return (
<button
@@ -75,18 +61,20 @@ export function GameCard({ game, ...props }: GameCardProps) {
<p className="game-card__title">{game.title}</p>
</div>
{uniqueRepackers.length > 0 ? (
{game.downloadSources.length > 0 ? (
<ul className="game-card__download-options">
{firstThreeRepackers.map((repacker) => (
<li key={repacker}>
<Badge>{repacker}</Badge>
{game.downloadSources.slice(0, 3).map((sourceName) => (
<li key={sourceName}>
<Badge>{sourceName}</Badge>
</li>
))}
{remainingCount > 0 && (
{game.downloadSources.length > 3 && (
<li>
<Badge>
+{remainingCount}{" "}
{t("game_card:available", { count: remainingCount })}
+{game.downloadSources.length - 3}{" "}
{t("game_card:available", {
count: game.downloadSources.length - 3,
})}
</Badge>
</li>
)}

View File

@@ -1,11 +1,4 @@
import {
createContext,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { createContext, useCallback, useEffect, useRef, useState } from "react";
import { setHeaderTitle } from "@renderer/features";
import { getSteamLanguage } from "@renderer/helpers";
@@ -13,11 +6,11 @@ import {
useAppDispatch,
useAppSelector,
useDownload,
useRepacks,
useUserDetails,
} from "@renderer/hooks";
import type {
GameRepack,
GameShop,
GameStats,
LibraryGame,
@@ -84,12 +77,7 @@ export function GameDetailsContextProvider({
const [isGameRunning, setIsGameRunning] = useState(false);
const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
const { getRepacksForObjectId } = useRepacks();
const repacks = useMemo(() => {
return getRepacksForObjectId(objectId);
}, [getRepacksForObjectId, objectId]);
const [repacks, setRepacks] = useState<GameRepack[]>([]);
const { i18n } = useTranslation("game_details");
const location = useLocation();
@@ -287,19 +275,6 @@ export function GameDetailsContextProvider({
}
}, [location]);
const lastDownloadedOption = useMemo(() => {
if (game?.download) {
const repack = repacks.find((repack) =>
repack.uris.some((uri) => uri.includes(game.download!.uri))
);
if (!repack) return null;
return repack;
}
return null;
}, [game?.download, repacks]);
useEffect(() => {
const unsubscribe = window.electron.onUpdateAchievements(
objectId,
@@ -315,6 +290,34 @@ export function GameDetailsContextProvider({
};
}, [objectId, shop, userDetails]);
useEffect(() => {
const fetchDownloadSources = async () => {
try {
const sources = await window.electron.getDownloadSources();
const params = {
take: 100,
skip: 0,
downloadSourceIds: sources.map((source) => source.id),
};
const downloads = await window.electron.hydraApi.get<GameRepack[]>(
`/games/${shop}/${objectId}/download-sources`,
{
params,
needsAuth: false,
}
);
setRepacks(downloads);
} catch (error) {
console.error("Failed to fetch download sources:", error);
}
};
fetchDownloadSources();
}, [shop, objectId]);
const getDownloadsPath = async () => {
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
return window.electron.getDefaultDownloadsPath();
@@ -359,7 +362,7 @@ export function GameDetailsContextProvider({
stats,
achievements,
hasNSFWContentBlocked,
lastDownloadedOption,
lastDownloadedOption: null,
setHasNSFWContentBlocked,
selectGameExecutable,
updateGame,

View File

@@ -31,8 +31,6 @@ import type {
Game,
DiskUsage,
DownloadSource,
DownloadSourceValidationResult,
GameRepack,
} from "@types";
import type { AxiosProgressEvent } from "axios";
@@ -210,20 +208,12 @@ declare global {
/* Download sources */
addDownloadSource: (url: string) => Promise<DownloadSource>;
updateMissingFingerprints: () => Promise<number>;
removeDownloadSource: (url: string, removeAll?: boolean) => Promise<void>;
getDownloadSources: () => Promise<
Pick<DownloadSource, "url" | "createdAt" | "updatedAt">[]
>;
deleteDownloadSource: (id: number) => Promise<void>;
deleteAllDownloadSources: () => Promise<void>;
validateDownloadSource: (
url: string
) => Promise<DownloadSourceValidationResult>;
syncDownloadSources: () => Promise<number>;
getDownloadSourcesList: () => Promise<DownloadSource[]>;
checkDownloadSourceExists: (url: string) => Promise<boolean>;
getAllRepacks: () => Promise<GameRepack[]>;
removeDownloadSource: (
removeAll = false,
downloadSourceId?: string
) => Promise<void>;
getDownloadSources: () => Promise<DownloadSource[]>;
syncDownloadSources: () => Promise<DownloadSource[]>;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskUsage>;

View File

@@ -1,21 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
export interface DownloadSourcesState {
isImporting: boolean;
}
const initialState: DownloadSourcesState = {
isImporting: false,
};
export const downloadSourcesSlice = createSlice({
name: "downloadSources",
initialState,
reducers: {
setIsImportingSources: (state, action) => {
state.isImporting = action.payload;
},
},
});
export const { setIsImportingSources } = downloadSourcesSlice.actions;

View File

@@ -6,6 +6,4 @@ export * from "./toast-slice";
export * from "./user-details-slice";
export * from "./game-running.slice";
export * from "./subscription-slice";
export * from "./repacks-slice";
export * from "./download-sources-slice";
export * from "./catalogue-search";

View File

@@ -5,5 +5,4 @@ export * from "./use-toast";
export * from "./redux";
export * from "./use-user-details";
export * from "./use-format";
export * from "./use-repacks";
export * from "./use-feature";

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { useAppDispatch } from "./redux";
import { setGenres, setTags } from "@renderer/features";
import type { DownloadSource } from "@types";
export const externalResourcesInstance = axios.create({
baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL,
@@ -12,6 +13,7 @@ export function useCatalogue() {
const [steamPublishers, setSteamPublishers] = useState<string[]>([]);
const [steamDevelopers, setSteamDevelopers] = useState<string[]>([]);
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
const getSteamUserTags = useCallback(() => {
externalResourcesInstance.get("/steam-user-tags.json").then((response) => {
@@ -37,17 +39,25 @@ export function useCatalogue() {
});
}, []);
const getDownloadSources = useCallback(() => {
window.electron.getDownloadSources().then((results) => {
setDownloadSources(results.filter((source) => !!source.fingerprint));
});
}, []);
useEffect(() => {
getSteamUserTags();
getSteamGenres();
getSteamPublishers();
getSteamDevelopers();
getDownloadSources();
}, [
getSteamUserTags,
getSteamGenres,
getSteamPublishers,
getSteamDevelopers,
getDownloadSources,
]);
return { steamPublishers, steamDevelopers };
return { steamPublishers, downloadSources, steamDevelopers };
}

View File

@@ -1,26 +0,0 @@
import { setRepacks } from "@renderer/features";
import { useCallback } from "react";
import { RootState } from "@renderer/store";
import { useSelector } from "react-redux";
import { useAppDispatch } from "./redux";
export function useRepacks() {
const dispatch = useAppDispatch();
const repacks = useSelector((state: RootState) => state.repacks.value);
const getRepacksForObjectId = useCallback(
(objectId: string) => {
return repacks.filter((repack) => repack.objectIds.includes(objectId));
},
[repacks]
);
const updateRepacks = useCallback(async () => {
const repacks = await window.electron.getAllRepacks();
dispatch(
setRepacks(repacks.filter((repack) => Array.isArray(repack.objectIds)))
);
}, [dispatch]);
return { getRepacksForObjectId, updateRepacks };
}

View File

@@ -1,4 +1,8 @@
import type { CatalogueSearchResult, DownloadSource } from "@types";
import type {
CatalogueSearchResult,
CatalogueSearchPayload,
DownloadSource,
} from "@types";
import { useAppDispatch, useAppSelector, useFormat } from "@renderer/hooks";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -29,13 +33,12 @@ export default function Catalogue() {
const abortControllerRef = useRef<AbortController | null>(null);
const cataloguePageRef = useRef<HTMLDivElement>(null);
const { steamDevelopers, steamPublishers } = useCatalogue();
const { steamDevelopers, steamPublishers, downloadSources } = useCatalogue();
const { steamGenres, steamUserTags } = useAppSelector(
(state) => state.catalogueSearch
);
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [results, setResults] = useState<CatalogueSearchResult[]>([]);
@@ -51,24 +54,41 @@ export default function Catalogue() {
const { t, i18n } = useTranslation("catalogue");
const debouncedSearch = useRef(
debounce(async (filters, pageSize, offset) => {
const abortController = new AbortController();
abortControllerRef.current = abortController;
debounce(
async (
filters: CatalogueSearchPayload,
downloadSources: DownloadSource[],
pageSize: number,
offset: number
) => {
const abortController = new AbortController();
abortControllerRef.current = abortController;
const response = await window.electron.hydraApi.post<{
edges: CatalogueSearchResult[];
count: number;
}>("/catalogue/search", {
data: { ...filters, take: pageSize, skip: offset },
needsAuth: false,
});
const requestData = {
...filters,
take: pageSize,
skip: offset,
downloadSourceIds: downloadSources.map(
(downloadSource) => downloadSource.id
),
};
if (abortController.signal.aborted) return;
const response = await window.electron.hydraApi.post<{
edges: CatalogueSearchResult[];
count: number;
}>("/catalogue/search", {
data: requestData,
needsAuth: false,
});
setResults(response.edges);
setItemsCount(response.count);
setIsLoading(false);
}, 500)
if (abortController.signal.aborted) return;
setResults(response.edges);
setItemsCount(response.count);
setIsLoading(false);
},
500
)
).current;
const decodeHTML = (s: string) =>
@@ -79,18 +99,17 @@ export default function Catalogue() {
setIsLoading(true);
abortControllerRef.current?.abort();
debouncedSearch(filters, PAGE_SIZE, (page - 1) * PAGE_SIZE);
debouncedSearch(
filters,
downloadSources,
PAGE_SIZE,
(page - 1) * PAGE_SIZE
);
return () => {
debouncedSearch.cancel();
};
}, [filters, page, debouncedSearch]);
useEffect(() => {
window.electron.getDownloadSourcesList().then((sources) => {
setDownloadSources(sources.filter((source) => !!source.fingerprint));
});
}, []);
}, [filters, downloadSources, page, debouncedSearch]);
const language = i18n.language.split("-")[0];
@@ -168,7 +187,7 @@ export default function Catalogue() {
value: publisher,
})),
];
}, [filters, steamUserTags, steamGenresMapping, language, downloadSources]);
}, [filters, steamUserTags, downloadSources, steamGenresMapping, language]);
const filterSections = useMemo(() => {
return [

View File

@@ -1,6 +1,6 @@
import { Badge } from "@renderer/components";
import { buildGameDetailsPath } from "@renderer/helpers";
import { useAppSelector, useRepacks, useLibrary } from "@renderer/hooks";
import { useAppSelector, useLibrary } from "@renderer/hooks";
import { useMemo, useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
@@ -23,10 +23,6 @@ export function GameItem({ game }: GameItemProps) {
const { steamGenres } = useAppSelector((state) => state.catalogueSearch);
const { getRepacksForObjectId } = useRepacks();
const repacks = getRepacksForObjectId(game.objectId);
const [isAddingToLibrary, setIsAddingToLibrary] = useState(false);
const [added, setAdded] = useState(false);
@@ -63,10 +59,6 @@ export function GameItem({ game }: GameItemProps) {
}
};
const uniqueRepackers = useMemo(() => {
return Array.from(new Set(repacks.map((repack) => repack.repacker)));
}, [repacks]);
const genres = useMemo(() => {
return game.genres?.map((genre) => {
const index = steamGenres["en"]?.findIndex(
@@ -117,8 +109,8 @@ export function GameItem({ game }: GameItemProps) {
<span className="game-item__genres">{genres.join(", ")}</span>
<div className="game-item__repackers">
{uniqueRepackers.map((repacker) => (
<Badge key={repacker}>{repacker}</Badge>
{game.downloadSources.map((sourceName) => (
<Badge key={sourceName}>{sourceName}</Badge>
))}
</div>
</div>

View File

@@ -144,8 +144,6 @@ export function GameReviews({
}
}, [objectId, userDetailsId, shop, game, onUserReviewedChange]);
console.log("reviews", reviews);
const loadReviews = useCallback(
async (reset = false) => {
if (!objectId) return;
@@ -440,8 +438,6 @@ export function GameReviews({
});
}, [reviews]);
console.log("reviews", reviews);
return (
<div className="game-details__reviews-section">
{showReviewPrompt &&

View File

@@ -54,7 +54,7 @@ export function RepacksModal({
{}
);
const { repacks, game } = useContext(gameDetailsContext);
const { game, repacks } = useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
@@ -88,6 +88,15 @@ export function RepacksModal({
});
}, [repacks, isFeatureEnabled, Feature]);
useEffect(() => {
const fetchDownloadSources = async () => {
const sources = await window.electron.getDownloadSources();
setDownloadSources(sources);
};
fetchDownloadSources();
}, []);
const sortedRepacks = useMemo(() => {
return orderBy(
repacks,
@@ -103,23 +112,13 @@ export function RepacksModal({
);
}, [repacks, hashesInDebrid]);
useEffect(() => {
window.electron.getDownloadSourcesList().then((sources) => {
const uniqueRepackers = new Set(sortedRepacks.map((r) => r.repacker));
const filteredSources = sources.filter(
(s) => s.name && uniqueRepackers.has(s.name) && !!s.fingerprint
);
setDownloadSources(filteredSources);
});
}, [sortedRepacks]);
useEffect(() => {
const term = filterTerm.trim().toLowerCase();
const byTerm = sortedRepacks.filter((repack) => {
if (!term) return true;
const lowerTitle = repack.title.toLowerCase();
const lowerRepacker = repack.repacker.toLowerCase();
const lowerRepacker = repack.downloadSourceName.toLowerCase();
return lowerTitle.includes(term) || lowerRepacker.includes(term);
});
@@ -130,7 +129,7 @@ export function RepacksModal({
(src) =>
src.fingerprint &&
selectedFingerprints.includes(src.fingerprint) &&
src.name === repack.repacker
src.name === repack.downloadSourceName
);
});
@@ -281,7 +280,7 @@ export function RepacksModal({
)}
<p className="repacks-modal__repack-info">
{repack.fileSize} - {repack.repacker} -{" "}
{repack.fileSize} - {repack.downloadSourceName} -{" "}
{repack.uploadDate ? formatDate(repack.uploadDate) : ""}
</p>

View File

@@ -40,14 +40,20 @@ export default function Home() {
setCurrentCatalogueCategory(category);
setIsLoading(true);
const params = new URLSearchParams({
take: "12",
skip: "0",
});
const downloadSources = await window.electron.getDownloadSources();
const params = {
take: 12,
skip: 0,
downloadSourceIds: downloadSources.map((source) => source.id),
};
const catalogue = await window.electron.hydraApi.get<ShopAssets[]>(
`/catalogue/${category}?${params.toString()}`,
{ needsAuth: false }
`/catalogue/${category}`,
{
params,
needsAuth: false,
}
);
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));

View File

@@ -38,4 +38,11 @@
animation: spin 1s linear infinite;
margin-right: calc(globals.$spacing-unit / 2);
}
&__actions {
display: flex;
justify-content: flex-end;
gap: globals.$spacing-unit;
margin-top: calc(globals.$spacing-unit * 2);
}
}

View File

@@ -1,15 +1,12 @@
import { useCallback, useContext, useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
import { settingsContext } from "@renderer/context";
import { useForm } from "react-hook-form";
import { useAppDispatch } from "@renderer/hooks";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import type { DownloadSourceValidationResult } from "@types";
import { setIsImportingSources } from "@renderer/features";
import { SyncIcon } from "@primer/octicons-react";
import "./add-download-source-modal.scss";
@@ -28,7 +25,6 @@ export function AddDownloadSourceModal({
onClose,
onAddDownloadSource,
}: Readonly<AddDownloadSourceModalProps>) {
const [url, setUrl] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation("settings");
@@ -48,77 +44,38 @@ export function AddDownloadSourceModal({
resolver: yupResolver(schema),
});
const [validationResult, setValidationResult] =
useState<DownloadSourceValidationResult | null>(null);
const { sourceUrl } = useContext(settingsContext);
const dispatch = useAppDispatch();
const onSubmit = async (values: FormValues) => {
setIsLoading(true);
const onSubmit = useCallback(
async (values: FormValues) => {
const exists = await window.electron.checkDownloadSourceExists(
values.url
);
try {
await window.electron.addDownloadSource(values.url);
if (exists) {
setError("url", {
type: "server",
message: t("source_already_exists"),
});
return;
}
const validationResult = await window.electron.validateDownloadSource(
values.url
);
setValidationResult(validationResult);
setUrl(values.url);
},
[setError, t]
);
onClose();
onAddDownloadSource();
} catch (error) {
console.error("Failed to add download source:", error);
setError("url", {
type: "server",
message: "Failed to add download source. Please try again.",
});
} finally {
setIsLoading(false);
}
};
useEffect(() => {
setValue("url", "");
clearErrors();
setIsLoading(false);
setValidationResult(null);
if (sourceUrl) {
setValue("url", sourceUrl);
handleSubmit(onSubmit)();
}
}, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
const handleAddDownloadSource = async () => {
if (validationResult) {
setIsLoading(true);
dispatch(setIsImportingSources(true));
try {
// Single call that handles: import → API sync → fingerprint
await window.electron.addDownloadSource(url);
// Close modal and update UI
onClose();
onAddDownloadSource();
} catch (error) {
console.error("Failed to add download source:", error);
setError("url", {
type: "server",
message: "Failed to import source. Please try again.",
});
} finally {
setIsLoading(false);
dispatch(setIsImportingSources(false));
}
}
};
}, [visible, clearErrors, setValue, sourceUrl]);
const handleClose = () => {
// Prevent closing while importing
if (isLoading) return;
onClose();
};
@@ -132,49 +89,32 @@ export function AddDownloadSourceModal({
clickOutsideToClose={!isLoading}
>
<div className="add-download-source-modal__container">
<TextField
{...register("url")}
label={t("download_source_url")}
placeholder={t("insert_valid_json_url")}
error={errors.url?.message}
rightContent={
<form onSubmit={handleSubmit(onSubmit)}>
<TextField
{...register("url")}
label={t("download_source_url")}
placeholder={t("insert_valid_json_url")}
error={errors.url?.message}
/>
<div className="add-download-source-modal__actions">
<Button
type="button"
theme="outline"
className="add-download-source-modal__validate-button"
onClick={handleSubmit(onSubmit)}
disabled={isSubmitting || isLoading}
>
{t("validate_download_source")}
</Button>
}
/>
{validationResult && (
<div className="add-download-source-modal__validation-result">
<div className="add-download-source-modal__validation-info">
<h4>{validationResult?.name}</h4>
<small>
{t("found_download_option", {
count: validationResult?.downloadCount,
countFormatted:
validationResult?.downloadCount.toLocaleString(),
})}
</small>
</div>
<Button
type="button"
onClick={handleAddDownloadSource}
onClick={handleClose}
disabled={isLoading}
>
{t("cancel")}
</Button>
<Button type="submit" disabled={isSubmitting || isLoading}>
{isLoading && (
<SyncIcon className="add-download-source-modal__spinner" />
)}
{isLoading ? t("importing") : t("import")}
{isLoading ? t("adding") : t("add_download_source")}
</Button>
</div>
)}
</form>
</div>
</Modal>
);

View File

@@ -16,7 +16,7 @@ import {
TrashIcon,
} from "@primer/octicons-react";
import { AddDownloadSourceModal } from "./add-download-source-modal";
import { useAppDispatch, useRepacks, useToast } from "@renderer/hooks";
import { useAppDispatch, useToast } from "@renderer/hooks";
import { DownloadSourceStatus } from "@shared";
import { settingsContext } from "@renderer/context";
import { useNavigate } from "react-router-dom";
@@ -35,7 +35,6 @@ export function SettingsDownloadSources() {
useState(false);
const [isRemovingDownloadSource, setIsRemovingDownloadSource] =
useState(false);
const [isFetchingSources, setIsFetchingSources] = useState(true);
const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
@@ -46,37 +45,29 @@ export function SettingsDownloadSources() {
const navigate = useNavigate();
const { updateRepacks } = useRepacks();
const getDownloadSources = async () => {
await window.electron
.getDownloadSourcesList()
.then((sources) => {
setDownloadSources(sources);
})
.finally(() => {
setIsFetchingSources(false);
});
};
useEffect(() => {
getDownloadSources();
}, []);
useEffect(() => {
if (sourceUrl) setShowAddDownloadSourceModal(true);
}, [sourceUrl]);
useEffect(() => {
const fetchDownloadSources = async () => {
const sources = await window.electron.getDownloadSources();
setDownloadSources(sources);
};
fetchDownloadSources();
}, []);
const handleRemoveSource = async (downloadSource: DownloadSource) => {
setIsRemovingDownloadSource(true);
try {
await window.electron.deleteDownloadSource(downloadSource.id);
await window.electron.removeDownloadSource(downloadSource.url);
await window.electron.removeDownloadSource(false, downloadSource.id);
const sources = await window.electron.getDownloadSources();
setDownloadSources(sources as DownloadSource[]);
showSuccessToast(t("removed_download_source"));
await getDownloadSources();
updateRepacks();
} catch (error) {
console.error("Failed to remove download source:", error);
} finally {
setIsRemovingDownloadSource(false);
}
@@ -86,53 +77,44 @@ export function SettingsDownloadSources() {
setIsRemovingDownloadSource(true);
try {
await window.electron.deleteAllDownloadSources();
await window.electron.removeDownloadSource("", true);
showSuccessToast(t("removed_download_sources"));
await getDownloadSources();
setShowConfirmationDeleteAllSourcesModal(false);
updateRepacks();
await window.electron.removeDownloadSource(true);
const sources = await window.electron.getDownloadSources();
setDownloadSources(sources as DownloadSource[]);
showSuccessToast(t("removed_all_download_sources"));
} catch (error) {
console.error("Failed to remove all download sources:", error);
} finally {
setIsRemovingDownloadSource(false);
setShowConfirmationDeleteAllSourcesModal(false);
}
};
const handleAddDownloadSource = async () => {
// Refresh sources list and repacks after import completes
await getDownloadSources();
// Force repacks update to ensure UI reflects new data
await updateRepacks();
showSuccessToast(t("added_download_source"));
try {
const sources = await window.electron.getDownloadSources();
setDownloadSources(sources as DownloadSource[]);
} catch (error) {
console.error("Failed to refresh download sources:", error);
}
};
const syncDownloadSources = async () => {
setIsSyncingDownloadSources(true);
try {
// Sync local sources (check for updates)
await window.electron.syncDownloadSources();
// Refresh sources and repacks AFTER sync completes
await getDownloadSources();
await updateRepacks();
showSuccessToast(t("download_sources_synced"));
} catch (error) {
console.error("Error syncing download sources:", error);
// Still refresh the UI even if sync fails
await getDownloadSources();
await updateRepacks();
const sources = await window.electron.syncDownloadSources();
setDownloadSources(sources);
} finally {
setIsSyncingDownloadSources(false);
}
};
const statusTitle = {
[DownloadSourceStatus.UpToDate]: t("download_source_up_to_date"),
[DownloadSourceStatus.Errored]: t("download_source_errored"),
[DownloadSourceStatus.PendingMatching]: t(
"download_source_pending_matching"
),
[DownloadSourceStatus.Matched]: t("download_source_matched"),
[DownloadSourceStatus.Matching]: t("download_source_matching"),
[DownloadSourceStatus.Failed]: t("download_source_failed"),
};
const handleModalClose = () => {
@@ -180,8 +162,7 @@ export function SettingsDownloadSources() {
disabled={
!downloadSources.length ||
isSyncingDownloadSources ||
isRemovingDownloadSource ||
isFetchingSources
isRemovingDownloadSource
}
onClick={syncDownloadSources}
>
@@ -197,8 +178,7 @@ export function SettingsDownloadSources() {
disabled={
isRemovingDownloadSource ||
isSyncingDownloadSources ||
!downloadSources.length ||
isFetchingSources
!downloadSources.length
}
>
<TrashIcon />
@@ -209,11 +189,7 @@ export function SettingsDownloadSources() {
type="button"
theme="outline"
onClick={() => setShowAddDownloadSourceModal(true)}
disabled={
isSyncingDownloadSources ||
isFetchingSources ||
isRemovingDownloadSource
}
disabled={isSyncingDownloadSources || isRemovingDownloadSource}
>
<PlusCircleIcon />
{t("add_download_source")}

View File

@@ -8,8 +8,6 @@ import {
userDetailsSlice,
gameRunningSlice,
subscriptionSlice,
repacksSlice,
downloadSourcesSlice,
catalogueSearchSlice,
} from "@renderer/features";
@@ -23,8 +21,6 @@ export const store = configureStore({
userDetails: userDetailsSlice.reducer,
gameRunning: gameRunningSlice.reducer,
subscription: subscriptionSlice.reducer,
repacks: repacksSlice.reducer,
downloadSources: downloadSourcesSlice.reducer,
catalogueSearch: catalogueSearchSlice.reducer,
},
});