mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding icon parser to download notification
This commit is contained in:
parent
6b8ab895e3
commit
4a4a800b07
17 changed files with 343 additions and 146 deletions
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`,
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: "{shop}"</small>
|
||||
</p>
|
||||
<p>
|
||||
<small>objectID: "{objectID}"</small>
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,5 +18,6 @@ export const [themeClass, vars] = createTheme({
|
|||
},
|
||||
size: {
|
||||
body: "14px",
|
||||
small: "12px",
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue