{achievement.points != undefined ? (
-
-
{achievement.points}
+
+
+ {achievement.points}
+
) : (
)}
{achievement.unlockTime != null && (
{formatDateTime(achievement.unlockTime)}
diff --git a/src/renderer/src/pages/achievements/achievements.scss b/src/renderer/src/pages/achievements/achievements.scss
new file mode 100644
index 00000000..5a5de8e6
--- /dev/null
+++ b/src/renderer/src/pages/achievements/achievements.scss
@@ -0,0 +1,262 @@
+@use "../../scss/globals.scss";
+@use "sass:math";
+
+$hero-height: 150px;
+$logo-height: 100px;
+$logo-max-width: 200px;
+
+.achievements {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ transition: all ease 0.3s;
+
+ &__hero {
+ width: 100%;
+ height: $hero-height;
+ min-height: $hero-height;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ transition: all ease 0.2s;
+
+ &-content {
+ padding: globals.$spacing-unit * 2;
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &-logo-backdrop {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ }
+
+ &-image-skeleton {
+ height: 150px;
+ }
+ }
+
+ &__game-logo {
+ width: $logo-max-width;
+ height: $logo-height;
+ object-fit: contain;
+ transition: all ease 0.2s;
+
+ &:hover {
+ transform: scale(1.05);
+ }
+ }
+
+ &__container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+ z-index: 1;
+ }
+
+ &__table-header {
+ width: 100%;
+ background-color: var(--color-dark-background);
+ transition: all ease 0.2s;
+ border-bottom: solid 1px var(--color-border);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+
+ &--stuck {
+ box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
+ }
+ }
+
+ &__list {
+ list-style: none;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: globals.$spacing-unit * 2;
+ padding: globals.$spacing-unit * 2;
+ width: 100%;
+ background-color: var(--color-background);
+ }
+
+ &__item {
+ display: flex;
+ transition: all ease 0.1s;
+ color: var(--color-muted);
+ width: 100%;
+ overflow: hidden;
+ border-radius: 4px;
+ padding: globals.$spacing-unit globals.$spacing-unit;
+ gap: globals.$spacing-unit * 2;
+ align-items: center;
+ text-align: left;
+
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.15);
+ text-decoration: none;
+ }
+
+ &-image {
+ width: 54px;
+ height: 54px;
+ border-radius: 4px;
+ object-fit: cover;
+
+ &--locked {
+ filter: grayscale(100%);
+ }
+ }
+
+ &-content {
+ flex: 1;
+ }
+
+ &-title {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ &-hidden-icon {
+ display: flex;
+ color: var(--color-warning);
+ opacity: 0.8;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ svg {
+ width: 12px;
+ height: 12px;
+ }
+ }
+
+ &-eye-closed {
+ width: 12px;
+ height: 12px;
+ color: globals.$warning-color;
+ scale: 4;
+ }
+
+ &-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &-points {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-right: 4px;
+ font-weight: 600;
+
+ &--locked {
+ cursor: pointer;
+ color: var(--color-warning);
+ }
+
+ &-icon {
+ width: 18px;
+ height: 18px;
+ }
+
+ &-value {
+ font-size: 1.1em;
+ }
+ }
+
+ &-unlock-time {
+ white-space: nowrap;
+ gap: 4px;
+ display: flex;
+ }
+
+ &-compared {
+ display: grid;
+ grid-template-columns: 3fr 1fr 1fr;
+
+ &--no-owner {
+ grid-template-columns: 3fr 2fr;
+ }
+ }
+
+ &-main {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: globals.$spacing-unit;
+ }
+
+ &-status {
+ display: flex;
+ padding: globals.$spacing-unit;
+ justify-content: center;
+
+ &--unlocked {
+ white-space: nowrap;
+ flex-direction: row;
+ gap: globals.$spacing-unit;
+ padding: 0;
+ }
+ }
+ }
+
+ &__progress-bar {
+ width: 100%;
+ height: 8px;
+ transition: all ease 0.2s;
+
+ &::-webkit-progress-bar {
+ background-color: rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ }
+
+ &::-webkit-progress-value {
+ background-color: var(--color-muted);
+ border-radius: 4px;
+ }
+ }
+
+ &__profile-avatar {
+ height: 54px;
+ width: 54px;
+ border-radius: 4px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: var(--color-background);
+ position: relative;
+ object-fit: cover;
+
+ &--small {
+ height: 32px;
+ width: 32px;
+ }
+ }
+
+ &__subscription-button {
+ text-decoration: none;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ gap: math.div(globals.$spacing-unit, 2);
+ color: var(--color-body);
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+}
diff --git a/src/renderer/src/pages/achievements/achievements.tsx b/src/renderer/src/pages/achievements/achievements.tsx
index 605300ef..f467cf89 100644
--- a/src/renderer/src/pages/achievements/achievements.tsx
+++ b/src/renderer/src/pages/achievements/achievements.tsx
@@ -44,7 +44,7 @@ export default function Achievements() {
.getComparedUnlockedAchievements(objectId, shop as GameShop, userId)
.then(setComparedAchievements);
}
- }, [objectId, shop, userId]);
+ }, [objectId, shop, userDetails?.id, userId]);
const otherUserId = userDetails?.id === userId ? null : userId;
diff --git a/src/renderer/src/pages/downloads/download-group.css.ts b/src/renderer/src/pages/downloads/download-group.css.ts
deleted file mode 100644
index cbbb4f8e..00000000
--- a/src/renderer/src/pages/downloads/download-group.css.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { style } from "@vanilla-extract/css";
-
-import { SPACING_UNIT, vars } from "../../theme.css";
-
-export const downloadTitleWrapper = style({
- display: "flex",
- alignItems: "center",
- marginBottom: `${SPACING_UNIT}px`,
- gap: `${SPACING_UNIT}px`,
-});
-
-export const downloadTitle = style({
- fontWeight: "bold",
- cursor: "pointer",
- color: vars.color.body,
- textAlign: "left",
- fontSize: "16px",
- display: "block",
- ":hover": {
- textDecoration: "underline",
- },
-});
-
-export const downloads = style({
- width: "100%",
- gap: `${SPACING_UNIT * 2}px`,
- display: "flex",
- flexDirection: "column",
- margin: "0",
- padding: "0",
- marginTop: `${SPACING_UNIT}px`,
-});
-
-export const downloadCover = style({
- width: "280px",
- minWidth: "280px",
- height: "auto",
- borderRight: `solid 1px ${vars.color.border}`,
- position: "relative",
- zIndex: "1",
-});
-
-export const downloadCoverContent = style({
- width: "100%",
- height: "100%",
- padding: `${SPACING_UNIT}px`,
- display: "flex",
- alignItems: "flex-end",
- justifyContent: "flex-end",
-});
-
-export const downloadCoverBackdrop = style({
- width: "100%",
- height: "100%",
- background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
- display: "flex",
- overflow: "hidden",
- zIndex: "1",
-});
-
-export const downloadCoverImage = style({
- width: "100%",
- height: "100%",
- position: "absolute",
- zIndex: "-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 5px 0px #000000",
- transition: "all ease 0.2s",
- height: "140px",
- minHeight: "140px",
- maxHeight: "140px",
-});
-
-export const downloadDetails = style({
- display: "flex",
- flexDirection: "column",
- flex: "1",
- justifyContent: "center",
- gap: `${SPACING_UNIT / 2}px`,
- fontSize: "14px",
-});
-
-export const downloadRightContent = style({
- display: "flex",
- padding: `${SPACING_UNIT * 2}px`,
- flex: "1",
- gap: `${SPACING_UNIT}px`,
- background: "linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%)",
-});
-
-export const downloadActions = style({
- display: "flex",
- alignItems: "center",
- gap: `${SPACING_UNIT}px`,
-});
-
-export const downloadGroup = style({
- display: "flex",
- flexDirection: "column",
- gap: `${SPACING_UNIT * 2}px`,
-});
diff --git a/src/renderer/src/pages/downloads/download-group.scss b/src/renderer/src/pages/downloads/download-group.scss
new file mode 100644
index 00000000..2c5e9701
--- /dev/null
+++ b/src/renderer/src/pages/downloads/download-group.scss
@@ -0,0 +1,140 @@
+@use "../../scss/globals.scss";
+
+.download-group {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 2);
+
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: calc(globals.$spacing-unit * 2);
+
+ &-divider {
+ flex: 1;
+ background-color: globals.$border-color;
+ height: 1px;
+ }
+
+ &-count {
+ font-weight: 400;
+ }
+ }
+
+ &__title-wrapper {
+ display: flex;
+ align-items: center;
+ margin-bottom: globals.$spacing-unit;
+ gap: globals.$spacing-unit;
+ }
+
+ &__title {
+ font-weight: bold;
+ cursor: pointer;
+ color: globals.$body-color;
+ text-align: left;
+ font-size: 16px;
+ display: block;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ &__downloads {
+ width: 100%;
+ gap: calc(globals.$spacing-unit * 2);
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+ margin-top: globals.$spacing-unit;
+ }
+
+ &__item {
+ width: 100%;
+ background-color: globals.$background-color;
+ display: flex;
+ border-radius: 8px;
+ border: solid 1px globals.$border-color;
+ overflow: hidden;
+ box-shadow: 0px 0px 5px 0px #000000;
+ transition: all ease 0.2s;
+ height: 140px;
+ min-height: 140px;
+ max-height: 140px;
+ position: relative;
+ }
+
+ &__cover {
+ width: 280px;
+ min-width: 280px;
+ height: auto;
+ border-right: solid 1px globals.$border-color;
+ position: relative;
+ z-index: 1;
+
+ &-content {
+ width: 100%;
+ height: 100%;
+ padding: globals.$spacing-unit;
+ display: flex;
+ align-items: flex-end;
+ justify-content: flex-end;
+ }
+
+ &-backdrop {
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 0deg,
+ rgba(0, 0, 0, 0.8) 5%,
+ transparent 100%
+ );
+ display: flex;
+ overflow: hidden;
+ z-index: 1;
+ }
+
+ &-image {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ z-index: -1;
+ }
+ }
+
+ &__right-content {
+ display: flex;
+ padding: calc(globals.$spacing-unit * 2);
+ flex: 1;
+ gap: globals.$spacing-unit;
+ background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
+ }
+
+ &__details {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ justify-content: center;
+ gap: calc(globals.$spacing-unit / 2);
+ font-size: 14px;
+ }
+
+ &__actions {
+ display: flex;
+ align-items: center;
+ gap: globals.$spacing-unit;
+ }
+
+ &__menu-button {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ border-radius: 50%;
+ border: none;
+ padding: 8px;
+ min-height: unset;
+ }
+}
diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx
index 88cf1433..d84d6601 100644
--- a/src/renderer/src/pages/downloads/download-group.tsx
+++ b/src/renderer/src/pages/downloads/download-group.tsx
@@ -1,6 +1,6 @@
import { useNavigate } from "react-router-dom";
-import type { LibraryGame, SeedingStatus } from "@types";
+import type { GameShop, LibraryGame, SeedingStatus } from "@types";
import { Badge, Button } from "@renderer/components";
import {
@@ -12,9 +12,8 @@ import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
import { DOWNLOADER_NAME } from "@renderer/constants";
import { useAppSelector, useDownload } from "@renderer/hooks";
-import * as styles from "./download-group.css";
+import "./download-group.scss";
import { useTranslation } from "react-i18next";
-import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { useMemo } from "react";
import {
DropdownMenu,
@@ -31,11 +30,14 @@ import {
XCircleIcon,
} from "@primer/octicons-react";
+import torBoxLogo from "@renderer/assets/icons/torbox.webp";
+import { SPACING_UNIT, vars } from "@renderer/theme.css";
+
export interface DownloadGroupProps {
library: LibraryGame[];
title: string;
- openDeleteGameModal: (gameId: number) => void;
- openGameInstaller: (gameId: number) => void;
+ openDeleteGameModal: (shop: GameShop, objectId: string) => void;
+ openGameInstaller: (shop: GameShop, objectId: string) => void;
seedingStatus: SeedingStatus[];
}
@@ -45,7 +47,7 @@ export function DownloadGroup({
openDeleteGameModal,
openGameInstaller,
seedingStatus,
-}: DownloadGroupProps) {
+}: Readonly
) {
const navigate = useNavigate();
const { t } = useTranslation("downloads");
@@ -66,18 +68,19 @@ export function DownloadGroup({
} = useDownload();
const getFinalDownloadSize = (game: LibraryGame) => {
- const isGameDownloading = lastPacket?.game.id === game.id;
+ const download = game.download!;
+ const isGameDownloading = lastPacket?.gameId === game.id;
- if (game.fileSize) return formatBytes(game.fileSize);
+ if (download.fileSize) return formatBytes(download.fileSize);
- if (lastPacket?.game.fileSize && isGameDownloading)
- return formatBytes(lastPacket?.game.fileSize);
+ if (lastPacket?.download.fileSize && isGameDownloading)
+ return formatBytes(lastPacket.download.fileSize);
return "N/A";
};
const seedingMap = useMemo(() => {
- const map = new Map();
+ const map = new Map();
seedingStatus.forEach((seed) => {
map.set(seed.gameId, seed);
@@ -87,7 +90,9 @@ export function DownloadGroup({
}, [seedingStatus]);
const getGameInfo = (game: LibraryGame) => {
- const isGameDownloading = lastPacket?.game.id === game.id;
+ const download = game.download!;
+
+ const isGameDownloading = lastPacket?.gameId === game.id;
const finalDownloadSize = getFinalDownloadSize(game);
const seedingStatus = seedingMap.get(game.id);
@@ -114,11 +119,11 @@ export function DownloadGroup({
{progress}
- {formatBytes(lastPacket?.game.bytesDownloaded)} /{" "}
+ {formatBytes(lastPacket.download.bytesDownloaded)} /{" "}
{finalDownloadSize}
- {game.downloader === Downloader.Torrent && (
+ {download.downloader === Downloader.Torrent && (
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
@@ -127,11 +132,11 @@ export function DownloadGroup({
);
}
- if (game.progress === 1) {
+ if (download.progress === 1) {
const uploadSpeed = formatBytes(seedingStatus?.uploadSpeed ?? 0);
- return game.status === "seeding" &&
- game.downloader === Downloader.Torrent ? (
+ return download.status === "seeding" &&
+ download.downloader === Downloader.Torrent ? (
<>
{t("seeding")}
{uploadSpeed && {uploadSpeed}/s
}
@@ -141,41 +146,44 @@ export function DownloadGroup({
);
}
- if (game.status === "paused") {
+ if (download.status === "paused") {
return (
<>
- {formatDownloadProgress(game.progress)}
- {t(game.downloadQueue && lastPacket ? "queued" : "paused")}
+ {formatDownloadProgress(download.progress)}
+ {t(download.queued ? "queued" : "paused")}
>
);
}
- if (game.status === "active") {
+ if (download.status === "active") {
return (
<>
- {formatDownloadProgress(game.progress)}
+ {formatDownloadProgress(download.progress)}
- {formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
+ {formatBytes(download.bytesDownloaded)} / {finalDownloadSize}
>
);
}
- return {t(game.status as string)}
;
+ return {t(download.status as string)}
;
};
const getGameActions = (game: LibraryGame): DropdownMenuItem[] => {
- const isGameDownloading = lastPacket?.game.id === game.id;
+ const download = lastPacket?.download;
+ const isGameDownloading = lastPacket?.gameId === game.id;
const deleting = isGameDeleting(game.id);
- if (game.progress === 1) {
+ if (download?.progress === 1) {
return [
{
label: t("install"),
disabled: deleting,
- onClick: () => openGameInstaller(game.id),
+ onClick: () => {
+ openGameInstaller(game.shop, game.objectId);
+ },
icon: ,
},
{
@@ -183,53 +191,73 @@ export function DownloadGroup({
disabled: deleting,
icon: ,
show:
- game.status === "seeding" && game.downloader === Downloader.Torrent,
- onClick: () => pauseSeeding(game.id),
+ download.status === "seeding" &&
+ download.downloader === Downloader.Torrent,
+ onClick: () => {
+ pauseSeeding(game.shop, game.objectId);
+ },
},
{
label: t("resume_seeding"),
disabled: deleting,
icon: ,
show:
- game.status !== "seeding" && game.downloader === Downloader.Torrent,
- onClick: () => resumeSeeding(game.id),
+ download.status !== "seeding" &&
+ download.downloader === Downloader.Torrent,
+ onClick: () => {
+ resumeSeeding(game.shop, game.objectId);
+ },
},
{
label: t("delete"),
disabled: deleting,
icon: ,
- onClick: () => openDeleteGameModal(game.id),
+ onClick: () => {
+ openDeleteGameModal(game.shop, game.objectId);
+ },
},
];
}
- if (isGameDownloading || game.status === "active") {
+ if (isGameDownloading || download?.status === "active") {
return [
{
label: t("pause"),
- onClick: () => pauseDownload(game.id),
+ onClick: () => {
+ pauseDownload(game.shop, game.objectId);
+ },
icon: ,
},
{
label: t("cancel"),
- onClick: () => cancelDownload(game.id),
+ onClick: () => {
+ cancelDownload(game.shop, game.objectId);
+ },
icon: ,
},
];
}
+ const isResumeDisabled =
+ (download?.downloader === Downloader.RealDebrid &&
+ !userPreferences?.realDebridApiToken) ||
+ (download?.downloader === Downloader.TorBox &&
+ !userPreferences?.torBoxApiToken);
+
return [
{
label: t("resume"),
- disabled:
- game.downloader === Downloader.RealDebrid &&
- !userPreferences?.realDebridApiToken,
- onClick: () => resumeDownload(game.id),
+ disabled: isResumeDisabled,
+ onClick: () => {
+ resumeDownload(game.shop, game.objectId);
+ },
icon: ,
},
{
label: t("cancel"),
- onClick: () => cancelDownload(game.id),
+ onClick: () => {
+ cancelDownload(game.shop, game.objectId);
+ },
icon: ,
},
];
@@ -238,59 +266,64 @@ export function DownloadGroup({
if (!library.length) return null;
return (
-
-
+
+
{title}
-
-
-
{library.length}
+
+
{library.length}
-
+
{library.map((game) => {
return (
- -
-
-
+
-
+
+
})
-
-
{DOWNLOADER_NAME[game.downloader]}
+
+ {game.download?.downloader === Downloader.TorBox ? (
+
+

+
TorBox
+
+ ) : (
+
+ {DOWNLOADER_NAME[game.download!.downloader]}
+
+ )}
-
-
-
+
+
+