Compare commits

...

13 Commits

Author SHA1 Message Date
Chubby Granny Chaser
b09e91a1cf fix: fixing analytics for ddl
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
2024-11-05 20:29:44 +00:00
Zamitto
9418c4acf7 fix: achievement filter 2024-11-05 17:09:46 -03:00
Chubby Granny Chaser
0451bc55aa chore: more relaxed CSP 2024-11-05 19:59:41 +00:00
Chubby Granny Chaser
2840110a21 ci: removing bump 2024-11-05 19:30:09 +00:00
Chubby Granny Chaser
8c4eacf045 chore: bump version 2024-11-05 19:18:13 +00:00
Chubby Granny Chaser
179785432d Merge pull request #1206 from hydralauncher/ci/adding-intercom-app-id
Ci/adding intercom app
2024-11-05 19:17:46 +00:00
Chubby Granny Chaser
c3da39205f ci: adding intercom app id 2024-11-05 19:12:12 +00:00
Chubby Granny Chaser
f071d1006b ci: adding intercom app id 2024-11-05 19:11:03 +00:00
Chubby Granny Chaser
6da1832799 ci: adding intercom app id 2024-11-05 19:08:37 +00:00
Chubby Granny Chaser
df0e124c3a Merge pull request #1204 from hydralauncher/feat/adding-intercom
Feat/adding intercom
2024-11-05 18:24:06 +00:00
Chubby Granny Chaser
25f1a72b48 Merge branch 'main' into feat/adding-intercom 2024-11-05 18:17:37 +00:00
Chubby Granny Chaser
2b5e76ffdd feat: adding intercom 2024-11-05 18:15:17 +00:00
Chubby Granny Chaser
9d75e3ad6f feat: adding intercom 2024-11-05 18:13:32 +00:00
25 changed files with 258 additions and 93 deletions

View File

@@ -1,4 +1,4 @@
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

View File

@@ -44,6 +44,7 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
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 }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
@@ -54,6 +55,7 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
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 }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact

View File

@@ -46,8 +46,8 @@ jobs:
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 }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
if: matrix.os == 'windows-latest'
run: yarn build:win
@@ -56,8 +56,8 @@ jobs:
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 }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -36,6 +36,7 @@
"@electron-toolkit/utils": "^3.0.0",
"@fontsource/noto-sans": "^5.0.22",
"@hookform/resolvers": "^3.9.0",
"@intercom/messenger-js-sdk": "^0.0.14",
"@primer/octicons-react": "^19.9.0",
"@reduxjs/toolkit": "^2.2.3",
"@vanilla-extract/css": "^1.14.2",

View File

