mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-31 14:51:02 +00:00
Compare commits
32 Commits
fix/fixing
...
v3.7.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f28fc8ca1 | ||
|
|
d1eb174429 | ||
|
|
82a125237b | ||
|
|
19e312d31e | ||
|
|
79b1f05cde | ||
|
|
cc9ac9dc0f | ||
|
|
19406dd051 | ||
|
|
8aa6e113e7 | ||
|
|
91ad4a68f7 | ||
|
|
a69a6ec510 | ||
|
|
fada6507c3 | ||
|
|
0479f1347b | ||
|
|
817870cdbb | ||
|
|
f44d5c8b49 | ||
|
|
c36109c092 | ||
|
|
b59fb7dc36 | ||
|
|
214a7af408 | ||
|
|
14679fc31e | ||
|
|
e872b2ea8a | ||
|
|
dd7c84b433 | ||
|
|
1546da29cf | ||
|
|
a89b0bb2a8 | ||
|
|
9bdb216e0f | ||
|
|
9779aed8c1 | ||
|
|
058a148c7f | ||
|
|
16e3d52508 | ||
|
|
7e0002cf95 | ||
|
|
bf8b3ca836 | ||
|
|
77e376e742 | ||
|
|
bd28b202c4 | ||
|
|
153b954e78 | ||
|
|
a9e63730be |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.7.4",
|
"version": "3.7.6",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
|||||||
@@ -458,6 +458,7 @@
|
|||||||
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
|
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
|
||||||
"button_delete_all_sources": "Eliminar todo",
|
"button_delete_all_sources": "Eliminar todo",
|
||||||
"added_download_source": "Añadir fuente de descarga",
|
"added_download_source": "Añadir fuente de descarga",
|
||||||
|
"adding": "Añadiendo…",
|
||||||
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
|
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
|
||||||
"insert_valid_json_url": "Introducí una URL de json válida",
|
"insert_valid_json_url": "Introducí una URL de json válida",
|
||||||
"found_download_option_zero": "Sin opciones de descargas encontrada",
|
"found_download_option_zero": "Sin opciones de descargas encontrada",
|
||||||
@@ -563,6 +564,19 @@
|
|||||||
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
|
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
|
||||||
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
|
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
|
||||||
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
|
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
|
||||||
|
"change_achievement_sound": "Cambiar sonido de logro",
|
||||||
|
"download_source_already_exists": "Esta fuente de descarga URL ya existe.",
|
||||||
|
"download_source_failed": "Error",
|
||||||
|
"download_source_matched": "Actualizado",
|
||||||
|
"download_source_matching": "Actualizando",
|
||||||
|
"download_source_no_information": "Sin información disponible",
|
||||||
|
"download_source_pending_matching": "Actualizando pronto",
|
||||||
|
"download_sources_synced_successfully": "Todas las fuentes de descarga están sincronizadas",
|
||||||
|
"failed_add_download_source": "Error al añadir la fuente de descarga. Por favor intentá de nuevo.",
|
||||||
|
"hydra_cloud": "Hydra Cloud",
|
||||||
|
"preview_sound": "Vista previa de sonido",
|
||||||
|
"remove_achievement_sound": "Eliminar sonido de logros",
|
||||||
|
"removed_all_download_sources": "Todas las fuentes de descarga eliminadas",
|
||||||
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
|
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ const resumeGameDownload = async (
|
|||||||
|
|
||||||
const download = await downloadsSublevel.get(gameKey);
|
const download = await downloadsSublevel.get(gameKey);
|
||||||
|
|
||||||
if (download?.status === "paused") {
|
if (
|
||||||
|
download &&
|
||||||
|
(download.status === "paused" || download.status === "active") &&
|
||||||
|
download.progress !== 1
|
||||||
|
) {
|
||||||
await DownloadManager.pauseDownload();
|
await DownloadManager.pauseDownload();
|
||||||
|
|
||||||
for await (const [key, value] of downloadsSublevel.iterator()) {
|
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { downloadsSublevel } from "./level/sublevels/downloads";
|
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||||
import { sortBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { levelKeys, db } from "./level";
|
import { levelKeys, db } from "./level";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
@@ -33,9 +33,7 @@ export const loadState = async () => {
|
|||||||
|
|
||||||
await import("./events");
|
await import("./events");
|
||||||
|
|
||||||
if (process.platform !== "darwin") {
|
Aria2.spawn();
|
||||||
Aria2.spawn();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
||||||
@@ -68,7 +66,7 @@ export const loadState = async () => {
|
|||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
.then((games) => {
|
.then((games) => {
|
||||||
return sortBy(games, "timestamp", "DESC");
|
return orderBy(games, "timestamp", "desc");
|
||||||
});
|
});
|
||||||
|
|
||||||
downloads.forEach((download) => {
|
downloads.forEach((download) => {
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ export class Aria2 {
|
|||||||
private static process: cp.ChildProcess | null = null;
|
private static process: cp.ChildProcess | null = null;
|
||||||
|
|
||||||
public static spawn() {
|
public static spawn() {
|
||||||
const binaryPath = app.isPackaged
|
const binaryPath =
|
||||||
? path.join(process.resourcesPath, "aria2c")
|
process.platform === "darwin"
|
||||||
: path.join(__dirname, "..", "..", "binaries", "aria2c");
|
? "aria2c"
|
||||||
|
: app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, "aria2c")
|
||||||
|
: path.join(__dirname, "..", "..", "binaries", "aria2c");
|
||||||
|
|
||||||
this.process = cp.spawn(
|
this.process = cp.spawn(
|
||||||
binaryPath,
|
binaryPath,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { RealDebridClient } from "./real-debrid";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { sortBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { TorBoxClient } from "./torbox";
|
import { TorBoxClient } from "./torbox";
|
||||||
import { GameFilesManager } from "../game-files-manager";
|
import { GameFilesManager } from "../game-files-manager";
|
||||||
import { HydraDebridClient } from "./hydra-debrid";
|
import { HydraDebridClient } from "./hydra-debrid";
|
||||||
@@ -194,10 +194,10 @@ export class DownloadManager {
|
|||||||
.values()
|
.values()
|
||||||
.all()
|
.all()
|
||||||
.then((games) => {
|
.then((games) => {
|
||||||
return sortBy(
|
return orderBy(
|
||||||
games.filter((game) => game.status === "paused" && game.queued),
|
games.filter((game) => game.status === "paused" && game.queued),
|
||||||
"timestamp",
|
"timestamp",
|
||||||
"DESC"
|
"desc"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,16 +36,13 @@ export class GofileApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getDownloadLink(id: string) {
|
public static async getDownloadLink(id: string) {
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
wt: WT,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await axios.get<{
|
const response = await axios.get<{
|
||||||
status: string;
|
status: string;
|
||||||
data: GofileContentsResponse;
|
data: GofileContentsResponse;
|
||||||
}>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
|
}>(`https://api.gofile.io/contents/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.token}`,
|
Authorization: `Bearer ${this.token}`,
|
||||||
|
"X-Website-Token": WT,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ export class HydraApi {
|
|||||||
const decodedBase64 = atob(payload as string);
|
const decodedBase64 = atob(payload as string);
|
||||||
const jsonData = JSON.parse(decodedBase64);
|
const jsonData = JSON.parse(decodedBase64);
|
||||||
|
|
||||||
const { accessToken, expiresIn, refreshToken } = jsonData;
|
const {
|
||||||
|
accessToken,
|
||||||
|
expiresIn,
|
||||||
|
refreshToken,
|
||||||
|
featurebaseJwt,
|
||||||
|
workwondersJwt,
|
||||||
|
} = jsonData;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
@@ -85,6 +91,8 @@ export class HydraApi {
|
|||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
|
featurebaseJwt,
|
||||||
|
workwondersJwt,
|
||||||
},
|
},
|
||||||
{ valueEncoding: "json" }
|
{ valueEncoding: "json" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ export class WindowManager {
|
|||||||
(details, callback) => {
|
(details, callback) => {
|
||||||
if (
|
if (
|
||||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||||
details.url.includes("chatwoot")
|
details.url.includes("chatwoot") ||
|
||||||
|
details.url.includes("workwonders")
|
||||||
) {
|
) {
|
||||||
return callback(details);
|
return callback(details);
|
||||||
}
|
}
|
||||||
@@ -159,7 +160,8 @@ export class WindowManager {
|
|||||||
if (
|
if (
|
||||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||||
details.url.includes("featurebase") ||
|
details.url.includes("featurebase") ||
|
||||||
details.url.includes("chatwoot")
|
details.url.includes("chatwoot") ||
|
||||||
|
details.url.includes("workwonders")
|
||||||
) {
|
) {
|
||||||
return callback(details);
|
return callback(details);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,10 +122,10 @@ export function BottomPanel() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-featurebase-changelog
|
data-open-workwonders-changelog-mini
|
||||||
className="bottom-panel__version-button"
|
className="bottom-panel__version-button"
|
||||||
>
|
>
|
||||||
<small data-featurebase-changelog>
|
<small>
|
||||||
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
||||||
{VERSION_CODENAME}"
|
{VERSION_CODENAME}"
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface DropdownMenuProps {
|
|||||||
side?: "top" | "bottom" | "left" | "right";
|
side?: "top" | "bottom" | "left" | "right";
|
||||||
align?: "start" | "center" | "end";
|
align?: "start" | "center" | "end";
|
||||||
alignOffset?: number;
|
alignOffset?: number;
|
||||||
|
collisionPadding?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownMenu({
|
export function DropdownMenu({
|
||||||
@@ -29,6 +30,7 @@ export function DropdownMenu({
|
|||||||
loop = true,
|
loop = true,
|
||||||
align = "center",
|
align = "center",
|
||||||
alignOffset = 0,
|
alignOffset = 0,
|
||||||
|
collisionPadding = 16,
|
||||||
}: Readonly<DropdownMenuProps>) {
|
}: Readonly<DropdownMenuProps>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Root>
|
<DropdownMenuPrimitive.Root>
|
||||||
@@ -43,6 +45,7 @@ export function DropdownMenu({
|
|||||||
loop={loop}
|
loop={loop}
|
||||||
align={align}
|
align={align}
|
||||||
alignOffset={alignOffset}
|
alignOffset={alignOffset}
|
||||||
|
collisionPadding={collisionPadding}
|
||||||
className="dropdown-menu__content"
|
className="dropdown-menu__content"
|
||||||
>
|
>
|
||||||
{title && (
|
{title && (
|
||||||
|
|||||||
@@ -18,17 +18,32 @@
|
|||||||
&__header {
|
&__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: calc(globals.$spacing-unit);
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
|
||||||
|
|
||||||
&-divider {
|
&-title-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: globals.$border-color;
|
|
||||||
height: 1px;
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-count {
|
&-count {
|
||||||
font-weight: 400;
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&--hero {
|
&--hero {
|
||||||
@@ -37,7 +52,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-bottom: calc(globals.$spacing-unit * 3);
|
padding-bottom: globals.$spacing-unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__hero-background {
|
&__hero-background {
|
||||||
@@ -80,65 +95,161 @@
|
|||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__hero-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__hero-logo {
|
&__hero-logo {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: scale 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
scale: 1.05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 600px;
|
max-width: 180px;
|
||||||
max-height: 200px;
|
max-height: 60px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 700px) {
|
||||||
|
max-width: 220px;
|
||||||
|
max-height: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 900px) {
|
||||||
|
max-width: 280px;
|
||||||
|
max-height: 95px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1200px) {
|
||||||
|
max-width: 340px;
|
||||||
|
max-height: 115px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1500px) {
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 130px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 64px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-shadow: 2px 2px 12px rgba(0, 0, 0, 0.9);
|
text-shadow: 2px 2px 12px rgba(0, 0, 0, 0.9);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
cursor: pointer;
|
||||||
}
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
&__hero-actions {
|
&:hover {
|
||||||
display: flex;
|
opacity: 0.8;
|
||||||
gap: calc(globals.$spacing-unit);
|
}
|
||||||
align-items: center;
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 700px) {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 900px) {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1200px) {
|
||||||
|
font-size: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container #{globals.$app-container} (min-width: 1500px) {
|
||||||
|
font-size: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__hero-action-row {
|
&__hero-action-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: flex-start;
|
||||||
gap: calc(globals.$spacing-unit * 3);
|
gap: calc(globals.$spacing-unit * 3);
|
||||||
margin-bottom: calc(globals.$spacing-unit * 3);
|
margin-top: calc(globals.$spacing-unit * 4);
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__hero-menu-btn {
|
&__hero-buttons {
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
display: flex;
|
||||||
padding: calc(globals.$spacing-unit * 1);
|
gap: calc(globals.$spacing-unit);
|
||||||
min-height: unset;
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
&__hero-menu-btn:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
&__glass-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__hero-progress {
|
&__hero-progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: calc(globals.$spacing-unit);
|
|
||||||
margin-bottom: calc(globals.$spacing-unit * 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__progress-header {
|
&__progress-info-row {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: calc(globals.$spacing-unit / 2);
|
}
|
||||||
|
|
||||||
|
&__progress-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&--bar {
|
||||||
|
margin-top: calc(globals.$spacing-unit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__progress-status {
|
&__progress-status {
|
||||||
@@ -153,22 +264,36 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
align-self: flex-end;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
&__progress-details {
|
span {
|
||||||
display: flex;
|
display: inline-block;
|
||||||
justify-content: space-between;
|
}
|
||||||
align-items: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
margin-top: calc(globals.$spacing-unit / 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__progress-size {
|
&__progress-size {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-status {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__progress-time {
|
&__progress-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
font-size: 13px;
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +315,7 @@
|
|||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
padding-right: calc(globals.$spacing-unit * 2);
|
padding-right: calc(globals.$spacing-unit * 2);
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__speed-chart {
|
&__speed-chart {
|
||||||
@@ -202,7 +328,7 @@
|
|||||||
|
|
||||||
&__speed-chart-canvas {
|
&__speed-chart-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px;
|
height: 80px;
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +345,9 @@
|
|||||||
|
|
||||||
&__stat-content {
|
&__stat-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
justify-content: space-between;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__stat-label {
|
&__stat-label {
|
||||||
@@ -251,14 +379,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all ease 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.02);
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__simple-thumbnail {
|
&__simple-thumbnail {
|
||||||
@@ -268,6 +389,22 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -281,7 +418,22 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: calc(globals.$spacing-unit / 2);
|
gap: calc(globals.$spacing-unit / 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-title-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__simple-title {
|
&__simple-title {
|
||||||
@@ -295,6 +447,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__simple-meta {
|
&__simple-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__simple-meta-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
@@ -303,9 +461,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__simple-size {
|
&__simple-size {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__simple-extracting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
font-weight: 500;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
&__simple-seeding {
|
&__simple-seeding {
|
||||||
color: #4ade80;
|
color: #4ade80;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -342,13 +511,20 @@
|
|||||||
min-height: unset;
|
min-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__progress-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
}
|
||||||
|
|
||||||
&__progress-bar {
|
&__progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
margin-top: calc(globals.$spacing-unit / 2);
|
||||||
|
|
||||||
&--small {
|
&--small {
|
||||||
height: 6px;
|
height: 6px;
|
||||||
@@ -357,10 +533,8 @@
|
|||||||
|
|
||||||
&__progress-fill {
|
&__progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: globals.$muted-color;
|
background-color: #fff;
|
||||||
transition:
|
transition: width 0.3s ease;
|
||||||
width 0.3s ease,
|
|
||||||
background 0.35s ease;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
||||||
|
|
||||||
import { Badge, Button } from "@renderer/components";
|
import { Badge, Button } from "@renderer/components";
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import {
|
||||||
|
formatDownloadProgress,
|
||||||
|
buildGameDetailsPath,
|
||||||
|
} from "@renderer/helpers";
|
||||||
|
|
||||||
import { Downloader, formatBytes, formatBytesToMbps } from "@shared";
|
import { Downloader, formatBytes, formatBytesToMbps } from "@shared";
|
||||||
import { formatDistance, addMilliseconds } from "date-fns";
|
import { addMilliseconds } from "date-fns";
|
||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
import {
|
||||||
|
useAppSelector,
|
||||||
|
useDownload,
|
||||||
|
useLibrary,
|
||||||
|
useDate,
|
||||||
|
} from "@renderer/hooks";
|
||||||
|
|
||||||
import "./download-group.scss";
|
import "./download-group.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
} from "@renderer/components/dropdown-menu/dropdown-menu";
|
} from "@renderer/components/dropdown-menu/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
|
ClockIcon,
|
||||||
ColumnsIcon,
|
ColumnsIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
FileDirectoryIcon,
|
FileDirectoryIcon,
|
||||||
@@ -29,22 +40,46 @@ import {
|
|||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { average } from "color.js";
|
import { average } from "color.js";
|
||||||
|
|
||||||
const getProgressGradient = (
|
interface AnimatedPercentageProps {
|
||||||
colorHex: string,
|
value: number;
|
||||||
isPaused = false
|
}
|
||||||
): string | undefined => {
|
|
||||||
const hex = isPaused ? "#ffffff" : colorHex || "#08ea79";
|
|
||||||
if (!hex.startsWith("#")) return undefined;
|
|
||||||
|
|
||||||
try {
|
function AnimatedPercentage({ value }: Readonly<AnimatedPercentageProps>) {
|
||||||
const r = Number.parseInt(hex.slice(1, 3), 16);
|
const percentageText = formatDownloadProgress(value);
|
||||||
const g = Number.parseInt(hex.slice(3, 5), 16);
|
const prevTextRef = useRef<string>(percentageText);
|
||||||
const b = Number.parseInt(hex.slice(5, 7), 16);
|
const chars = percentageText.split("");
|
||||||
return `linear-gradient(90deg, rgba(${r},${g},${b},0.95) 0%, rgba(${r},${g},${b},0.65) 100%)`;
|
const prevChars = prevTextRef.current.split("");
|
||||||
} catch {
|
|
||||||
return undefined;
|
useEffect(() => {
|
||||||
}
|
prevTextRef.current = percentageText;
|
||||||
};
|
}, [percentageText]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{chars.map((char, index) => {
|
||||||
|
const prevChar = prevChars[index];
|
||||||
|
const charChanged = prevChar !== char;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence key={`${index}`} mode="wait" initial={false}>
|
||||||
|
<motion.span
|
||||||
|
key={`${char}-${value}-${index}`}
|
||||||
|
initial={
|
||||||
|
charChanged ? { y: 10, opacity: 0 } : { y: 0, opacity: 1 }
|
||||||
|
}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={charChanged ? { y: -10, opacity: 0 } : undefined}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
style={{ display: "inline-block" }}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface SpeedChartProps {
|
interface SpeedChartProps {
|
||||||
speeds: number[];
|
speeds: number[];
|
||||||
@@ -67,6 +102,7 @@ function SpeedChart({
|
|||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
let animationFrameId: number;
|
let animationFrameId: number;
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
const clientWidth = canvas.clientWidth;
|
const clientWidth = canvas.clientWidth;
|
||||||
@@ -78,10 +114,12 @@ function SpeedChart({
|
|||||||
|
|
||||||
const width = clientWidth;
|
const width = clientWidth;
|
||||||
const height = 100;
|
const height = 100;
|
||||||
const totalBars = 120;
|
|
||||||
const barWidth = 4;
|
const barWidth = 4;
|
||||||
const barGap = 10;
|
const barGap = 10;
|
||||||
const barSpacing = barWidth + barGap;
|
const barSpacing = barWidth + barGap;
|
||||||
|
|
||||||
|
// Calculate how many bars can fit in the available width
|
||||||
|
const totalBars = Math.max(1, Math.floor((width + barGap) / barSpacing));
|
||||||
const maxHeight = peakSpeed || Math.max(...speeds, 1);
|
const maxHeight = peakSpeed || Math.max(...speeds, 1);
|
||||||
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
@@ -138,8 +176,22 @@ function SpeedChart({
|
|||||||
|
|
||||||
animationFrameId = requestAnimationFrame(draw);
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
// Handle resize - trigger redraw when canvas size changes
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
// Cancel any pending animation frame to force immediate redraw
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
// Trigger a redraw that will recalculate bars based on new width
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(canvas);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(animationFrameId);
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [speeds, peakSpeed, color]);
|
}, [speeds, peakSpeed, color]);
|
||||||
|
|
||||||
@@ -158,12 +210,11 @@ interface HeroDownloadViewProps {
|
|||||||
dominantColor: string;
|
dominantColor: string;
|
||||||
lastPacket: ReturnType<typeof useDownload>["lastPacket"];
|
lastPacket: ReturnType<typeof useDownload>["lastPacket"];
|
||||||
speedHistory: number[];
|
speedHistory: number[];
|
||||||
getGameActions: (game: LibraryGame) => DropdownMenuItem[];
|
|
||||||
getStatusText: (game: LibraryGame) => string;
|
|
||||||
formatSpeed: (speed: number) => string;
|
formatSpeed: (speed: number) => string;
|
||||||
calculateETA: () => string;
|
calculateETA: () => string;
|
||||||
pauseDownload: (shop: GameShop, objectId: string) => void;
|
pauseDownload: (shop: GameShop, objectId: string) => void;
|
||||||
resumeDownload: (shop: GameShop, objectId: string) => void;
|
resumeDownload: (shop: GameShop, objectId: string) => void;
|
||||||
|
cancelDownload: (shop: GameShop, objectId: string) => void;
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,14 +228,19 @@ function HeroDownloadView({
|
|||||||
dominantColor,
|
dominantColor,
|
||||||
lastPacket,
|
lastPacket,
|
||||||
speedHistory,
|
speedHistory,
|
||||||
getGameActions,
|
|
||||||
getStatusText,
|
|
||||||
formatSpeed,
|
formatSpeed,
|
||||||
calculateETA,
|
calculateETA,
|
||||||
pauseDownload,
|
pauseDownload,
|
||||||
resumeDownload,
|
resumeDownload,
|
||||||
|
cancelDownload,
|
||||||
t,
|
t,
|
||||||
}: Readonly<HeroDownloadViewProps>) {
|
}: Readonly<HeroDownloadViewProps>) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogoClick = useCallback(() => {
|
||||||
|
navigate(buildGameDetailsPath(game));
|
||||||
|
}, [navigate, game]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="download-group download-group--hero">
|
<div className="download-group download-group--hero">
|
||||||
<div className="download-group__hero-background">
|
<div className="download-group__hero-background">
|
||||||
@@ -196,88 +252,101 @@ function HeroDownloadView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="download-group__hero-content">
|
<div className="download-group__hero-content">
|
||||||
<div className="download-group__hero-header">
|
|
||||||
<div className="download-group__hero-actions">
|
|
||||||
<DropdownMenu align="end" items={getGameActions(game)}>
|
|
||||||
<Button className="download-group__hero-menu-btn" theme="outline">
|
|
||||||
<ThreeBarsIcon />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="download-group__hero-action-row">
|
<div className="download-group__hero-action-row">
|
||||||
<div className="download-group__hero-logo">
|
<div className="download-group__hero-logo">
|
||||||
{game.logoImageUrl ? (
|
{game.logoImageUrl ? (
|
||||||
<img src={game.logoImageUrl} alt={game.title} />
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogoClick}
|
||||||
|
className="download-group__hero-logo-button"
|
||||||
|
>
|
||||||
|
<img src={game.logoImageUrl} alt={game.title} />
|
||||||
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<h1>{game.title}</h1>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogoClick}
|
||||||
|
className="download-group__hero-logo-button"
|
||||||
|
>
|
||||||
|
<h1>{game.title}</h1>
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isGameDownloading ? (
|
|
||||||
<Button
|
|
||||||
theme="primary"
|
|
||||||
onClick={() => pauseDownload(game.shop, game.objectId)}
|
|
||||||
className="download-group__hero-action-btn"
|
|
||||||
style={{
|
|
||||||
backgroundColor: dominantColor,
|
|
||||||
borderColor: dominantColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ColumnsIcon size={16} />
|
|
||||||
{t("pause")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
theme="primary"
|
|
||||||
onClick={() => resumeDownload(game.shop, game.objectId)}
|
|
||||||
className="download-group__hero-action-btn"
|
|
||||||
style={{
|
|
||||||
backgroundColor: dominantColor,
|
|
||||||
borderColor: dominantColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlayIcon size={16} />
|
|
||||||
{t("resume")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="download-group__hero-progress">
|
<div className="download-group__hero-progress">
|
||||||
<div className="download-group__progress-header">
|
<div className="download-group__progress-row download-group__progress-row--bar">
|
||||||
<span className="download-group__progress-status">
|
<div className="download-group__progress-wrapper">
|
||||||
{getStatusText(game)}
|
<div className="download-group__progress-info-row">
|
||||||
</span>
|
{lastPacket?.isCheckingFiles ? (
|
||||||
<span className="download-group__progress-percentage">
|
<span className="download-group__progress-status">
|
||||||
{formatDownloadProgress(currentProgress)}
|
{t("checking_files")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
) : (
|
||||||
<div className="download-group__progress-bar">
|
<span className="download-group__progress-size">
|
||||||
<div
|
<DownloadIcon size={14} />
|
||||||
className="download-group__progress-fill"
|
{isGameDownloading && lastPacket
|
||||||
style={{
|
? `${formatBytes(lastPacket.download.bytesDownloaded)} / ${finalDownloadSize}`
|
||||||
width: `${currentProgress * 100}%`,
|
: `0 B / ${finalDownloadSize}`}
|
||||||
background: getProgressGradient(
|
</span>
|
||||||
dominantColor,
|
)}
|
||||||
game.download?.status === "paused"
|
<span></span>
|
||||||
),
|
</div>
|
||||||
}}
|
<div className="download-group__progress-info-row">
|
||||||
/>
|
{!lastPacket?.isCheckingFiles && (
|
||||||
</div>
|
<span className="download-group__progress-time">
|
||||||
<div className="download-group__progress-details">
|
{isGameDownloading &&
|
||||||
<span className="download-group__progress-size">
|
lastPacket?.timeRemaining &&
|
||||||
{isGameDownloading && lastPacket
|
lastPacket.timeRemaining > 0 && (
|
||||||
? `${formatBytes(lastPacket.download.bytesDownloaded)} / ${finalDownloadSize}`
|
<>
|
||||||
: `0 B / ${finalDownloadSize}`}
|
<ClockIcon size={14} />
|
||||||
</span>
|
{calculateETA()}
|
||||||
<span className="download-group__progress-time">
|
</>
|
||||||
{isGameDownloading &&
|
)}
|
||||||
lastPacket?.timeRemaining &&
|
</span>
|
||||||
lastPacket.timeRemaining > 0
|
)}
|
||||||
? calculateETA()
|
<span className="download-group__progress-percentage">
|
||||||
: ""}
|
<AnimatedPercentage value={currentProgress} />
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="download-group__progress-bar">
|
||||||
|
<div
|
||||||
|
className="download-group__progress-fill"
|
||||||
|
style={{
|
||||||
|
width: `${currentProgress * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="download-group__hero-buttons">
|
||||||
|
{isGameDownloading ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => pauseDownload(game.shop, game.objectId)}
|
||||||
|
className="download-group__glass-btn"
|
||||||
|
>
|
||||||
|
<ColumnsIcon size={14} />
|
||||||
|
{t("pause")}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => resumeDownload(game.shop, game.objectId)}
|
||||||
|
className="download-group__glass-btn"
|
||||||
|
>
|
||||||
|
<PlayIcon size={14} />
|
||||||
|
{t("resume")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => cancelDownload(game.shop, game.objectId)}
|
||||||
|
className="download-group__glass-btn"
|
||||||
|
>
|
||||||
|
<XCircleIcon size={14} />
|
||||||
|
{t("cancel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -328,6 +397,14 @@ function HeroDownloadView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{game.download?.downloader && (
|
||||||
|
<div className="download-group__stat-item">
|
||||||
|
<div className="download-group__stat-content">
|
||||||
|
<Badge>{DOWNLOADER_NAME[game.download.downloader]}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="download-group__speed-chart">
|
<div className="download-group__speed-chart">
|
||||||
@@ -359,6 +436,7 @@ export function DownloadGroup({
|
|||||||
seedingStatus,
|
seedingStatus,
|
||||||
}: Readonly<DownloadGroupProps>) {
|
}: Readonly<DownloadGroupProps>) {
|
||||||
const { t } = useTranslation("downloads");
|
const { t } = useTranslation("downloads");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const userPreferences = useAppSelector(
|
const userPreferences = useAppSelector(
|
||||||
(state) => state.userPreferences.value
|
(state) => state.userPreferences.value
|
||||||
@@ -368,19 +446,64 @@ export function DownloadGroup({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
lastPacket,
|
lastPacket,
|
||||||
pauseDownload,
|
pauseDownload: pauseDownloadOriginal,
|
||||||
resumeDownload,
|
resumeDownload: resumeDownloadOriginal,
|
||||||
cancelDownload,
|
cancelDownload,
|
||||||
isGameDeleting,
|
isGameDeleting,
|
||||||
pauseSeeding,
|
pauseSeeding,
|
||||||
resumeSeeding,
|
resumeSeeding,
|
||||||
} = useDownload();
|
} = useDownload();
|
||||||
|
|
||||||
const peakSpeedsRef = useRef<Record<string, number>>({});
|
// Wrap resumeDownload with optimistic update
|
||||||
|
const resumeDownload = useCallback(
|
||||||
|
async (shop: GameShop, objectId: string) => {
|
||||||
|
const gameId = `${shop}:${objectId}`;
|
||||||
|
|
||||||
|
// Optimistically mark as downloading
|
||||||
|
setOptimisticallyResumed((prev) => ({ ...prev, [gameId]: true }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await resumeDownloadOriginal(shop, objectId);
|
||||||
|
} catch (error) {
|
||||||
|
// If resume fails, remove optimistic state
|
||||||
|
setOptimisticallyResumed((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[gameId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[resumeDownloadOriginal]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrap pauseDownload to clear optimistic state
|
||||||
|
const pauseDownload = useCallback(
|
||||||
|
async (shop: GameShop, objectId: string) => {
|
||||||
|
const gameId = `${shop}:${objectId}`;
|
||||||
|
|
||||||
|
// Clear optimistic state when pausing
|
||||||
|
setOptimisticallyResumed((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[gameId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
await pauseDownloadOriginal(shop, objectId);
|
||||||
|
},
|
||||||
|
[pauseDownloadOriginal]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { formatDistance } = useDate();
|
||||||
|
|
||||||
|
const [peakSpeeds, setPeakSpeeds] = useState<Record<string, number>>({});
|
||||||
const speedHistoryRef = useRef<Record<string, number[]>>({});
|
const speedHistoryRef = useRef<Record<string, number[]>>({});
|
||||||
const [dominantColors, setDominantColors] = useState<Record<string, string>>(
|
const [dominantColors, setDominantColors] = useState<Record<string, string>>(
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
const [optimisticallyResumed, setOptimisticallyResumed] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
|
|
||||||
const extractDominantColor = useCallback(
|
const extractDominantColor = useCallback(
|
||||||
async (imageUrl: string, gameId: string) => {
|
async (imageUrl: string, gameId: string) => {
|
||||||
@@ -398,13 +521,55 @@ export function DownloadGroup({
|
|||||||
[dominantColors]
|
[dominantColors]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Clear optimistic state when actual download starts or library updates
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastPacket?.gameId) {
|
||||||
|
const gameId = lastPacket.gameId;
|
||||||
|
|
||||||
|
// Clear optimistic state when actual download starts
|
||||||
|
setOptimisticallyResumed((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[gameId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [lastPacket?.gameId]);
|
||||||
|
|
||||||
|
// Clear optimistic state for games that are no longer active after library update
|
||||||
|
useEffect(() => {
|
||||||
|
setOptimisticallyResumed((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
for (const gameId in next) {
|
||||||
|
if (next[gameId]) {
|
||||||
|
const game = library.find((g) => g.id === gameId);
|
||||||
|
// Clear if game doesn't exist or download status is not active
|
||||||
|
if (
|
||||||
|
!game ||
|
||||||
|
game.download?.status !== "active" ||
|
||||||
|
lastPacket?.gameId === gameId
|
||||||
|
) {
|
||||||
|
delete next[gameId];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? next : prev;
|
||||||
|
});
|
||||||
|
}, [library, lastPacket?.gameId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) {
|
if (lastPacket?.gameId && lastPacket.downloadSpeed !== undefined) {
|
||||||
const gameId = lastPacket.gameId;
|
const gameId = lastPacket.gameId;
|
||||||
|
|
||||||
const currentPeak = peakSpeedsRef.current[gameId] || 0;
|
const currentPeak = peakSpeeds[gameId] || 0;
|
||||||
if (lastPacket.downloadSpeed > currentPeak) {
|
if (lastPacket.downloadSpeed > currentPeak) {
|
||||||
peakSpeedsRef.current[gameId] = lastPacket.downloadSpeed;
|
setPeakSpeeds((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[gameId]: lastPacket.downloadSpeed,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!speedHistoryRef.current[gameId]) {
|
if (!speedHistoryRef.current[gameId]) {
|
||||||
@@ -417,7 +582,7 @@ export function DownloadGroup({
|
|||||||
speedHistoryRef.current[gameId].shift();
|
speedHistoryRef.current[gameId].shift();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [lastPacket?.gameId, lastPacket?.downloadSpeed]);
|
}, [lastPacket?.gameId, lastPacket?.downloadSpeed, peakSpeeds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const game of library) {
|
for (const game of library) {
|
||||||
@@ -429,7 +594,7 @@ export function DownloadGroup({
|
|||||||
// Fresh download - clear any old data
|
// Fresh download - clear any old data
|
||||||
if (speedHistoryRef.current[game.id]?.length > 0) {
|
if (speedHistoryRef.current[game.id]?.length > 0) {
|
||||||
speedHistoryRef.current[game.id] = [];
|
speedHistoryRef.current[game.id] = [];
|
||||||
peakSpeedsRef.current[game.id] = 0;
|
setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,7 +610,7 @@ export function DownloadGroup({
|
|||||||
) {
|
) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
speedHistoryRef.current[game.id] = [];
|
speedHistoryRef.current[game.id] = [];
|
||||||
peakSpeedsRef.current[game.id] = 0;
|
setPeakSpeeds((prev) => ({ ...prev, [game.id]: 0 }));
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
timeouts.push(timeout);
|
timeouts.push(timeout);
|
||||||
}
|
}
|
||||||
@@ -478,10 +643,12 @@ export function DownloadGroup({
|
|||||||
const isGameDownloadingMap = useMemo(() => {
|
const isGameDownloadingMap = useMemo(() => {
|
||||||
const map: Record<string, boolean> = {};
|
const map: Record<string, boolean> = {};
|
||||||
for (const game of library) {
|
for (const game of library) {
|
||||||
map[game.id] = lastPacket?.gameId === game.id;
|
map[game.id] =
|
||||||
|
lastPacket?.gameId === game.id ||
|
||||||
|
optimisticallyResumed[game.id] === true;
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [library, lastPacket?.gameId]);
|
}, [library, lastPacket?.gameId, optimisticallyResumed]);
|
||||||
|
|
||||||
const getFinalDownloadSize = (game: LibraryGame) => {
|
const getFinalDownloadSize = (game: LibraryGame) => {
|
||||||
const download = game.download!;
|
const download = game.download!;
|
||||||
@@ -517,53 +684,6 @@ export function DownloadGroup({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCompletedStatusText = (game: LibraryGame) => {
|
|
||||||
const isTorrent = game.download?.downloader === Downloader.Torrent;
|
|
||||||
if (isTorrent) {
|
|
||||||
return isGameSeeding(game)
|
|
||||||
? `${t("completed")} (${t("seeding")})`
|
|
||||||
: `${t("completed")} (${t("paused")})`;
|
|
||||||
}
|
|
||||||
return t("completed");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (game: LibraryGame) => {
|
|
||||||
const isGameDownloading = isGameDownloadingMap[game.id];
|
|
||||||
const status = game.download?.status;
|
|
||||||
|
|
||||||
if (game.download?.extracting) {
|
|
||||||
return t("extracting");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGameDeleting(game.id)) {
|
|
||||||
return t("deleting");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.download?.progress === 1) {
|
|
||||||
return getCompletedStatusText(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGameDownloading && lastPacket) {
|
|
||||||
if (lastPacket.isDownloadingMetadata) {
|
|
||||||
return t("downloading_metadata");
|
|
||||||
}
|
|
||||||
if (lastPacket.isCheckingFiles) {
|
|
||||||
return t("checking_files");
|
|
||||||
}
|
|
||||||
return t("download_in_progress");
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case "paused":
|
|
||||||
case "error":
|
|
||||||
return t("paused");
|
|
||||||
case "waiting":
|
|
||||||
return t("calculating_eta");
|
|
||||||
default:
|
|
||||||
return t("paused");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractGameDownload = useCallback(
|
const extractGameDownload = useCallback(
|
||||||
async (shop: GameShop, objectId: string) => {
|
async (shop: GameShop, objectId: string) => {
|
||||||
await window.electron.extractGameDownload(shop, objectId);
|
await window.electron.extractGameDownload(shop, objectId);
|
||||||
@@ -682,7 +802,13 @@ export function DownloadGroup({
|
|||||||
progress: game.download?.progress || 0,
|
progress: game.download?.progress || 0,
|
||||||
isSeeding: isGameSeeding(game),
|
isSeeding: isGameSeeding(game),
|
||||||
})),
|
})),
|
||||||
[library, lastPacket?.gameId]
|
[
|
||||||
|
library,
|
||||||
|
lastPacket?.gameId,
|
||||||
|
lastPacket?.download.fileSize,
|
||||||
|
isGameDownloadingMap,
|
||||||
|
seedingStatus,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!library.length) return null;
|
if (!library.length) return null;
|
||||||
@@ -698,7 +824,7 @@ export function DownloadGroup({
|
|||||||
? (lastPacket?.downloadSpeed ?? 0)
|
? (lastPacket?.downloadSpeed ?? 0)
|
||||||
: 0;
|
: 0;
|
||||||
const finalDownloadSize = getFinalDownloadSize(game);
|
const finalDownloadSize = getFinalDownloadSize(game);
|
||||||
const peakSpeed = peakSpeedsRef.current[game.id] || 0;
|
const peakSpeed = peakSpeeds[game.id] || 0;
|
||||||
const currentProgress =
|
const currentProgress =
|
||||||
isGameDownloading && lastPacket
|
isGameDownloading && lastPacket
|
||||||
? lastPacket.progress
|
? lastPacket.progress
|
||||||
@@ -717,12 +843,11 @@ export function DownloadGroup({
|
|||||||
dominantColor={dominantColor}
|
dominantColor={dominantColor}
|
||||||
lastPacket={lastPacket}
|
lastPacket={lastPacket}
|
||||||
speedHistory={speedHistoryRef.current[game.id] || []}
|
speedHistory={speedHistoryRef.current[game.id] || []}
|
||||||
getGameActions={getGameActions}
|
|
||||||
getStatusText={getStatusText}
|
|
||||||
formatSpeed={formatSpeed}
|
formatSpeed={formatSpeed}
|
||||||
calculateETA={calculateETA}
|
calculateETA={calculateETA}
|
||||||
pauseDownload={pauseDownload}
|
pauseDownload={pauseDownload}
|
||||||
resumeDownload={resumeDownload}
|
resumeDownload={resumeDownload}
|
||||||
|
cancelDownload={cancelDownload}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -733,29 +858,53 @@ export function DownloadGroup({
|
|||||||
className={`download-group ${isQueuedGroup ? "download-group--queued" : ""} ${isCompletedGroup ? "download-group--completed" : ""}`}
|
className={`download-group ${isQueuedGroup ? "download-group--queued" : ""} ${isCompletedGroup ? "download-group--completed" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="download-group__header">
|
<div className="download-group__header">
|
||||||
<h2>{title}</h2>
|
<div className="download-group__header-title-group">
|
||||||
<div className="download-group__header-divider" />
|
<h2>{title}</h2>
|
||||||
<h3 className="download-group__header-count">{library.length}</h3>
|
<h3 className="download-group__header-count">{library.length}</h3>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="download-group__simple-list">
|
<ul className="download-group__simple-list">
|
||||||
{downloadInfo.map(({ game, size, progress, isSeeding: seeding }) => {
|
{downloadInfo.map(({ game, size, progress, isSeeding: seeding }) => {
|
||||||
return (
|
return (
|
||||||
<li key={game.id} className="download-group__simple-card">
|
<li key={game.id} className="download-group__simple-card">
|
||||||
<div className="download-group__simple-thumbnail">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
|
className="download-group__simple-thumbnail"
|
||||||
|
>
|
||||||
<img src={game.libraryImageUrl || ""} alt={game.title} />
|
<img src={game.libraryImageUrl || ""} alt={game.title} />
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<div className="download-group__simple-info">
|
<div className="download-group__simple-info">
|
||||||
<h3 className="download-group__simple-title">{game.title}</h3>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
|
className="download-group__simple-title-button"
|
||||||
|
>
|
||||||
|
<h3 className="download-group__simple-title">{game.title}</h3>
|
||||||
|
</button>
|
||||||
<div className="download-group__simple-meta">
|
<div className="download-group__simple-meta">
|
||||||
<Badge>{DOWNLOADER_NAME[game.download!.downloader]}</Badge>
|
<div className="download-group__simple-meta-row">
|
||||||
<span className="download-group__simple-size">{size}</span>
|
<Badge>{DOWNLOADER_NAME[game.download!.downloader]}</Badge>
|
||||||
{game.download?.progress === 1 && seeding && (
|
</div>
|
||||||
<span className="download-group__simple-seeding">
|
<div className="download-group__simple-meta-row">
|
||||||
{t("seeding")}
|
{game.download?.extracting ? (
|
||||||
</span>
|
<span className="download-group__simple-extracting">
|
||||||
)}
|
{t("extracting")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="download-group__simple-size">
|
||||||
|
<DownloadIcon size={14} />
|
||||||
|
{size}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{game.download?.progress === 1 && seeding && (
|
||||||
|
<span className="download-group__simple-seeding">
|
||||||
|
{t("seeding")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -784,7 +933,7 @@ export function DownloadGroup({
|
|||||||
disabled={isGameDeleting(game.id)}
|
disabled={isGameDeleting(game.id)}
|
||||||
className="download-group__simple-menu-btn"
|
className="download-group__simple-menu-btn"
|
||||||
>
|
>
|
||||||
<DownloadIcon size={16} />
|
<PlayIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isQueuedGroup && game.download?.progress !== 1 && (
|
{isQueuedGroup && game.download?.progress !== 1 && (
|
||||||
@@ -792,8 +941,9 @@ export function DownloadGroup({
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
onClick={() => resumeDownload(game.shop, game.objectId)}
|
onClick={() => resumeDownload(game.shop, game.objectId)}
|
||||||
className="download-group__simple-menu-btn"
|
className="download-group__simple-menu-btn"
|
||||||
|
tooltip={t("resume")}
|
||||||
>
|
>
|
||||||
<PlayIcon size={16} />
|
<DownloadIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<DropdownMenu align="end" items={getGameActions(game)}>
|
<DropdownMenu align="end" items={getGameActions(game)}>
|
||||||
|
|||||||
@@ -231,9 +231,19 @@ export function RepacksModal({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastCheckUtc = new Date(lastCheckTimestamp).toISOString();
|
try {
|
||||||
|
const lastCheckDate = new Date(lastCheckTimestamp);
|
||||||
|
|
||||||
return repack.createdAt > lastCheckUtc;
|
if (isNaN(lastCheckDate.getTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCheckUtc = lastCheckDate.toISOString();
|
||||||
|
|
||||||
|
return repack.createdAt > lastCheckUtc;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface Auth {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
tokenExpirationTimestamp: number;
|
tokenExpirationTimestamp: number;
|
||||||
|
featurebaseJwt: string;
|
||||||
|
workwondersJwt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -6330,7 +6330,7 @@ jsonwebtoken@^9.0.2:
|
|||||||
object.assign "^4.1.4"
|
object.assign "^4.1.4"
|
||||||
object.values "^1.1.6"
|
object.values "^1.1.6"
|
||||||
|
|
||||||
jwa@^1.4.1:
|
jwa@^1.4.2:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
|
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
|
||||||
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
|
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
|
||||||
@@ -6340,11 +6340,11 @@ jwa@^1.4.1:
|
|||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
jws@^3.2.2:
|
jws@^3.2.2:
|
||||||
version "3.2.2"
|
version "3.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
|
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1"
|
||||||
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
|
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==
|
||||||
dependencies:
|
dependencies:
|
||||||
jwa "^1.4.1"
|
jwa "^1.4.2"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
keyv@^4.0.0, keyv@^4.5.3:
|
keyv@^4.0.0, keyv@^4.5.3:
|
||||||
|
|||||||
Reference in New Issue
Block a user