diff --git a/.env.example b/.env.example index 8ea7af55..3f914eb3 100644 --- a/.env.example +++ b/.env.example @@ -3,5 +3,3 @@ MAIN_VITE_AUTH_URL= MAIN_VITE_WS_URL= RENDERER_VITE_REAL_DEBRID_REFERRAL_ID= RENDERER_VITE_TORBOX_REFERRAL_CODE= -VITE_GG_DEALS_API_URL=https://api.gg.deals/v1/prices/by-steam-app-id -VITE_GG_DEALS_API_KEY= \ No newline at end of file diff --git a/package.json b/package.json index 3186535f..e21c962a 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "diskusage": "^1.2.0", "electron-log": "^5.2.4", "electron-updater": "^6.6.2", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", "file-type": "^20.5.0", "framer-motion": "^12.15.0", "i18next": "^23.11.2", @@ -63,6 +65,8 @@ "lodash-es": "^4.17.21", "parse-torrent": "^11.0.18", "rc-virtual-list": "^3.18.3", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-hook-form": "^7.53.0", "react-i18next": "^14.1.0", "react-loading-skeleton": "^3.4.0", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 781cd946..ea7fff89 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Successfully signed in" }, "home": { - "featured": "Featured", "surprise_me": "Surprise me", "no_results": "No results found", "start_typing": "Starting typing to search...", @@ -241,7 +240,6 @@ "keyshop_price": "Keyshop price", "historical_retail": "Historical retail", "historical_keyshop": "Historical keyshop", - "supported_languages": "Supported languages", "language": "Language", "caption": "Caption", "audio": "Audio" @@ -290,7 +288,6 @@ "change": "Update", "notifications": "Notifications", "enable_download_notifications": "When a download is complete", - "gg_deals_api_key_description": "gg deals api key. Used to show the lowest price. (https://gg.deals/api/)", "enable_repack_list_notifications": "When a new repack is added", "real_debrid_api_token_label": "Real-Debrid API token", "quit_app_instead_hiding": "Don't hide Hydra when closing", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index fd6fbd97..7f7f8cc1 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Autenticado com sucesso" }, "home": { - "featured": "Destaques", "hot": "Populares", "weekly": "📅 Mais baixados da semana", "achievements": "🏆 Pra platinar", @@ -218,7 +217,6 @@ "keyshop_price": "Preço em keyshops", "historical_retail": "Preço histórico de lojas oficiais", "historical_keyshop": "Preço histórico em keyshops", - "supported_languages": "Idiomas suportados", "language": "Idioma", "caption": "Legenda", "audio": "Áudio" @@ -267,7 +265,6 @@ "change": "Explorar...", "notifications": "Notificações", "enable_download_notifications": "Quando um download for concluído", - "gg_deals_api_key_description": "gg deals api key. Usado para mostrar o menor preço. (https://gg.deals/api/)", "enable_repack_list_notifications": "Quando a lista de repacks for atualizada", "real_debrid_api_token_label": "Token de API do Real-Debrid", "quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar", diff --git a/src/renderer/src/app.scss b/src/renderer/src/app.scss index 18d46dd4..4c5374e8 100644 --- a/src/renderer/src/app.scss +++ b/src/renderer/src/app.scss @@ -10,16 +10,16 @@ } ::-webkit-scrollbar-track { - background-color: rgba(255, 255, 255, 0.03); + background-color: rgba(0, 0, 0, 0.2); } ::-webkit-scrollbar-thumb { - background-color: rgba(255, 255, 255, 0.08); + background-color: rgba(255, 255, 255, 0.15); border-radius: 24px; } ::-webkit-scrollbar-thumb:hover { - background-color: rgba(255, 255, 255, 0.16); + background-color: rgba(255, 255, 255, 0.25); } html, diff --git a/src/renderer/src/components/badge/badge.scss b/src/renderer/src/components/badge/badge.scss index 69c43b3e..f90f8749 100644 --- a/src/renderer/src/components/badge/badge.scss +++ b/src/renderer/src/components/badge/badge.scss @@ -4,9 +4,14 @@ color: globals.$muted-color; font-size: 10px; padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit; - border: solid 1px globals.$muted-color; - border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 6px; display: flex; gap: 4px; align-items: center; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: all ease 0.2s; } diff --git a/src/renderer/src/components/hero/hero.scss b/src/renderer/src/components/hero/hero.scss index ea14c059..f9ec4d36 100644 --- a/src/renderer/src/components/hero/hero.scss +++ b/src/renderer/src/components/hero/hero.scss @@ -2,16 +2,36 @@ .hero { width: 100%; - height: 280px; - min-height: 280px; - max-height: 280px; - border-radius: 4px; + height: 180px; + min-height: 150px; + border-radius: 0; color: #dadbe1; overflow: hidden; box-shadow: 0px 0px 15px 0px #000000; cursor: pointer; border: solid 1px globals.$border-color; z-index: 1; + flex-shrink: 0; + + @media (min-width: 480px) { + height: 220px; + min-height: 200px; + } + + @media (min-width: 768px) { + height: 300px; + min-height: 300px; + } + + @media (min-width: 1024px) and (min-height: 800px) { + height: 400px; + min-height: 400px; + } + + @media (min-width: 1024px) and (max-height: 799px) { + height: 300px; + min-height: 250px; + } &__media { object-fit: cover; @@ -47,10 +67,42 @@ &__content { width: 100%; height: 100%; - padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3); - gap: calc(globals.$spacing-unit * 2); + padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2); + gap: calc(globals.$spacing-unit); display: flex; flex-direction: column; justify-content: flex-end; + + @media (min-width: 768px) { + padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 3); + gap: calc(globals.$spacing-unit * 1.5); + } + + @media (min-width: 1024px) { + padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3); + gap: calc(globals.$spacing-unit * 2); + } + } + + &__logo { + max-width: 100%; + height: auto; + width: 120px; + + @media (min-width: 480px) { + width: 150px; + } + + @media (min-width: 768px) { + width: 200px; + } + + @media (min-width: 1024px) and (min-height: 800px) { + width: 250px; + } + + @media (min-width: 1024px) and (max-height: 799px) { + width: 200px; + } } } diff --git a/src/renderer/src/components/hero/hero.tsx b/src/renderer/src/components/hero/hero.tsx index f177c598..ce73d144 100644 --- a/src/renderer/src/components/hero/hero.tsx +++ b/src/renderer/src/components/hero/hero.tsx @@ -53,6 +53,7 @@ export function Hero() { width="250px" alt={game.description ?? ""} loading="eager" + className="hero__logo" />

