feat: adding initial download sources

This commit is contained in:
Chubby Granny Chaser
2025-04-01 21:39:54 +01:00
parent 73f4b0e869
commit 0d75878b07
41 changed files with 306 additions and 520 deletions

View File

@@ -143,6 +143,10 @@ export function App() {
const existingDownloadSources: DownloadSource[] =
await downloadSourcesTable.toArray();
window.electron.createDownloadSources(
existingDownloadSources.map((source) => source.url)
);
await Promise.allSettled(
downloadSources.map(async (source) => {
return new Promise((resolve) => {

View File

@@ -160,12 +160,15 @@ declare global {
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
onExtractionComplete: (
cb: (shop: GameShop, objectId: string) => void
) => () => Electron.IpcRenderer;
/* Download sources */
putDownloadSource: (
objectIds: string[]
) => Promise<{ fingerprint: string }>;
createDownloadSource: (url: string) => Promise<void>;
createDownloadSources: (urls: string[]) => Promise<void>;
removeDownloadSource: (url: string, removeAll?: boolean) => Promise<void>;
getDownloadSources: () => Promise<
Pick<DownloadSource, "url" | "createdAt" | "updatedAt">[]

View File

@@ -1,19 +1,7 @@
import { formatDate, getDateLocale } from "@shared";
import { format, formatDistance, subMilliseconds } from "date-fns";
import type { FormatDistanceOptions } from "date-fns";
import {
ptBR,
enUS,
es,
fr,
pl,
hu,
tr,
ru,
it,
be,
zhCN,
da,
} from "date-fns/locale";
import { enUS } from "date-fns/locale";
import { useTranslation } from "react-i18next";
export function useDate() {
@@ -21,22 +9,6 @@ export function useDate() {
const { language } = i18n;
const getDateLocale = () => {
if (language.startsWith("pt")) return ptBR;
if (language.startsWith("es")) return es;
if (language.startsWith("fr")) return fr;
if (language.startsWith("hu")) return hu;
if (language.startsWith("pl")) return pl;
if (language.startsWith("tr")) return tr;
if (language.startsWith("ru")) return ru;
if (language.startsWith("it")) return it;
if (language.startsWith("be")) return be;
if (language.startsWith("zh")) return zhCN;
if (language.startsWith("da")) return da;
return enUS;
};
return {
formatDistance: (
date: string | number | Date,
@@ -46,7 +18,7 @@ export function useDate() {
try {
return formatDistance(date, baseDate, {
...options,
locale: getDateLocale(),
locale: getDateLocale(language),
});
} catch (err) {
return "";
@@ -61,7 +33,7 @@ export function useDate() {
try {
return formatDistance(subMilliseconds(new Date(), millis), baseDate, {
...options,
locale: getDateLocale(),
locale: getDateLocale(language),
});
} catch (err) {
return "";
@@ -69,18 +41,13 @@ export function useDate() {
},
formatDateTime: (date: number | Date | string): string => {
const locale = getDateLocale();
const locale = getDateLocale(language);
return format(
date,
locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy HH:mm"
);
},
formatDate: (date: number | Date | string): string => {
if (isNaN(new Date(date).getDate())) return "N/A";
const locale = getDateLocale();
return format(date, locale == enUS ? "MM/dd/yyyy" : "dd/MM/yyyy");
},
formatDate: (date: number | Date | string) => formatDate(date, language),
};
}

View File

@@ -18,14 +18,11 @@ export function Pagination({
if (totalPages <= 1) return null;
// Number of visible pages
const visiblePages = 3;
// Calculate the start and end of the visible range
let startPage = Math.max(1, page - 1); // Shift range slightly back
let startPage = Math.max(1, page - 1);
let endPage = startPage + visiblePages - 1;
// Adjust the range if we're near the start or end
if (endPage > totalPages) {
endPage = totalPages;
startPage = Math.max(1, endPage - visiblePages + 1);
@@ -33,7 +30,6 @@ export function Pagination({
return (
<div className="pagination">
{/* Previous Button */}
<Button
theme="outline"
onClick={() => onPageChange(page - 1)}
@@ -45,7 +41,6 @@ export function Pagination({
{page > 2 && (
<>
{/* initial page */}
<Button
theme="outline"
onClick={() => onPageChange(1)}
@@ -55,14 +50,12 @@ export function Pagination({
{1}
</Button>
{/* ellipsis */}
<div className="pagination__ellipsis">
<span className="pagination__ellipsis-text">...</span>
</div>
</>
)}
{/* Page Buttons */}
{Array.from(
{ length: endPage - startPage + 1 },
(_, i) => startPage + i
@@ -79,12 +72,10 @@ export function Pagination({
{page < totalPages - 1 && (
<>
{/* ellipsis */}
<div className="pagination__ellipsis">
<span className="pagination__ellipsis-text">...</span>
</div>
{/* last page */}
<Button
theme="outline"
onClick={() => onPageChange(totalPages)}
@@ -96,7 +87,6 @@ export function Pagination({
</>
)}
{/* Next Button */}
<Button
theme="outline"
onClick={() => onPageChange(page + 1)}

View File

@@ -96,6 +96,10 @@ export function DownloadGroup({
const finalDownloadSize = getFinalDownloadSize(game);
const seedingStatus = seedingMap.get(game.id);
if (download.extracting) {
return <p>{t("extracting")}</p>;
}
if (isGameDeleting(game.id)) {
return <p>{t("deleting")}</p>;
}

View File

@@ -38,7 +38,13 @@ export default function Downloads() {
useEffect(() => {
window.electron.onSeedingStatus((value) => setSeedingStatus(value));
}, []);
const unsubscribe = window.electron.onExtractionComplete(() => {
updateLibrary();
});
return () => unsubscribe();
}, [updateLibrary]);
const handleOpenGameInstaller = (shop: GameShop, objectId: string) =>
window.electron.openGameInstaller(shop, objectId).then((isBinaryInPath) => {
@@ -67,7 +73,7 @@ export default function Downloads() {
if (!next.download) return prev;
/* Is downloading */
if (lastPacket?.gameId === next.id)
if (lastPacket?.gameId === next.id || next.download.extracting)
return { ...prev, downloading: [...prev.downloading, next] };
/* Is either queued or paused */

View File

@@ -4,7 +4,6 @@ import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
import "./cloud-sync-modal.scss";
import { formatBytes } from "@shared";
import { format } from "date-fns";
import {
ClockIcon,
DeviceDesktopIcon,
@@ -14,7 +13,7 @@ import {
TrashIcon,
UploadIcon,
} from "@primer/octicons-react";
import { useAppSelector, useToast } from "@renderer/hooks";
import { useAppSelector, useDate, useToast } from "@renderer/hooks";
import { useTranslation } from "react-i18next";
import { AxiosProgressEvent } from "axios";
import { formatDownloadProgress } from "@renderer/helpers";
@@ -29,6 +28,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
const { t } = useTranslation("game_details");
const { formatDate, formatDateTime } = useDate();
const {
artifacts,
backupPreview,
@@ -205,7 +206,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
<h3>
{artifact.label ??
t("backup_from", {
date: format(artifact.createdAt, "dd/MM/yyyy"),
date: formatDate(artifact.createdAt),
})}
</h3>
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
@@ -223,7 +224,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
<span className="cloud-sync-modal__artifact-meta">
<ClockIcon size={14} />
{format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")}
{formatDateTime(artifact.createdAt)}
</span>
</div>

View File

@@ -98,7 +98,8 @@ export default function GameDetails() {
const handleStartDownload = async (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
downloadPath: string,
automaticallyExtract: boolean
) => {
const response = await startDownload({
repackId: repack.id,
@@ -108,6 +109,7 @@ export default function GameDetails() {
shop,
downloadPath,
uri: selectRepackUri(repack, downloader),
automaticallyExtract: automaticallyExtract,
});
if (response.ok) {

View File

@@ -1,6 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Button, Link, Modal, TextField } from "@renderer/components";
import {
Button,
CheckboxField,
Link,
Modal,
TextField,
} from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
import type { GameRepack } from "@types";
@@ -14,7 +20,8 @@ export interface DownloadSettingsModalProps {
startDownload: (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
downloadPath: string,
automaticallyExtract: boolean
) => Promise<{ ok: boolean; error?: string }>;
repack: GameRepack | null;
}
@@ -32,6 +39,8 @@ export function DownloadSettingsModal({
const [diskFreeSpace, setDiskFreeSpace] = useState<number | null>(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
const [automaticExtractionEnabled, setAutomaticExtractionEnabled] =
useState(true);
const [selectedDownloader, setSelectedDownloader] =
useState<Downloader | null>(null);
const [hasWritePermission, setHasWritePermission] = useState<boolean | null>(
@@ -72,6 +81,21 @@ export function DownloadSettingsModal({
return getDownloadersForUris(repack?.uris ?? []);
}, [repack?.uris]);
const getDefaultDownloader = useCallback(
(availableDownloaders: Downloader[]) => {
if (availableDownloaders.includes(Downloader.TorBox)) {
return Downloader.TorBox;
}
if (availableDownloaders.includes(Downloader.RealDebrid)) {
return Downloader.RealDebrid;
}
return availableDownloaders[0];
},
[]
);
useEffect(() => {
if (userPreferences?.downloadsPath) {
setSelectedPath(userPreferences.downloadsPath);
@@ -89,13 +113,9 @@ export function DownloadSettingsModal({
return true;
});
/* Gives preference to TorBox */
const selectedDownloader = filteredDownloaders.includes(Downloader.TorBox)
? Downloader.TorBox
: filteredDownloaders[0];
setSelectedDownloader(selectedDownloader ?? null);
setSelectedDownloader(getDefaultDownloader(filteredDownloaders));
}, [
getDefaultDownloader,
userPreferences?.downloadsPath,
downloaders,
userPreferences?.realDebridApiToken,
@@ -122,7 +142,8 @@ export function DownloadSettingsModal({
const response = await startDownload(
repack,
selectedDownloader!,
selectedPath
selectedPath,
automaticExtractionEnabled
);
if (response.ok) {
@@ -217,6 +238,16 @@ export function DownloadSettingsModal({
</p>
</div>
{selectedDownloader !== Downloader.Torrent && (
<CheckboxField
label={t("automatically_extract_downloaded_files")}
checked={automaticExtractionEnabled}
onChange={() =>
setAutomaticExtractionEnabled(!automaticExtractionEnabled)
}
/>
)}
<Button
onClick={handleStartClick}
disabled={

View File

@@ -16,7 +16,8 @@ export interface RepacksModalProps {
startDownload: (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
downloadPath: string,
automaticallyExtract: boolean
) => Promise<{ ok: boolean; error?: string }>;
onClose: () => void;
}

View File

@@ -120,7 +120,7 @@ export function AddDownloadSourceModal({
downloadSourcesWorker.postMessage(["IMPORT_DOWNLOAD_SOURCE", url]);
channel.onmessage = () => {
window.electron.createDownloadSource(url);
window.electron.createDownloadSources([url]);
setIsLoading(false);
putDownloadSource();

View File

@@ -139,9 +139,9 @@ export function SettingsGeneral() {
}))}
/>
<p className="settings-general__notifications-title">
<h2 className="settings-general__notifications-title">
{t("notifications")}
</p>
</h2>
<CheckboxField
label={t("enable_download_notifications")}