feat: adding icon parser to download notification

This commit is contained in:
Chubby Granny Chaser 2024-06-05 20:15:59 +01:00
parent 6b8ab895e3
commit 4a4a800b07
No known key found for this signature in database
17 changed files with 343 additions and 146 deletions

View file

@ -133,7 +133,9 @@
"download_in_progress": "In progress",
"queued_downloads": "Queued downloads",
"downloads_completed": "Completed",
"queued": "Queued"
"queued": "Queued",
"no_downloads_title": "Such empty",
"no_downloads_description": "You haven't downloaded anything with Hydra yet, but it's never too late to start."
},
"settings": {
"downloads_path": "Downloads path",

View file

@ -130,7 +130,9 @@
"download_in_progress": "Baixando agora",
"queued_downloads": "Na fila",
"downloads_completed": "Completo",
"queued": "Na fila"
"queued": "Na fila",
"no_downloads_title": "Nada por aqui…",
"no_downloads_description": "Você ainda não baixou nada pelo Hydra, mas nunca é tarde para começar."
},
"settings": {
"downloads_path": "Diretório dos downloads",

View file

@ -34,33 +34,32 @@ const deleteGameFolder = async (
game.folderName
);
if (!fs.existsSync(folderPath)) {
await gameRepository.update(
{ id: gameId },
{ downloadPath: null, folderName: null }
);
}
if (fs.existsSync(folderPath)) {
await new Promise<void>((resolve, reject) => {
fs.rm(
folderPath,
{ recursive: true, force: true, maxRetries: 5, retryDelay: 200 },
(error) => {
if (error) {
logger.error(error);
reject();
}
return new Promise<void>((resolve, reject) => {
fs.rm(
folderPath,
{ recursive: true, force: true, maxRetries: 5, retryDelay: 200 },
(error) => {
if (error) {
logger.error(error);
reject();
const aria2ControlFilePath = `${folderPath}.aria2`;
if (fs.existsSync(aria2ControlFilePath))
fs.rmSync(aria2ControlFilePath);
resolve();
}
resolve();
}
);
}).then(async () => {
await gameRepository.update(
{ id: gameId },
{ downloadPath: null, folderName: null }
);
});
);
});
}
}
await gameRepository.update(
{ id: gameId },
{ downloadPath: null, folderName: null, status: null, progress: 0 }
);
};
registerEvent("deleteGameFolder", deleteGameFolder);

View file

