diff --git a/package.json b/package.json index 0fce540b..d3be2a32 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", "lodash-es": "^4.17.21", - "lottie-react": "^2.4.0", "parse-torrent": "^11.0.17", "piscina": "^4.5.1", "react-hook-form": "^7.53.0", @@ -110,6 +109,7 @@ "prettier": "^3.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "sass-embedded": "^1.80.6", "ts-node": "^10.9.2", "typescript": "^5.3.3", "vite": "^5.0.12", diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index b61c991a..5cddef5c 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -63,7 +63,7 @@ export class WindowManager { minWidth: 1024, minHeight: 540, backgroundColor: "#1c1c1c", - titleBarStyle: process.platform === "win32" ? "hidden" : "default", + titleBarStyle: process.platform === "linux" ? "default" : "hidden", ...(process.platform === "linux" ? { icon } : {}), trafficLightPosition: { x: 16, y: 16 }, titleBarOverlay: { diff --git a/src/main/workers/ludusavi.worker.ts b/src/main/workers/ludusavi.worker.ts index 4b1f339a..f101b8c9 100644 --- a/src/main/workers/ludusavi.worker.ts +++ b/src/main/workers/ludusavi.worker.ts @@ -5,6 +5,8 @@ import { workerData } from "node:worker_threads"; const { binaryPath } = workerData; +let backupGameProcess: cp.ChildProcess | null = null; + export const backupGame = ({ title, backupPath, @@ -16,15 +18,32 @@ export const backupGame = ({ preview?: boolean; winePrefix?: string; }) => { - const args = ["backup", title, "--api", "--force"]; + if (backupGameProcess && !backupGameProcess.killed) { + backupGameProcess.kill(); + backupGameProcess = null; + } - if (preview) args.push("--preview"); - if (backupPath) args.push("--path", backupPath); - if (winePrefix) args.push("--wine-prefix", winePrefix); + return new Promise((resolve, reject) => { + const args = ["backup", title, "--api", "--force"]; - const result = cp.execFileSync(binaryPath, args); + if (preview) args.push("--preview"); + if (backupPath) args.push("--path", backupPath); + if (winePrefix) args.push("--wine-prefix", winePrefix); - return JSON.parse(result.toString("utf-8")) as LudusaviBackup; + backupGameProcess = cp.execFile( + binaryPath, + args, + (err: cp.ExecFileException | null, stdout: string) => { + if (err) { + backupGameProcess = null; + return reject(err); + } + + backupGameProcess = null; + return resolve(JSON.parse(stdout) as LudusaviBackup); + } + ); + }); }; export const restoreBackup = (backupPath: string) => { diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 905bb943..7c572a56 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useRef } from "react"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; @@ -29,11 +29,6 @@ import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { downloadSourcesWorker } from "./workers"; import { repacksContext } from "./context"; import { logger } from "./logger"; -import { SubscriptionTourModal } from "./pages/shared-modals/subscription-tour-modal"; - -interface TourModals { - subscriptionModal?: boolean; -} export interface AppProps { children: React.ReactNode; @@ -77,9 +72,6 @@ export function App() { const { showSuccessToast } = useToast(); - const [showSubscritionTourModal, setShowSubscritionTourModal] = - useState(false); - useEffect(() => { Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then( ([preferences]) => { @@ -125,16 +117,6 @@ export function App() { }); }, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]); - useEffect(() => { - const tourModalsString = window.localStorage.getItem("tourModals") || "{}"; - - const tourModals = JSON.parse(tourModalsString) as TourModals; - - if (!tourModals.subscriptionModal) { - setShowSubscritionTourModal(true); - } - }, []); - const onSignIn = useCallback(() => { fetchUserDetails().then((response) => { if (response) { @@ -280,14 +262,6 @@ export function App() { }); }, [indexRepacks]); - const handleCloseSubscriptionTourModal = () => { - setShowSubscritionTourModal(false); - window.localStorage.setItem( - "tourModals", - JSON.stringify({ subscriptionModal: true } as TourModals) - ); - }; - const handleToastClose = useCallback(() => { dispatch(closeToast()); }, [dispatch]); @@ -307,11 +281,6 @@ export function App() { onClose={handleToastClose} /> - - {userDetails && ( +
{src ? ( - {alt} + {alt} ) : ( )} diff --git a/src/renderer/src/components/button/button.css.ts b/src/renderer/src/components/button/button.css.ts deleted file mode 100644 index 51f7509e..00000000 --- a/src/renderer/src/components/button/button.css.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { style, styleVariants } from "@vanilla-extract/css"; -import { SPACING_UNIT, vars } from "../../theme.css"; - -const base = style({ - padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`, - backgroundColor: vars.color.muted, - borderRadius: "8px", - border: "solid 1px transparent", - transition: "all ease 0.2s", - cursor: "pointer", - minHeight: "40px", - display: "flex", - alignItems: "center", - justifyContent: "center", - gap: `${SPACING_UNIT}px`, - ":active": { - opacity: vars.opacity.active, - }, - ":disabled": { - opacity: vars.opacity.disabled, - cursor: "not-allowed", - }, -}); - -export const button = styleVariants({ - primary: [ - base, - { - ":hover": { - backgroundColor: "#DADBE1", - }, - ":disabled": { - backgroundColor: vars.color.muted, - }, - }, - ], - outline: [ - base, - { - backgroundColor: "transparent", - border: `solid 1px ${vars.color.border}`, - color: vars.color.muted, - ":hover": { - backgroundColor: "rgba(255, 255, 255, 0.1)", - }, - ":disabled": { - backgroundColor: "transparent", - }, - }, - ], - dark: [ - base, - { - backgroundColor: vars.color.darkBackground, - color: "#c0c1c7", - }, - ], - danger: [ - base, - { - borderColor: "transparent", - backgroundColor: "#a31533", - color: "#c0c1c7", - ":hover": { - backgroundColor: "#b3203f", - }, - }, - ], -}); diff --git a/src/renderer/src/components/button/button.scss b/src/renderer/src/components/button/button.scss new file mode 100644 index 00000000..0dea5576 --- /dev/null +++ b/src/renderer/src/components/button/button.scss @@ -0,0 +1,63 @@ +@use "../../scss/globals.scss"; + +.button { + padding: globals.$spacing-unit globals.$spacing-unit * 2; + background-color: globals.$muted-color; + border-radius: 8px; + border: solid 1px transparent; + transition: all ease 0.2s; + cursor: pointer; + min-height: 40px; + display: flex; + align-items: center; + justify-content: center; + gap: globals.$spacing-unit; + + &:active { + opacity: globals.$active-opacity; + } + + &:disabled { + opacity: globals.$disabled-opacity; + cursor: not-allowed; + } + + &--primary { + &:hover { + background-color: #dadbe1; + } + + &:disabled { + background-color: globals.$muted-color; + } + } + + &--outline { + background-color: transparent; + border: solid 1px globals.$border-color; + color: globals.$muted-color; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + &:disabled { + background-color: transparent; + } + } + + &--dark { + background-color: globals.$dark-background-color; + color: globals.$muted-color; + } + + &--danger { + border-color: transparent; + background-color: globals.$danger-color; + color: globals.$muted-color; + + &:hover { + background-color: #b3203f; + } + } +} diff --git a/src/renderer/src/components/button/button.tsx b/src/renderer/src/components/button/button.tsx index 66a67889..8d8bf1dd 100644 --- a/src/renderer/src/components/button/button.tsx +++ b/src/renderer/src/components/button/button.tsx @@ -1,12 +1,13 @@ import cn from "classnames"; -import * as styles from "./button.css"; + +import "./button.scss"; export interface ButtonProps extends React.DetailedHTMLProps< React.ButtonHTMLAttributes, HTMLButtonElement > { - theme?: keyof typeof styles.button; + theme?: "primary" | "outline" | "dark" | "danger"; } export function Button({ @@ -18,7 +19,7 @@ export function Button({ return ( diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 11003295..44513093 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -21,7 +21,6 @@ import { store } from "./store"; import resources from "@locales"; import { AchievementNotification } from "./pages/achievements/notification/achievement-notification"; -import "./workers"; import { RepacksContextProvider } from "./context"; import { SuspenseWrapper } from "./components"; diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index d24051e4..70ce165f 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -11,9 +11,8 @@ import * as styles from "./game-details.css"; import { useTranslation } from "react-i18next"; import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; import { steamUrlBuilder } from "@shared"; -import Lottie from "lottie-react"; -import cloudAnimation from "@renderer/assets/lottie/cloud.json"; +import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif"; import { useUserDetails } from "@renderer/hooks"; const HERO_ANIMATION_THRESHOLD = 25; @@ -165,10 +164,9 @@ export function GameDetailsContent() { position: "relative", }} > -
diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index a4b64225..4fbcc855 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -6,9 +6,8 @@ import type { GameRepack, GameShop, Steam250Game } from "@types"; import { Button, ConfirmationModal } from "@renderer/components"; import { buildGameDetailsPath } from "@renderer/helpers"; -import starsAnimation from "@renderer/assets/lottie/stars.json"; +import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif"; -import Lottie from "lottie-react"; import { useTranslation } from "react-i18next"; import { SkeletonTheme } from "react-loading-skeleton"; import { GameDetailsSkeleton } from "./game-details-skeleton"; @@ -194,15 +193,15 @@ export default function GameDetails() {
-
{t("next_suggestion")} diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index 88eb7c63..c1b8cff3 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -1,4 +1,9 @@ -import { GearIcon, PlayIcon, PlusCircleIcon } from "@primer/octicons-react"; +import { + DownloadIcon, + GearIcon, + PlayIcon, + PlusCircleIcon, +} from "@primer/octicons-react"; import { Button } from "@renderer/components"; import { useDownload, useLibrary } from "@renderer/hooks"; import { useContext, useState } from "react"; @@ -6,7 +11,6 @@ import { useTranslation } from "react-i18next"; import * as styles from "./hero-panel-actions.css"; import { gameDetailsContext } from "@renderer/context"; -import { DownloadIcon } from "@renderer/components/sidebar/download-icon"; export function HeroPanelActions() { const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] = @@ -125,7 +129,7 @@ export function HeroPanelActions() { disabled={isGameDownloading || !repacks.length} className={styles.heroPanelAction} > - + {t("download")} ); diff --git a/src/renderer/src/pages/home/home.css.ts b/src/renderer/src/pages/home/home.css.ts index ed04de7e..4c605377 100644 --- a/src/renderer/src/pages/home/home.css.ts +++ b/src/renderer/src/pages/home/home.css.ts @@ -68,3 +68,10 @@ export const buttonsList = style({ padding: "0", gap: `${SPACING_UNIT}px`, }); + +export const flameIcon = style({ + width: "30px", + top: "-10px", + left: "-5px", + position: "absolute", +}); diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index 9fc4a5cd..c19f6fd1 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -7,12 +7,12 @@ import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; import { Button, GameCard, Hero } from "@renderer/components"; import type { Steam250Game, CatalogueEntry } from "@types"; -import starsAnimation from "@renderer/assets/lottie/stars.json"; -import flameAnimation from "@renderer/assets/lottie/flame.json"; +import flameIconStatic from "@renderer/assets/icons/flame-static.png"; +import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif"; +import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif"; import * as styles from "./home.css"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; -import Lottie, { type LottieRefCurrentProps } from "lottie-react"; import { buildGameDetailsPath } from "@renderer/helpers"; import { CatalogueCategory } from "@shared"; @@ -20,8 +20,7 @@ export default function Home() { const { t } = useTranslation("home"); const navigate = useNavigate(); - const flameAnimationRef = useRef(null); - + const [animateFlame, setAnimateFlame] = useState(false); const [isLoading, setIsLoading] = useState(false); const [randomGame, setRandomGame] = useState(null); @@ -88,13 +87,13 @@ export default function Home() { const handleMouseEnterCategory = (category: CatalogueCategory) => { if (category === CatalogueCategory.Hot) { - flameAnimationRef?.current?.play(); + setAnimateFlame(true); } }; const handleMouseLeaveCategory = (category: CatalogueCategory) => { if (category === CatalogueCategory.Hot) { - flameAnimationRef?.current?.stop(); + setAnimateFlame(false); } }; @@ -123,17 +122,17 @@ export default function Home() {
- + Flame animation
)} @@ -150,10 +149,10 @@ export default function Home() { disabled={!randomGame} >
-
{t("surprise_me")} @@ -163,10 +162,9 @@ export default function Home() {

{currentCatalogueCategory === CatalogueCategory.Hot && (
- void; -} - -export const SubscriptionTourModal = ({ - visible, - onClose, -}: UserFriendsModalProps) => { - const { t } = useTranslation("tour"); - - const handleSubscribeClick = () => { - window.electron.openCheckout().finally(onClose); - }; - - return ( - -
-
-
-

Hydra Cloud

-
    -
  • - {t("cloud_saving")} -
  • -
  • - {t("cloud_achievements")} -
  • -
  • - {t("show_and_compare_achievements")} -
  • -
  • - {t("animated_profile_banner")} -
  • -
  • - {t("animated_profile_picture")} -
  • -
  • - {t("premium_support")} -
  • -
-
-
- -
-
- ); -}; diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx index 91126923..23444e70 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx @@ -101,6 +101,7 @@ export const UserFriendModalAddFriend = ({ > {isAddingFriend ? t("sending") : t("add")} +