diff --git a/package.json b/package.json index 6dcba624..80b58b71 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.7.0", "react-hook-form": "^7.53.0", @@ -112,6 +111,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/locales/en/translation.json b/src/locales/en/translation.json index 755b1408..abecb50d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -130,7 +130,7 @@ "download": "Download", "executable_path_in_use": "Executable already in use by \"{{game}}\"", "warning": "Warning:", - "hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util its conclusion. In case Hydra closes before the conclusion, you will lose your progress.", + "hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util it's completed. If Hydra closes before completing, you will lose your progress.", "achievements": "Achievements", "achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}", "cloud_save": "Cloud save", @@ -215,7 +215,7 @@ "language": "Language", "real_debrid_api_token": "API Token", "enable_real_debrid": "Enable Real-Debrid", - "real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.", + "real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to quickly download files, only limited by your internet speed.", "real_debrid_invalid_token": "Invalid API token", "real_debrid_api_token_hint": "You can get your API token <0>here", "real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid", @@ -230,7 +230,7 @@ "download_count_one": "{{countFormatted}} download option", "download_count_other": "{{countFormatted}} download options", "download_source_url": "Download source URL", - "add_download_source_description": "Insert the URL containing the .json file", + "add_download_source_description": "Insert the URL of the .json file", "download_source_up_to_date": "Up-to-date", "download_source_errored": "Errored", "sync_download_sources": "Sync sources", @@ -249,11 +249,11 @@ "profile_visibility": "Profile visibility", "profile_visibility_description": "Choose who can see your profile and library", "required_field": "This field is required", - "source_already_exists": "This source has been already added", + "source_already_exists": "This source has already been added", "must_be_valid_url": "The source must be a valid URL", "blocked_users": "Blocked users", "user_unblocked": "User has been unblocked", - "enable_achievement_notifications": "When an achievement in unlocked", + "enable_achievement_notifications": "When an achievement is unlocked", "launch_minimized": "Launch Hydra minimized" }, "notifications": { @@ -289,7 +289,7 @@ "amount_hours": "{{amount}} hours", "amount_minutes": "{{amount}} minutes", "last_time_played": "Last played {{period}}", - "activity": "Recent activity", + "activity": "Recent Activity", "library": "Library", "total_play_time": "Total playtime: {{amount}}", "no_recent_activity_title": "Hmmm… nothing here", @@ -328,7 +328,7 @@ "user_block_modal_text": "This will block {{displayName}}", "blocked_users": "Blocked users", "unblock": "Unblock", - "no_friends_added": "You still don't have added friends", + "no_friends_added": "You have no added friends", "pending": "Pending", "no_pending_invites": "You have no pending invites", "no_blocked_users": "You have no blocked users", diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index ecd805f6..8da2dd5e 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -47,7 +47,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 581e3ce8..fc0a0176 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -20,7 +20,6 @@ import { store } from "./store"; import resources from "@locales"; -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")} +