- Hydra is a game launcher with its own embedded bittorrent client and a self-managed repack scraper.
+ Hydra is a game launcher with its own embedded bittorrent client.
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
@@ -50,17 +50,15 @@
## About
-**Hydra** is a **Game Launcher** with its own embedded **BitTorrent Client** and a **self-managed repack scraper**.
+**Hydra** is a **Game Launcher** with its own embedded **BitTorrent Client**.
The launcher is written in TypeScript (Electron) and Python, which handles the torrenting system by using libtorrent.
## Features
-- Self-Managed repack scraper among all the most reliable websites on the [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/")
- Own embedded bittorrent client
- How Long To Beat (HLTB) integration on game page
- Downloads path customization
-- Repack list update notifications
- Windows and Linux support
- Constantly updated
- And more ...
@@ -134,9 +132,8 @@ pip install -r requirements.txt
## Environment variables
You'll need an SteamGridDB API Key in order to fetch the game icons on installation.
-If you want to have onlinefix as a repacker you'll need to add your credentials to the .env
-Once you have it, you can copy or rename the `.env.example` file to `.env` and put it on`STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
+Once you have it, you can copy or rename the `.env.example` file to `.env` and put it on`STEAMGRIDDB_API_KEY`.
## Running
diff --git a/package.json b/package.json
index d7996e6a..4693f65b 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"@reduxjs/toolkit": "^2.2.3",
"@sentry/electron": "^5.1.0",
"@vanilla-extract/css": "^1.14.2",
+ "@vanilla-extract/dynamic": "^2.1.1",
"@vanilla-extract/recipes": "^0.5.2",
"aria2": "^4.1.2",
"auto-launch": "^5.0.6",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index e44dc7ea..3f1bcdcd 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -241,6 +241,15 @@
"successfully_signed_out": "Successfully signed out",
"sign_out": "Sign out",
"playing_for": "Playing for {{amount}}",
- "sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?"
+ "sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?",
+ "add_friends": "Add Friends",
+ "add": "Add",
+ "friend_code": "Friend code",
+ "see_profile": "See profile",
+ "sending": "Sending",
+ "friend_request_sent": "Friend request sent",
+ "friends": "Friends",
+ "friends_list": "Friends list",
+ "user_not_found": "User not found"
}
}
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index 77b43dd8..5e016d34 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -48,7 +48,7 @@
"download_options_zero": "No hay opciones de descargas disponibles",
"download_options_one": "{{count}} opción de descarga",
"download_options_other": "{{count}} opciones de descargas",
- "updated_at": "Actualizado el {{updated_at}}",
+ "updated_at": "Actualizado el: {{updated_at}}",
"install": "Instalar",
"resume": "Continuar",
"pause": "Pausa",
@@ -74,7 +74,7 @@
"remove_from_library": "Eliminar de la biblioteca",
"no_downloads": "No hay descargas disponibles",
"play_time": "Jugado por {{amount}}",
- "last_time_played": "Jugado por última vez {{period}}",
+ "last_time_played": "Jugado por última vez: {{period}}",
"not_played_yet": "Aún no has jugado a {{title}}",
"next_suggestion": "Siguiente sugerencia",
"play": "Jugar",
@@ -107,8 +107,8 @@
"executable_section_description": "Ruta del archivo que se ejecutará cuando se presione \"Jugar\"",
"downloads_secion_title": "Descargas",
"downloads_section_description": "Buscar actualizaciones u otras versiones de este juego",
- "danger_zone_section_title": "Zona de Peligro",
- "danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra",
+ "danger_zone_section_title": "Opciones Avanzadas",
+ "danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra (Esto solo eliminará los archivos de instalación y no el juego instalado)",
"download_in_progress": "Descarga en progreso",
"download_paused": "Descarga pausada",
"last_downloaded_option": "Última opción descargada",
@@ -138,7 +138,7 @@
"deleting": "Eliminando instalador…",
"delete": "Eliminar instalador",
"delete_modal_title": "¿Estás seguro?",
- "delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
+ "delete_modal_description": "Esto eliminará todos los archivos de la instalación del repack del juego de tu computadora. (Si ya instalaste el juego, puedes eliminar esto, no afectará al juego)",
"install": "Instalar",
"download_in_progress": "En progreso",
"queued_downloads": "Descargas en cola",
@@ -200,7 +200,8 @@
"repack_list_updated": "Lista de repacks actualizadas",
"repack_count_one": "{{count}} repack ha sido añadido",
"repack_count_other": "{{count}} repacks añadidos",
- "new_update_available": "Version {{version}} disponible"
+ "new_update_available": "Version {{version}} disponible",
+ "restart_to_install_update": "Reinicia Hydra para instalar la actualización"
},
"system_tray": {
"open": "Abrir Hydra",
@@ -223,13 +224,13 @@
"user_profile": {
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
- "last_time_played": "Última vez jugado {{period}}",
+ "last_time_played": "Última vez jugado: {{period}}",
"activity": "Actividad reciente",
"library": "Biblioteca",
"total_play_time": "Total de tiempo jugado: {{amount}}",
- "no_recent_activity_title": "Que raro, no hay nada por acá, ¿que tal si jugamos algo para empezar?",
+ "no_recent_activity_title": "Que raro, no hay nada por acá...",
"no_recent_activity_description": "No has jugado ningún juego recientemente, ¡vamos a cambiar eso ahora!",
- "display_name": "Nombre a mostrar",
+ "display_name": "Nombre en pantalla",
"saving": "Guardando",
"save": "Guardar",
"edit_profile": "Editar perfil",
diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json
index f1a4e77e..568116f8 100644
--- a/src/locales/pt/translation.json
+++ b/src/locales/pt/translation.json
@@ -12,11 +12,11 @@
"catalogue": "Catálogo",
"downloads": "Downloads",
"settings": "Ajustes",
- "my_library": "Minha biblioteca",
+ "my_library": "Biblioteca",
"downloading_metadata": "{{title}} (Baixando metadados…)",
"paused": "{{title}} (Pausado)",
"downloading": "{{title}} ({{percentage}} - Baixando…)",
- "filter": "Filtrar biblioteca",
+ "filter": "Buscar",
"home": "Início",
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado",
@@ -45,7 +45,7 @@
"download_options_one": "{{count}} opção de download",
"download_options_other": "{{count}} opções de download",
"updated_at": "Atualizado {{updated_at}}",
- "resume": "Resumir",
+ "resume": "Retomar",
"pause": "Pausar",
"cancel": "Cancelar",
"remove": "Remover",
@@ -54,7 +54,7 @@
"calculating_eta": "Calculando tempo restante…",
"downloading_metadata": "Baixando metadados…",
"filter": "Filtrar repacks",
- "requirements": "Requisitos do sistema",
+ "requirements": "Requisitos de sistema",
"minimum": "Mínimos",
"recommended": "Recomendados",
"paused": "Pausado",
@@ -68,16 +68,16 @@
"add_to_library": "Adicionar à biblioteca",
"remove_from_library": "Remover da biblioteca",
"no_downloads": "Nenhum download disponível",
- "play_time": "Jogado por {{amount}}",
+ "play_time": "Jogou por {{amount}}",
"next_suggestion": "Próxima sugestão",
"install": "Instalar",
- "last_time_played": "Jogou por último {{period}}",
+ "last_time_played": "Última sessão {{period}}",
"play": "Jogar",
"not_played_yet": "Você ainda não jogou {{title}}",
"close": "Fechar",
"deleting": "Excluindo instalador…",
"playing_now": "Jogando agora",
- "change": "Mudar",
+ "change": "Explorar",
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
"select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes0>",
"download_now": "Iniciar download",
@@ -90,13 +90,13 @@
"open_screenshot": "Ver captura de tela {{number}}",
"download_settings": "Ajustes do download",
"downloader": "Downloader",
- "select_executable": "Selecionar",
+ "select_executable": "Explorar",
"no_executable_selected": "Nenhum executável selecionado",
"open_folder": "Abrir pasta",
"open_download_location": "Ver arquivos baixados",
"create_shortcut": "Criar atalho na área de trabalho",
"remove_files": "Remover arquivos",
- "options": "Opções",
+ "options": "Gerenciar",
"remove_from_library_description": "Isso irá remover {{game}} da sua biblioteca",
"remove_from_library_title": "Tem certeza?",
"executable_section_title": "Executável",
@@ -120,7 +120,7 @@
"loading": "Carregando…"
},
"downloads": {
- "resume": "Resumir",
+ "resume": "Retomar",
"pause": "Pausar",
"eta": "Conclusão {{eta}}",
"paused": "Pausado",
@@ -146,12 +146,12 @@
},
"settings": {
"downloads_path": "Diretório dos downloads",
- "change": "Mudar",
+ "change": "Explorar...",
"notifications": "Notificações",
"enable_download_notifications": "Quando um download for concluído",
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
"real_debrid_api_token_label": "Token de API do Real-Debrid",
- "quit_app_instead_hiding": "Encerrar o Hydra ao invés de minimizá-lo ao fechar",
+ "quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar.",
"launch_with_system": "Iniciar o Hydra junto com o sistema",
"general": "Geral",
"behavior": "Comportamento",
@@ -208,7 +208,7 @@
},
"binary_not_found_modal": {
"title": "Programas não instalados",
- "description": "Não foram encontrados no seu sistema os executáveis do Wine ou Lutris",
+ "description": "Os executáveis do Wine ou Lutris não foram encontrados em seu sistema.",
"instructions": "Verifique a forma correta de instalar algum deles no seu distro Linux, garantindo assim a execução normal do jogo"
},
"catalogue": {
@@ -224,8 +224,8 @@
"user_profile": {
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
- "last_time_played": "Jogou {{period}}",
- "activity": "Atividade recente",
+ "last_time_played": "Última sessão {{period}}",
+ "activity": "Atividades recentes",
"library": "Biblioteca",
"total_play_time": "Tempo total de jogo: {{amount}}",
"no_recent_activity_title": "Hmmm… nada por aqui",
@@ -233,7 +233,7 @@
"display_name": "Nome de exibição",
"saving": "Salvando…",
"save": "Salvar",
- "edit_profile": "Editar Perfil",
+ "edit_profile": "Editar perfil",
"saved_successfully": "Salvo com sucesso",
"try_again": "Por favor, tente novamente",
"cancel": "Cancelar",
@@ -241,6 +241,15 @@
"sign_out": "Sair da conta",
"sign_out_modal_title": "Tem certeza?",
"playing_for": "Jogando por {{amount}}",
- "sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?"
+ "sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?",
+ "add_friends": "Adicionar Amigos",
+ "friend_code": "Código de amigo",
+ "see_profile": "Ver perfil",
+ "friend_request_sent": "Pedido de amizade enviado",
+ "friends": "Amigos",
+ "add": "Adicionar",
+ "sending": "Enviando",
+ "friends_list": "Lista de amigos",
+ "user_not_found": "Usuário não encontrado"
}
}
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
index 69a9c33d..f6f18d11 100644
--- a/src/locales/ru/translation.json
+++ b/src/locales/ru/translation.json
@@ -36,7 +36,8 @@
"no_downloads_in_progress": "Нет активных загрузок",
"downloading_metadata": "Загрузка метаданных {{title}}…",
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}",
- "calculating_eta": "Загрузка {{title}}… ({{percentage}} завершено) - Подсчёт оставшегося времени…"
+ "calculating_eta": "Загрузка {{title}}… ({{percentage}} завершено) - Подсчёт оставшегося времени…",
+ "checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)"
},
"catalogue": {
"next_page": "Следующая страница",
@@ -144,7 +145,8 @@
"downloads_completed": "Завершено",
"queued": "В очереди",
"no_downloads_title": "Здесь так пусто...",
- "no_downloads_description": "Вы ещё ничего не скачали через Hydra, но никогда не поздно начать."
+ "no_downloads_description": "Вы ещё ничего не скачали через Hydra, но никогда не поздно начать.",
+ "checking_files": "Проверка файлов…"
},
"settings": {
"downloads_path": "Путь загрузок",
@@ -198,7 +200,8 @@
"repack_list_updated": "Список репаков обновлен",
"repack_count_one": "{{count}} репак добавлен",
"repack_count_other": "{{count}} репаков добавлено",
- "new_update_available": "Доступна версия {{version}}"
+ "new_update_available": "Доступна версия {{version}}",
+ "restart_to_install_update": "Перезапустите Hydra для установки обновления"
},
"system_tray": {
"open": "Открыть Hydra",
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 1b500be9..dd5e3263 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -43,8 +43,11 @@ import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
import "./user/get-user";
+import "./profile/get-friend-requests";
import "./profile/get-me";
+import "./profile/update-friend-request";
import "./profile/update-profile";
+import "./profile/send-friend-request";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());
diff --git a/src/main/events/profile/get-friend-requests.ts b/src/main/events/profile/get-friend-requests.ts
new file mode 100644
index 00000000..11d8a884
--- /dev/null
+++ b/src/main/events/profile/get-friend-requests.ts
@@ -0,0 +1,11 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { FriendRequest } from "@types";
+
+const getFriendRequests = async (
+ _event: Electron.IpcMainInvokeEvent
+): Promise => {
+ return HydraApi.get(`/profile/friend-requests`).catch(() => []);
+};
+
+registerEvent("getFriendRequests", getFriendRequests);
diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts
index 83463680..1626125b 100644
--- a/src/main/events/profile/get-me.ts
+++ b/src/main/events/profile/get-me.ts
@@ -9,9 +9,7 @@ const getMe = async (
_event: Electron.IpcMainInvokeEvent
): Promise => {
return HydraApi.get(`/profile/me`)
- .then((response) => {
- const me = response.data;
-
+ .then((me) => {
userAuthRepository.upsert(
{
id: 1,
@@ -26,12 +24,18 @@ const getMe = async (
return me;
})
- .catch((err) => {
+ .catch(async (err) => {
if (err instanceof UserNotLoggedInError) {
return null;
}
- return userAuthRepository.findOne({ where: { id: 1 } });
+ const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
+
+ if (loggedUser) {
+ return { ...loggedUser, id: loggedUser.userId };
+ }
+
+ return null;
});
};
diff --git a/src/main/events/profile/send-friend-request.ts b/src/main/events/profile/send-friend-request.ts
new file mode 100644
index 00000000..d696606f
--- /dev/null
+++ b/src/main/events/profile/send-friend-request.ts
@@ -0,0 +1,11 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+
+const sendFriendRequest = async (
+ _event: Electron.IpcMainInvokeEvent,
+ userId: string
+) => {
+ return HydraApi.post("/profile/friend-requests", { friendCode: userId });
+};
+
+registerEvent("sendFriendRequest", sendFriendRequest);
diff --git a/src/main/events/profile/update-friend-request.ts b/src/main/events/profile/update-friend-request.ts
new file mode 100644
index 00000000..24929544
--- /dev/null
+++ b/src/main/events/profile/update-friend-request.ts
@@ -0,0 +1,19 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { FriendRequestAction } from "@types";
+
+const updateFriendRequest = async (
+ _event: Electron.IpcMainInvokeEvent,
+ userId: string,
+ action: FriendRequestAction
+) => {
+ if (action == "CANCEL") {
+ return HydraApi.delete(`/profile/friend-requests/${userId}`);
+ }
+
+ return HydraApi.patch(`/profile/friend-requests/${userId}`, {
+ requestState: action,
+ });
+};
+
+registerEvent("updateFriendRequest", updateFriendRequest);
diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts
index fe79d345..8620eaa1 100644
--- a/src/main/events/profile/update-profile.ts
+++ b/src/main/events/profile/update-profile.ts
@@ -26,11 +26,9 @@ const updateProfile = async (
_event: Electron.IpcMainInvokeEvent,
displayName: string,
newProfileImagePath: string | null
-) => {
+): Promise => {
if (!newProfileImagePath) {
- return patchUserProfile(displayName).then(
- (response) => response.data as UserProfile
- );
+ return patchUserProfile(displayName);
}
const stats = fs.statSync(newProfileImagePath);
@@ -42,7 +40,7 @@ const updateProfile = async (
imageLength: fileSizeInBytes,
})
.then(async (preSignedResponse) => {
- const { presignedUrl, profileImageUrl } = preSignedResponse.data;
+ const { presignedUrl, profileImageUrl } = preSignedResponse;
const mimeType = await fileTypeFromFile(newProfileImagePath);
@@ -51,13 +49,11 @@ const updateProfile = async (
"Content-Type": mimeType?.mime,
},
});
- return profileImageUrl;
+ return profileImageUrl as string;
})
.catch(() => undefined);
- return patchUserProfile(displayName, profileImageUrl).then(
- (response) => response.data as UserProfile
- );
+ return patchUserProfile(displayName, profileImageUrl);
};
registerEvent("updateProfile", updateProfile);
diff --git a/src/main/events/user/get-user.ts b/src/main/events/user/get-user.ts
index 596df084..7b4c0aa8 100644
--- a/src/main/events/user/get-user.ts
+++ b/src/main/events/user/get-user.ts
@@ -10,8 +10,7 @@ const getUser = async (
userId: string
): Promise => {
try {
- const response = await HydraApi.get(`/user/${userId}`);
- const profile = response.data;
+ const profile = await HydraApi.get(`/user/${userId}`);
const recentGames = await Promise.all(
profile.recentGames.map(async (game) => {
diff --git a/src/main/index.ts b/src/main/index.ts
index e288302b..9ff74bf6 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -20,6 +20,8 @@ autoUpdater.setFeedURL({
autoUpdater.logger = logger;
+logger.log("Init Hydra");
+
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
@@ -121,6 +123,7 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => {
/* Disconnects libtorrent */
PythonInstance.kill();
+ logger.log("Quit Hydra");
});
app.on("activate", () => {
diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts
index 98b783f3..97517b5b 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -10,7 +10,7 @@ import { UserNotLoggedInError } from "@shared";
export class HydraApi {
private static instance: AxiosInstance;
- private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5;
+ private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
@@ -45,6 +45,8 @@ export class HydraApi {
expirationTimestamp: tokenExpirationTimestamp,
};
+ logger.log("Sign in received", this.userAuth);
+
await userAuthRepository.upsert(
{
id: 1,
@@ -74,7 +76,7 @@ export class HydraApi {
return request;
},
(error) => {
- logger.log("request error", error);
+ logger.error("request error", error);
return Promise.reject(error);
}
);
@@ -95,12 +97,18 @@ export class HydraApi {
const { config } = error;
- logger.error(config.method, config.baseURL, config.url, config.headers);
+ logger.error(
+ config.method,
+ config.baseURL,
+ config.url,
+ config.headers,
+ config.data
+ );
if (error.response) {
- logger.error(error.response.status, error.response.data);
+ logger.error("Response", error.response.status, error.response.data);
} else if (error.request) {
- logger.error(error.request);
+ logger.error("Request", error.request);
} else {
logger.error("Error", error.message);
}
@@ -146,6 +154,8 @@ export class HydraApi {
this.userAuth.authToken = accessToken;
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
+ logger.log("Token refreshed", this.userAuth);
+
userAuthRepository.upsert(
{
id: 1,
@@ -170,6 +180,8 @@ export class HydraApi {
private static handleUnauthorizedError = (err) => {
if (err instanceof AxiosError && err.response?.status === 401) {
+ logger.error("401 - Current credentials:", this.userAuth);
+
this.userAuth = {
authToken: "",
expirationTimestamp: 0,
@@ -190,6 +202,7 @@ export class HydraApi {
await this.revalidateAccessTokenIfExpired();
return this.instance
.get(url, this.getAxiosConfig())
+ .then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
@@ -199,6 +212,7 @@ export class HydraApi {
await this.revalidateAccessTokenIfExpired();
return this.instance
.post(url, data, this.getAxiosConfig())
+ .then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
@@ -208,6 +222,7 @@ export class HydraApi {
await this.revalidateAccessTokenIfExpired();
return this.instance
.put(url, data, this.getAxiosConfig())
+ .then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
@@ -217,6 +232,7 @@ export class HydraApi {
await this.revalidateAccessTokenIfExpired();
return this.instance
.patch(url, data, this.getAxiosConfig())
+ .then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
@@ -226,6 +242,7 @@ export class HydraApi {
await this.revalidateAccessTokenIfExpired();
return this.instance
.delete(url, this.getAxiosConfig())
+ .then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
}
diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts
index c0e8b1f8..b66a1897 100644
--- a/src/main/services/library-sync/create-game.ts
+++ b/src/main/services/library-sync/create-game.ts
@@ -10,11 +10,7 @@ export const createGame = async (game: Game) => {
lastTimePlayed: game.lastTimePlayed,
})
.then((response) => {
- const {
- id: remoteId,
- playTimeInMilliseconds,
- lastTimePlayed,
- } = response.data;
+ const { id: remoteId, playTimeInMilliseconds, lastTimePlayed } = response;
gameRepository.update(
{ objectID: game.objectID },
diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts
index 2162ea58..2a6b5bb5 100644
--- a/src/main/services/library-sync/merge-with-remote-games.ts
+++ b/src/main/services/library-sync/merge-with-remote-games.ts
@@ -6,7 +6,7 @@ import { getSteamAppAsset } from "@main/helpers";
export const mergeWithRemoteGames = async () => {
return HydraApi.get("/games")
.then(async (response) => {
- for (const game of response.data) {
+ for (const game of response) {
const localGame = await gameRepository.findOne({
where: {
objectID: game.objectId,
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 0cadbc03..91722606 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -9,6 +9,7 @@ import type {
AppUpdaterEvent,
StartGameDownloadPayload,
GameRunning,
+ FriendRequestAction,
} from "@types";
contextBridge.exposeInMainWorld("electron", {
@@ -136,6 +137,11 @@ contextBridge.exposeInMainWorld("electron", {
getMe: () => ipcRenderer.invoke("getMe"),
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
+ getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
+ updateFriendRequest: (userId: string, action: FriendRequestAction) =>
+ ipcRenderer.invoke("updateFriendRequest", userId, action),
+ sendFriendRequest: (userId: string) =>
+ ipcRenderer.invoke("sendFriendRequest", userId),
/* User */
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 543b85a9..52276268 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -6,7 +6,7 @@
Hydra
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index afce9622..24c7bed6 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -25,6 +25,7 @@ import {
setGameRunning,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
+import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
export interface AppProps {
children: React.ReactNode;
@@ -38,6 +39,13 @@ export function App() {
const { clearDownload, setLastPacket } = useDownload();
+ const {
+ isFriendsModalVisible,
+ friendRequetsModalTab,
+ updateFriendRequests,
+ hideFriendsModal,
+ } = useUserDetails();
+
const { fetchUserDetails, updateUserDetails, clearUserDetails } =
useUserDetails();
@@ -94,7 +102,10 @@ export function App() {
}
fetchUserDetails().then((response) => {
- if (response) updateUserDetails(response);
+ if (response) {
+ updateUserDetails(response);
+ updateFriendRequests();
+ }
});
}, [fetchUserDetails, updateUserDetails, dispatch]);
@@ -102,6 +113,7 @@ export function App() {
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
+ updateFriendRequests();
showSuccessToast(t("successfully_signed_in"));
}
});
@@ -206,6 +218,12 @@ export function App() {
onClose={handleToastClose}
/>
+
+
diff --git a/src/renderer/src/components/sidebar/sidebar-profile.css.ts b/src/renderer/src/components/sidebar/sidebar-profile.css.ts
index d01b07f1..ba29c850 100644
--- a/src/renderer/src/components/sidebar/sidebar-profile.css.ts
+++ b/src/renderer/src/components/sidebar/sidebar-profile.css.ts
@@ -1,7 +1,18 @@
-import { style } from "@vanilla-extract/css";
+import { createVar, style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
+export const profileContainerBackground = createVar();
+
+export const profileContainer = style({
+ background: profileContainerBackground,
+ position: "relative",
+ cursor: "pointer",
+ ":hover": {
+ backgroundColor: "rgba(255, 255, 255, 0.15)",
+ },
+});
+
export const profileButton = style({
display: "flex",
cursor: "pointer",
@@ -10,9 +21,8 @@ export const profileButton = style({
color: vars.color.muted,
borderBottom: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
- ":hover": {
- backgroundColor: "rgba(255, 255, 255, 0.15)",
- },
+ width: "100%",
+ zIndex: "10",
});
export const profileButtonContent = style({
@@ -64,3 +74,25 @@ export const profileButtonTitle = style({
textOverflow: "ellipsis",
whiteSpace: "nowrap",
});
+
+export const friendRequestContainer = style({
+ position: "absolute",
+ padding: "8px",
+ right: `${SPACING_UNIT}px`,
+ display: "flex",
+ top: 0,
+ bottom: 0,
+ alignItems: "center",
+});
+
+export const friendRequestButton = style({
+ color: vars.color.success,
+ cursor: "pointer",
+ borderRadius: "50%",
+ overflow: "hidden",
+ width: "40px",
+ height: "40px",
+ ":hover": {
+ color: vars.color.muted,
+ },
+});
diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx
index 914481b0..66c4d82d 100644
--- a/src/renderer/src/components/sidebar/sidebar-profile.tsx
+++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx
@@ -1,17 +1,20 @@
import { useNavigate } from "react-router-dom";
-import { PersonIcon } from "@primer/octicons-react";
+import { PersonAddIcon, PersonIcon } from "@primer/octicons-react";
import * as styles from "./sidebar-profile.css";
-
+import { assignInlineVars } from "@vanilla-extract/dynamic";
import { useAppSelector, useUserDetails } from "@renderer/hooks";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
+import { profileContainerBackground } from "./sidebar-profile.css";
+import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
export function SidebarProfile() {
const navigate = useNavigate();
const { t } = useTranslation("sidebar");
- const { userDetails, profileBackground } = useUserDetails();
+ const { userDetails, profileBackground, friendRequests, showFriendsModal } =
+ useUserDetails();
const { gameRunning } = useAppSelector((state) => state.gameRunning);
@@ -30,46 +33,64 @@ export function SidebarProfile() {
}, [profileBackground]);
return (
-