Merge branch 'hydralauncher:main' into translation-tr

This commit is contained in:
Spydea
2025-03-18 19:10:09 +03:00
committed by GitHub
32 changed files with 368 additions and 136 deletions

View File

@@ -1,2 +1,4 @@
MAIN_VITE_API_URL=API_URL
MAIN_VITE_AUTH_URL=AUTH_URL
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
RENDERER_VITE_TORBOX_REFERRAL_CODE=

View File

@@ -47,12 +47,13 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -61,14 +62,15 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Test Upload build
- name: Upload build
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}

View File

@@ -49,13 +49,13 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -64,13 +64,13 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Create artifact
uses: actions/upload-artifact@v4

13
.github/workflows/trigger-lp.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Trigger Landing Page Build
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Trigger Landing Page build
run: curl --location --request POST '${{ secrets.LP_TRIGGER_DEPLOY_URL }}'

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.3.0",
"version": "3.3.1",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",

File diff suppressed because one or more lines are too long

View File

@@ -272,7 +272,7 @@
"confirm_button_confirmation_delete_all_sources": "Yes, delete everything",
"title_confirmation_delete_all_sources": "Delete all download sources",
"description_confirmation_delete_all_sources": "You will delete all download sources",
"button_delete_all_sources": "Remove all download sources",
"button_delete_all_sources": "Remove all",
"added_download_source": "Added download source",
"download_sources_synced": "All download sources are synced",
"insert_valid_json_url": "Insert a valid JSON url",
@@ -335,12 +335,16 @@
"enable_torbox": "Enable Torbox",
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
"torbox_account_linked": "TorBox account linked",
"create_real_debrid_account": "Click here if you don't have a Real-Debrid account yet",
"create_torbox_account": "Click here if you don't have a TorBox account yet",
"real_debrid_account_linked": "Real-Debrid account linked",
"name_min_length": "Theme name must be at least 3 characters long",
"import_theme": "Import theme",
"import_theme_description": "You will import {{theme}} from the theme store",
"error_importing_theme": "Error importing theme",
"theme_imported": "Theme imported successfully"
"theme_imported": "Theme imported successfully",
"enable_friend_request_notifications": "When a friend request is received",
"enable_auto_install": "Download updates automatically"
},
"notifications": {
"download_complete": "Download complete",
@@ -351,7 +355,9 @@
"new_update_available": "Version {{version}} available",
"restart_to_install_update": "Restart Hydra to install the update",
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked"
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
"new_friend_request_description": "You have received a new friend request",
"new_friend_request_title": "New friend request"
},
"system_tray": {
"open": "Open Hydra",

View File

@@ -304,7 +304,6 @@
"no_email_account": "No has configurado un correo aún",
"no_subscription": "Disfruta Hydra de la mejor manera",
"no_users_blocked": "No tienes usuarios bloqueados",
"notifications": "Notificaciones",
"renew_subscription": "Renovar Hydra Cloud",
"subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}",
"subscription_expired_at": "Tú suscripción expiró el {{date}}",
@@ -341,7 +340,9 @@
"torbox_account_linked": "Cuenta de TorBox vinculada",
"torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.",
"unset_theme": "Desactivar tema",
"web_store": "Tienda Web"
"web_store": "Tienda Web",
"enable_friend_request_notifications": "Cuando se recibe una solicitud de amistad",
"enable_auto_install": "Descargar actualizaciones automáticamente"
},
"notifications": {
"download_complete": "Descarga completada",
@@ -352,7 +353,9 @@
"new_update_available": "Version {{version}} disponible",
"restart_to_install_update": "Reinicia Hydra para instalar la actualización",
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados"
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
"new_friend_request_title": "Nueva solicitud de amistad",
"new_friend_request_description": "Has recibido una nueva solicitud de amistad"
},
"system_tray": {
"open": "Abrir Hydra",

View File

@@ -31,7 +31,6 @@
},
"header": {
"search": "Buscar jogos",
"catalogue": "Catálogo",
"downloads": "Downloads",
"search_results": "Resultados da busca",
@@ -261,8 +260,8 @@
"cancel_button_confirmation_delete_all_sources": "Não",
"confirm_button_confirmation_delete_all_sources": "Sim, excluir tudo",
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
"description_confirmation_delete_all_sources": "Você irá remover todas as fontes de download",
"button_delete_all_sources": "Remover todas as fontes de download",
"description_confirmation_delete_all_sources": "Você irá remover todas as fontes de download. Deseja prosseguir?",
"button_delete_all_sources": "Remover todas",
"added_download_source": "Fonte adicionada",
"download_sources_synced": "As fontes foram sincronizadas",
"insert_valid_json_url": "Insira a url de um JSON válido",
@@ -323,12 +322,16 @@
"enable_torbox": "Habilitar Torbox",
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
"torbox_account_linked": "Conta do TorBox vinculada",
"create_real_debrid_account": "Clique aqui se você ainda não tem uma conta do Real-Debrid",
"create_torbox_account": "Clique aqui se você ainda não tem uma conta do TorBox",
"real_debrid_account_linked": "Conta Real-Debrid associada",
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
"import_theme": "Importar tema",
"import_theme_description": "Você irá importar {{theme}} da loja de temas",
"error_importing_theme": "Erro ao importar tema",
"theme_imported": "Tema importado com sucesso"
"theme_imported": "Tema importado com sucesso",
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
"enable_auto_install": "Baixar atualizações automaticamente"
},
"notifications": {
"download_complete": "Download concluído",
@@ -337,7 +340,9 @@
"repack_count_one": "{{count}} novo repack",
"repack_count_other": "{{count}} novos repacks",
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão"
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão",
"new_friend_request_title": "Novo pedido de amizade",
"new_friend_request_description": "Você recebeu um novo pedido de amizade"
},
"system_tray": {
"open": "Abrir Hydra",

View File

@@ -30,7 +30,6 @@
},
"header": {
"search": "Procurar jogos",
"catalogue": "Catálogo",
"downloads": "Transferências",
"search_results": "Resultados da pesquisa",
@@ -261,7 +260,7 @@
"description_confirmation_delete_all_sources": "Irá remover todas as fontes de download",
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
"removed_download_sources": "Fontes de download removidas",
"button_delete_all_sources": "Remover todas as fontes de download",
"button_delete_all_sources": "Remover todas",
"added_download_source": "Fonte adicionada",
"download_sources_synced": "As fontes foram sincronizadas",
"insert_valid_json_url": "Insere o URL de um JSON válido",
@@ -281,6 +280,7 @@
"blocked_users": "Utilizadores bloqueados",
"user_unblocked": "Utilizador desbloqueado",
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
"launch_minimized": "Iniciar Hydra minimizado",
"disable_nsfw_alert": "Desativar alertas NSFW",
"seed_after_download_complete": "Semear após concluir o download",
@@ -338,7 +338,9 @@
"repack_count_one": "{{count}} novo repack",
"repack_count_other": "{{count}} novos repacks",
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão"
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão",
"new_friend_request_title": "Novo pedido de amizade",
"new_friend_request_description": "Recebeste um novo pedido de amizade"
},
"system_tray": {
"open": "Abrir o Hydra",

View File

@@ -338,7 +338,9 @@
"import_theme": "Импортировать тему",
"import_theme_description": "Вы импортируете {{theme}} из магазина тем",
"error_importing_theme": "Ошибка при импорте темы",
"theme_imported": "Тема успешно импортирована"
"theme_imported": "Тема успешно импортирована",
"enable_friend_request_notifications": "При получении запроса на добавление в друзья",
"enable_auto_install": "Загружать обновления автоматически"
},
"notifications": {
"download_complete": "Загрузка завершена",
@@ -349,7 +351,9 @@
"new_update_available": "Доступна новая версия {{version}}",
"restart_to_install_update": "Перезапустите Hydra для установки обновления",
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}"
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}",
"new_friend_request_title": "Новый запрос на добавление в друзья",
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья"
},
"system_tray": {
"open": "Открыть Hydra",

View File

@@ -31,3 +31,5 @@ export const achievementSoundPath = app.isPackaged
export const backupsPath = path.join(app.getPath("userData"), "Backups");
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
export const MAIN_LOOP_INTERVAL = 1500;

View File

@@ -4,11 +4,15 @@ import updater from "electron-updater";
const { autoUpdater } = updater;
const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => {
export const restartAndInstallUpdate = () => {
autoUpdater.removeAllListeners();
if (app.isPackaged) {
autoUpdater.quitAndInstall(false);
}
};
registerEvent("restartAndInstallUpdate", restartAndInstallUpdate);
const restartAndInstallUpdateEvent = async (
_event: Electron.IpcMainInvokeEvent
) => restartAndInstallUpdate();
registerEvent("restartAndInstallUpdate", restartAndInstallUpdateEvent);

View File

@@ -1,17 +1,59 @@
import { MAIN_LOOP_INTERVAL } from "@main/constants";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { HydraApi, WindowManager } from "@main/services";
import { publishNewFriendRequestNotification } from "@main/services/notifications";
import { UserNotLoggedInError } from "@shared";
import type { FriendRequestSync } from "@types";
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`).catch(
(err) => {
if (err instanceof UserNotLoggedInError) {
return { friendRequests: [] };
}
throw err;
}
);
interface SyncState {
friendRequestCount: number | null;
tick: number;
}
const ticksToUpdate = (2 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 2 minutes
const syncState: SyncState = {
friendRequestCount: null,
tick: 0,
};
registerEvent("syncFriendRequests", syncFriendRequests);
const syncFriendRequests = async () => {
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`)
.then((res) => {
if (
syncState.friendRequestCount != null &&
syncState.friendRequestCount < res.friendRequestCount
) {
publishNewFriendRequestNotification();
}
syncState.friendRequestCount = res.friendRequestCount;
WindowManager.mainWindow?.webContents.send(
"on-sync-friend-requests",
res
);
return res;
})
.catch((err) => {
if (err instanceof UserNotLoggedInError) {
return { friendRequestCount: 0 } as FriendRequestSync;
}
throw err;
});
};
const syncFriendRequestsEvent = async (_event: Electron.IpcMainInvokeEvent) => {
return syncFriendRequests();
};
export const watchFriendRequests = async () => {
if (syncState.tick % ticksToUpdate === 0) {
await syncFriendRequests();
}
syncState.tick++;
};
registerEvent("syncFriendRequests", syncFriendRequestsEvent);

