From 524bfe91efaca1186b91d2c323cf2345dc7d7e52 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 17 Jun 2024 02:13:43 +0100 Subject: [PATCH 1/7] feat: adding background to profile --- src/renderer/src/pages/user/user-content.tsx | 23 ++++++++++++++++---- src/renderer/src/pages/user/user.css.tsx | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx index 6b6eb39e..2defbad1 100644 --- a/src/renderer/src/pages/user/user-content.tsx +++ b/src/renderer/src/pages/user/user-content.tsx @@ -1,5 +1,8 @@ import { UserGame, UserProfile } from "@types"; import cn from "classnames"; +import { average } from "color.js"; +import Color from "color"; + import * as styles from "./user.css"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { useMemo } from "react"; @@ -58,18 +61,30 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => { navigate("/"); }; + const handleAvatarLoad = async () => { + console.log(userProfile.profileImageUrl); + const output = await average(userProfile.profileImageUrl!, { + amount: 1, + format: "hex", + }); + + const backgroundColor = output + ? (new Color(output).darken(0.7).toString() as string) + : ""; + + console.log(backgroundColor); + }; + return ( <> -
+
{userProfile.profileImageUrl ? ( {userProfile.displayName} ) : ( diff --git a/src/renderer/src/pages/user/user.css.tsx b/src/renderer/src/pages/user/user.css.tsx index 2bae1b5a..d6494bbd 100644 --- a/src/renderer/src/pages/user/user.css.tsx +++ b/src/renderer/src/pages/user/user.css.tsx @@ -12,6 +12,7 @@ export const wrapper = style({ export const profileContentBox = style({ display: "flex", gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`, + padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 2}px`, alignItems: "center", borderRadius: "4px", border: `solid 1px ${vars.color.border}`, From 2ae10decf7b336b49d57918761b5c2c00489d0ab Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 17 Jun 2024 23:04:10 +0100 Subject: [PATCH 2/7] feat: adding average color to profile hero --- src/renderer/src/helpers.ts | 5 ++++ src/renderer/src/pages/user/user-content.tsx | 29 ++++++++++++------- .../pages/user/{user.css.tsx => user.css.ts} | 8 ++++- 3 files changed, 30 insertions(+), 12 deletions(-) rename src/renderer/src/pages/user/{user.css.tsx => user.css.ts} (91%) diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index 9d5a4700..19d1969c 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -1,5 +1,7 @@ import type { GameShop } from "@types"; +import Color from "color"; + export const steamUrlBuilder = { library: (objectID: string) => `https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`, @@ -40,3 +42,6 @@ export const buildGameDetailsPath = ( const searchParams = new URLSearchParams({ title: game.title, ...params }); return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`; }; + +export const darkenColor = (color: string, amount: number) => + new Color(color).darken(amount).toString(); diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx index 2defbad1..c120f9d1 100644 --- a/src/renderer/src/pages/user/user-content.tsx +++ b/src/renderer/src/pages/user/user-content.tsx @@ -1,16 +1,15 @@ import { UserGame, UserProfile } from "@types"; import cn from "classnames"; import { average } from "color.js"; -import Color from "color"; import * as styles from "./user.css"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; -import { useMemo } from "react"; +import { useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import { useDate } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { buildGameDetailsPath } from "@renderer/helpers"; +import { buildGameDetailsPath, darkenColor } from "@renderer/helpers"; import { PersonIcon } from "@primer/octicons-react"; import { Button } from "@renderer/components"; import { useUserAuth } from "@renderer/hooks/use-user-auth"; @@ -25,6 +24,10 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => { const { userAuth, signOut } = useUserAuth(); + const profileImageRef = useRef(null); + + const [backgroundColors, setBackgroundColors] = useState([]); + const navigate = useNavigate(); const numberFormatter = useMemo(() => { @@ -62,25 +65,29 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => { }; const handleAvatarLoad = async () => { - console.log(userProfile.profileImageUrl); - const output = await average(userProfile.profileImageUrl!, { + const output = await average(profileImageRef.current!, { amount: 1, format: "hex", }); - const backgroundColor = output - ? (new Color(output).darken(0.7).toString() as string) - : ""; - - console.log(backgroundColor); + setBackgroundColors([ + darkenColor(output as string, 0.6), + darkenColor(output as string, 0.7), + ]); }; return ( <> -
+
{userProfile.profileImageUrl ? ( {userProfile.displayName} Date: Tue, 18 Jun 2024 00:09:26 +0100 Subject: [PATCH 3/7] feat: adding profile picture background --- src/renderer/src/app.tsx | 16 ++++-- .../components/sidebar/sidebar-profile.tsx | 23 +++++--- .../src/components/sidebar/sidebar.css.ts | 2 + src/renderer/src/features/index.ts | 2 +- src/renderer/src/features/user-auth-slice.ts | 22 ------- .../src/features/user-details-slice.ts | 32 +++++++++++ src/renderer/src/hooks/index.ts | 1 + src/renderer/src/hooks/use-user-auth.ts | 37 ------------ src/renderer/src/hooks/use-user-details.ts | 57 +++++++++++++++++++ src/renderer/src/pages/user/user-content.tsx | 41 +++++-------- src/renderer/src/store.ts | 4 +- src/types/index.ts | 2 +- 12 files changed, 136 insertions(+), 103 deletions(-) delete mode 100644 src/renderer/src/features/user-auth-slice.ts create mode 100644 src/renderer/src/features/user-details-slice.ts delete mode 100644 src/renderer/src/hooks/use-user-auth.ts create mode 100644 src/renderer/src/hooks/use-user-details.ts diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index cf510df0..792a2df2 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -7,6 +7,7 @@ import { useAppSelector, useDownload, useLibrary, + useUserDetails, } from "@renderer/hooks"; import * as styles from "./app.css"; @@ -19,7 +20,6 @@ import { toggleDraggingDisabled, closeToast, } from "@renderer/features"; -import { useUserAuth } from "./hooks/use-user-auth"; export interface AppProps { children: React.ReactNode; @@ -31,7 +31,7 @@ export function App() { const { clearDownload, setLastPacket } = useDownload(); - const { updateUserAuth, clearUserAuth } = useUserAuth(); + const { updateUser, clearUser } = useUserDetails(); const dispatch = useAppDispatch(); @@ -39,9 +39,11 @@ export function App() { const location = useLocation(); const search = useAppSelector((state) => state.search.value); + const draggingDisabled = useAppSelector( (state) => state.window.draggingDisabled ); + const toast = useAppSelector((state) => state.toast); useEffect(() => { @@ -70,20 +72,24 @@ export function App() { }; }, [clearDownload, setLastPacket, updateLibrary]); + useEffect(() => { + updateUser(); + }, [updateUser]); + useEffect(() => { const listeners = [ window.electron.onSignIn(() => { - updateUserAuth(); + updateUser(); }), window.electron.onSignOut(() => { - clearUserAuth(); + clearUser(); }), ]; return () => { listeners.forEach((unsubscribe) => unsubscribe()); }; - }, [clearUserAuth, updateUserAuth]); + }, [updateUser, clearUser]); const handleSearch = useCallback( (query: string) => { diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index c92b3ec0..c81b24fb 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -1,24 +1,28 @@ import { useNavigate } from "react-router-dom"; import { PersonIcon } from "@primer/octicons-react"; import * as styles from "./sidebar.css"; -import { useUserAuth } from "@renderer/hooks/use-user-auth"; +import { useUserDetails } from "@renderer/hooks"; +import { useMemo } from "react"; export function SidebarProfile() { const navigate = useNavigate(); - const { userAuth, isLoading } = useUserAuth(); + const { userDetails, profileBackground } = useUserDetails(); const handleClickProfile = () => { - navigate(`/user/${userAuth!.id}`); + navigate(`/user/${userDetails!.id}`); }; const handleClickLogin = () => { window.electron.openExternal("https://auth.hydra.losbroxas.org"); }; - if (isLoading) return null; + const profileButtonBackground = useMemo(() => { + if (profileBackground) return profileBackground; + return undefined; + }, [profileBackground]); - if (userAuth == null) { + if (userDetails == null) { return ( <> diff --git a/src/renderer/src/components/sidebar/sidebar.css.ts b/src/renderer/src/components/sidebar/sidebar.css.ts index 6677bf63..5a96e87a 100644 --- a/src/renderer/src/components/sidebar/sidebar.css.ts +++ b/src/renderer/src/components/sidebar/sidebar.css.ts @@ -149,7 +149,9 @@ export const profileAvatar = style({ justifyContent: "center", alignItems: "center", backgroundColor: vars.color.background, + border: `solid 1px ${vars.color.border}`, position: "relative", + objectFit: "cover", }); export const profileButtonInformation = style({ diff --git a/src/renderer/src/features/index.ts b/src/renderer/src/features/index.ts index d7b75692..f3132520 100644 --- a/src/renderer/src/features/index.ts +++ b/src/renderer/src/features/index.ts @@ -4,4 +4,4 @@ export * from "./use-preferences-slice"; export * from "./download-slice"; export * from "./window-slice"; export * from "./toast-slice"; -export * from "./user-auth-slice"; +export * from "./user-details-slice"; diff --git a/src/renderer/src/features/user-auth-slice.ts b/src/renderer/src/features/user-auth-slice.ts deleted file mode 100644 index 0daf3407..00000000 --- a/src/renderer/src/features/user-auth-slice.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import type { UserAuth } from "@types"; - -export interface UserAuthState { - userAuth: UserAuth | null; -} - -const initialState: UserAuthState = { - userAuth: null, -}; - -export const userAuthSlice = createSlice({ - name: "user-auth", - initialState, - reducers: { - setUserAuth: (state, userAuth: PayloadAction) => { - state.userAuth = userAuth.payload; - }, - }, -}); - -export const { setUserAuth } = userAuthSlice.actions; diff --git a/src/renderer/src/features/user-details-slice.ts b/src/renderer/src/features/user-details-slice.ts new file mode 100644 index 00000000..af14ce56 --- /dev/null +++ b/src/renderer/src/features/user-details-slice.ts @@ -0,0 +1,32 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import type { UserDetails } from "@types"; + +export interface UserDetailsState { + userDetails: UserDetails | null; + profileBackground: null | string; +} + +const initialState: UserDetailsState = { + userDetails: null, + profileBackground: null, +}; + +export const userDetailsSlice = createSlice({ + name: "user-details", + initialState, + reducers: { + setUserDetails: (state, action: PayloadAction) => { + state.userDetails = action.payload; + }, + setProfileBackground: (state, action: PayloadAction) => { + state.profileBackground = action.payload; + }, + clearUserDetails: (state) => { + state.userDetails = null; + state.profileBackground = null; + }, + }, +}); + +export const { setUserDetails, setProfileBackground, clearUserDetails } = + userDetailsSlice.actions; diff --git a/src/renderer/src/hooks/index.ts b/src/renderer/src/hooks/index.ts index 563e3ff1..5bc287b8 100644 --- a/src/renderer/src/hooks/index.ts +++ b/src/renderer/src/hooks/index.ts @@ -3,3 +3,4 @@ export * from "./use-library"; export * from "./use-date"; export * from "./use-toast"; export * from "./redux"; +export * from "./use-user-details"; diff --git a/src/renderer/src/hooks/use-user-auth.ts b/src/renderer/src/hooks/use-user-auth.ts deleted file mode 100644 index ad376ce7..00000000 --- a/src/renderer/src/hooks/use-user-auth.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { useAppDispatch, useAppSelector } from "./redux"; -import { setUserAuth } from "@renderer/features"; - -export function useUserAuth() { - const dispatch = useAppDispatch(); - - const [isLoading, setIsLoading] = useState(true); - - const { userAuth } = useAppSelector((state) => state.userAuth); - - const signOut = useCallback(async () => { - dispatch(setUserAuth(null)); - return window.electron.signOut(); - }, [dispatch]); - - const updateUserAuth = useCallback(async () => { - setIsLoading(true); - - return window.electron - .getMe() - .then((userAuth) => dispatch(setUserAuth(userAuth))) - .finally(() => { - setIsLoading(false); - }); - }, [dispatch]); - - useEffect(() => { - updateUserAuth(); - }, [updateUserAuth]); - - const clearUserAuth = useCallback(async () => { - dispatch(setUserAuth(null)); - }, [dispatch]); - - return { userAuth, isLoading, updateUserAuth, signOut, clearUserAuth }; -} diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts new file mode 100644 index 00000000..fe4c0505 --- /dev/null +++ b/src/renderer/src/hooks/use-user-details.ts @@ -0,0 +1,57 @@ +import { useCallback } from "react"; +import { average } from "color.js"; + +import { useAppDispatch, useAppSelector } from "./redux"; +import { + clearUserDetails, + setProfileBackground, + setUserDetails, +} from "@renderer/features"; +import { darkenColor } from "@renderer/helpers"; + +export function useUserDetails() { + const dispatch = useAppDispatch(); + + const { userDetails, profileBackground } = useAppSelector( + (state) => state.userDetails + ); + + const clearUser = useCallback(async () => { + dispatch(clearUserDetails()); + }, [dispatch]); + + const signOut = useCallback(async () => { + clearUser(); + + return window.electron.signOut(); + }, [clearUser]); + + const updateUser = useCallback(async () => { + return window.electron.getMe().then(async (userDetails) => { + if (userDetails) { + dispatch(setUserDetails(userDetails)); + + if (userDetails.profileImageUrl) { + const output = await average(userDetails.profileImageUrl, { + amount: 1, + format: "hex", + }); + + dispatch( + setProfileBackground( + `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.7)})` + ) + ); + } + } + }); + }, [dispatch]); + + return { + userDetails, + updateUser, + signOut, + clearUser, + profileBackground, + }; +} diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx index c120f9d1..600f9128 100644 --- a/src/renderer/src/pages/user/user-content.tsx +++ b/src/renderer/src/pages/user/user-content.tsx @@ -1,32 +1,27 @@ import { UserGame, UserProfile } from "@types"; import cn from "classnames"; -import { average } from "color.js"; import * as styles from "./user.css"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; -import { useMemo, useRef, useState } from "react"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; -import { useDate } from "@renderer/hooks"; +import { useDate, useUserDetails } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { buildGameDetailsPath, darkenColor } from "@renderer/helpers"; +import { buildGameDetailsPath } from "@renderer/helpers"; import { PersonIcon } from "@primer/octicons-react"; import { Button } from "@renderer/components"; -import { useUserAuth } from "@renderer/hooks/use-user-auth"; const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120; + export interface ProfileContentProps { userProfile: UserProfile; } -export const UserContent = ({ userProfile }: ProfileContentProps) => { +export function UserContent({ userProfile }: ProfileContentProps) { const { t, i18n } = useTranslation("user_profile"); - const { userAuth, signOut } = useUserAuth(); - - const profileImageRef = useRef(null); - - const [backgroundColors, setBackgroundColors] = useState([]); + const { userDetails, profileBackground, signOut } = useUserDetails(); const navigate = useNavigate(); @@ -64,34 +59,28 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => { navigate("/"); }; - const handleAvatarLoad = async () => { - const output = await average(profileImageRef.current!, { - amount: 1, - format: "hex", - }); + const isMe = userDetails?.id == userProfile.id; - setBackgroundColors([ - darkenColor(output as string, 0.6), - darkenColor(output as string, 0.7), - ]); - }; + const profileContentBoxBackground = useMemo(() => { + if (profileBackground) return profileBackground; + /* TODO: Render background colors for other users */ + return undefined; + }, [profileBackground]); return ( <>
{userProfile.profileImageUrl ? ( {userProfile.displayName} ) : ( @@ -102,7 +91,7 @@ export const UserContent = ({ userProfile }: ProfileContentProps) => {

{userProfile.displayName}

- {userAuth && userAuth.id == userProfile.id && ( + {isMe && (
); -}; +} diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts index 589fa17f..9bc0c950 100644 --- a/src/renderer/src/store.ts +++ b/src/renderer/src/store.ts @@ -6,7 +6,7 @@ import { searchSlice, userPreferencesSlice, toastSlice, - userAuthSlice, + userDetailsSlice, } from "@renderer/features"; export const store = configureStore({ @@ -17,7 +17,7 @@ export const store = configureStore({ userPreferences: userPreferencesSlice.reducer, download: downloadSlice.reducer, toast: toastSlice.reducer, - userAuth: userAuthSlice.reducer, + userDetails: userDetailsSlice.reducer, }, }); diff --git a/src/types/index.ts b/src/types/index.ts index acb8d10f..153fdc9e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -244,7 +244,7 @@ export interface RealDebridUser { expiration: string; } -export interface UserAuth { +export interface UserDetails { id: string; displayName: string; profileImageUrl: string | null; From da5cc11bff3ebeff1c1a3cad71c69ce9637ea2a4 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 15 Jun 2024 02:15:58 -0300 Subject: [PATCH 4/7] feat: sync library --- src/main/entity/game.entity.ts | 3 + .../events/library/add-game-to-library.ts | 21 ++++++ .../library/remove-game-from-library.ts | 7 ++ src/main/main.ts | 75 ++++++++++++++++++- src/main/services/hydra-api.ts | 9 +++ src/main/services/process-watcher.ts | 59 ++++++++++++++- 6 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 49ad2716..83cc4001 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -22,6 +22,9 @@ export class Game { @Column("text", { unique: true }) objectID: string; + @Column("text", { unique: true, nullable: true }) + remoteId: string; + @Column("text") title: string; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 1c7447e5..7f283636 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -6,6 +6,7 @@ import type { GameShop } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; import { steamGamesWorker } from "@main/workers"; +import { HydraApi } from "@main/services/hydra-api"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -49,6 +50,26 @@ const addGameToLibrary = async ( } }); } + + const game = await gameRepository.findOne({ where: { objectID } }); + + HydraApi.post("/games", { + objectId: objectID, + playTimeInMilliseconds: game?.playTimeInMilliseconds, + shop, + lastTimePlayed: game?.lastTimePlayed, + }).then((response) => { + const { + id: remoteId, + playTimeInMilliseconds, + lastTimePlayed, + } = response.data; + + gameRepository.update( + { objectID }, + { remoteId, playTimeInMilliseconds, lastTimePlayed } + ); + }); }); }; diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 29a7a635..8bbd83e3 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,5 +1,6 @@ import { registerEvent } from "../register-event"; import { gameRepository } from "../../repository"; +import { HydraApi } from "@main/services/hydra-api"; const removeGameFromLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -9,6 +10,12 @@ const removeGameFromLibrary = async ( { id: gameId }, { isDeleted: true, executablePath: null } ); + + const game = await gameRepository.findOne({ where: { id: gameId } }); + + if (game?.remoteId) { + HydraApi.delete(`/games/${game.remoteId}`); + } }; registerEvent("removeGameFromLibrary", removeGameFromLibrary); diff --git a/src/main/main.ts b/src/main/main.ts index 7e5692d5..a866972a 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,15 +1,22 @@ -import { DownloadManager, RepacksManager, startMainLoop } from "./services"; +import { + DownloadManager, + RepacksManager, + logger, + startMainLoop, +} from "./services"; import { downloadQueueRepository, + gameRepository, repackRepository, userPreferencesRepository, } from "./repository"; import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; -import { fetchDownloadSourcesAndUpdate } from "./helpers"; +import { fetchDownloadSourcesAndUpdate, getSteamAppAsset } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; +import { steamGamesWorker } from "./workers"; startMainLoop(); @@ -21,7 +28,69 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); - HydraApi.setupApi(); + HydraApi.setupApi() + .then(async () => { + if (HydraApi.isLoggedIn()) { + const games = await HydraApi.get("/games"); + + for (const game of games.data) { + const localGame = await gameRepository.findOne({ + where: { + objectID: game.objectId, + }, + }); + + if (localGame) { + const updatedLastTimePlayed = + localGame.lastTimePlayed == null || + new Date(game.lastTimePlayed) > localGame.lastTimePlayed + ? new Date(game.lastTimePlayed) + : localGame.lastTimePlayed; + + const updatedPlayTime = + localGame.playTimeInMilliseconds < game.playTimeInMilliseconds + ? game.playTimeInMilliseconds + : localGame.playTimeInMilliseconds; + + gameRepository.update( + { + objectID: game.objectId, + shop: "steam", + lastTimePlayed: updatedLastTimePlayed, + playTimeInMilliseconds: updatedPlayTime, + }, + { remoteId: game.id } + ); + } else { + const steamGame = await steamGamesWorker.run( + Number(game.objectId), + { + name: "getById", + } + ); + + if (steamGame) { + const iconUrl = steamGame?.clientIcon + ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon) + : null; + + gameRepository.insert({ + objectID: game.objectId, + title: steamGame?.name, + remoteId: game.id, + shop: game.shop, + iconUrl, + lastTimePlayed: game.lastTimePlayed, + playTimeInMilliseconds: game.playTimeInMilliseconds, + }); + } + } + } + } + }) + .catch((err) => { + logger.error("erro api GET: /games", err); + }); const [nextQueueItem] = await downloadQueueRepository.find({ order: { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 6c275df0..a4463723 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -14,6 +14,10 @@ export class HydraApi { expirationTimestamp: 0, }; + static isLoggedIn() { + return this.userAuth.authToken !== ""; + } + static async handleExternalAuth(auth: string) { const { payload } = url.parse(auth, true).query; @@ -140,4 +144,9 @@ export class HydraApi { await this.revalidateAccessTokenIfExpired(); return this.instance.patch(url, data, this.getAxiosConfig()); } + + static async delete(url: string) { + await this.revalidateAccessTokenIfExpired(); + return this.instance.delete(url, this.getAxiosConfig()); + } } diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index ea1b6355..577a7770 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -4,8 +4,12 @@ import { IsNull, Not } from "typeorm"; import { gameRepository } from "@main/repository"; import { getProcesses } from "@main/helpers"; import { WindowManager } from "./window-manager"; +import { HydraApi } from "./hydra-api"; -const gamesPlaytime = new Map(); +const gamesPlaytime = new Map< + number, + { lastTick: number; firstTick: number } +>(); export const watchProcesses = async () => { const games = await gameRepository.find({ @@ -37,7 +41,9 @@ export const watchProcesses = async () => { if (gameProcess) { if (gamesPlaytime.has(game.id)) { - const zero = gamesPlaytime.get(game.id) ?? 0; + const gamePlaytime = gamesPlaytime.get(game.id)!; + + const zero = gamePlaytime.lastTick; const delta = performance.now() - zero; if (WindowManager.mainWindow) { @@ -48,12 +54,57 @@ export const watchProcesses = async () => { playTimeInMilliseconds: game.playTimeInMilliseconds + delta, lastTimePlayed: new Date(), }); - } - gamesPlaytime.set(game.id, performance.now()); + gamesPlaytime.set(game.id, { + ...gamePlaytime, + lastTick: performance.now(), + }); + } else { + if (game.remoteId) { + HydraApi.put(`/games/${game.remoteId}`, { + playTimeDeltaInMilliseconds: 0, + lastTimePlayed: new Date(), + }); + } else { + HydraApi.post("/games", { + objectId: game.objectID, + playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: new Date(), + }).then((response) => { + const { id: remoteId } = response.data; + gameRepository.update({ objectID: game.objectID }, { remoteId }); + }); + } + + gamesPlaytime.set(game.id, { + lastTick: performance.now(), + firstTick: performance.now(), + }); + } } else if (gamesPlaytime.has(game.id)) { + const gamePlaytime = gamesPlaytime.get(game.id)!; gamesPlaytime.delete(game.id); + if (game.remoteId) { + HydraApi.put(`/games/${game.remoteId}`, { + playTimeInMilliseconds: Math.round( + performance.now() - gamePlaytime.firstTick + ), + lastTimePlayed: game.lastTimePlayed, + }); + } else { + HydraApi.post("/games", { + objectId: game.objectID, + playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }).then((response) => { + const { id: remoteId } = response.data; + gameRepository.update({ objectID: game.objectID }, { remoteId }); + }); + } + if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-game-close", game.id); } From 7fc376b47fe7be4f67ac22d2bbab64990a04a099 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 16 Jun 2024 23:43:18 -0300 Subject: [PATCH 5/7] feat: batch games and code refactor --- src/main/entity/game.entity.ts | 2 +- .../events/library/add-game-to-library.ts | 9 +-- src/main/main.ts | 80 ++----------------- src/main/services/hydra-api.ts | 4 + src/main/services/library-sync/create-game.ts | 11 +++ .../services/library-sync/get-remote-games.ts | 69 ++++++++++++++++ src/main/services/library-sync/index.ts | 4 + .../library-sync/update-game-playtime.ts | 13 +++ .../library-sync/upload-batch-games.ts | 35 ++++++++ src/main/services/process-watcher.ts | 40 ++++------ 10 files changed, 161 insertions(+), 106 deletions(-) create mode 100644 src/main/services/library-sync/create-game.ts create mode 100644 src/main/services/library-sync/get-remote-games.ts create mode 100644 src/main/services/library-sync/index.ts create mode 100644 src/main/services/library-sync/update-game-playtime.ts create mode 100644 src/main/services/library-sync/upload-batch-games.ts diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 83cc4001..b8607b73 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -23,7 +23,7 @@ export class Game { objectID: string; @Column("text", { unique: true, nullable: true }) - remoteId: string; + remoteId: string | null; @Column("text") title: string; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 7f283636..8187d41e 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -6,7 +6,7 @@ import type { GameShop } from "@types"; import { getFileBase64, getSteamAppAsset } from "@main/helpers"; import { steamGamesWorker } from "@main/workers"; -import { HydraApi } from "@main/services/hydra-api"; +import { createGame } from "@main/services/library-sync"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -53,12 +53,7 @@ const addGameToLibrary = async ( const game = await gameRepository.findOne({ where: { objectID } }); - HydraApi.post("/games", { - objectId: objectID, - playTimeInMilliseconds: game?.playTimeInMilliseconds, - shop, - lastTimePlayed: game?.lastTimePlayed, - }).then((response) => { + createGame(game!).then((response) => { const { id: remoteId, playTimeInMilliseconds, diff --git a/src/main/main.ts b/src/main/main.ts index a866972a..f1a365fc 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,22 +1,16 @@ -import { - DownloadManager, - RepacksManager, - logger, - startMainLoop, -} from "./services"; +import { DownloadManager, RepacksManager, startMainLoop } from "./services"; import { downloadQueueRepository, - gameRepository, repackRepository, userPreferencesRepository, } from "./repository"; import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; -import { fetchDownloadSourcesAndUpdate, getSteamAppAsset } from "./helpers"; +import { fetchDownloadSourcesAndUpdate } from "./helpers"; import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; -import { steamGamesWorker } from "./workers"; +import { getRemoteGames } from "./services/library-sync"; startMainLoop(); @@ -28,69 +22,11 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (userPreferences?.realDebridApiToken) RealDebridClient.authorize(userPreferences?.realDebridApiToken); - HydraApi.setupApi() - .then(async () => { - if (HydraApi.isLoggedIn()) { - const games = await HydraApi.get("/games"); - - for (const game of games.data) { - const localGame = await gameRepository.findOne({ - where: { - objectID: game.objectId, - }, - }); - - if (localGame) { - const updatedLastTimePlayed = - localGame.lastTimePlayed == null || - new Date(game.lastTimePlayed) > localGame.lastTimePlayed - ? new Date(game.lastTimePlayed) - : localGame.lastTimePlayed; - - const updatedPlayTime = - localGame.playTimeInMilliseconds < game.playTimeInMilliseconds - ? game.playTimeInMilliseconds - : localGame.playTimeInMilliseconds; - - gameRepository.update( - { - objectID: game.objectId, - shop: "steam", - lastTimePlayed: updatedLastTimePlayed, - playTimeInMilliseconds: updatedPlayTime, - }, - { remoteId: game.id } - ); - } else { - const steamGame = await steamGamesWorker.run( - Number(game.objectId), - { - name: "getById", - } - ); - - if (steamGame) { - const iconUrl = steamGame?.clientIcon - ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon) - : null; - - gameRepository.insert({ - objectID: game.objectId, - title: steamGame?.name, - remoteId: game.id, - shop: game.shop, - iconUrl, - lastTimePlayed: game.lastTimePlayed, - playTimeInMilliseconds: game.playTimeInMilliseconds, - }); - } - } - } - } - }) - .catch((err) => { - logger.error("erro api GET: /games", err); - }); + HydraApi.setupApi().then(async () => { + if (HydraApi.isLoggedIn()) { + getRemoteGames(); + } + }); const [nextQueueItem] = await downloadQueueRepository.find({ order: { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index a4463723..7d2a1fdf 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -2,6 +2,7 @@ import { userAuthRepository } from "@main/repository"; import axios, { AxiosError, AxiosInstance } from "axios"; import { WindowManager } from "./window-manager"; import url from "url"; +import { getRemoteGames, uploadBatchGames } from "./library-sync"; export class HydraApi { private static instance: AxiosInstance; @@ -49,6 +50,9 @@ export class HydraApi { if (WindowManager.mainWindow) { WindowManager.mainWindow.webContents.send("on-signin"); + + await uploadBatchGames(); + await getRemoteGames(); } } diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts new file mode 100644 index 00000000..823f56a6 --- /dev/null +++ b/src/main/services/library-sync/create-game.ts @@ -0,0 +1,11 @@ +import { Game } from "@main/entity"; +import { HydraApi } from "../hydra-api"; + +export const createGame = async (game: Game) => { + return HydraApi.post(`/games`, { + objectId: game.objectID, + playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }); +}; diff --git a/src/main/services/library-sync/get-remote-games.ts b/src/main/services/library-sync/get-remote-games.ts new file mode 100644 index 00000000..1a85ca2d --- /dev/null +++ b/src/main/services/library-sync/get-remote-games.ts @@ -0,0 +1,69 @@ +import { gameRepository } from "@main/repository"; +import { HydraApi } from "../hydra-api"; +import { steamGamesWorker } from "@main/workers"; +import { getSteamAppAsset } from "@main/helpers"; +import { logger } from "../logger"; +import { AxiosError } from "axios"; + +export const getRemoteGames = async () => { + try { + const games = await HydraApi.get("/games"); + + for (const game of games.data) { + const localGame = await gameRepository.findOne({ + where: { + objectID: game.objectId, + }, + }); + + if (localGame) { + const updatedLastTimePlayed = + localGame.lastTimePlayed == null || + new Date(game.lastTimePlayed) > localGame.lastTimePlayed + ? new Date(game.lastTimePlayed) + : localGame.lastTimePlayed; + + const updatedPlayTime = + localGame.playTimeInMilliseconds < game.playTimeInMilliseconds + ? game.playTimeInMilliseconds + : localGame.playTimeInMilliseconds; + + gameRepository.update( + { + objectID: game.objectId, + shop: "steam", + lastTimePlayed: updatedLastTimePlayed, + playTimeInMilliseconds: updatedPlayTime, + }, + { remoteId: game.id } + ); + } else { + const steamGame = await steamGamesWorker.run(Number(game.objectId), { + name: "getById", + }); + + if (steamGame) { + const iconUrl = steamGame?.clientIcon + ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon) + : null; + + gameRepository.insert({ + objectID: game.objectId, + title: steamGame?.name, + remoteId: game.id, + shop: game.shop, + iconUrl, + lastTimePlayed: game.lastTimePlayed, + playTimeInMilliseconds: game.playTimeInMilliseconds, + }); + } + } + } + } catch (err) { + if (err instanceof AxiosError) { + logger.error("getRemoteGames", err.response, err.message); + } else { + logger.error("getRemoteGames", err); + } + } +}; diff --git a/src/main/services/library-sync/index.ts b/src/main/services/library-sync/index.ts new file mode 100644 index 00000000..aa0d94de --- /dev/null +++ b/src/main/services/library-sync/index.ts @@ -0,0 +1,4 @@ +export * from "./get-remote-games"; +export * from "./upload-batch-games"; +export * from "./update-game-playtime"; +export * from "./create-game"; diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts new file mode 100644 index 00000000..271dc6a5 --- /dev/null +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -0,0 +1,13 @@ +import { Game } from "@main/entity"; +import { HydraApi } from "../hydra-api"; + +export const updateGamePlaytime = async ( + game: Game, + delta: number, + lastTimePlayed: Date +) => { + return HydraApi.put(`/games/${game.remoteId}`, { + playTimeDeltaInSeconds: delta, + lastTimePlayed, + }); +}; diff --git a/src/main/services/library-sync/upload-batch-games.ts b/src/main/services/library-sync/upload-batch-games.ts new file mode 100644 index 00000000..cfea9d39 --- /dev/null +++ b/src/main/services/library-sync/upload-batch-games.ts @@ -0,0 +1,35 @@ +import { gameRepository } from "@main/repository"; +import { chunk } from "lodash-es"; +import { IsNull } from "typeorm"; +import { HydraApi } from "../hydra-api"; +import { logger } from "../logger"; +import { AxiosError } from "axios"; + +export const uploadBatchGames = async () => { + try { + const games = await gameRepository.find({ + where: { remoteId: IsNull(), isDeleted: false }, + }); + + const gamesChunks = chunk(games, 200); + for (const chunk of gamesChunks) { + await HydraApi.post( + "/games/batch", + chunk.map((game) => { + return { + objectId: game.objectID, + playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), + shop: game.shop, + lastTimePlayed: game.lastTimePlayed, + }; + }) + ); + } + } catch (err) { + if (err instanceof AxiosError) { + logger.error("uploadBatchGames", err.response, err.message); + } else { + logger.error("uploadBatchGames", err); + } + } +}; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 577a7770..de3af727 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -4,7 +4,7 @@ import { IsNull, Not } from "typeorm"; import { gameRepository } from "@main/repository"; import { getProcesses } from "@main/helpers"; import { WindowManager } from "./window-manager"; -import { HydraApi } from "./hydra-api"; +import { createGame, updateGamePlaytime } from "./library-sync"; const gamesPlaytime = new Map< number, @@ -61,20 +61,14 @@ export const watchProcesses = async () => { }); } else { if (game.remoteId) { - HydraApi.put(`/games/${game.remoteId}`, { - playTimeDeltaInMilliseconds: 0, - lastTimePlayed: new Date(), - }); + updateGamePlaytime(game, 0, new Date()); } else { - HydraApi.post("/games", { - objectId: game.objectID, - playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), - shop: game.shop, - lastTimePlayed: new Date(), - }).then((response) => { - const { id: remoteId } = response.data; - gameRepository.update({ objectID: game.objectID }, { remoteId }); - }); + createGame({ ...game, lastTimePlayed: new Date() }).then( + (response) => { + const { id: remoteId } = response.data; + gameRepository.update({ objectID: game.objectID }, { remoteId }); + } + ); } gamesPlaytime.set(game.id, { @@ -87,19 +81,13 @@ export const watchProcesses = async () => { gamesPlaytime.delete(game.id); if (game.remoteId) { - HydraApi.put(`/games/${game.remoteId}`, { - playTimeInMilliseconds: Math.round( - performance.now() - gamePlaytime.firstTick - ), - lastTimePlayed: game.lastTimePlayed, - }); + updateGamePlaytime( + game, + performance.now() - gamePlaytime.firstTick, + game.lastTimePlayed! + ); } else { - HydraApi.post("/games", { - objectId: game.objectID, - playTimeInMilliseconds: Math.round(game.playTimeInMilliseconds), - shop: game.shop, - lastTimePlayed: game.lastTimePlayed, - }).then((response) => { + createGame(game).then((response) => { const { id: remoteId } = response.data; gameRepository.update({ objectID: game.objectID }, { remoteId }); }); From c01ed86071e3ec1d3282421fe94c1b1b752ed6e8 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:22:35 -0300 Subject: [PATCH 6/7] debug logs --- src/main/services/hydra-api.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 7d2a1fdf..52ccce92 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -61,6 +61,35 @@ export class HydraApi { baseURL: import.meta.env.MAIN_VITE_API_URL, }); + this.instance.interceptors.request.use( + (request) => { + console.log(" ---- REQUEST -----"); + console.log(request.method, request.baseURL, request.data); + return request; + }, + (error) => { + console.log("request error", error); + return Promise.reject(error); + } + ); + + this.instance.interceptors.response.use( + (response) => { + console.log(" ---- RESPONSE -----"); + console.log( + response.status, + response.config.method, + response.config.url, + response.data + ); + return response; + }, + (error) => { + console.log("response error", error); + return Promise.reject(error); + } + ); + const userAuth = await userAuthRepository.findOne({ where: { id: 1 }, }); From 7eb69f6e1622835ca77f21cad6f18843df51d8ce Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:25:56 -0300 Subject: [PATCH 7/7] fix debug log --- src/main/services/hydra-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 52ccce92..7b50f7a9 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -64,7 +64,7 @@ export class HydraApi { this.instance.interceptors.request.use( (request) => { console.log(" ---- REQUEST -----"); - console.log(request.method, request.baseURL, request.data); + console.log(request.method, request.url, request.data); return request; }, (error) => {