{game.description}

diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index ce2923b2..864fd482 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -38,14 +38,12 @@ export const gameDetailsContext = createContext({ isGameRunning: false, isLoading: false, objectId: undefined, - gameColor: "", showRepacksModal: false, showGameOptionsModal: false, stats: null, achievements: null, hasNSFWContentBlocked: false, lastDownloadedOption: null, - setGameColor: () => {}, selectGameExecutable: async () => null, updateGame: async () => {}, setShowGameOptionsModal: () => {}, @@ -82,7 +80,6 @@ export function GameDetailsContextProvider({ const [stats, setStats] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [gameColor, setGameColor] = useState(""); const [isGameRunning, setIsGameRunning] = useState(false); const [showRepacksModal, setShowRepacksModal] = useState(false); const [showGameOptionsModal, setShowGameOptionsModal] = useState(false); @@ -286,7 +283,6 @@ export function GameDetailsContextProvider({ isGameRunning, isLoading, objectId, - gameColor, showGameOptionsModal, showRepacksModal, stats, @@ -294,7 +290,6 @@ export function GameDetailsContextProvider({ hasNSFWContentBlocked, lastDownloadedOption, setHasNSFWContentBlocked, - setGameColor, selectGameExecutable, updateGame, setShowRepacksModal, diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts index 99c7b293..302460b7 100644 --- a/src/renderer/src/context/game-details/game-details.context.types.ts +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -16,14 +16,12 @@ export interface GameDetailsContext { isGameRunning: boolean; isLoading: boolean; objectId: string | undefined; - gameColor: string; showRepacksModal: boolean; showGameOptionsModal: boolean; stats: GameStats | null; achievements: UserAchievement[] | null; hasNSFWContentBlocked: boolean; lastDownloadedOption: GameRepack | null; - setGameColor: React.Dispatch>; selectGameExecutable: () => Promise; updateGame: () => Promise; setShowRepacksModal: React.Dispatch>; diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index 477925c7..e555212c 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -119,15 +119,8 @@ export function AchievementsContent({ const containerRef = useRef(null); const [isHeaderStuck, setIsHeaderStuck] = useState(false); - const { - gameTitle, - objectId, - shop, - shopDetails, - achievements, - gameColor, - setGameColor, - } = useContext(gameDetailsContext); + const { gameTitle, objectId, shop, shopDetails, achievements } = + useContext(gameDetailsContext); const dispatch = useAppDispatch(); @@ -136,22 +129,6 @@ export function AchievementsContent({ dispatch(setHeaderTitle(gameTitle)); }, [dispatch, gameTitle]); - const handleHeroLoad = async () => { - const output = await average( - shopDetails?.assets?.libraryHeroImageUrl ?? "", - { - amount: 1, - format: "hex", - } - ); - - const backgroundColor = output - ? (new Color(output).darken(0.7).toString() as string) - : ""; - - setGameColor(backgroundColor); - }; - const onScroll: React.UIEventHandler = (event) => { const heroHeight = heroRef.current?.clientHeight ?? 150; @@ -191,7 +168,6 @@ export function AchievementsContent({ src={shopDetails?.assets?.libraryHeroImageUrl ?? ""} className="achievements-content__achievements-list__image" alt={gameTitle} - onLoad={handleHeroLoad} />
-
+
(null); - const mediaContainerRef = useRef(null); - const { t } = useTranslation("game_details"); const hasScreenshots = shopDetails && shopDetails.screenshots?.length; - const hasMovies = shopDetails && shopDetails.movies?.length; - const mediaCount = useMemo(() => { - if (!shopDetails) return 0; + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false }); + const [selectedIndex, setSelectedIndex] = useState(0); - if (shopDetails.screenshots && shopDetails.movies) { - return shopDetails.screenshots.length + shopDetails.movies.length; - } else if (shopDetails.movies) { - return shopDetails.movies.length; - } else if (shopDetails.screenshots) { - return shopDetails.screenshots.length; - } + const scrollPrev = useCallback(() => { + if (emblaApi) emblaApi.scrollPrev(); + }, [emblaApi]); - return 0; - }, [shopDetails]); + const scrollNext = useCallback(() => { + if (emblaApi) emblaApi.scrollNext(); + }, [emblaApi]); - const [mediaIndex, setMediaIndex] = useState(0); - const [showArrows, setShowArrows] = useState(false); + const scrollTo = useCallback( + (index: number) => { + if (emblaApi) emblaApi.scrollTo(index); + }, + [emblaApi] + ); - const showNextImage = () => { - setMediaIndex((index: number) => { - if (index === mediaCount - 1) return 0; + const scrollToPreview = useCallback( + (index: number, event: React.MouseEvent) => { + scrollTo(index); - return index + 1; - }); - }; + const button = event.currentTarget; + const previewContainer = button.parentElement; - const showPrevImage = () => { - setMediaIndex((index: number) => { - if (index === 0) return mediaCount - 1; + if (previewContainer) { + const containerRect = previewContainer.getBoundingClientRect(); + const buttonRect = button.getBoundingClientRect(); - return index - 1; - }); - }; + const isOffScreenLeft = buttonRect.left < containerRect.left; + const isOffScreenRight = buttonRect.right > containerRect.right; - useEffect(() => { - setMediaIndex(0); - }, [shopDetails]); - - useEffect(() => { - if (hasMovies && mediaContainerRef.current) { - mediaContainerRef.current.childNodes.forEach((node, index) => { - if (node instanceof HTMLVideoElement) { - if (index !== mediaIndex) { - node.pause(); - } + if (isOffScreenLeft || isOffScreenRight) { + button.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); } + } + }, + [scrollTo] + ); + + useEffect(() => { + if (!emblaApi) return; + + let isInitialLoad = true; + + const onSelect = () => { + const newIndex = emblaApi.selectedScrollSnap(); + setSelectedIndex(newIndex); + + if (!isInitialLoad) { + const videos = document.querySelectorAll(".gallery-slider__media"); + videos.forEach((video) => { + if (video instanceof HTMLVideoElement) { + video.pause(); + } + }); + } + + isInitialLoad = false; + }; + + emblaApi.on("select", onSelect); + onSelect(); + + return () => { + emblaApi.off("select", onSelect); + }; + }, [emblaApi]); + + const mediaItems = useMemo(() => { + const items: Array<{ + id: string; + type: "video" | "image"; + src?: string; + poster?: string; + videoSrc?: string; + alt: string; + }> = []; + + if (shopDetails?.movies) { + shopDetails.movies.forEach((video, index) => { + items.push({ + id: String(video.id), + type: "video", + poster: video.thumbnail, + videoSrc: video.mp4.max.startsWith("http://") + ? video.mp4.max.replace("http://", "https://") + : video.mp4.max, + alt: t("video", { number: String(index + 1) }), + }); }); } - }, [hasMovies, mediaContainerRef, mediaIndex]); - useEffect(() => { - if (scrollContainerRef.current) { - const container = scrollContainerRef.current; - const totalWidth = container.scrollWidth - container.clientWidth; - const itemWidth = totalWidth / (mediaCount - 1); - const scrollLeft = mediaIndex * itemWidth; - container.scrollLeft = scrollLeft; + if (shopDetails?.screenshots) { + shopDetails.screenshots.forEach((image, index) => { + items.push({ + id: String(image.id), + type: "image", + src: image.path_full, + alt: t("screenshot", { number: String(index + 1) }), + }); + }); } - }, [shopDetails, mediaIndex, mediaCount]); + + return items; + }, [shopDetails, t]); const previews = useMemo(() => { const screenshotPreviews = shopDetails?.screenshots?.map(({ id, path_thumbnail }) => ({ id, thumbnail: path_thumbnail, + type: "image" as const, })) ?? []; if (shopDetails?.movies) { const moviePreviews = shopDetails.movies.map(({ id, thumbnail }) => ({ id, thumbnail, + type: "video" as const, })); return [...moviePreviews, ...screenshotPreviews]; @@ -93,96 +147,87 @@ export function GallerySlider() { return screenshotPreviews; }, [shopDetails]); + if (!hasScreenshots) { + return null; + } + return ( - <> - {hasScreenshots && ( -
-
setShowArrows(true)} - onMouseLeave={() => setShowArrows(false)} - className="gallery-slider__animation-container" - ref={mediaContainerRef} - > - {shopDetails.movies && - shopDetails.movies.map((video) => ( +
+
+
+ {mediaItems.map((item) => ( +
+ {item.type === "video" ? ( - ))} - - {hasScreenshots && - shopDetails.screenshots?.map((image, i) => ( + ) : ( {t("screenshot", - ))} - - - - -
- -
- {previews.map((media, i) => ( - - ))} -
+ )} +
+ ))}
- )} - + + + + +
+ +
+ {previews.map((media, i) => ( + + ))} +
+
); } 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 40436614..1ce80da4 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -1,6 +1,4 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { average } from "color.js"; -import Color from "color"; import { HeroPanel } from "./hero"; import { DescriptionHeader } from "./description-header/description-header"; @@ -21,14 +19,8 @@ export function GameDetailsContent() { const { t } = useTranslation("game_details"); - const { - objectId, - shopDetails, - game, - gameColor, - setGameColor, - hasNSFWContentBlocked, - } = useContext(gameDetailsContext); + const { objectId, shopDetails, game, hasNSFWContentBlocked } = + useContext(gameDetailsContext); const { showHydraCloudModal } = useSubscription(); @@ -58,22 +50,6 @@ export function GameDetailsContent() { const [backdropOpacity, setBackdropOpacity] = useState(1); - const handleHeroLoad = async () => { - const output = await average( - shopDetails?.assets?.libraryHeroImageUrl ?? "", - { - amount: 1, - format: "hex", - } - ); - - const backgroundColor = output - ? new Color(output).darken(0.7).toString() - : ""; - - setGameColor(backgroundColor); - }; - useEffect(() => { setBackdropOpacity(1); }, [objectId]); @@ -106,12 +82,10 @@ export function GameDetailsContent() { src={shopDetails?.assets?.libraryHeroImageUrl ?? ""} className="game-details__hero-image" alt={game?.title} - onLoad={handleHeroLoad} />
diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index 899d654a..e488db5f 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -18,7 +18,6 @@ $hero-height: 300px; &__wrapper { display: flex; flex-direction: column; - overflow: hidden; width: 100%; height: 100%; transition: all ease 0.3s; @@ -64,8 +63,8 @@ $hero-height: 300px; &__hero-image { width: 100%; - height: $hero-height; - min-height: $hero-height; + height: calc($hero-height + 72px); + min-height: calc($hero-height + 72px); object-fit: cover; object-position: top; transition: all ease 0.2s; @@ -74,8 +73,8 @@ $hero-height: 300px; @media (min-width: 1250px) { object-position: center; - height: 350px; - min-height: 350px; + height: calc(350px + 72px); + min-height: calc(350px + 72px); } } @@ -97,7 +96,6 @@ $hero-height: 300px; height: 100%; display: flex; flex-direction: column; - overflow: auto; z-index: 1; } @@ -105,6 +103,7 @@ $hero-height: 300px; display: flex; width: 100%; flex: 1; + min-width: 0; background: linear-gradient( 0deg, globals.$background-color 50%, @@ -115,6 +114,8 @@ $hero-height: 300px; &__description-content { width: 100%; height: 100%; + min-width: 0; + flex: 1; } &__description { diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.scss b/src/renderer/src/pages/game-details/hero/hero-panel.scss index 066ce196..4dd1cc22 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.scss +++ b/src/renderer/src/pages/game-details/hero/hero-panel.scss @@ -5,18 +5,24 @@ height: 72px; min-height: 72px; padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3); - background-color: globals.$dark-background-color; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: solid 1px rgba(255, 255, 255, 0.15); display: flex; align-items: center; justify-content: space-between; transition: all ease 0.2s; border-bottom: solid 1px globals.$border-color; - position: sticky; overflow: hidden; top: 0; z-index: 2; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); &--stuck { + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8); } diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.tsx b/src/renderer/src/pages/game-details/hero/hero-panel.tsx index 3a07daa1..7f8de0b0 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel.tsx @@ -14,7 +14,7 @@ export function HeroPanel() { const { formatDate } = useDate(); - const { game, repacks, gameColor } = useContext(gameDetailsContext); + const { game, repacks } = useContext(gameDetailsContext); const { lastPacket } = useDownload(); @@ -50,7 +50,7 @@ export function HeroPanel() { game?.download?.status === "paused"; return ( -
+
{getInfo()}
diff --git a/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss b/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss index 5ea421c3..8674b044 100644 --- a/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss +++ b/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss @@ -2,8 +2,7 @@ .sidebar-section { &__button { - height: 72px; - padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2); + padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2); display: flex; align-items: center; background-color: globals.$background-color; diff --git a/src/renderer/src/pages/game-details/sidebar/game-language-section.scss b/src/renderer/src/pages/game-details/sidebar/game-language-section.scss new file mode 100644 index 00000000..896316ec --- /dev/null +++ b/src/renderer/src/pages/game-details/sidebar/game-language-section.scss @@ -0,0 +1,85 @@ +@use "../../../scss/globals.scss"; + +.game-language-section { + background-color: rgba(255, 255, 255, 0.02); + overflow: hidden; + + &__header { + display: flex; + background-color: rgba(255, 255, 255, 0.05); + border-bottom: 1px solid globals.$border-color; + } + + &__header-item { + display: flex; + align-items: center; + color: globals.$muted-color; + font-size: globals.$small-font-size; + font-weight: 600; + flex: 1; + padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2.5); + + &--center { + justify-content: flex-start; + flex: 0 0 60px; + } + } + + &__content { + display: flex; + flex-direction: column; + } + + &__row { + display: flex; + transition: background-color 0.2s ease; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + + &:hover { + background-color: rgba(255, 255, 255, 0.03); + } + + &:last-child { + border-bottom: none; + } + } + + &__cell { + padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2.5); + font-size: globals.$body-font-size; + color: globals.$body-color; + display: flex; + align-items: center; + flex: 1; + + &--language { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &--center { + justify-content: flex-start; + flex: 0 0 60px; + } + } + + &__check { + color: globals.$body-color; + opacity: 0.8; + } + + &__cross { + color: globals.$body-color; + opacity: 0.8; + } + + @media (max-width: 320px) { + &__header, + &__cell { + padding: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 0.5); + font-size: calc(globals.$small-font-size * 0.9); + } + } +} diff --git a/src/renderer/src/pages/game-details/sidebar/game-language-section.tsx b/src/renderer/src/pages/game-details/sidebar/game-language-section.tsx index f67e4dfa..874a588e 100755 --- a/src/renderer/src/pages/game-details/sidebar/game-language-section.tsx +++ b/src/renderer/src/pages/game-details/sidebar/game-language-section.tsx @@ -1,65 +1,71 @@ -import { useContext } from "react"; +import { useContext, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { CheckIcon, XIcon } from "@primer/octicons-react"; import { gameDetailsContext } from "@renderer/context/game-details/game-details.context"; import { SidebarSection } from "../sidebar-section/sidebar-section"; +import "./game-language-section.scss"; export function GameLanguageSection() { const { t } = useTranslation("game_details"); - const { shopDetails, objectId } = useContext(gameDetailsContext); + const { shopDetails } = useContext(gameDetailsContext); - const getLanguages = () => { - let languages = shopDetails?.supported_languages; - if (!languages) return []; - languages = languages?.split("
")[0]; - const arrayIdiomas = languages?.split(","); - const listLanguages: { - language: string; - caption: string; - audio: string; - }[] = []; - arrayIdiomas?.forEach((lang) => { - const objectLanguage = { - language: lang.replace("*", ""), - caption: "✔", - audio: lang.includes("*") ? "✔" : "", - }; - listLanguages.push(objectLanguage); - }); - return listLanguages; - }; + const languages = useMemo(() => { + const supportedLanguages = shopDetails?.supported_languages; + if (!supportedLanguages) return []; + + const languagesString = supportedLanguages.split("
")[0]; + const languageArray = languagesString?.split(",") || []; + + return languageArray.map((lang) => ({ + language: lang.replace("*", "").trim(), + hasAudio: lang.includes("*"), + })); + }, [shopDetails?.supported_languages]); + + if (languages.length === 0) { + return null; + } return ( -
-

{t("supported_languages")}

- - - - - - - - - - {getLanguages().map((lang) => ( - - - - - - ))} - -
{t("language")}{t("caption")}{t("audio")}
{lang.language}{lang.caption}{lang.audio}
-
-
- - Link Steam - +
+
+
+ {t("language")} +
+
+ {t("caption")} +
+
+ {t("audio")} +
+
+ +
+ {languages.map((lang) => ( +
+
+ {lang.language} +
+
+ +
+
+ {lang.hasAudio ? ( + + ) : ( + + )} +
+
+ ))} +
); diff --git a/src/renderer/src/pages/game-details/sidebar/game-prices-section.tsx b/src/renderer/src/pages/game-details/sidebar/game-prices-section.tsx deleted file mode 100755 index 0753cdad..00000000 --- a/src/renderer/src/pages/game-details/sidebar/game-prices-section.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useCallback, useContext, useEffect, useState } from "react"; -import { SidebarSection } from "../sidebar-section/sidebar-section"; -import { useTranslation } from "react-i18next"; -import { gameDetailsContext } from "@renderer/context/game-details/game-details.context"; -import { useAppSelector } from "@renderer/hooks"; - -export function GamePricesSection() { - const userPreferences = useAppSelector( - (state) => state.userPreferences.value - ); - const { t } = useTranslation("game_details"); - const [priceData, setPriceData] = useState(null); - const [isLoadingPrices, setIsLoadingPrices] = useState(false); - const { objectId } = useContext(gameDetailsContext); - - const fetchGamePrices = useCallback(async (steamAppId: string) => { - setIsLoadingPrices(true); - try { - const apiKey = - userPreferences?.ggDealsApiKey || import.meta.env.VITE_GG_DEALS_API_KEY; - if (!apiKey) { - setPriceData(null); - setIsLoadingPrices(false); - return; - } - const url = `${import.meta.env.VITE_GG_DEALS_API_URL}/?ids=${steamAppId}&key=${apiKey}®ion=br`; - const response = await fetch(url); - if (!response.ok) { - throw new Error("Network response was not ok"); - } - const data = await response.json(); - setPriceData(data.data?.[steamAppId] ?? null); - } catch (error) { - setPriceData(null); - } finally { - setIsLoadingPrices(false); - } - }, []); - - useEffect(() => { - if (objectId) { - fetchGamePrices(objectId.toString()); - } - }, [objectId, fetchGamePrices]); - - return ( - - {isLoadingPrices ? ( -
{t("loading")}
- ) : priceData ? ( -
-
    -
  • - {t("retail_price")}: {t("currency_symbol")} - {priceData.prices.currentRetail} -
  • -
  • - {t("keyshop_price")}: {t("currency_symbol")} - {priceData.prices.currentKeyshops} -
  • -
  • - {t("historical_retail")}: {t("currency_symbol")} - {priceData.prices.historicalRetail} -
  • -
  • - {t("historical_keyshop")}: {t("currency_symbol")} - {priceData.prices.historicalKeyshops} -
  • -
  • - - {t("view_all_prices")} - -
  • -
-
- ) : ( -
{t("no_prices_found")}
- )} -
- ); -} diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.scss b/src/renderer/src/pages/game-details/sidebar/sidebar.scss index 84386f12..d1c54f84 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.scss +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.scss @@ -3,17 +3,30 @@ .content-sidebar { border-left: solid 1px globals.$border-color; background-color: globals.$dark-background-color; - width: 100%; height: 100%; + flex-shrink: 0; + width: 280px; @media (min-width: 1024px) { - max-width: 300px; - width: 100%; + width: 320px; } @media (min-width: 1280px) { - width: 100%; - max-width: 400px; + width: 380px; + } + + @media (min-width: 1440px) { + width: 420px; + } + + @media (max-width: 768px) { + width: 35%; + min-width: 220px; + } + + @media (max-width: 480px) { + width: 40%; + min-width: 200px; } } @@ -194,25 +207,3 @@ .achievements-placeholder__blur { filter: blur(4px); } - -.table-languages { - width: 100%; - border-collapse: collapse; - text-align: left; - - th, - td { - padding: globals.$spacing-unit; - border-bottom: solid 1px globals.$border-color; - } - - th { - font-size: globals.$small-font-size; - color: globals.$muted-color; - font-weight: normal; - } - - td { - font-size: globals.$body-font-size; - } -} diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index af72dc92..0a24c418 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -21,7 +21,6 @@ import { SidebarSection } from "../sidebar-section/sidebar-section"; import { buildGameAchievementPath } from "@renderer/helpers"; import { useSubscription } from "@renderer/hooks/use-subscription"; import "./sidebar.scss"; -import { GamePricesSection } from "./game-prices-section"; import { GameLanguageSection } from "./game-language-section"; const achievementsPlaceholder: UserAchievement[] = [ @@ -117,9 +116,6 @@ export function Sidebar() { return ( ); } diff --git a/src/renderer/src/pages/home/home.scss b/src/renderer/src/pages/home/home.scss index 878b84f1..497f074e 100644 --- a/src/renderer/src/pages/home/home.scss +++ b/src/renderer/src/pages/home/home.scss @@ -6,8 +6,8 @@ height: 100%; display: flex; flex-direction: column; - gap: calc(globals.$spacing-unit * 3); - padding: calc(globals.$spacing-unit * 3); + gap: calc(globals.$spacing-unit * 2); + padding: 0; flex: 1; overflow-y: auto; } @@ -17,6 +17,7 @@ gap: globals.$spacing-unit; justify-content: space-between; align-items: center; + padding: calc(globals.$spacing-unit * 3); } &__buttons-list { @@ -27,25 +28,6 @@ gap: globals.$spacing-unit; } - &__cards { - display: grid; - grid-template-columns: repeat(1, 1fr); - gap: calc(globals.$spacing-unit * 2); - transition: all ease 0.2s; - - @media (min-width: 768px) { - grid-template-columns: repeat(2, 1fr); - } - - @media (min-width: 1250px) { - grid-template-columns: repeat(3, 1fr); - } - - @media (min-width: 1600px) { - grid-template-columns: repeat(4, 1fr); - } - } - &__card-skeleton { width: 100%; height: 180px; @@ -99,5 +81,26 @@ &__title { display: flex; gap: globals.$spacing-unit; + padding: 0 calc(globals.$spacing-unit * 3); + } + + &__cards { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: calc(globals.$spacing-unit * 2); + transition: all ease 0.2s; + padding: 0 calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 3); + + @media (min-width: 768px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1250px) { + grid-template-columns: repeat(3, 1fr); + } + + @media (min-width: 1600px) { + grid-template-columns: repeat(4, 1fr); + } } } diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index ccf81566..e2f66283 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -97,8 +97,6 @@ export default function Home() { return (
-

{t("featured")}

-
diff --git a/src/renderer/src/pages/profile/profile-content/friends-box.tsx b/src/renderer/src/pages/profile/profile-content/friends-box.tsx index 8ab3808a..bee4b35c 100644 --- a/src/renderer/src/pages/profile/profile-content/friends-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/friends-box.tsx @@ -31,10 +31,14 @@ export function FriendsBox() { return (
-

{t("friends")}

- {userStats && ( - {numberFormatter.format(userStats.friendsCount)} - )} +
+

{t("friends")}

+ {userStats && ( + + {numberFormatter.format(userStats.friendsCount)} + + )} +
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss index bd580b74..c3c71d9a 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.scss +++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss @@ -151,5 +151,29 @@ @container #{globals.$app-container} (min-width: 3000px) { grid-template-columns: repeat(12, 1fr); } + + &--drag-over { + background: rgba(255, 255, 255, 0.05); + border: 2px dashed rgba(255, 255, 255, 0.3); + position: relative; + transition: all ease 0.2s; + + &::before { + content: "Drop here to " attr(data-action); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: globals.$muted-color; + font-size: 14px; + font-weight: 500; + z-index: 10; + pointer-events: none; + background: rgba(0, 0, 0, 0.8); + padding: 8px 16px; + border-radius: 4px; + backdrop-filter: blur(10px); + } + } } } diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index ab1f3456..f072fdd5 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -7,10 +7,26 @@ position: relative; display: flex; transition: all ease 0.2s; + cursor: grab; &:hover { transform: scale(1.05); } + + &:active { + cursor: grabbing; + transform: scale(1.02); + } + + &[draggable="true"] { + cursor: grab; + + &:active { + cursor: grabbing; + opacity: 0.8; + transform: scale(1.02) rotate(2deg); + } + } } &__cover { @@ -75,29 +91,47 @@ } &__favorite-icon { - color: white; - background-color: rgba(0, 0, 0, 0.7); + color: rgba(255, 255, 255, 0.8); + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: solid 1px rgba(255, 255, 255, 0.15); border-radius: 50%; padding: 6px; display: flex; align-items: center; justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all ease 0.2s; + + &:hover { + background: rgba(0, 0, 0, 0.5); + border-color: rgba(255, 255, 255, 0.25); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + } } &__pin-button { - color: white; - background-color: rgba(0, 0, 0, 0.7); - border: none; + color: rgba(255, 255, 255, 0.8); + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: solid 1px rgba(255, 255, 255, 0.15); border-radius: 50%; padding: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; - transition: background-color 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all ease 0.2s; &:hover { - background-color: rgba(0, 0, 0, 0.9); + background: rgba(0, 0, 0, 0.5); + border-color: rgba(255, 255, 255, 0.25); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } &:disabled { @@ -107,14 +141,25 @@ } &__playtime { - background-color: globals.$background-color; - color: globals.$muted-color; - border: solid 1px globals.$border-color; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + color: rgba(255, 255, 255, 0.8); + border: solid 1px rgba(255, 255, 255, 0.15); border-radius: 4px; display: flex; align-items: center; gap: 4px; padding: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all ease 0.2s; + + &:hover { + background: rgba(0, 0, 0, 0.5); + border-color: rgba(255, 255, 255, 0.25); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + } } &__manual-playtime { color: globals.$warning-color; diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index b952dfa0..c698440d 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -35,7 +35,6 @@ export function SettingsGeneral() { const [form, setForm] = useState({ downloadsPath: "", - ggDealsApiKey: "", downloadNotificationsEnabled: false, repackUpdatesNotificationsEnabled: false, friendRequestNotificationsEnabled: false, @@ -101,7 +100,6 @@ export function SettingsGeneral() { setForm((prev) => ({ ...prev, downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath, - ggDealsApiKey: userPreferences.ggDealsApiKey ?? "", downloadNotificationsEnabled: userPreferences.downloadNotificationsEnabled ?? false, repackUpdatesNotificationsEnabled: @@ -208,12 +206,6 @@ export function SettingsGeneral() { } /> - handleChange({ ggDealsApiKey: e.target.value })} - /> -