diff --git a/package.json b/package.json index 7122e3df..fb794294 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydralauncher", - "version": "2.1.1", + "version": "2.1.3", "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index f507341a..54e63a3b 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -59,6 +59,7 @@ import "./profile/update-friend-request"; import "./profile/update-profile"; import "./profile/process-profile-image"; import "./profile/send-friend-request"; +import "./profile/sync-friend-requests"; import { isPortableVersion } from "@main/helpers"; ipcMain.handle("ping", () => "pong"); diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts index 5154da8d..1eeecbf9 100644 --- a/src/main/events/profile/get-me.ts +++ b/src/main/events/profile/get-me.ts @@ -1,32 +1,14 @@ import { registerEvent } from "../register-event"; import * as Sentry from "@sentry/electron/main"; -import { HydraApi, logger } from "@main/services"; -import { UserProfile } from "@types"; +import { HydraApi } from "@main/services"; +import { ProfileVisibility, UserDetails } from "@types"; import { userAuthRepository } from "@main/repository"; -import { steamUrlBuilder, 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; - } -}; +import { UserNotLoggedInError } from "@shared"; const getMe = async ( _event: Electron.IpcMainInvokeEvent -): Promise => { - return HydraApi.get(`/profile/me`) +): Promise => { + return HydraApi.get(`/profile/me`) .then(async (me) => { userAuthRepository.upsert( { @@ -38,17 +20,6 @@ const getMe = async ( ["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 }); return me; @@ -61,7 +32,13 @@ const getMe = async ( const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } }); if (loggedUser) { - return { ...loggedUser, id: loggedUser.userId }; + return { + ...loggedUser, + id: loggedUser.userId, + username: "", + bio: "", + profileVisibility: "PUBLIC" as ProfileVisibility, + }; } return null; diff --git a/src/main/events/profile/sync-friend-requests.ts b/src/main/events/profile/sync-friend-requests.ts new file mode 100644 index 00000000..4b89701a --- /dev/null +++ b/src/main/events/profile/sync-friend-requests.ts @@ -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(`/profile/friend-requests/sync`); +}; + +registerEvent("syncFriendRequests", syncFriendRequests); diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index a24e0ffb..253ab159 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -2,7 +2,7 @@ import { registerEvent } from "../register-event"; import type { StartGameDownloadPayload } from "@types"; import { getFileBase64 } from "@main/helpers"; -import { DownloadManager } from "@main/services"; +import { DownloadManager, HydraApi, logger } from "@main/services"; import { Not } from "typeorm"; import { steamGamesWorker } from "@main/workers"; @@ -101,6 +101,17 @@ const startGameDownload = async ( 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.startDownload(updatedGame!); diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts index 9d045e3a..d7206677 100644 --- a/src/main/knex-client.ts +++ b/src/main/knex-client.ts @@ -3,12 +3,19 @@ import { databasePath } from "./constants"; import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3"; import { RepackUris } from "./migrations/20240830143906_RepackUris"; 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 }; class MigrationSource implements Knex.MigrationSource { getMigrations(): Promise { - return Promise.resolve([Hydra2_0_3, RepackUris, UpdateUserLanguage]); + return Promise.resolve([ + Hydra2_0_3, + RepackUris, + UpdateUserLanguage, + EnsureRepackUris, + ]); } getMigrationName(migration: HydraMigration): string { return migration.name; @@ -19,6 +26,7 @@ class MigrationSource implements Knex.MigrationSource { } export const knexClient = knex({ + debug: !app.isPackaged, client: "better-sqlite3", connection: { filename: databasePath, diff --git a/src/main/migrations/20240830143906_RepackUris.ts b/src/main/migrations/20240830143906_RepackUris.ts index 0785d50d..18bb9a59 100644 --- a/src/main/migrations/20240830143906_RepackUris.ts +++ b/src/main/migrations/20240830143906_RepackUris.ts @@ -4,55 +4,15 @@ import type { Knex } from "knex"; export const RepackUris: HydraMigration = { name: "RepackUris", up: async (knex: Knex) => { - await knex.schema.createTable("temporary_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"); + await knex.schema.alterTable("repack", (table) => { 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) => { - await knex.schema.renameTable("repack", "temporary_repack"); - await knex.schema.createTable("repack", (table) => { - table.increments("id").primary(); - table.text("title").notNullable().unique(); - table.text("magnet").notNullable().unique(); + await knex.schema.alterTable("repack", (table) => { table.integer("page"); - 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.dropColumn("uris"); }); - 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"); }, }; diff --git a/src/main/migrations/20240915035339_ensure_repack_uris.ts b/src/main/migrations/20240915035339_ensure_repack_uris.ts new file mode 100644 index 00000000..64fbcd2e --- /dev/null +++ b/src/main/migrations/20240915035339_ensure_repack_uris.ts @@ -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) => {}, +}; diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 3a167851..90149038 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -6,6 +6,7 @@ import { uploadGamesBatch } from "./library-sync"; import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id"; import { logger } from "./logger"; import { UserNotLoggedInError } from "@shared"; +import { omit } from "lodash-es"; interface HydraApiOptions { needsAuth: boolean; @@ -96,11 +97,14 @@ export class HydraApi { this.instance.interceptors.response.use( (response) => { logger.log(" ---- RESPONSE -----"); + const data = Array.isArray(response.data) + ? response.data + : omit(response.data, ["username", "accessToken", "refreshToken"]); logger.log( response.status, response.config.method, response.config.url, - response.data + data ); return response; }, @@ -166,7 +170,10 @@ export class HydraApi { this.userAuth.authToken = accessToken; this.userAuth.expirationTimestamp = tokenExpirationTimestamp; - logger.log("Token refreshed", this.userAuth); + logger.log( + "Token refreshed. New expiration:", + this.userAuth.expirationTimestamp + ); userAuthRepository.upsert( { diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts index 396ddbdd..6c701c9a 100644 --- a/src/main/services/library-sync/create-game.ts +++ b/src/main/services/library-sync/create-game.ts @@ -1,20 +1,8 @@ import { Game } from "@main/entity"; import { HydraApi } from "../hydra-api"; import { gameRepository } from "@main/repository"; -import { logger } from "../logger"; 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`, { objectId: game.objectID, playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds), diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 64b14f4d..2a194bf2 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -119,7 +119,7 @@ const onCloseGame = (game: Game) => { if (game.remoteId) { updateGamePlaytime( game, - performance.now() - gamePlaytime.firstTick, + performance.now() - gamePlaytime.lastSyncTick, game.lastTimePlayed! ).catch(() => {}); } else { diff --git a/src/preload/index.ts b/src/preload/index.ts index 8d56073c..0f135b99 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -150,6 +150,7 @@ contextBridge.exposeInMainWorld("electron", { processProfileImage: (imagePath: string) => ipcRenderer.invoke("processProfileImage", imagePath), getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"), + syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"), updateFriendRequest: (userId: string, action: FriendRequestAction) => ipcRenderer.invoke("updateFriendRequest", userId, action), sendFriendRequest: (userId: string) => diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 2b9ac187..5b9e44ca 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -43,7 +43,7 @@ export function App() { isFriendsModalVisible, friendRequetsModalTab, friendModalUserId, - fetchFriendRequests, + syncFriendRequests, hideFriendsModal, } = useUserDetails(); @@ -105,22 +105,22 @@ export function App() { fetchUserDetails().then((response) => { if (response) { updateUserDetails(response); - fetchFriendRequests(); + syncFriendRequests(); } }); - }, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]); + }, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]); const onSignIn = useCallback(() => { fetchUserDetails().then((response) => { if (response) { updateUserDetails(response); - fetchFriendRequests(); + syncFriendRequests(); showSuccessToast(t("successfully_signed_in")); } }); }, [ fetchUserDetails, - fetchFriendRequests, + syncFriendRequests, t, showSuccessToast, updateUserDetails, diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index f9cd1b43..52223653 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -15,15 +15,15 @@ export function SidebarProfile() { const { t } = useTranslation("sidebar"); - const { userDetails, friendRequests, showFriendsModal, fetchFriendRequests } = - useUserDetails(); + const { + userDetails, + friendRequestCount, + showFriendsModal, + syncFriendRequests, + } = useUserDetails(); const { gameRunning } = useAppSelector((state) => state.gameRunning); - const receivedRequests = useMemo(() => { - return friendRequests.filter((request) => request.type === "RECEIVED"); - }, [friendRequests]); - const handleProfileClick = () => { if (userDetails === null) { window.electron.openAuthWindow(); @@ -35,7 +35,7 @@ export function SidebarProfile() { useEffect(() => { pollingInterval.current = setInterval(() => { - fetchFriendRequests(); + syncFriendRequests(); }, LONG_POLLING_INTERVAL); return () => { @@ -43,7 +43,7 @@ export function SidebarProfile() { clearInterval(pollingInterval.current); } }; - }, [fetchFriendRequests]); + }, [syncFriendRequests]); const friendsButton = useMemo(() => { if (!userDetails) return null; @@ -57,16 +57,16 @@ export function SidebarProfile() { } title={t("friends")} > - {receivedRequests.length > 0 && ( + {friendRequestCount > 0 && ( - {receivedRequests.length > 99 ? "99+" : receivedRequests.length} + {friendRequestCount > 99 ? "99+" : friendRequestCount} )} ); - }, [userDetails, t, receivedRequests, showFriendsModal]); + }, [userDetails, t, friendRequestCount, showFriendsModal]); return (
@@ -100,6 +100,7 @@ export function SidebarProfile() { textOverflow: "ellipsis", whiteSpace: "nowrap", width: "100%", + textAlign: "left", }} > {gameRunning.title} diff --git a/src/renderer/src/components/sidebar/sidebar.css.ts b/src/renderer/src/components/sidebar/sidebar.css.ts index 10faf795..bfdb4eea 100644 --- a/src/renderer/src/components/sidebar/sidebar.css.ts +++ b/src/renderer/src/components/sidebar/sidebar.css.ts @@ -26,7 +26,7 @@ export const sidebar = recipe({ paddingTop: `${SPACING_UNIT * 6}px`, }, false: { - paddingTop: `${SPACING_UNIT * 2}px`, + paddingTop: `${SPACING_UNIT}px`, }, }, }, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 8e341ed8..70b77eec 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -23,6 +23,8 @@ import type { GameStats, TrendingGame, UserStats, + UserDetails, + FriendRequestSync, } from "@types"; import type { DiskSpace } from "check-disk-space"; @@ -153,7 +155,7 @@ declare global { ) => Promise; /* Profile */ - getMe: () => Promise; + getMe: () => Promise; undoFriendship: (userId: string) => Promise; updateProfile: ( updateProfile: UpdateProfileRequest @@ -163,6 +165,7 @@ declare global { path: string ) => Promise<{ imagePath: string; mimeType: string }>; getFriendRequests: () => Promise; + syncFriendRequests: () => Promise; updateFriendRequest: ( userId: string, action: FriendRequestAction diff --git a/src/renderer/src/features/user-details-slice.ts b/src/renderer/src/features/user-details-slice.ts index 00542020..8994f180 100644 --- a/src/renderer/src/features/user-details-slice.ts +++ b/src/renderer/src/features/user-details-slice.ts @@ -1,11 +1,12 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; 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 { - userDetails: UserProfile | null; + userDetails: UserDetails | null; profileBackground: null | string; friendRequests: FriendRequest[]; + friendRequestCount: number; isFriendsModalVisible: boolean; friendRequetsModalTab: UserFriendModalTab | null; friendModalUserId: string; @@ -15,6 +16,7 @@ const initialState: UserDetailsState = { userDetails: null, profileBackground: null, friendRequests: [], + friendRequestCount: 0, isFriendsModalVisible: false, friendRequetsModalTab: null, friendModalUserId: "", @@ -24,7 +26,7 @@ export const userDetailsSlice = createSlice({ name: "user-details", initialState, reducers: { - setUserDetails: (state, action: PayloadAction) => { + setUserDetails: (state, action: PayloadAction) => { state.userDetails = action.payload; }, setProfileBackground: (state, action: PayloadAction) => { @@ -33,6 +35,9 @@ export const userDetailsSlice = createSlice({ setFriendRequests: (state, action: PayloadAction) => { state.friendRequests = action.payload; }, + setFriendRequestCount: (state, action: PayloadAction) => { + state.friendRequestCount = action.payload; + }, setFriendsModalVisible: ( state, action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }> @@ -52,6 +57,7 @@ export const { setUserDetails, setProfileBackground, setFriendRequests, + setFriendRequestCount, setFriendsModalVisible, setFriendsModalHidden, } = userDetailsSlice.actions; diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index a826b008..d92038aa 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -6,11 +6,12 @@ import { setFriendRequests, setFriendsModalVisible, setFriendsModalHidden, + setFriendRequestCount, } from "@renderer/features"; import type { FriendRequestAction, UpdateProfileRequest, - UserProfile, + UserDetails, } from "@types"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; @@ -21,6 +22,7 @@ export function useUserDetails() { userDetails, profileBackground, friendRequests, + friendRequestCount, isFriendsModalVisible, friendModalUserId, friendRequetsModalTab, @@ -40,7 +42,7 @@ export function useUserDetails() { }, [clearUserDetails]); const updateUserDetails = useCallback( - async (userDetails: UserProfile) => { + async (userDetails: UserDetails) => { dispatch(setUserDetails(userDetails)); if (userDetails.profileImageUrl) { @@ -83,7 +85,10 @@ export function useUserDetails() { const patchUser = useCallback( async (values: UpdateProfileRequest) => { const response = await window.electron.updateProfile(values); - return updateUserDetails(response); + return updateUserDetails({ + ...response, + username: userDetails?.username || "", + }); }, [updateUserDetails] ); @@ -92,11 +97,21 @@ export function useUserDetails() { return window.electron .getFriendRequests() .then((friendRequests) => { + syncFriendRequests(); dispatch(setFriendRequests(friendRequests)); }) .catch(() => {}); }, [dispatch]); + const syncFriendRequests = useCallback(async () => { + return window.electron + .syncFriendRequests() + .then((sync) => { + dispatch(setFriendRequestCount(sync.friendRequestCount)); + }) + .catch(() => {}); + }, [dispatch]); + const showFriendsModal = useCallback( (initialTab: UserFriendModalTab, userId: string) => { dispatch(setFriendsModalVisible({ initialTab, userId })); @@ -140,6 +155,7 @@ export function useUserDetails() { userDetails, profileBackground, friendRequests, + friendRequestCount, friendRequetsModalTab, isFriendsModalVisible, friendModalUserId, @@ -152,6 +168,7 @@ export function useUserDetails() { patchUser, sendFriendRequest, fetchFriendRequests, + syncFriendRequests, updateFriendRequestState, blockUser, unblockUser, diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index a301110a..35982b19 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -160,12 +160,15 @@ export function DownloadSettingsModal({ ))}
- {selectedDownloader && selectedDownloader !== Downloader.Torrent && ( -

- {t("warning")}{" "} - {t("hydra_needs_to_remain_open")} -

- )} + {selectedDownloader != null && + selectedDownloader !== Downloader.Torrent && ( +

+ + {t("warning")} + {" "} + {t("hydra_needs_to_remain_open")} +

+ )}
({ isLoading: true, data: null }); @@ -17,27 +17,26 @@ export function Sidebar() { const [activeRequirement, setActiveRequirement] = useState("minimum"); - const { gameTitle, shopDetails, objectID, stats } = - useContext(gameDetailsContext); + const { gameTitle, shopDetails, stats } = useContext(gameDetailsContext); const { t } = useTranslation("game_details"); const { numberFormatter } = useFormat(); - useEffect(() => { - if (objectID) { - setHowLongToBeat({ isLoading: true, data: null }); + // useEffect(() => { + // if (objectID) { + // setHowLongToBeat({ isLoading: true, data: null }); - window.electron - .getHowLongToBeat(objectID, "steam", gameTitle) - .then((howLongToBeat) => { - setHowLongToBeat({ isLoading: false, data: howLongToBeat }); - }) - .catch(() => { - setHowLongToBeat({ isLoading: false, data: null }); - }); - } - }, [objectID, gameTitle]); + // window.electron + // .getHowLongToBeat(objectID, "steam", gameTitle) + // .then((howLongToBeat) => { + // setHowLongToBeat({ isLoading: false, data: howLongToBeat }); + // }) + // .catch(() => { + // setHowLongToBeat({ isLoading: false, data: null }); + // }); + // } + // }, [objectID, gameTitle]); return (