View File

@@ -3,18 +3,21 @@ import { DownloadManager } from "./download";
import { watchProcesses } from "./process-watcher";
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
import { UpdateManager } from "./update-manager";
import { watchFriendRequests } from "@main/events/profile/sync-friend-requests";
import { MAIN_LOOP_INTERVAL } from "@main/constants";
export const startMainLoop = async () => {
// eslint-disable-next-line no-constant-condition
while (true) {
await Promise.allSettled([
watchProcesses(),
watchFriendRequests(),
DownloadManager.watchDownloads(),
AchievementWatcherManager.watchAchievements(),
DownloadManager.getSeedStatus(),
UpdateManager.checkForUpdatePeriodically(),
]);
await sleep(1500);
await sleep(MAIN_LOOP_INTERVAL);
}
};

View File

@@ -12,6 +12,7 @@ import { logger } from "../logger";
import { WindowManager } from "../window-manager";
import type { Game, UserPreferences } from "@types";
import { db, levelKeys } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
async function downloadImage(url: string | null) {
if (!url) return undefined;
@@ -72,10 +73,33 @@ export const publishNotificationUpdateReadyToInstall = async (
ns: "notifications",
}),
icon: trayIcon,
}).show();
})
.on("click", () => {
restartAndInstallUpdate();
})
.show();
};
export const publishNewFriendRequestNotification = async () => {};
export const publishNewFriendRequestNotification = async () => {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (!userPreferences?.friendRequestNotificationsEnabled) return;
new Notification({
title: t("new_friend_request_title", {
ns: "notifications",
}),
body: t("new_friend_request_description", {
ns: "notifications",
}),
icon: trayIcon,
}).show();
};
export const publishCombinedNewAchievementNotification = async (
achievementCount,

View File

@@ -1,14 +1,14 @@
import updater, { UpdateInfo } from "electron-updater";
import { logger, WindowManager } from "@main/services";
import { AppUpdaterEvent } from "@types";
import { AppUpdaterEvent, UserPreferences } from "@types";
import { app } from "electron";
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
const isAutoInstallAvailable =
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
import { db, levelKeys } from "@main/level";
import { MAIN_LOOP_INTERVAL } from "@main/constants";
const { autoUpdater } = updater;
const sendEventsForDebug = false;
const ticksToUpdate = (50 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 50 minutes
export class UpdateManager {
private static hasNotified = false;
@@ -16,7 +16,7 @@ export class UpdateManager {
private static checkTick = 0;
private static mockValuesForDebug() {
this.sendEvent({ type: "update-available", info: { version: "1.3.0" } });
this.sendEvent({ type: "update-available", info: { version: "3.3.1" } });
this.sendEvent({ type: "update-downloaded" });
}
@@ -24,7 +24,27 @@ export class UpdateManager {
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
}
public static checkForUpdates() {
private static async isAutoInstallEnabled() {
if (process.platform === "darwin") return false;
if (process.platform === "win32") {
return process.env.PORTABLE_EXECUTABLE_FILE == null;
}
if (process.platform === "linux") {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
return userPreferences?.enableAutoInstall === true;
}
return false;
}
public static async checkForUpdates() {
autoUpdater
.once("update-available", (info: UpdateInfo) => {
this.sendEvent({ type: "update-available", info });
@@ -39,6 +59,8 @@ export class UpdateManager {
}
});
const isAutoInstallAvailable = await this.isAutoInstallEnabled();
if (app.isPackaged) {
autoUpdater.autoDownload = isAutoInstallAvailable;
autoUpdater.checkForUpdates().then((result) => {
@@ -52,7 +74,7 @@ export class UpdateManager {
}
public static checkForUpdatePeriodically() {
if (this.checkTick % 2000 == 0) {
if (this.checkTick % ticksToUpdate == 0) {
this.checkForUpdates();
}
this.checkTick++;

View File

@@ -15,6 +15,7 @@ import type {
SeedingStatus,
GameAchievement,
Theme,
FriendRequestSync,
} from "@types";
import type { AuthPage, CatalogueCategory } from "@shared";
import type { AxiosProgressEvent } from "axios";
@@ -306,6 +307,15 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("processProfileImage", imagePath),
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
onSyncFriendRequests: (cb: (friendRequests: FriendRequestSync) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
friendRequests: FriendRequestSync
) => cb(friendRequests);
ipcRenderer.on("on-sync-friend-requests", listener);
return () =>
ipcRenderer.removeListener("on-sync-friend-requests", listener);
},
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
ipcRenderer.invoke("updateFriendRequest", userId, action),
sendFriendRequest: (userId: string) =>

View File

@@ -20,6 +20,7 @@ import {
setUserDetails,
setProfileBackground,
setGameRunning,
setFriendRequestCount,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
@@ -51,7 +52,6 @@ export function App() {
isFriendsModalVisible,
friendRequetsModalTab,
friendModalUserId,
syncFriendRequests,
hideFriendsModal,
fetchUserDetails,
updateUserDetails,
@@ -123,7 +123,7 @@ export function App() {
.then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
window.electron.syncFriendRequests();
}
})
.finally(() => {
@@ -134,23 +134,27 @@ export function App() {
$script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`;
document.head.appendChild($script);
});
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
}, [fetchUserDetails, updateUserDetails, dispatch]);
const onSignIn = useCallback(() => {
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
window.electron.syncFriendRequests();
showSuccessToast(t("successfully_signed_in"));
}
});
}, [
fetchUserDetails,
syncFriendRequests,
t,
showSuccessToast,
updateUserDetails,
]);
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
useEffect(() => {
const unsubscribe = window.electron.onSyncFriendRequests((result) => {
dispatch(setFriendRequestCount(result.friendRequestCount));
});
return () => {
unsubscribe();
};
}, [dispatch]);
useEffect(() => {
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {

View File

@@ -1,7 +1,7 @@
import { useNavigate } from "react-router-dom";
import { PeopleIcon } from "@primer/octicons-react";
import { useAppSelector, useUserDetails } from "@renderer/hooks";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
@@ -9,19 +9,13 @@ import { Avatar } from "../avatar/avatar";
import { AuthPage } from "@shared";
import "./sidebar-profile.scss";
const LONG_POLLING_INTERVAL = 120_000;
export function SidebarProfile() {
const navigate = useNavigate();
const { t } = useTranslation("sidebar");
const {
userDetails,
friendRequestCount,
showFriendsModal,
syncFriendRequests,
} = useUserDetails();
const { userDetails, friendRequestCount, showFriendsModal } =
useUserDetails();
const { gameRunning } = useAppSelector((state) => state.gameRunning);
@@ -34,16 +28,6 @@ export function SidebarProfile() {
navigate(`/profile/${userDetails.id}`);
};
useEffect(() => {
const pollingInterval = setInterval(() => {
syncFriendRequests();
}, LONG_POLLING_INTERVAL);
return () => {
clearInterval(pollingInterval);
};
}, [syncFriendRequests]);
const friendsButton = useMemo(() => {
if (!userDetails) return null;

View File

@@ -280,7 +280,10 @@ declare global {
path: string
) => Promise<{ imagePath: string; mimeType: string }>;
getFriendRequests: () => Promise<FriendRequest[]>;
syncFriendRequests: () => Promise<FriendRequestSync>;
syncFriendRequests: () => Promise<void>;
onSyncFriendRequests: (
cb: (friendRequests: FriendRequestSync) => void
) => () => Electron.IpcRenderer;
updateFriendRequest: (
userId: string,
action: FriendRequestAction

View File

@@ -6,7 +6,6 @@ import {
setFriendRequests,
setFriendsModalVisible,
setFriendsModalHidden,
setFriendRequestCount,
} from "@renderer/features";
import type {
FriendRequestAction,
@@ -88,24 +87,15 @@ export function useUserDetails() {
]
);
const syncFriendRequests = useCallback(async () => {
return window.electron
.syncFriendRequests()
.then((sync) => {
dispatch(setFriendRequestCount(sync.friendRequestCount));
})
.catch(() => {});
}, [dispatch]);
const fetchFriendRequests = useCallback(async () => {
return window.electron
.getFriendRequests()
.then((friendRequests) => {
syncFriendRequests();
window.electron.syncFriendRequests();
dispatch(setFriendRequests(friendRequests));
})
.catch(() => {});
}, [dispatch, syncFriendRequests]);
}, [dispatch]);
const showFriendsModal = useCallback(
(initialTab: UserFriendModalTab, userId: string) => {
@@ -167,7 +157,6 @@ export function useUserDetails() {
patchUser,
sendFriendRequest,
fetchFriendRequests,
syncFriendRequests,
updateFriendRequestState,
blockUser,
unblockUser,

View File

@@ -20,6 +20,7 @@ export function SettingsBehavior() {
runAtStartup: false,
startMinimized: false,
disableNsfwAlert: false,
enableAutoInstall: false,
seedAfterDownloadComplete: false,
showHiddenAchievementsDescription: false,
});
@@ -34,6 +35,7 @@ export function SettingsBehavior() {
runAtStartup: userPreferences.runAtStartup ?? false,
startMinimized: userPreferences.startMinimized ?? false,
disableNsfwAlert: userPreferences.disableNsfwAlert ?? false,
enableAutoInstall: userPreferences.enableAutoInstall ?? false,
seedAfterDownloadComplete:
userPreferences.seedAfterDownloadComplete ?? false,
showHiddenAchievementsDescription:
@@ -99,6 +101,16 @@ export function SettingsBehavior() {
</div>
)}
{window.electron.platform === "linux" && (
<CheckboxField
label={t("enable_auto_install")}
checked={form.enableAutoInstall}
onChange={() =>
handleChange({ enableAutoInstall: !form.enableAutoInstall })
}
/>
)}
<CheckboxField
label={t("disable_nsfw_alert")}
checked={form.disableNsfwAlert}

View File

@@ -54,8 +54,8 @@
}
}
&__remove_all_sources_button {
&__buttons-container {
display: flex;
justify-content: flex-end;
gap: globals.$spacing-unit;
}
}

View File

@@ -13,7 +13,7 @@ import {
NoEntryIcon,
PlusCircleIcon,
SyncIcon,
XIcon,
TrashIcon,
} from "@primer/octicons-react";
import { AddDownloadSourceModal } from "./add-download-source-modal";
import { useAppDispatch, useRepacks, useToast } from "@renderer/hooks";
@@ -173,7 +173,8 @@ export function SettingsDownloadSources() {
disabled={
!downloadSources.length ||
isSyncingDownloadSources ||
isRemovingDownloadSource
isRemovingDownloadSource ||
isFetchingSources
}
onClick={syncDownloadSources}
>
@@ -181,30 +182,37 @@ export function SettingsDownloadSources() {
{t("sync_download_sources")}
</Button>
<Button
type="button"
theme="outline"
onClick={() => setShowAddDownloadSourceModal(true)}
disabled={isSyncingDownloadSources}
>
<PlusCircleIcon />
{t("add_download_source")}
</Button>
</div>
{!isFetchingSources && downloadSources.length >= 2 && (
<div className="settings-download-sources__remove_all_sources_button">
<div className="settings-download-sources__buttons-container">
<Button
type="button"
theme="danger"
onClick={() => setShowConfirmationDeleteAllSourcesModal(true)}
disabled={isRemovingDownloadSource}
disabled={
isRemovingDownloadSource ||
isSyncingDownloadSources ||
!downloadSources.length ||
isFetchingSources
}
>
<XIcon />
<TrashIcon />
{t("button_delete_all_sources")}
</Button>
<Button
type="button"
theme="outline"
onClick={() => setShowAddDownloadSourceModal(true)}
disabled={
isSyncingDownloadSources ||
isFetchingSources ||
isRemovingDownloadSource
}
>
<PlusCircleIcon />
{t("add_download_source")}
</Button>
</div>
)}
</div>
<ul className="settings-download-sources__list">
{downloadSources.map((downloadSource) => (

View File

@@ -32,6 +32,7 @@ export function SettingsGeneral() {
downloadNotificationsEnabled: false,
repackUpdatesNotificationsEnabled: false,
achievementNotificationsEnabled: false,
friendRequestNotificationsEnabled: false,
language: "",
customStyles: window.localStorage.getItem("customStyles") || "",
@@ -82,6 +83,8 @@ export function SettingsGeneral() {
userPreferences.repackUpdatesNotificationsEnabled ?? false,
achievementNotificationsEnabled:
userPreferences.achievementNotificationsEnabled ?? false,
friendRequestNotificationsEnabled:
userPreferences.friendRequestNotificationsEnabled ?? false,
language: language ?? "en",
}));
}
@@ -171,6 +174,17 @@ export function SettingsGeneral() {
})
}
/>
<CheckboxField
label={t("enable_friend_request_notifications")}
checked={form.friendRequestNotificationsEnabled}
onChange={() =>
handleChange({
friendRequestNotificationsEnabled:
!form.friendRequestNotificationsEnabled,
})
}
/>
</div>
);
}

View File

@@ -7,12 +7,22 @@
gap: globals.$spacing-unit;
}
&__description {
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__submit-button {
align-self: flex-end;
width: 100%;
}
&__description-container {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
margin-bottom: calc(globals.$spacing-unit * 2);
align-items: flex-start;
}
&__create-account {
display: flex;
align-items: center;
gap: globals.$spacing-unit;
}
}

View File

@@ -7,7 +7,14 @@ import "./settings-real-debrid.scss";
import { useAppSelector, useToast } from "@renderer/hooks";
import { settingsContext } from "@renderer/context";
import { LinkExternalIcon } from "@primer/octicons-react";
const realDebridReferralId = import.meta.env
.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID;
const REAL_DEBRID_URL = realDebridReferralId
? `https://real-debrid.com/?id=${realDebridReferralId}`
: "https://real-debrid.com";
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
export function SettingsRealDebrid() {
@@ -74,24 +81,43 @@ export function SettingsRealDebrid() {
}
};
const toggleRealDebrid = () => {
const updatedValue = !form.useRealDebrid;
setForm((prev) => ({
...prev,
useRealDebrid: updatedValue,
}));
if (!updatedValue) {
updateUserPreferences({
realDebridApiToken: null,
});
}
};
const isButtonDisabled =
(form.useRealDebrid && !form.realDebridApiToken) || isLoading;
return (
<form className="settings-real-debrid__form" onSubmit={handleFormSubmit}>
<p className="settings-real-debrid__description">
{t("real_debrid_description")}
</p>
<div className="settings-real-debrid__description-container">
<p className="settings-real-debrid__description">
{t("real_debrid_description")}
</p>
<Link
to={REAL_DEBRID_URL}
className="settings-real-debrid__create-account"
>
<LinkExternalIcon />
{t("create_real_debrid_account")}
</Link>
</div>
<CheckboxField
label={t("enable_real_debrid")}
checked={form.useRealDebrid}
onChange={() => {
setForm((prev) => ({
...prev,
useRealDebrid: !form.useRealDebrid,
}));
}}
onChange={toggleRealDebrid}
/>
{form.useRealDebrid && (

View File

@@ -7,12 +7,22 @@
gap: globals.$spacing-unit;
}
&__description {
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__submit-button {
align-self: flex-end;
width: 100%;
}
&__description-container {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
margin-bottom: calc(globals.$spacing-unit * 2);
align-items: flex-start;
}
&__create-account {
display: flex;
align-items: center;
gap: globals.$spacing-unit;
}
}

View File

@@ -7,7 +7,13 @@ import "./settings-torbox.scss";
import { useAppSelector, useToast } from "@renderer/hooks";
import { settingsContext } from "@renderer/context";
import { LinkExternalIcon } from "@primer/octicons-react";
const torBoxReferralCode = import.meta.env.RENDERER_VITE_TORBOX_REFERRAL_CODE;
const TORBOX_URL = torBoxReferralCode
? `https://torbox.app/subscription?referral=${torBoxReferralCode}`
: "https://torbox.app";
const TORBOX_API_TOKEN_URL = "https://torbox.app/settings";
export function SettingsTorbox() {
@@ -69,19 +75,37 @@ export function SettingsTorbox() {
const isButtonDisabled =
(form.useTorBox && !form.torBoxApiToken) || isLoading;
const toggleTorBox = () => {
const updatedValue = !form.useTorBox;
setForm((prev) => ({
...prev,
useTorBox: updatedValue,
}));
if (!updatedValue) {
updateUserPreferences({
torBoxApiToken: null,
});
}
};
return (
<form className="settings-torbox__form" onSubmit={handleFormSubmit}>
<p className="settings-torbox__description">{t("torbox_description")}</p>
<div className="settings-torbox__description-container">
<p className="settings-torbox__description">
{t("torbox_description")}
</p>
<Link to={TORBOX_URL} className="settings-torbox__create-account">
<LinkExternalIcon />
{t("create_torbox_account")}
</Link>
</div>
<CheckboxField
label={t("enable_torbox")}
checked={form.useTorBox}
onChange={() =>
setForm((prev) => ({
...prev,
useTorBox: !form.useTorBox,
}))
}
onChange={toggleTorBox}
/>
{form.useTorBox && (

View File

@@ -4,6 +4,8 @@
interface ImportMetaEnv {
readonly RENDERER_VITE_EXTERNAL_RESOURCES_URL: string;
readonly RENDERER_VITE_SENTRY_DSN: string;
readonly RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: string;
readonly RENDERER_VITE_TORBOX_REFERRAL_CODE: string;
}
interface ImportMeta {

View File

@@ -76,11 +76,13 @@ export interface UserPreferences {
runAtStartup?: boolean;
startMinimized?: boolean;
disableNsfwAlert?: boolean;
enableAutoInstall?: boolean;
seedAfterDownloadComplete?: boolean;
showHiddenAchievementsDescription?: boolean;
downloadNotificationsEnabled?: boolean;
repackUpdatesNotificationsEnabled?: boolean;
achievementNotificationsEnabled?: boolean;
friendRequestNotificationsEnabled?: boolean;
}
export interface ScreenState {