diff --git a/.env.example b/.env.example index 991a06ff..b12e9517 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,3 @@ MAIN_VITE_API_URL=API_URL MAIN_VITE_AUTH_URL=AUTH_URL MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY -RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 89392011..0811967b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,16 +22,6 @@ jobs: with: node-version: 20.18.0 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Push build to R2 - run: aws s3 sync ./docs s3://${{ vars.BUILDS_BUCKET_NAME }} - - name: Install dependencies run: yarn @@ -58,6 +48,7 @@ jobs: MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_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.RENDERER_VITE_EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build Windows @@ -69,6 +60,7 @@ jobs: MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_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.RENDERER_VITE_EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create artifact diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff97c937..b91cc743 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,7 @@ jobs: 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.RENDERER_VITE_EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build Windows if: matrix.os == 'windows-latest' @@ -57,6 +58,7 @@ jobs: 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.RENDERER_VITE_EXTERNAL_RESOURCES_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create artifact uses: actions/upload-artifact@v4 diff --git a/src/main/events/catalogue/get-how-long-to-beat.ts b/src/main/events/catalogue/get-how-long-to-beat.ts index 01966afc..2a1492ef 100644 --- a/src/main/events/catalogue/get-how-long-to-beat.ts +++ b/src/main/events/catalogue/get-how-long-to-beat.ts @@ -1,23 +1,21 @@ -import type { HowLongToBeatCategory } from "@types"; -import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services"; +import type { GameShop, HowLongToBeatCategory } from "@types"; import { registerEvent } from "../register-event"; -import { formatName } from "@shared"; +import { HydraApi } from "@main/services"; const getHowLongToBeat = async ( _event: Electron.IpcMainInvokeEvent, - title: string + objectId: string, + shop: GameShop ): Promise => { - const response = await searchHowLongToBeat(title); - - const game = response.data.find((game) => { - return formatName(game.game_name) === formatName(title); + const params = new URLSearchParams({ + objectId, + shop, }); - if (!game) return null; - const howLongToBeat = await getHowLongToBeatGame(String(game.game_id)); - - return howLongToBeat; + return HydraApi.get(`/games/how-long-to-beat?${params.toString()}`, null, { + needsAuth: false, + }); }; registerEvent("getHowLongToBeat", getHowLongToBeat); diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts deleted file mode 100644 index 1e5f3279..00000000 --- a/src/main/services/how-long-to-beat.ts +++ /dev/null @@ -1,108 +0,0 @@ -import axios from "axios"; -import { requestWebPage } from "@main/helpers"; -import type { - HowLongToBeatCategory, - HowLongToBeatSearchResponse, -} from "@types"; -import { formatName } from "@shared"; -import { logger } from "./logger"; -import UserAgent from "user-agents"; - -const state = { - apiKey: null as string | null, -}; - -const getHowLongToBeatSearchApiKey = async () => { - const userAgent = new UserAgent(); - - const document = await requestWebPage("https://howlongtobeat.com/"); - const scripts = Array.from(document.querySelectorAll("script")); - - const appScript = scripts.find((script) => - script.src.startsWith("/_next/static/chunks/pages/_app") - ); - - if (!appScript) return null; - - const response = await axios.get( - `https://howlongtobeat.com${appScript.src}`, - { - headers: { - "User-Agent": userAgent.toString(), - }, - } - ); - - const results = /fetch\("\/api\/search\/"\.concat\("(.*?)"\)/gm.exec( - response.data - ); - - if (!results) return null; - - return results[1]; -}; - -export const searchHowLongToBeat = async (gameName: string) => { - state.apiKey = state.apiKey ?? (await getHowLongToBeatSearchApiKey()); - if (!state.apiKey) return { data: [] }; - - const userAgent = new UserAgent(); - - const response = await axios - .post( - `https://howlongtobeat.com/api/search/${state.apiKey}`, - { - searchType: "games", - searchTerms: formatName(gameName).split(" "), - searchPage: 1, - size: 20, - }, - { - headers: { - "User-Agent": userAgent.toString(), - Referer: "https://howlongtobeat.com/", - }, - } - ) - .catch((error) => { - logger.error("Error searching HowLongToBeat:", error?.response?.status); - return { data: { data: [] } }; - }); - - return response.data as HowLongToBeatSearchResponse; -}; - -const parseListItems = ($lis: Element[]) => { - return $lis.map(($li) => { - const title = $li.querySelector("h4")?.textContent; - const [, accuracyClassName] = Array.from(($li as HTMLElement).classList); - - const accuracy = accuracyClassName.split("time_").at(1); - - return { - title: title ?? "", - duration: $li.querySelector("h5")?.textContent ?? "", - accuracy: accuracy ?? "", - }; - }); -}; - -export const getHowLongToBeatGame = async ( - id: string -): Promise => { - const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`); - - const $ul = document.querySelector(".shadow_shadow ul"); - if (!$ul) return []; - - const $lis = Array.from($ul.children); - - const [$firstLi] = $lis; - - if ($firstLi.tagName === "DIV") { - const $pcData = $lis.find(($li) => $li.textContent?.includes("PC")); - return parseListItems(Array.from($pcData?.querySelectorAll("li") ?? [])); - } - - return parseListItems($lis); -}; diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 27abf579..498159c9 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -4,7 +4,6 @@ export * from "./steam-250"; export * from "./steam-grid"; export * from "./window-manager"; export * from "./download"; -export * from "./how-long-to-beat"; export * from "./process-watcher"; export * from "./main-loop"; export * from "./hydra-api"; diff --git a/src/preload/index.ts b/src/preload/index.ts index 0166ba2a..3f2f677a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -42,8 +42,8 @@ contextBridge.exposeInMainWorld("electron", { getGameShopDetails: (objectId: string, shop: GameShop, language: string) => ipcRenderer.invoke("getGameShopDetails", objectId, shop, language), getRandomGame: () => ipcRenderer.invoke("getRandomGame"), - getHowLongToBeat: (title: string) => - ipcRenderer.invoke("getHowLongToBeat", title), + getHowLongToBeat: (objectId: string, shop: GameShop) => + ipcRenderer.invoke("getHowLongToBeat", objectId, shop), getGames: (take?: number, skip?: number) => ipcRenderer.invoke("getGames", take, skip), searchGameRepacks: (query: string) => diff --git a/src/renderer/index.html b/src/renderer/index.html index bfc3a206..6290dfa4 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -6,7 +6,7 @@ Hydra diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 5a479879..bb2f096a 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -2,8 +2,6 @@ import { useCallback, useContext, useEffect, useRef } from "react"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; -import Intercom from "@intercom/messenger-js-sdk"; - import { useAppDispatch, useAppSelector, @@ -36,10 +34,6 @@ export interface AppProps { children: React.ReactNode; } -Intercom({ - app_id: import.meta.env.RENDERER_VITE_INTERCOM_APP_ID, -}); - export function App() { const contentRef = useRef(null); const { updateLibrary, library } = useLibrary(); @@ -120,12 +114,33 @@ export function App() { dispatch(setProfileBackground(profileBackground)); } - fetchUserDetails().then((response) => { - if (response) { - updateUserDetails(response); - syncFriendRequests(); - } - }); + fetchUserDetails() + .then((response) => { + if (response) { + updateUserDetails(response); + syncFriendRequests(); + + const $existingScript = document.getElementById("user-details"); + + const content = `window.userDetails = ${JSON.stringify(response)};`; + + if ($existingScript) { + $existingScript.textContent = content; + } else { + const $script = document.createElement("script"); + $script.id = "user-details"; + $script.type = "text/javascript"; + $script.textContent = content; + + document.head.appendChild($script); + } + } + }) + .finally(() => { + const $script = document.createElement("script"); + $script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}?t=${Date.now()}`; + document.head.appendChild($script); + }); }, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]); const onSignIn = useCallback(() => { @@ -215,9 +230,7 @@ export function App() { useEffect(() => { new MutationObserver(() => { - const modal = document.body.querySelector( - "[role=dialog]:not([data-intercom-frame='true'])" - ); + const modal = document.body.querySelector("[data-hydra-dialog]"); dispatch(toggleDraggingDisabled(Boolean(modal))); }).observe(document.body, { diff --git a/src/renderer/src/components/modal/modal.tsx b/src/renderer/src/components/modal/modal.tsx index eb2894de..af15feb5 100644 --- a/src/renderer/src/components/modal/modal.tsx +++ b/src/renderer/src/components/modal/modal.tsx @@ -107,6 +107,7 @@ export function Modal({ aria-labelledby={title} aria-describedby={description} ref={modalContentRef} + data-hydra-dialog >
diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index 787cf66a..f487681c 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -22,8 +22,6 @@ import { SidebarProfile } from "./sidebar-profile"; import { sortBy } from "lodash-es"; import { CommentDiscussionIcon } from "@primer/octicons-react"; -import { show, update } from "@intercom/messenger-js-sdk"; - const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_MAX_WIDTH = 450; @@ -50,23 +48,7 @@ export function Sidebar() { return sortBy(library, (game) => game.title); }, [library]); - const { userDetails, hasActiveSubscription } = useUserDetails(); - - useEffect(() => { - if (userDetails) { - window.electron.isStaging().then((isStaging) => { - update({ - user_id: userDetails.id + (isStaging ? "-staging" : ""), - name: userDetails.displayName, - Username: userDetails.username, - email: userDetails.email ?? undefined, - Email: userDetails.email, - "Subscription expiration date": userDetails?.subscription?.expiresAt, - "Payment status": userDetails?.subscription?.status, - }); - }); - } - }, [userDetails, hasActiveSubscription]); + const { hasActiveSubscription } = useUserDetails(); const { lastPacket, progress } = useDownload(); @@ -269,7 +251,11 @@ export function Sidebar() {
{hasActiveSubscription && ( -