mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 17:23:57 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16c45692da | ||
|
|
30aa3f5470 | ||
|
|
ef16732c0a | ||
|
|
84c472a3fa | ||
|
|
2610f8b17b | ||
|
|
705b12019f | ||
|
|
39be8fdf53 | ||
|
|
cc7c3455fa | ||
|
|
d1f4bc7207 | ||
|
|
aa4ca25653 | ||
|
|
405ea0a824 | ||
|
|
43bc0cb08f | ||
|
|
3c200aa2eb | ||
|
|
cc5967814b | ||
|
|
ec16efed2f | ||
|
|
09d0e5b4ef | ||
|
|
5b18aba2b8 | ||
|
|
192008c76c | ||
|
|
1de3a9836c | ||
|
|
ee02811aea | ||
|
|
c21ebe1ce2 | ||
|
|
214e39adda | ||
|
|
8258127616 | ||
|
|
f9906bfe95 | ||
|
|
ff91284a91 | ||
|
|
b4f99418e9 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "2.1.0",
|
"version": "2.1.4",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"surprise_me": "Surprise me",
|
"surprise_me": "Surprise me",
|
||||||
"no_results": "No results found",
|
"no_results": "No results found",
|
||||||
"start_typing": "Starting typing to search...",
|
"start_typing": "Starting typing to search...",
|
||||||
"hot": "🔥 Hot now",
|
"hot": "Hot now",
|
||||||
"weekly": "📅 Top games of the week"
|
"weekly": "📅 Top games of the week"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"trending": "Tendencias",
|
"trending": "Tendencias",
|
||||||
"surprise_me": "¡Sorpréndeme!",
|
"surprise_me": "¡Sorpréndeme!",
|
||||||
"no_results": "No se encontraron resultados",
|
"no_results": "No se encontraron resultados",
|
||||||
"hot": "🔥 Caliente ahora",
|
"hot": "Caliente ahora",
|
||||||
"weekly": "📅 Los mejores juegos de la semana",
|
"weekly": "📅 Los mejores juegos de la semana",
|
||||||
"start_typing": "Empieza a escribir para buscar..."
|
"start_typing": "Empieza a escribir para buscar..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"featured": "Destaques",
|
"featured": "Destaques",
|
||||||
"trending": "Populares",
|
"trending": "Populares",
|
||||||
"hot": "🔥 Populares agora",
|
"hot": "Populares agora",
|
||||||
"weekly": "📅 Mais baixados da semana",
|
"weekly": "📅 Mais baixados da semana",
|
||||||
"surprise_me": "Surpreenda-me",
|
"surprise_me": "Surpreenda-me",
|
||||||
"no_results": "Nenhum resultado encontrado",
|
"no_results": "Nenhum resultado encontrado",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"trending": "В тренде",
|
"trending": "В тренде",
|
||||||
"surprise_me": "Удиви меня",
|
"surprise_me": "Удиви меня",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
"hot": "🔥 Сейчас жарко",
|
"hot": "Сейчас жарко",
|
||||||
"start_typing": "Начинаю вводить текст для поиска...",
|
"start_typing": "Начинаю вводить текст для поиска...",
|
||||||
"weekly": "📅 Лучшие игры недели"
|
"weekly": "📅 Лучшие игры недели"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import "./profile/update-friend-request";
|
|||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
import "./profile/process-profile-image";
|
import "./profile/process-profile-image";
|
||||||
import "./profile/send-friend-request";
|
import "./profile/send-friend-request";
|
||||||
|
import "./profile/sync-friend-requests";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import { HydraApi, logger } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { UserProfile } from "@types";
|
import { ProfileVisibility, UserDetails } from "@types";
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { userAuthRepository } from "@main/repository";
|
||||||
import { steamUrlBuilder, UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
|
||||||
|
|
||||||
const getSteamGame = async (objectId: string) => {
|
|
||||||
try {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: steamGame.name,
|
|
||||||
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to get Steam game", err);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMe = async (
|
const getMe = async (
|
||||||
_event: Electron.IpcMainInvokeEvent
|
_event: Electron.IpcMainInvokeEvent
|
||||||
): Promise<UserProfile | null> => {
|
): Promise<UserDetails | null> => {
|
||||||
return HydraApi.get(`/profile/me`)
|
return HydraApi.get<UserDetails>(`/profile/me`)
|
||||||
.then(async (me) => {
|
.then(async (me) => {
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
@@ -38,17 +20,6 @@ const getMe = async (
|
|||||||
["id"]
|
["id"]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (me.currentGame) {
|
|
||||||
const steamGame = await getSteamGame(me.currentGame.objectId);
|
|
||||||
|
|
||||||
if (steamGame) {
|
|
||||||
me.currentGame = {
|
|
||||||
...me.currentGame,
|
|
||||||
...steamGame,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Sentry.setUser({ id: me.id, username: me.username });
|
Sentry.setUser({ id: me.id, username: me.username });
|
||||||
|
|
||||||
return me;
|
return me;
|
||||||
@@ -61,7 +32,13 @@ const getMe = async (
|
|||||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||||
|
|
||||||
if (loggedUser) {
|
if (loggedUser) {
|
||||||
return { ...loggedUser, id: loggedUser.userId };
|
return {
|
||||||
|
...loggedUser,
|
||||||
|
id: loggedUser.userId,
|
||||||
|
username: "",
|
||||||
|
bio: "",
|
||||||
|
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
9
src/main/events/profile/sync-friend-requests.ts
Normal file
9
src/main/events/profile/sync-friend-requests.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { FriendRequestSync } from "@types";
|
||||||
|
|
||||||
|
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("syncFriendRequests", syncFriendRequests);
|
||||||
@@ -2,7 +2,7 @@ import { registerEvent } from "../register-event";
|
|||||||
|
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
import { getFileBase64 } from "@main/helpers";
|
import { getFileBase64 } from "@main/helpers";
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
@@ -101,6 +101,17 @@ const startGameDownload = async (
|
|||||||
|
|
||||||
createGame(updatedGame!).catch(() => {});
|
createGame(updatedGame!).catch(() => {});
|
||||||
|
|
||||||
|
HydraApi.post(
|
||||||
|
"/games/download",
|
||||||
|
{
|
||||||
|
objectId: updatedGame!.objectID,
|
||||||
|
shop: updatedGame!.shop,
|
||||||
|
},
|
||||||
|
{ needsAuth: false }
|
||||||
|
).catch((err) => {
|
||||||
|
logger.error("Failed to create game download", err);
|
||||||
|
});
|
||||||
|
|
||||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
await DownloadManager.startDownload(updatedGame!);
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,19 @@ import { databasePath } from "./constants";
|
|||||||
import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3";
|
import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3";
|
||||||
import { RepackUris } from "./migrations/20240830143906_RepackUris";
|
import { RepackUris } from "./migrations/20240830143906_RepackUris";
|
||||||
import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_language";
|
import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_language";
|
||||||
|
import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris";
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
export type HydraMigration = Knex.Migration & { name: string };
|
export type HydraMigration = Knex.Migration & { name: string };
|
||||||
|
|
||||||
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||||
getMigrations(): Promise<HydraMigration[]> {
|
getMigrations(): Promise<HydraMigration[]> {
|
||||||
return Promise.resolve([Hydra2_0_3, RepackUris, UpdateUserLanguage]);
|
return Promise.resolve([
|
||||||
|
Hydra2_0_3,
|
||||||
|
RepackUris,
|
||||||
|
UpdateUserLanguage,
|
||||||
|
EnsureRepackUris,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
getMigrationName(migration: HydraMigration): string {
|
getMigrationName(migration: HydraMigration): string {
|
||||||
return migration.name;
|
return migration.name;
|
||||||
@@ -19,6 +26,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const knexClient = knex({
|
export const knexClient = knex({
|
||||||
|
debug: !app.isPackaged,
|
||||||
client: "better-sqlite3",
|
client: "better-sqlite3",
|
||||||
connection: {
|
connection: {
|
||||||
filename: databasePath,
|
filename: databasePath,
|
||||||
|
|||||||
@@ -4,55 +4,15 @@ import type { Knex } from "knex";
|
|||||||
export const RepackUris: HydraMigration = {
|
export const RepackUris: HydraMigration = {
|
||||||
name: "RepackUris",
|
name: "RepackUris",
|
||||||
up: async (knex: Knex) => {
|
up: async (knex: Knex) => {
|
||||||
await knex.schema.createTable("temporary_repack", (table) => {
|
await knex.schema.alterTable("repack", (table) => {
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
table.increments("id").primary();
|
|
||||||
table
|
|
||||||
.text("title")
|
|
||||||
.notNullable()
|
|
||||||
.unique({ indexName: "repack_title_unique_" + timestamp });
|
|
||||||
table
|
|
||||||
.text("magnet")
|
|
||||||
.notNullable()
|
|
||||||
.unique({ indexName: "repack_magnet_unique_" + timestamp });
|
|
||||||
table.text("repacker").notNullable();
|
|
||||||
table.text("fileSize").notNullable();
|
|
||||||
table.datetime("uploadDate").notNullable();
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table
|
|
||||||
.integer("downloadSourceId")
|
|
||||||
.references("download_source.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
table.text("uris").notNullable().defaultTo("[]");
|
table.text("uris").notNullable().defaultTo("[]");
|
||||||
});
|
});
|
||||||
await knex.raw(
|
|
||||||
`INSERT INTO "temporary_repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`
|
|
||||||
);
|
|
||||||
await knex.schema.dropTable("repack");
|
|
||||||
await knex.schema.renameTable("temporary_repack", "repack");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
down: async (knex: Knex) => {
|
||||||
await knex.schema.renameTable("repack", "temporary_repack");
|
await knex.schema.alterTable("repack", (table) => {
|
||||||
await knex.schema.createTable("repack", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table.text("title").notNullable().unique();
|
|
||||||
table.text("magnet").notNullable().unique();
|
|
||||||
table.integer("page");
|
table.integer("page");
|
||||||
table.text("repacker").notNullable();
|
table.dropColumn("uris");
|
||||||
table.text("fileSize").notNullable();
|
|
||||||
table.datetime("uploadDate").notNullable();
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table
|
|
||||||
.integer("downloadSourceId")
|
|
||||||
.references("download_source.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
});
|
});
|
||||||
await knex.raw(
|
|
||||||
`INSERT INTO "repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`
|
|
||||||
);
|
|
||||||
await knex.schema.dropTable("temporary_repack");
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
17
src/main/migrations/20240915035339_ensure_repack_uris.ts
Normal file
17
src/main/migrations/20240915035339_ensure_repack_uris.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const EnsureRepackUris: HydraMigration = {
|
||||||
|
name: "EnsureRepackUris",
|
||||||
|
up: async (knex: Knex) => {
|
||||||
|
await knex.schema.hasColumn("repack", "uris").then(async (exists) => {
|
||||||
|
if (!exists) {
|
||||||
|
await knex.schema.table("repack", (table) => {
|
||||||
|
table.text("uris").notNullable().defaultTo("[]");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (_knex: Knex) => {},
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import { uploadGamesBatch } from "./library-sync";
|
|||||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
|
import { omit } from "lodash-es";
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth: boolean;
|
needsAuth: boolean;
|
||||||
@@ -81,54 +82,57 @@ export class HydraApi {
|
|||||||
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
// this.instance.interceptors.request.use(
|
this.instance.interceptors.request.use(
|
||||||
// (request) => {
|
(request) => {
|
||||||
// logger.log(" ---- REQUEST -----");
|
logger.log(" ---- REQUEST -----");
|
||||||
// logger.log(request.method, request.url, request.params, request.data);
|
logger.log(request.method, request.url, request.params, request.data);
|
||||||
// return request;
|
return request;
|
||||||
// },
|
},
|
||||||
// (error) => {
|
(error) => {
|
||||||
// logger.error("request error", error);
|
logger.error("request error", error);
|
||||||
// return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
|
|
||||||
// this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
// (response) => {
|
(response) => {
|
||||||
// logger.log(" ---- RESPONSE -----");
|
logger.log(" ---- RESPONSE -----");
|
||||||
// logger.log(
|
const data = Array.isArray(response.data)
|
||||||
// response.status,
|
? response.data
|
||||||
// response.config.method,
|
: omit(response.data, ["username", "accessToken", "refreshToken"]);
|
||||||
// response.config.url,
|
logger.log(
|
||||||
// response.data
|
response.status,
|
||||||
// );
|
response.config.method,
|
||||||
// return response;
|
response.config.url,
|
||||||
// },
|
data
|
||||||
// (error) => {
|
);
|
||||||
// logger.error(" ---- RESPONSE ERROR -----");
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
logger.error(" ---- RESPONSE ERROR -----");
|
||||||
|
|
||||||
// const { config } = error;
|
const { config } = error;
|
||||||
|
|
||||||
// logger.error(
|
logger.error(
|
||||||
// config.method,
|
config.method,
|
||||||
// config.baseURL,
|
config.baseURL,
|
||||||
// config.url,
|
config.url,
|
||||||
// config.headers,
|
config.headers,
|
||||||
// config.data
|
config.data
|
||||||
// );
|
);
|
||||||
|
|
||||||
// if (error.response) {
|
if (error.response) {
|
||||||
// logger.error("Response", error.response.status, error.response.data);
|
logger.error("Response", error.response.status, error.response.data);
|
||||||
// } else if (error.request) {
|
} else if (error.request) {
|
||||||
// logger.error("Request", error.request);
|
logger.error("Request", error.request);
|
||||||
// } else {
|
} else {
|
||||||
// logger.error("Error", error.message);
|
logger.error("Error", error.message);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// logger.error(" ----- END RESPONSE ERROR -------");
|
logger.error(" ----- END RESPONSE ERROR -------");
|
||||||
// return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
// }
|
}
|
||||||
// );
|
);
|
||||||
|
|
||||||
const userAuth = await userAuthRepository.findOne({
|
const userAuth = await userAuthRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
@@ -166,7 +170,10 @@ export class HydraApi {
|
|||||||
this.userAuth.authToken = accessToken;
|
this.userAuth.authToken = accessToken;
|
||||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||||
|
|
||||||
logger.log("Token refreshed", this.userAuth);
|
logger.log(
|
||||||
|
"Token refreshed. New expiration:",
|
||||||
|
this.userAuth.expirationTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
@@ -192,7 +199,11 @@ export class HydraApi {
|
|||||||
|
|
||||||
private static handleUnauthorizedError = (err) => {
|
private static handleUnauthorizedError = (err) => {
|
||||||
if (err instanceof AxiosError && err.response?.status === 401) {
|
if (err instanceof AxiosError && err.response?.status === 401) {
|
||||||
logger.error("401 - Current credentials:", this.userAuth);
|
logger.error(
|
||||||
|
"401 - Current credentials:",
|
||||||
|
this.userAuth,
|
||||||
|
err.response?.data
|
||||||
|
);
|
||||||
|
|
||||||
this.userAuth = {
|
this.userAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { logger } from "../logger";
|
|
||||||
|
|
||||||
export const createGame = async (game: Game) => {
|
export const createGame = async (game: Game) => {
|
||||||
HydraApi.post(
|
|
||||||
"/games/download",
|
|
||||||
{
|
|
||||||
objectId: game.objectID,
|
|
||||||
shop: game.shop,
|
|
||||||
},
|
|
||||||
{ needsAuth: false }
|
|
||||||
).catch((err) => {
|
|
||||||
logger.error("Failed to create game download", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
return HydraApi.post(`/profile/games`, {
|
return HydraApi.post(`/profile/games`, {
|
||||||
objectId: game.objectID,
|
objectId: game.objectID,
|
||||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const onCloseGame = (game: Game) => {
|
|||||||
if (game.remoteId) {
|
if (game.remoteId) {
|
||||||
updateGamePlaytime(
|
updateGamePlaytime(
|
||||||
game,
|
game,
|
||||||
performance.now() - gamePlaytime.firstTick,
|
performance.now() - gamePlaytime.lastSyncTick,
|
||||||
game.lastTimePlayed!
|
game.lastTimePlayed!
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export class WindowManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
authWindow.loadURL(
|
authWindow.loadURL(
|
||||||
`https://auth.hydra.losbroxas.org/?${searchParams.toString()}`
|
`https://auth.hydralauncher.gg/?${searchParams.toString()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
authWindow.once("ready-to-show", () => {
|
authWindow.once("ready-to-show", () => {
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
processProfileImage: (imagePath: string) =>
|
processProfileImage: (imagePath: string) =>
|
||||||
ipcRenderer.invoke("processProfileImage", imagePath),
|
ipcRenderer.invoke("processProfileImage", imagePath),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
|
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
|
||||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||||
sendFriendRequest: (userId: string) =>
|
sendFriendRequest: (userId: string) =>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function App() {
|
|||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
friendModalUserId,
|
friendModalUserId,
|
||||||
fetchFriendRequests,
|
syncFriendRequests,
|
||||||
hideFriendsModal,
|
hideFriendsModal,
|
||||||
} = useUserDetails();
|
} = useUserDetails();
|
||||||
|
|
||||||
@@ -105,22 +105,22 @@ export function App() {
|
|||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
fetchFriendRequests();
|
syncFriendRequests();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
|
||||||
|
|
||||||
const onSignIn = useCallback(() => {
|
const onSignIn = useCallback(() => {
|
||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
fetchFriendRequests();
|
syncFriendRequests();
|
||||||
showSuccessToast(t("successfully_signed_in"));
|
showSuccessToast(t("successfully_signed_in"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
fetchUserDetails,
|
fetchUserDetails,
|
||||||
fetchFriendRequests,
|
syncFriendRequests,
|
||||||
t,
|
t,
|
||||||
showSuccessToast,
|
showSuccessToast,
|
||||||
updateUserDetails,
|
updateUserDetails,
|
||||||
|
|||||||
1735
src/renderer/src/assets/lottie/flame.json
Normal file
1735
src/renderer/src/assets/lottie/flame.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import Lottie from "lottie-react";
|
import Lottie from "lottie-react";
|
||||||
|
|
||||||
import downloadingAnimation from "@renderer/assets/lottie/downloading.json";
|
import downloadingAnimation from "@renderer/assets/lottie/downloading.json";
|
||||||
@@ -8,11 +7,8 @@ export interface DownloadIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DownloadIcon({ isDownloading }: DownloadIconProps) {
|
export function DownloadIcon({ isDownloading }: DownloadIconProps) {
|
||||||
const lottieRef = useRef(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Lottie
|
<Lottie
|
||||||
lottieRef={lottieRef}
|
|
||||||
animationData={downloadingAnimation}
|
animationData={downloadingAnimation}
|
||||||
loop={isDownloading}
|
loop={isDownloading}
|
||||||
autoplay={isDownloading}
|
autoplay={isDownloading}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useEffect, useMemo, useRef } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
|
||||||
const LONG_POLLING_INTERVAL = 10_000;
|
const LONG_POLLING_INTERVAL = 60_000;
|
||||||
|
|
||||||
export function SidebarProfile() {
|
export function SidebarProfile() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -15,15 +15,15 @@ export function SidebarProfile() {
|
|||||||
|
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
|
|
||||||
const { userDetails, friendRequests, showFriendsModal, fetchFriendRequests } =
|
const {
|
||||||
useUserDetails();
|
userDetails,
|
||||||
|
friendRequestCount,
|
||||||
|
showFriendsModal,
|
||||||
|
syncFriendRequests,
|
||||||
|
} = useUserDetails();
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
const receivedRequests = useMemo(() => {
|
|
||||||
return friendRequests.filter((request) => request.type === "RECEIVED");
|
|
||||||
}, [friendRequests]);
|
|
||||||
|
|
||||||
const handleProfileClick = () => {
|
const handleProfileClick = () => {
|
||||||
if (userDetails === null) {
|
if (userDetails === null) {
|
||||||
window.electron.openAuthWindow();
|
window.electron.openAuthWindow();
|
||||||
@@ -35,7 +35,7 @@ export function SidebarProfile() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pollingInterval.current = setInterval(() => {
|
pollingInterval.current = setInterval(() => {
|
||||||
fetchFriendRequests();
|
syncFriendRequests();
|
||||||
}, LONG_POLLING_INTERVAL);
|
}, LONG_POLLING_INTERVAL);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -43,7 +43,7 @@ export function SidebarProfile() {
|
|||||||
clearInterval(pollingInterval.current);
|
clearInterval(pollingInterval.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [fetchFriendRequests]);
|
}, [syncFriendRequests]);
|
||||||
|
|
||||||
const friendsButton = useMemo(() => {
|
const friendsButton = useMemo(() => {
|
||||||
if (!userDetails) return null;
|
if (!userDetails) return null;
|
||||||
@@ -57,16 +57,16 @@ export function SidebarProfile() {
|
|||||||
}
|
}
|
||||||
title={t("friends")}
|
title={t("friends")}
|
||||||
>
|
>
|
||||||
{receivedRequests.length > 0 && (
|
{friendRequestCount > 0 && (
|
||||||
<small className={styles.friendsButtonBadge}>
|
<small className={styles.friendsButtonBadge}>
|
||||||
{receivedRequests.length > 99 ? "99+" : receivedRequests.length}
|
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PeopleIcon size={16} />
|
<PeopleIcon size={16} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}, [userDetails, t, receivedRequests, showFriendsModal]);
|
}, [userDetails, t, friendRequestCount, showFriendsModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.profileContainer}>
|
<div className={styles.profileContainer}>
|
||||||
@@ -100,6 +100,7 @@ export function SidebarProfile() {
|
|||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
textAlign: "left",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<small>{gameRunning.title}</small>
|
<small>{gameRunning.title}</small>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const sidebar = recipe({
|
|||||||
paddingTop: `${SPACING_UNIT * 6}px`,
|
paddingTop: `${SPACING_UNIT * 6}px`,
|
||||||
},
|
},
|
||||||
false: {
|
false: {
|
||||||
paddingTop: `${SPACING_UNIT * 2}px`,
|
paddingTop: `${SPACING_UNIT}px`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function GameDetailsContextProvider({
|
|||||||
setShopDetails(appDetailsResult.value);
|
setShopDetails(appDetailsResult.value);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
appDetailsResult.value!.content_descriptors.ids.includes(
|
appDetailsResult.value?.content_descriptors.ids.includes(
|
||||||
SteamContentDescriptor.AdultOnlySexualContent
|
SteamContentDescriptor.AdultOnlySexualContent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|||||||
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@@ -23,6 +23,8 @@ import type {
|
|||||||
GameStats,
|
GameStats,
|
||||||
TrendingGame,
|
TrendingGame,
|
||||||
UserStats,
|
UserStats,
|
||||||
|
UserDetails,
|
||||||
|
FriendRequestSync,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@@ -153,7 +155,7 @@ declare global {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserDetails | null>;
|
||||||
undoFriendship: (userId: string) => Promise<void>;
|
undoFriendship: (userId: string) => Promise<void>;
|
||||||
updateProfile: (
|
updateProfile: (
|
||||||
updateProfile: UpdateProfileRequest
|
updateProfile: UpdateProfileRequest
|
||||||
@@ -163,6 +165,7 @@ declare global {
|
|||||||
path: string
|
path: string
|
||||||
) => Promise<{ imagePath: string; mimeType: string }>;
|
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||||
|
syncFriendRequests: () => Promise<FriendRequestSync>;
|
||||||
updateFriendRequest: (
|
updateFriendRequest: (
|
||||||
userId: string,
|
userId: string,
|
||||||
action: FriendRequestAction
|
action: FriendRequestAction
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
import type { FriendRequest, UserProfile } from "@types";
|
import type { FriendRequest, UserDetails } from "@types";
|
||||||
|
|
||||||
export interface UserDetailsState {
|
export interface UserDetailsState {
|
||||||
userDetails: UserProfile | null;
|
userDetails: UserDetails | null;
|
||||||
profileBackground: null | string;
|
profileBackground: null | string;
|
||||||
friendRequests: FriendRequest[];
|
friendRequests: FriendRequest[];
|
||||||
|
friendRequestCount: number;
|
||||||
isFriendsModalVisible: boolean;
|
isFriendsModalVisible: boolean;
|
||||||
friendRequetsModalTab: UserFriendModalTab | null;
|
friendRequetsModalTab: UserFriendModalTab | null;
|
||||||
friendModalUserId: string;
|
friendModalUserId: string;
|
||||||
@@ -15,6 +16,7 @@ const initialState: UserDetailsState = {
|
|||||||
userDetails: null,
|
userDetails: null,
|
||||||
profileBackground: null,
|
profileBackground: null,
|
||||||
friendRequests: [],
|
friendRequests: [],
|
||||||
|
friendRequestCount: 0,
|
||||||
isFriendsModalVisible: false,
|
isFriendsModalVisible: false,
|
||||||
friendRequetsModalTab: null,
|
friendRequetsModalTab: null,
|
||||||
friendModalUserId: "",
|
friendModalUserId: "",
|
||||||
@@ -24,7 +26,7 @@ export const userDetailsSlice = createSlice({
|
|||||||
name: "user-details",
|
name: "user-details",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setUserDetails: (state, action: PayloadAction<UserProfile | null>) => {
|
setUserDetails: (state, action: PayloadAction<UserDetails | null>) => {
|
||||||
state.userDetails = action.payload;
|
state.userDetails = action.payload;
|
||||||
},
|
},
|
||||||
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
||||||
@@ -33,6 +35,9 @@ export const userDetailsSlice = createSlice({
|
|||||||
setFriendRequests: (state, action: PayloadAction<FriendRequest[]>) => {
|
setFriendRequests: (state, action: PayloadAction<FriendRequest[]>) => {
|
||||||
state.friendRequests = action.payload;
|
state.friendRequests = action.payload;
|
||||||
},
|
},
|
||||||
|
setFriendRequestCount: (state, action: PayloadAction<number>) => {
|
||||||
|
state.friendRequestCount = action.payload;
|
||||||
|
},
|
||||||
setFriendsModalVisible: (
|
setFriendsModalVisible: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }>
|
action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }>
|
||||||
@@ -52,6 +57,7 @@ export const {
|
|||||||
setUserDetails,
|
setUserDetails,
|
||||||
setProfileBackground,
|
setProfileBackground,
|
||||||
setFriendRequests,
|
setFriendRequests,
|
||||||
|
setFriendRequestCount,
|
||||||
setFriendsModalVisible,
|
setFriendsModalVisible,
|
||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
} = userDetailsSlice.actions;
|
} = userDetailsSlice.actions;
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import {
|
|||||||
setFriendRequests,
|
setFriendRequests,
|
||||||
setFriendsModalVisible,
|
setFriendsModalVisible,
|
||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
|
setFriendRequestCount,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import type {
|
import type {
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
UserProfile,
|
UserDetails,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export function useUserDetails() {
|
|||||||
userDetails,
|
userDetails,
|
||||||
profileBackground,
|
profileBackground,
|
||||||
friendRequests,
|
friendRequests,
|
||||||
|
friendRequestCount,
|
||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
friendModalUserId,
|
friendModalUserId,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
@@ -40,7 +42,7 @@ export function useUserDetails() {
|
|||||||
}, [clearUserDetails]);
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const updateUserDetails = useCallback(
|
const updateUserDetails = useCallback(
|
||||||
async (userDetails: UserProfile) => {
|
async (userDetails: UserDetails) => {
|
||||||
dispatch(setUserDetails(userDetails));
|
dispatch(setUserDetails(userDetails));
|
||||||
|
|
||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
@@ -83,7 +85,10 @@ export function useUserDetails() {
|
|||||||
const patchUser = useCallback(
|
const patchUser = useCallback(
|
||||||
async (values: UpdateProfileRequest) => {
|
async (values: UpdateProfileRequest) => {
|
||||||
const response = await window.electron.updateProfile(values);
|
const response = await window.electron.updateProfile(values);
|
||||||
return updateUserDetails(response);
|
return updateUserDetails({
|
||||||
|
...response,
|
||||||
|
username: userDetails?.username || "",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
);
|
);
|
||||||
@@ -92,11 +97,21 @@ export function useUserDetails() {
|
|||||||
return window.electron
|
return window.electron
|
||||||
.getFriendRequests()
|
.getFriendRequests()
|
||||||
.then((friendRequests) => {
|
.then((friendRequests) => {
|
||||||
|
syncFriendRequests();
|
||||||
dispatch(setFriendRequests(friendRequests));
|
dispatch(setFriendRequests(friendRequests));
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const syncFriendRequests = useCallback(async () => {
|
||||||
|
return window.electron
|
||||||
|
.syncFriendRequests()
|
||||||
|
.then((sync) => {
|
||||||
|
dispatch(setFriendRequestCount(sync.friendRequestCount));
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const showFriendsModal = useCallback(
|
const showFriendsModal = useCallback(
|
||||||
(initialTab: UserFriendModalTab, userId: string) => {
|
(initialTab: UserFriendModalTab, userId: string) => {
|
||||||
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
||||||
@@ -140,6 +155,7 @@ export function useUserDetails() {
|
|||||||
userDetails,
|
userDetails,
|
||||||
profileBackground,
|
profileBackground,
|
||||||
friendRequests,
|
friendRequests,
|
||||||
|
friendRequestCount,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
friendModalUserId,
|
friendModalUserId,
|
||||||
@@ -152,6 +168,7 @@ export function useUserDetails() {
|
|||||||
patchUser,
|
patchUser,
|
||||||
sendFriendRequest,
|
sendFriendRequest,
|
||||||
fetchFriendRequests,
|
fetchFriendRequests,
|
||||||
|
syncFriendRequests,
|
||||||
updateFriendRequestState,
|
updateFriendRequestState,
|
||||||
blockUser,
|
blockUser,
|
||||||
unblockUser,
|
unblockUser,
|
||||||
|
|||||||
@@ -149,8 +149,7 @@ export const randomizerButton = style({
|
|||||||
animationName: slideIn,
|
animationName: slideIn,
|
||||||
animationDuration: "0.2s",
|
animationDuration: "0.2s",
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
/* Bottom panel height + spacing */
|
bottom: `${SPACING_UNIT * 3}px`,
|
||||||
bottom: `${26 + SPACING_UNIT * 2}px`,
|
|
||||||
/* Scroll bar + spacing */
|
/* Scroll bar + spacing */
|
||||||
right: `${9 + SPACING_UNIT * 2}px`,
|
right: `${9 + SPACING_UNIT * 2}px`,
|
||||||
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
|
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
|
||||||
|
|||||||
@@ -160,12 +160,15 @@ export function DownloadSettingsModal({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedDownloader && selectedDownloader !== Downloader.Torrent && (
|
{selectedDownloader != null &&
|
||||||
<p style={{ marginTop: `${SPACING_UNIT}px` }}>
|
selectedDownloader !== Downloader.Torrent && (
|
||||||
<span style={{ color: vars.color.warning }}>{t("warning")}</span>{" "}
|
<p style={{ marginTop: `${SPACING_UNIT}px` }}>
|
||||||
{t("hydra_needs_to_remain_open")}
|
<span style={{ color: vars.color.warning }}>
|
||||||
</p>
|
{t("warning")}
|
||||||
)}
|
</span>{" "}
|
||||||
|
{t("hydra_needs_to_remain_open")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
@@ -9,7 +9,7 @@ import { useFormat } from "@renderer/hooks";
|
|||||||
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const [_howLongToBeat, setHowLongToBeat] = useState<{
|
const [_howLongToBeat, _setHowLongToBeat] = useState<{
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
data: HowLongToBeatCategory[] | null;
|
data: HowLongToBeatCategory[] | null;
|
||||||
}>({ isLoading: true, data: null });
|
}>({ isLoading: true, data: null });
|
||||||
@@ -17,27 +17,26 @@ export function Sidebar() {
|
|||||||
const [activeRequirement, setActiveRequirement] =
|
const [activeRequirement, setActiveRequirement] =
|
||||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||||
|
|
||||||
const { gameTitle, shopDetails, objectID, stats } =
|
const { gameTitle, shopDetails, stats } = useContext(gameDetailsContext);
|
||||||
useContext(gameDetailsContext);
|
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (objectID) {
|
// if (objectID) {
|
||||||
setHowLongToBeat({ isLoading: true, data: null });
|
// setHowLongToBeat({ isLoading: true, data: null });
|
||||||
|
|
||||||
window.electron
|
// window.electron
|
||||||
.getHowLongToBeat(objectID, "steam", gameTitle)
|
// .getHowLongToBeat(objectID, "steam", gameTitle)
|
||||||
.then((howLongToBeat) => {
|
// .then((howLongToBeat) => {
|
||||||
setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
// setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||||
})
|
// })
|
||||||
.catch(() => {
|
// .catch(() => {
|
||||||
setHowLongToBeat({ isLoading: false, data: null });
|
// setHowLongToBeat({ isLoading: false, data: null });
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}, [objectID, gameTitle]);
|
// }, [objectID, gameTitle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.contentSidebar}>
|
<aside className={styles.contentSidebar}>
|
||||||
@@ -72,16 +71,12 @@ export function Sidebar() {
|
|||||||
<p>{numberFormatter.format(stats?.playerCount)}</p>
|
<p>{numberFormatter.format(stats?.playerCount)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
className={styles.contentSidebarTitle}
|
|
||||||
style={{ border: "none" }}
|
|
||||||
>
|
|
||||||
<h3>{t("requirements")}</h3>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className={styles.contentSidebarTitle} style={{ border: "none" }}>
|
||||||
|
<h3>{t("requirements")}</h3>
|
||||||
|
</div>
|
||||||
<div className={styles.requirementButtonContainer}>
|
<div className={styles.requirementButtonContainer}>
|
||||||
<Button
|
<Button
|
||||||
className={styles.requirementButton}
|
className={styles.requirementButton}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@@ -8,10 +8,11 @@ import { Button, GameCard, Hero } from "@renderer/components";
|
|||||||
import type { Steam250Game, CatalogueEntry } from "@types";
|
import type { Steam250Game, CatalogueEntry } from "@types";
|
||||||
|
|
||||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
|
import flameAnimation from "@renderer/assets/lottie/flame.json";
|
||||||
|
|
||||||
import * as styles from "./home.css";
|
import * as styles from "./home.css";
|
||||||
import { vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import Lottie from "lottie-react";
|
import Lottie, { type LottieRefCurrentProps } from "lottie-react";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { CatalogueCategory } from "@shared";
|
import { CatalogueCategory } from "@shared";
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ export function Home() {
|
|||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation("home");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const flameAnimationRef = useRef<LottieRefCurrentProps>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
|
|
||||||
@@ -82,6 +85,18 @@ export function Home() {
|
|||||||
|
|
||||||
const categories = Object.values(CatalogueCategory);
|
const categories = Object.values(CatalogueCategory);
|
||||||
|
|
||||||
|
const handleMouseEnterCategory = (category: CatalogueCategory) => {
|
||||||
|
if (category === CatalogueCategory.Hot) {
|
||||||
|
flameAnimationRef?.current?.play();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeaveCategory = (category: CatalogueCategory) => {
|
||||||
|
if (category === CatalogueCategory.Hot) {
|
||||||
|
flameAnimationRef?.current?.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
<section className={styles.content}>
|
<section className={styles.content}>
|
||||||
@@ -100,7 +115,28 @@ export function Home() {
|
|||||||
: "outline"
|
: "outline"
|
||||||
}
|
}
|
||||||
onClick={() => handleCategoryClick(category)}
|
onClick={() => handleCategoryClick(category)}
|
||||||
|
onMouseEnter={() => handleMouseEnterCategory(category)}
|
||||||
|
onMouseLeave={() => handleMouseLeaveCategory(category)}
|
||||||
>
|
>
|
||||||
|
{category === CatalogueCategory.Hot && (
|
||||||
|
<div
|
||||||
|
style={{ width: 16, height: 16, position: "relative" }}
|
||||||
|
>
|
||||||
|
<Lottie
|
||||||
|
lottieRef={flameAnimationRef}
|
||||||
|
animationData={flameAnimation}
|
||||||
|
loop
|
||||||
|
autoplay={false}
|
||||||
|
style={{
|
||||||
|
width: 30,
|
||||||
|
top: -10,
|
||||||
|
left: -5,
|
||||||
|
position: "absolute",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{t(category)}
|
{t(category)}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
@@ -116,14 +152,32 @@ export function Home() {
|
|||||||
<Lottie
|
<Lottie
|
||||||
animationData={starsAnimation}
|
animationData={starsAnimation}
|
||||||
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
||||||
loop
|
loop={Boolean(randomGame)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{t("surprise_me")}
|
{t("surprise_me")}
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h2>{t(currentCatalogueCategory)}</h2>
|
<h2 style={{ display: "flex", gap: SPACING_UNIT }}>
|
||||||
|
{currentCatalogueCategory === CatalogueCategory.Hot && (
|
||||||
|
<div style={{ width: 24, height: 24, position: "relative" }}>
|
||||||
|
<Lottie
|
||||||
|
animationData={flameAnimation}
|
||||||
|
loop
|
||||||
|
autoplay
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
top: -10,
|
||||||
|
left: -5,
|
||||||
|
position: "absolute",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{t(currentCatalogueCategory)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<section className={styles.cards}>
|
<section className={styles.cards}>
|
||||||
{isLoading
|
{isLoading
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export const listItemImage = style({
|
|||||||
width: "32px",
|
width: "32px",
|
||||||
height: "32px",
|
height: "32px",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
|
objectFit: "cover",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listItemDetails = style({
|
export const listItemDetails = style({
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ export interface UserBlocks {
|
|||||||
blocks: UserFriend[];
|
blocks: UserFriend[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FriendRequestSync {
|
||||||
|
friendRequestCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FriendRequest {
|
export interface FriendRequest {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -190,23 +194,34 @@ export interface UserProfileCurrentGame extends Omit<GameRunning, "objectID"> {
|
|||||||
sessionDurationInSeconds: number;
|
sessionDurationInSeconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
|
|
||||||
|
export interface UserDetails {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
profileImageUrl: string | null;
|
||||||
|
profileVisibility: ProfileVisibility;
|
||||||
|
bio: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
profileVisibility: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
profileVisibility: ProfileVisibility;
|
||||||
totalPlayTimeInSeconds: number;
|
|
||||||
libraryGames: UserGame[];
|
libraryGames: UserGame[];
|
||||||
recentGames: UserGame[];
|
recentGames: UserGame[];
|
||||||
friends: UserFriend[];
|
friends: UserFriend[];
|
||||||
totalFriends: number;
|
totalFriends: number;
|
||||||
relation: UserRelation | null;
|
relation: UserRelation | null;
|
||||||
currentGame: UserProfileCurrentGame | null;
|
currentGame: UserProfileCurrentGame | null;
|
||||||
|
bio: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProfileRequest {
|
export interface UpdateProfileRequest {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
profileVisibility?: ProfileVisibility;
|
||||||
profileImageUrl?: string | null;
|
profileImageUrl?: string | null;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user