Compare commits

..

26 Commits

Author SHA1 Message Date
Chubby Granny Chaser
16c45692da Merge pull request #982 from hydralauncher/feature/adding-flame-animation
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
chore: bump version
2024-09-17 16:53:06 +01:00
Chubby Granny Chaser
30aa3f5470 chore: bump version 2024-09-17 16:50:26 +01:00
Chubby Granny Chaser
ef16732c0a Merge pull request #981 from hydralauncher/feature/adding-flame-animation
feat: adding flame animation
2024-09-17 16:42:24 +01:00
Chubby Granny Chaser
84c472a3fa chore: bump version 2024-09-17 16:37:25 +01:00
Chubby Granny Chaser
2610f8b17b Merge branch 'feature/adding-flame-animation' of github.com:hydralauncher/hydra into feature/adding-flame-animation 2024-09-17 16:32:11 +01:00
Chubby Granny Chaser
705b12019f Merge branch 'main' of github.com:hydralauncher/hydra into feature/adding-flame-animation 2024-09-17 16:31:44 +01:00
Zamitto
39be8fdf53 Merge branch 'main' into feature/adding-flame-animation 2024-09-17 12:28:59 -03:00
Chubby Granny Chaser
cc7c3455fa feat: adding flame animation 2024-09-17 16:28:59 +01:00
Chubby Granny Chaser
d1f4bc7207 feat: adding flame animation 2024-09-17 16:26:12 +01:00
Chubby Granny Chaser
aa4ca25653 feat: adding flame animation 2024-09-17 16:25:19 +01:00
Zamitto
405ea0a824 fix: playtime count 2024-09-17 11:10:44 -03:00
Zamitto
43bc0cb08f Merge pull request #977 from hydralauncher/feat/polling-from-sync
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
feat: polling from sync
2024-09-16 16:46:29 -03:00
Zamitto
3c200aa2eb fix: rename variable 2024-09-16 14:48:15 -03:00
Zamitto
cc5967814b fix: adjust when calling /games/download 2024-09-16 14:07:00 -03:00
Zamitto
ec16efed2f feat: create use details functions 2024-09-16 14:03:54 -03:00
Zamitto
09d0e5b4ef Merge pull request #976 from hydralauncher/feat/update-typing-to-match-get-me
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
feat: update typing to match get me
2024-09-16 13:36:26 -03:00
Zamitto
5b18aba2b8 feat: omit username and tokens in logs 2024-09-16 13:09:50 -03:00
Zamitto
192008c76c fix: not showing repacks and stats if game details request fails 2024-09-16 12:56:29 -03:00
Zamitto
1de3a9836c feat: update typing to match get me endpoint 2024-09-16 11:22:41 -03:00
Zamitto
ee02811aea chore: remove space in version string
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
2024-09-15 12:21:41 -03:00
Zamitto
c21ebe1ce2 fix: database migration 2024-09-15 12:09:51 -03:00
Zamitto
214e39adda chore: version
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
2024-09-15 01:17:16 -03:00
Zamitto
8258127616 chore: version 2024-09-15 01:07:46 -03:00
Zamitto
f9906bfe95 fix: message and migration 2024-09-15 01:00:44 -03:00
Chubby Granny Chaser
ff91284a91 chore: fix version
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
2024-09-15 03:46:38 +01:00
Chubby Granny Chaser
b4f99418e9 chore: changing auth url 2024-09-15 03:39:13 +01:00
32 changed files with 2022 additions and 214 deletions

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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..."
}, },

View File

@@ -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",

View File

@@ -8,7 +8,7 @@
"trending": "В тренде", "trending": "В тренде",
"surprise_me": "Удиви меня", "surprise_me": "Удиви меня",
"no_results": "Ничего не найдено", "no_results": "Ничего не найдено",
"hot": "🔥 Сейчас жарко", "hot": "Сейчас жарко",
"start_typing": "Начинаю вводить текст для поиска...", "start_typing": "Начинаю вводить текст для поиска...",
"weekly": "📅 Лучшие игры недели" "weekly": "📅 Лучшие игры недели"
}, },

View File

@@ -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");

View File

@@ -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;

View 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);

View File

@@ -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!);

View File

@@ -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,

View File

@@ -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");
}, },
}; };

View 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) => {},
};

View File

@@ -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: "",

View File

@@ -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),

View File

@@ -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 {

View File

@@ -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", () => {

View File

@@ -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) =>

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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}

View File

@@ -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>

View File

@@ -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`,
}, },
}, },
}, },

View File

@@ -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
) )
) { ) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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",

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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({

View File

@@ -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;
} }