@@ -25,7 +25,8 @@
"queued": "{{title}} (Queued)",
"game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in",
"friends": "Friends"
"friends": "Friends",
"need_help": "Need help?"
},
"header": {
"search": "Search games",

View File

@@ -25,7 +25,8 @@
"queued": "{{title}} (En cola)",
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
"sign_in": "Iniciar sesión",
"friends": "Amigos"
"friends": "Amigos",
"need_help": "¿Necesitas ayuda?"
},
"header": {
"search": "Buscar juegos",

View File

@@ -25,7 +25,8 @@
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login",
"friends": "Amigos"
"friends": "Amigos",
"need_help": "Precisa de ajuda?"
},
"header": {
"search": "Buscar jogos",

View File

@@ -24,7 +24,8 @@
"queued": "{{title}} (В очереди)",
"game_has_no_executable": "Файл запуска игры не выбран",
"sign_in": "Войти",
"friends": "Друзья"
"friends": "Друзья",
"need_help": "Нужна помощь?"
},
"header": {
"search": "Поиск",

View File

@@ -91,9 +91,15 @@ const startGameDownload = async (
logger.error("Failed to create game download", err);
});
const { infoHash } = await parseTorrent(payload.uri);
if (infoHash) {
HydraAnalytics.postDownload(infoHash).catch(() => {});
if (uri.startsWith("magnet:")) {
try {
const { infoHash } = await parseTorrent(payload.uri);
if (infoHash) {
HydraAnalytics.postDownload(infoHash).catch(() => {});
}
} catch (err) {
logger.error("Failed to parse torrent", err);
}
}
await DownloadManager.cancelDownload(updatedGame!.id);

View File

@@ -102,7 +102,7 @@ export const mergeAchievements = async (
);
});
})
.filter((achievement) => achievement)
.filter((achievement) => Boolean(achievement))
.map((achievement) => {
return {
displayName: achievement!.displayName,

View File

@@ -56,6 +56,7 @@ export const getUserData = () => {
id: loggedUser.userId,
username: "",
bio: "",
email: null,
profileVisibility: "PUBLIC" as ProfileVisibility,
subscription: loggedUser.subscription
? {

View File

@@ -85,6 +85,10 @@ export class WindowManager {
return callback(details);
}
if (details.url.includes("intercom.io")) {
return callback(details);
}
const headers = {
"access-control-allow-origin": ["*"],
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],

View File

@@ -6,7 +6,7 @@
<title>Hydra</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
content="default-src 'self'; script-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *; connect-src *; font-src *;"
/>
</head>
<body>

View File

@@ -126,3 +126,9 @@ export const titleBar = style({
zIndex: "4",
borderBottom: `1px solid ${vars.color.border}`,
} as ComplexStyleRule);
export const cloudText = style({
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
backgroundClip: "text",
color: "transparent",
});

View File

@@ -2,6 +2,8 @@ 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,
@@ -34,6 +36,12 @@ export interface AppProps {
children: React.ReactNode;
}
console.log(import.meta.env);
Intercom({
app_id: import.meta.env.RENDERER_VITE_INTERCOM_APP_ID,
});
export function App() {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary, library } = useLibrary();
@@ -54,8 +62,13 @@ export function App() {
hideFriendsModal,
} = useUserDetails();
const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
useUserDetails();
const {
userDetails,
hasActiveSubscription,
fetchUserDetails,
updateUserDetails,
clearUserDetails,
} = useUserDetails();
const dispatch = useAppDispatch();
@@ -204,7 +217,9 @@ export function App() {
useEffect(() => {
new MutationObserver(() => {
const modal = document.body.querySelector("[role=dialog]");
const modal = document.body.querySelector(
"[role=dialog]:not([data-intercom-frame='true'])"
);
dispatch(toggleDraggingDisabled(Boolean(modal)));
}).observe(document.body, {
@@ -270,7 +285,12 @@ export function App() {
<>
{window.electron.platform === "win32" && (
<div className={styles.titleBar}>
<h4>Hydra</h4>
<h4>
Hydra
{hasActiveSubscription && (
<span className={styles.cloudText}> Cloud</span>
)}
</h4>
</div>
)}

View File

@@ -4,7 +4,7 @@
color: globals.$muted-color;
font-size: 10px;
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
border: solid 1px globals.$border-color;
border: solid 1px globals.$muted-color;
border-radius: 4px;
display: flex;
align-items: center;

View File

@@ -13,6 +13,7 @@ export const sidebar = recipe({
borderRight: `solid 1px ${vars.color.border}`,
position: "relative",
overflow: "hidden",
justifyContent: "space-between",
},
variants: {
resizing: {
@@ -124,3 +125,28 @@ export const section = style({
flexDirection: "column",
paddingBottom: `${SPACING_UNIT}px`,
});
export const helpButton = style({
color: vars.color.muted,
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
gap: "9px",
display: "flex",
alignItems: "center",
cursor: "pointer",
borderTop: `solid 1px ${vars.color.border}`,
transition: "background-color ease 0.1s",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const helpButtonIcon = style({
background: "linear-gradient(0deg, #16B195 50%, #3E62C0 100%)",
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#fff",
borderRadius: "50%",
});

View File

@@ -5,7 +5,12 @@ import { useLocation, useNavigate } from "react-router-dom";
import type { LibraryGame } from "@types";
import { TextField } from "@renderer/components";
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
import {
useDownload,
useLibrary,
useToast,
useUserDetails,
} from "@renderer/hooks";
import { routes } from "./routes";
@@ -15,6 +20,9 @@ import { buildGameDetailsPath } from "@renderer/helpers";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
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;
@@ -42,6 +50,20 @@ export function Sidebar() {
return sortBy(library, (game) => game.title);
}, [library]);
const { userDetails, hasActiveSubscription } = useUserDetails();
useEffect(() => {
if (userDetails) {
update({
name: userDetails.displayName,
Username: userDetails.username,
Email: userDetails.email,
"Subscription expiration date": userDetails?.subscription?.expiresAt,
"Payment status": userDetails?.subscription?.status,
});
}
}, [userDetails, hasActiveSubscription]);
const { lastPacket, progress } = useDownload();
const { showWarningToast } = useToast();
@@ -166,77 +188,91 @@ export function Sidebar() {
maxWidth: sidebarWidth,
}}
>
<SidebarProfile />
<div
style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}
>
<SidebarProfile />
<div className={styles.content}>
<section className={styles.section}>
<ul className={styles.menu}>
{routes.map(({ nameKey, path, render }) => (
<li
key={nameKey}
className={styles.menuItem({
active: location.pathname === path,
})}
>
<button
type="button"
className={styles.menuItemButton}
onClick={() => handleSidebarItemClick(path)}
<div className={styles.content}>
<section className={styles.section}>
<ul className={styles.menu}>
{routes.map(({ nameKey, path, render }) => (
<li
key={nameKey}
className={styles.menuItem({
active: location.pathname === path,
})}
>
{render()}
<span>{t(nameKey)}</span>
</button>
</li>
))}
</ul>
</section>
<button
type="button"
className={styles.menuItemButton}
onClick={() => handleSidebarItemClick(path)}
>
{render()}
<span>{t(nameKey)}</span>
</button>
</li>
))}
</ul>
</section>
<section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small>
<section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small>
<TextField
ref={filterRef}
placeholder={t("filter")}
onChange={handleFilter}
theme="dark"
/>
<TextField
ref={filterRef}
placeholder={t("filter")}
onChange={handleFilter}
theme="dark"
/>
<ul className={styles.menu}>
{filteredLibrary.map((game) => (
<li
key={game.id}
className={styles.menuItem({
active:
location.pathname === `/game/${game.shop}/${game.objectID}`,
muted: game.status === "removed",
})}
>
<button
type="button"
className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)}
<ul className={styles.menu}>
{filteredLibrary.map((game) => (
<li
key={game.id}
className={styles.menuItem({
active:
location.pathname ===
`/game/${game.shop}/${game.objectID}`,
muted: game.status === "removed",
})}
>
{game.iconUrl ? (
<img
className={styles.gameIcon}
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className={styles.gameIcon} />
)}
<button
type="button"
className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)}
>
{game.iconUrl ? (
<img
className={styles.gameIcon}
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className={styles.gameIcon} />
)}
<span className={styles.menuItemButtonLabel}>
{getGameTitle(game)}
</span>
</button>
</li>
))}
</ul>
</section>
<span className={styles.menuItemButtonLabel}>
{getGameTitle(game)}
</span>
</button>
</li>
))}
</ul>
</section>
</div>
</div>
{hasActiveSubscription && (
<button type="button" className={styles.helpButton} onClick={show}>
<div className={styles.helpButtonIcon}>
<CommentDiscussionIcon size={14} />
</div>
<span>{t("need_help")}</span>
</button>
)}
<button
type="button"
className={styles.handle}

View File

@@ -10,12 +10,22 @@ export interface HowLongToBeatEntry {
updatedAt: Date;
}
export interface CatalogueCache {
id?: number;
category: string;
games: { objectId: string; shop: GameShop }[];
createdAt: Date;
updatedAt: Date;
expiresAt: Date;
}
export const db = new Dexie("Hydra");
db.version(4).stores({
db.version(5).stores({
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
catalogueCache: `++id, category, games, createdAt, updatedAt, expiresAt`,
});
export const downloadSourcesTable = db.table("downloadSources");
@@ -24,4 +34,6 @@ export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>(
"howLongToBeatEntries"
);
export const catalogueCacheTable = db.table<CatalogueCache>("catalogueCache");
db.open();

View File

@@ -15,6 +15,14 @@ import * as styles from "./home.css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { buildGameDetailsPath } from "@renderer/helpers";
import { CatalogueCategory } from "@shared";
import { catalogueCacheTable, db } from "@renderer/dexie";
import { add } from "date-fns";
const categoryCacheDurationInSeconds = {
[CatalogueCategory.Hot]: 60 * 60 * 2,
[CatalogueCategory.Weekly]: 60 * 60 * 24,
[CatalogueCategory.Achievements]: 60 * 60 * 24,
};
export default function Home() {
const { t } = useTranslation("home");
@@ -36,19 +44,43 @@ export default function Home() {
[CatalogueCategory.Achievements]: [],
});
const getCatalogue = useCallback((category: CatalogueCategory) => {
setCurrentCatalogueCategory(category);
setIsLoading(true);
const getCatalogue = useCallback(async (category: CatalogueCategory) => {
try {
const catalogueCache = await catalogueCacheTable
.where("expiresAt")
.above(new Date())
.and((cache) => cache.category === category)
.first();
window.electron
.getCatalogue(category)
.then((catalogue) => {
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
})
.catch(() => {})
.finally(() => {
setIsLoading(false);
setCurrentCatalogueCategory(category);
setIsLoading(true);
if (catalogueCache)
return setCatalogue((prev) => ({
...prev,
[category]: catalogueCache.games,
}));
const catalogue = await window.electron.getCatalogue(category);
db.transaction("rw", catalogueCacheTable, async () => {
await catalogueCacheTable.where("category").equals(category).delete();
await catalogueCacheTable.add({
category,
games: catalogue,
createdAt: new Date(),
updatedAt: new Date(),
expiresAt: add(new Date(), {
seconds: categoryCacheDurationInSeconds[category],
}),
});
});
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
} finally {
setIsLoading(false);
}
}, []);
const getRandomGame = useCallback(() => {

View File

@@ -1,2 +1,10 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv {
readonly RENDERER_VITE_INTERCOM_APP_ID: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -245,6 +245,7 @@ export interface Subscription {
export interface UserDetails {
id: string;
username: string;
email: string | null;
displayName: string;
profileImageUrl: string | null;
backgroundImageUrl: string | null;
@@ -257,6 +258,7 @@ export interface UserProfile {
id: string;
displayName: string;
profileImageUrl: string | null;
email: string | null;
backgroundImageUrl: string | null;
profileVisibility: ProfileVisibility;
libraryGames: UserGame[];
@@ -373,4 +375,4 @@ export interface ComparedAchievements {
export * from "./steam.types";
export * from "./real-debrid.types";
export * from "./ludusavi.types";
export * from "./howlongtobeat.types";
export * from "./how-long-to-beat.types";

View File

@@ -1066,6 +1066,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@intercom/messenger-js-sdk@^0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.14.tgz#a27999370cc0a82a2a57a779426df25a57891863"
integrity sha512-2dH4BDAh9EI90K7hUkAdZ76W79LM45Sd1OBX7t6Vzy8twpNiQ5X+7sH9G5hlJlkSGnf+vFWlFcy9TOYAyEs1hA==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"