@ -1,12 +1,14 @@
import { DownloadManager, RepacksManager, startMainLoop } from "./services";
import {
downloadQueueRepository,
repackRepository,
userPreferencesRepository,
} from "./repository";
import { UserPreferences } from "./entity";
import { RealDebridClient } from "./services/real-debrid";
import { fetchDownloadSourcesAndUpdate } from "./helpers";
import { publishNewRepacksNotifications } from "./services/notifications";
import { MoreThan } from "typeorm";
startMainLoop();
@ -30,8 +32,16 @@ const loadState = async (userPreferences: UserPreferences | null) => {
if (nextQueueItem?.game.status === "active")
DownloadManager.startDownload(nextQueueItem.game);
fetchDownloadSourcesAndUpdate().then(() => {
publishNewRepacksNotifications(300);
const now = new Date();
fetchDownloadSourcesAndUpdate().then(async () => {
const newRepacksCount = await repackRepository.count({
where: {
createdAt: MoreThan(now),
},
});
if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount);
});
};

View file

@ -1,5 +1,7 @@
import Aria2, { StatusResponse } from "aria2";
import path from "node:path";
import { downloadQueueRepository, gameRepository } from "@main/repository";
import { WindowManager } from "./window-manager";
@ -67,7 +69,11 @@ export class DownloadManager {
private static getFolderName(status: StatusResponse) {
if (status.bittorrent?.info) return status.bittorrent.info.name;
return "";
const [file] = status.files;
if (file) return path.win32.basename(file.path);
return null;
}
private static async getRealDebridDownloadUrl() {
@ -198,7 +204,7 @@ export class DownloadManager {
}
if (progress === 1 && this.game && !isDownloadingMetadata) {
await publishDownloadCompleteNotification(this.game);
publishDownloadCompleteNotification(this.game);
await downloadQueueRepository.delete({ game: this.game });
@ -220,7 +226,9 @@ export class DownloadManager {
},
});
this.resumeDownload(nextQueueItem!.game);
if (nextQueueItem) {
this.resumeDownload(nextQueueItem.game);
}
}
}
@ -237,7 +245,7 @@ export class DownloadManager {
const gid = this.downloads.get(gameId);
if (gid) {
await this.aria2.call("remove", gid);
await this.aria2.call("forceRemove", gid);
if (this.gid === gid) {
this.clearCurrentDownload();

View file

@ -1,13 +1,40 @@
import { Notification } from "electron";
import { Notification, nativeImage } from "electron";
import { t } from "i18next";
import { parseICO } from "icojs";
import { Game } from "@main/entity";
import { userPreferencesRepository } from "@main/repository";
import { gameRepository, userPreferencesRepository } from "@main/repository";
const getGameIconNativeImage = async (gameId: number) => {
try {
const game = await gameRepository.findOne({
where: {
id: gameId,
},
});
if (!game?.iconUrl) return undefined;
const images = await parseICO(
Buffer.from(game.iconUrl.split("base64,")[1], "base64")
);
const highResIcon = images.find((image) => image.width >= 128);
if (!highResIcon) return undefined;
return nativeImage.createFromBuffer(Buffer.from(highResIcon.buffer));
} catch (err) {
return undefined;
}
};
export const publishDownloadCompleteNotification = async (game: Game) => {
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
const icon = await getGameIconNativeImage(game.id);
if (userPreferences?.downloadNotificationsEnabled) {
new Notification({
title: t("download_complete", {
@ -19,6 +46,7 @@ export const publishDownloadCompleteNotification = async (game: Game) => {
lng: userPreferences.language,
title: game.title,
}),
icon,
}).show();
}
};
@ -28,7 +56,7 @@ export const publishNewRepacksNotifications = async (count: number) => {
where: { id: 1 },
});
if (count > 0 && userPreferences?.repackUpdatesNotificationsEnabled) {
if (userPreferences?.repackUpdatesNotificationsEnabled) {
new Notification({
title: t("repack_list_updated", {
ns: "notifications",

View file

@ -40,8 +40,6 @@ export function Sidebar() {
updateLibrary();
}, [lastPacket?.game.id, updateLibrary]);
console.log(library);
const isDownloading = library.some(
(game) => game.status === "active" && game.progress !== 1
);

View file

@ -41,12 +41,6 @@ export function useDownload() {
return updateLibrary();
};
const cancelDownload = async (gameId: number) => {
await window.electron.cancelGameDownload(gameId);
dispatch(clearDownload());
updateLibrary();
};
const removeGameInstaller = async (gameId: number) => {
dispatch(setGameDeleting(gameId));
@ -58,6 +52,14 @@ export function useDownload() {
}
};
const cancelDownload = async (gameId: number) => {
await window.electron.cancelGameDownload(gameId);
dispatch(clearDownload());
updateLibrary();
removeGameInstaller(gameId);
};
const removeGameFromLibrary = (gameId: number) =>
window.electron.removeGameFromLibrary(gameId).then(() => {
updateLibrary();

View file

@ -1,6 +1,5 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
export const downloadTitleWrapper = style({
display: "flex",
@ -28,6 +27,7 @@ export const downloads = style({
flexDirection: "column",
margin: "0",
padding: "0",
marginTop: `${SPACING_UNIT}px`,
});
export const downloadCover = style({
@ -64,30 +64,18 @@ export const downloadCoverImage = style({
zIndex: "-1",
});
export const download = recipe({
base: {
width: "100%",
backgroundColor: vars.color.background,
display: "flex",
borderRadius: "8px",
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
boxShadow: "0px 0px 15px 0px #000000",
transition: "all ease 0.2s",
height: "140px",
minHeight: "140px",
maxHeight: "140px",
},
variants: {
cancelled: {
true: {
opacity: vars.opacity.disabled,
":hover": {
opacity: "1",
},
},
},
},
export const download = style({
width: "100%",
backgroundColor: vars.color.background,
display: "flex",
borderRadius: "8px",
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
boxShadow: "0px 0px 15px 0px #000000",
transition: "all ease 0.2s",
height: "140px",
minHeight: "140px",
maxHeight: "140px",
});
export const downloadDetails = style({

View file

@ -15,6 +15,7 @@ import { useAppSelector, useDownload } from "@renderer/hooks";
import * as styles from "./download-group.css";
import { useTranslation } from "react-i18next";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
export interface DownloadGroupProps {
library: LibraryGame[];
@ -42,7 +43,6 @@ export function DownloadGroup({
progress,
pauseDownload,
resumeDownload,
removeGameFromLibrary,
cancelDownload,
isGameDeleting,
} = useDownload();
@ -149,42 +149,20 @@ export function DownloadGroup({
);
}
if (game.status === "paused") {
return (
<>
<Button
onClick={() => resumeDownload(game.id)}
theme="outline"
disabled={
game.downloader === Downloader.RealDebrid &&
!userPreferences?.realDebridApiToken
}
>
{t("resume")}
</Button>
<Button onClick={() => cancelDownload(game.id)} theme="outline">
{t("cancel")}
</Button>
</>
);
}
return (
<>
<Button
onClick={() => navigate(buildGameDetailsPath(game))}
onClick={() => resumeDownload(game.id)}
theme="outline"
disabled={deleting}
disabled={
game.downloader === Downloader.RealDebrid &&
!userPreferences?.realDebridApiToken
}
>
{t("download_again")}
{t("resume")}
</Button>
<Button
onClick={() => removeGameFromLibrary(game.id)}
theme="outline"
disabled={deleting}
>
{t("remove_from_list")}
<Button onClick={() => cancelDownload(game.id)} theme="outline">
{t("cancel")}
</Button>
</>
);
@ -194,17 +172,30 @@ export function DownloadGroup({
return (
<div className={styles.downloadGroup}>
<h2>{title}</h2>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<h2>{title}</h2>
<div
style={{
flex: 1,
backgroundColor: vars.color.border,
height: "1px",
}}
/>
<h3 style={{ fontWeight: "400" }}>{library.length}</h3>
</div>
<ul className={styles.downloads}>
{library.map((game) => {
return (
<li
key={game.id}
className={styles.download({
cancelled: game.status === "removed",
})}
>
<li key={game.id} className={styles.download}>
<div className={styles.downloadCover}>
<div className={styles.downloadCoverBackdrop}>
<img

View file

@ -13,3 +13,24 @@ export const downloadGroups = style({
gap: `${SPACING_UNIT * 3}px`,
flexDirection: "column",
});
export const arrowIcon = style({
width: "60px",
height: "60px",
borderRadius: "50%",
backgroundColor: "rgba(255, 255, 255, 0.06)",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: `${SPACING_UNIT * 2}px`,
});
export const noDownloads = style({
display: "flex",
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
});

View file

@ -9,6 +9,7 @@ import { DeleteGameModal } from "./delete-game-modal";
import { DownloadGroup } from "./download-group";
import { LibraryGame } from "@types";
import { orderBy } from "lodash-es";
import { ArrowDownIcon } from "@primer/octicons-react";
export function Downloads() {
const { library, updateLibrary } = useLibrary();
@ -48,8 +49,8 @@ export function Downloads() {
};
const result = library.reduce((prev, next) => {
/* Game has been manually added to the library */
if (!next.status) return prev;
/* Game has been manually added to the library or has been canceled */
if (!next.status || next.status === "removed") return prev;
/* Is downloading */
if (lastPacket?.game.id === next.id)
@ -94,8 +95,12 @@ export function Downloads() {
},
];
const hasItemsInLibrary = useMemo(() => {
return Object.values(libraryGroup).some((group) => group.length > 0);
}, [libraryGroup]);
return (
<section className={styles.downloadsContainer}>
<>
<BinaryNotFoundModal
visible={showBinaryNotFoundModal}
onClose={() => setShowBinaryNotFoundModal(false)}
@ -107,17 +112,31 @@ export function Downloads() {
deleteGame={handleDeleteGame}
/>
<div className={styles.downloadGroups}>
{downloadGroups.map((group) => (
<DownloadGroup
key={group.title}
title={group.title}
library={group.library}
openDeleteGameModal={handleOpenDeleteGameModal}
openGameInstaller={handleOpenGameInstaller}
/>
))}
</div>
</section>
{hasItemsInLibrary ? (
<section className={styles.downloadsContainer}>
<div className={styles.downloadGroups}>
{downloadGroups.map((group) => (
<DownloadGroup
key={group.title}
title={group.title}
library={group.library}
openDeleteGameModal={handleOpenDeleteGameModal}
openGameInstaller={handleOpenGameInstaller}
/>
))}
</div>
</section>
) : (
<div className={styles.noDownloads}>
<div className={styles.arrowIcon}>
<ArrowDownIcon size={24} />
</div>
<h2>{t("no_downloads_title")}</h2>
<p style={{ fontFamily: "Fira Sans" }}>
{t("no_downloads_description")}
</p>
</div>
)}
</>
);
}

View file

@ -6,7 +6,6 @@ export const contentSidebar = style({
borderLeft: `solid 1px ${vars.color.border};`,
width: "100%",
height: "100%",
position: "relative",
"@media": {
"(min-width: 768px)": {
width: "100%",
@ -87,14 +86,6 @@ export const howLongToBeatCategorySkeleton = style({
height: "76px",
});
export const technicalDetailsContainer = style({
padding: `0 ${SPACING_UNIT * 2}px`,
color: vars.color.body,
userSelect: "text",
position: "absolute",
bottom: `${SPACING_UNIT}px`,
});
globalStyle(`${requirementsDetails} a`, {
display: "flex",
color: vars.color.body,

View file

@ -16,8 +16,7 @@ export function Sidebar() {
const [activeRequirement, setActiveRequirement] =
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
const { gameTitle, shopDetails, shop, objectID } =
useContext(gameDetailsContext);
const { gameTitle, shopDetails, objectID } = useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
@ -75,15 +74,6 @@ export function Sidebar() {
}),
}}
/>
<div className={styles.technicalDetailsContainer}>
<p>
<small>shop: &quot;{shop}&quot;</small>
</p>
<p>
<small>objectID: &quot;{objectID}&quot;</small>
</p>
</div>
</aside>
);
}

View file

@ -18,5 +18,6 @@ export const [themeClass, vars] = createTheme({
},
size: {
body: "14px",
small: "12px",
},
});