mirror of
				https://github.com/hydralauncher/hydra.git
				synced 2025-03-09 15:40:26 +00:00 
			
		
		
		
	Merge branch 'feature/custom-themes' into feat/polychrome
This commit is contained in:
		
							parent
							
								
									0f0a67b55e
								
							
						
					
					
						commit
						d4c414b96b
					
				
					 174 changed files with 4791 additions and 4126 deletions
				
			
		
							
								
								
									
										136
									
								
								src/renderer/src/app.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/renderer/src/app.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,136 @@
 | 
			
		|||
@use "./scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
* {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar {
 | 
			
		||||
  width: 9px;
 | 
			
		||||
  background-color: globals.$dark-background-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar-track {
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.03);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar-thumb {
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.08);
 | 
			
		||||
  border-radius: 24px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar-thumb:hover {
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.16);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html,
 | 
			
		||||
body,
 | 
			
		||||
#root,
 | 
			
		||||
main {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  font-family:
 | 
			
		||||
    Noto Sans,
 | 
			
		||||
    sans-serif;
 | 
			
		||||
  font-size: globals.$body-font-size;
 | 
			
		||||
  color: globals.$body-color;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
  border: none;
 | 
			
		||||
  font-family: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3,
 | 
			
		||||
h4,
 | 
			
		||||
h5,
 | 
			
		||||
h6,
 | 
			
		||||
p {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
  line-height: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#root,
 | 
			
		||||
main {
 | 
			
		||||
  display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#root {
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input::-webkit-outer-spin-button,
 | 
			
		||||
input::-webkit-inner-spin-button {
 | 
			
		||||
  -webkit-appearance: none;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
label {
 | 
			
		||||
  font-size: globals.$body-font-size;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
img {
 | 
			
		||||
  -webkit-user-drag: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
progress[value] {
 | 
			
		||||
  -webkit-appearance: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  container-name: globals.$app-container;
 | 
			
		||||
  container-type: inline-size;
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: linear-gradient(
 | 
			
		||||
      0deg,
 | 
			
		||||
      globals.$dark-background-color 50%,
 | 
			
		||||
      globals.$background-color 100%
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title-bar {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 35px;
 | 
			
		||||
  min-height: 35px;
 | 
			
		||||
  background-color: globals.$dark-background-color;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding: 0 calc(globals.$spacing-unit * 2);
 | 
			
		||||
  -webkit-app-region: drag;
 | 
			
		||||
  z-index: 4;
 | 
			
		||||
  border-bottom: 1px solid globals.$border-color;
 | 
			
		||||
 | 
			
		||||
  &__cloud-text {
 | 
			
		||||
    background: linear-gradient(270deg, #16b195 50%, #3e62c0 100%);
 | 
			
		||||
    background-clip: text;
 | 
			
		||||
    color: transparent;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -12,8 +12,6 @@ import {
 | 
			
		|||
  useUserDetails,
 | 
			
		||||
} from "@renderer/hooks";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./app.css";
 | 
			
		||||
 | 
			
		||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
 | 
			
		||||
import {
 | 
			
		||||
  setUserPreferences,
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +28,8 @@ import { downloadSourcesTable } from "./dexie";
 | 
			
		|||
import { useSubscription } from "./hooks/use-subscription";
 | 
			
		||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
 | 
			
		||||
 | 
			
		||||
import "./app.scss";
 | 
			
		||||
 | 
			
		||||
export interface AppProps {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -240,11 +240,11 @@ export function App() {
 | 
			
		|||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {window.electron.platform === "win32" && (
 | 
			
		||||
        <div className={styles.titleBar}>
 | 
			
		||||
        <div className="title-bar">
 | 
			
		||||
          <h4>
 | 
			
		||||
            Hydra
 | 
			
		||||
            {hasActiveSubscription && (
 | 
			
		||||
              <span className={styles.cloudText}> Cloud</span>
 | 
			
		||||
              <span className="title-bar__cloud-text"> Cloud</span>
 | 
			
		||||
            )}
 | 
			
		||||
          </h4>
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -275,10 +275,10 @@ export function App() {
 | 
			
		|||
      <main>
 | 
			
		||||
        <Sidebar />
 | 
			
		||||
 | 
			
		||||
        <article className={styles.container}>
 | 
			
		||||
        <article className="container">
 | 
			
		||||
          <Header />
 | 
			
		||||
 | 
			
		||||
          <section ref={contentRef} className={styles.content}>
 | 
			
		||||
          <section ref={contentRef} className="container__content">
 | 
			
		||||
            <Outlet />
 | 
			
		||||
          </section>
 | 
			
		||||
        </article>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,54 +0,0 @@
 | 
			
		|||
import { keyframes } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const backdropFadeIn = keyframes({
 | 
			
		||||
  "0%": { backdropFilter: "blur(0px)", backgroundColor: "rgba(0, 0, 0, 0.5)" },
 | 
			
		||||
  "100%": {
 | 
			
		||||
    backdropFilter: "blur(2px)",
 | 
			
		||||
    backgroundColor: "rgba(0, 0, 0, 0.7)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const backdropFadeOut = keyframes({
 | 
			
		||||
  "0%": { backdropFilter: "blur(2px)", backgroundColor: "rgba(0, 0, 0, 0.7)" },
 | 
			
		||||
  "100%": {
 | 
			
		||||
    backdropFilter: "blur(0px)",
 | 
			
		||||
    backgroundColor: "rgba(0, 0, 0, 0)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const backdrop = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    animationName: backdropFadeIn,
 | 
			
		||||
    animationDuration: "0.4s",
 | 
			
		||||
    backgroundColor: "rgba(0, 0, 0, 0.7)",
 | 
			
		||||
    position: "absolute",
 | 
			
		||||
    width: "100%",
 | 
			
		||||
    height: "100%",
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    justifyContent: "center",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
    zIndex: vars.zIndex.backdrop,
 | 
			
		||||
    top: "0",
 | 
			
		||||
    padding: `${SPACING_UNIT * 3}px`,
 | 
			
		||||
    backdropFilter: "blur(2px)",
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    closing: {
 | 
			
		||||
      true: {
 | 
			
		||||
        animationName: backdropFadeOut,
 | 
			
		||||
        backdropFilter: "blur(0px)",
 | 
			
		||||
        backgroundColor: "rgba(0, 0, 0, 0)",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    windows: {
 | 
			
		||||
      true: {
 | 
			
		||||
        // SPACING_UNIT * 3 + title bar spacing
 | 
			
		||||
        paddingTop: `${SPACING_UNIT * 3 + 35}px`,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										50
									
								
								src/renderer/src/components/backdrop/backdrop.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/renderer/src/components/backdrop/backdrop.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.backdrop {
 | 
			
		||||
  animation-name: backdrop-fade-in;
 | 
			
		||||
  animation-duration: 0.4s;
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.7);
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  z-index: globals.$backdrop-z-index;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  padding: calc(globals.$spacing-unit * 3);
 | 
			
		||||
  backdrop-filter: blur(2px);
 | 
			
		||||
  transition: all ease 0.2s;
 | 
			
		||||
 | 
			
		||||
  &--closing {
 | 
			
		||||
    animation-name: backdrop-fade-out;
 | 
			
		||||
    backdrop-filter: blur(0px);
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--windows {
 | 
			
		||||
    padding-top: calc(#{globals.$spacing-unit * 3} + 35);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes backdrop-fade-in {
 | 
			
		||||
  0% {
 | 
			
		||||
    backdrop-filter: blur(0px);
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.5);
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    backdrop-filter: blur(2px);
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.7);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes backdrop-fade-out {
 | 
			
		||||
  0% {
 | 
			
		||||
    backdrop-filter: blur(2px);
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.7);
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    backdrop-filter: blur(0px);
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import * as styles from "./backdrop.css";
 | 
			
		||||
import "./backdrop.scss";
 | 
			
		||||
import cn from "classnames";
 | 
			
		||||
 | 
			
		||||
export interface BackdropProps {
 | 
			
		||||
  isClosing?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -8,9 +9,9 @@ export interface BackdropProps {
 | 
			
		|||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={styles.backdrop({
 | 
			
		||||
        closing: isClosing,
 | 
			
		||||
        windows: window.electron.platform === "win32",
 | 
			
		||||
      className={cn("backdrop", {
 | 
			
		||||
        "backdrop--closing": isClosing,
 | 
			
		||||
        "backdrop--windows": window.electron.platform === "win32",
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,57 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
export const checkboxField = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "row",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const checkbox = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    width: "20px",
 | 
			
		||||
    height: "20px",
 | 
			
		||||
    borderRadius: "4px",
 | 
			
		||||
    backgroundColor: vars.color.darkBackground,
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    justifyContent: "center",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
    position: "relative",
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    minWidth: "20px",
 | 
			
		||||
    minHeight: "20px",
 | 
			
		||||
    color: vars.color.darkBackground,
 | 
			
		||||
    ":hover": {
 | 
			
		||||
      borderColor: "rgba(255, 255, 255, 0.5)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    checked: {
 | 
			
		||||
      true: {
 | 
			
		||||
        backgroundColor: vars.color.muted,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const checkboxInput = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  margin: "0",
 | 
			
		||||
  padding: "0",
 | 
			
		||||
  opacity: "0",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const checkboxLabel = style({
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  textOverflow: "ellipsis",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  whiteSpace: "nowrap",
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.checkbox-field {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: globals.$spacing-unit;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  &:has(input:disabled) {
 | 
			
		||||
    cursor: not-allowed;
 | 
			
		||||
    opacity: globals.$disabled-opacity;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__checkbox {
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    background-color: globals.$dark-background-color;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    border: solid 1px globals.$border-color;
 | 
			
		||||
 | 
			
		||||
    &:hover:not(:has(input:disabled)) {
 | 
			
		||||
      border-color: rgba(255, 255, 255, 0.5);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__input {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
 | 
			
		||||
    &:disabled {
 | 
			
		||||
      cursor: not-allowed;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__label {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
 | 
			
		||||
    &:has(+ input:disabled) {
 | 
			
		||||
      cursor: not-allowed;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { useId } from "react";
 | 
			
		||||
import * as styles from "./checkbox-field.css";
 | 
			
		||||
import { CheckIcon } from "@primer/octicons-react";
 | 
			
		||||
import "./checkbox-field.scss";
 | 
			
		||||
 | 
			
		||||
export interface CheckboxFieldProps
 | 
			
		||||
  extends React.DetailedHTMLProps<
 | 
			
		||||
| 
						 | 
				
			
			@ -14,17 +14,19 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
 | 
			
		|||
  const id = useId();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.checkboxField}>
 | 
			
		||||
      <div className={styles.checkbox({ checked: props.checked })}>
 | 
			
		||||
    <div className="checkbox-field">
 | 
			
		||||
      <div
 | 
			
		||||
        className={`checkbox-field__checkbox ${props.checked ? "checked" : ""}`}
 | 
			
		||||
      >
 | 
			
		||||
        <input
 | 
			
		||||
          id={id}
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          className={styles.checkboxInput}
 | 
			
		||||
          className="checkbox-field__input"
 | 
			
		||||
          {...props}
 | 
			
		||||
        />
 | 
			
		||||
        {props.checked && <CheckIcon />}
 | 
			
		||||
      </div>
 | 
			
		||||
      <label htmlFor={id} className={styles.checkboxLabel}>
 | 
			
		||||
      <label htmlFor={id} className="checkbox-field__label">
 | 
			
		||||
        {label}
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
import { SPACING_UNIT } from "../../theme.css";
 | 
			
		||||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
export const actions = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignSelf: "flex-end",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const descriptionText = style({
 | 
			
		||||
  fontSize: "16px",
 | 
			
		||||
  lineHeight: "24px",
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.confirmation-modal {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
 | 
			
		||||
  &__actions {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-self: flex-end;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  }
 | 
			
		||||
  &__description {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    line-height: 24px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { Button } from "../button/button";
 | 
			
		||||
import { Modal, type ModalProps } from "../modal/modal";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./confirmation-modal.css";
 | 
			
		||||
import "./confirmation-modal.scss";
 | 
			
		||||
 | 
			
		||||
export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
 | 
			
		||||
  confirmButtonLabel: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -31,10 +31,10 @@ export function ConfirmationModal({
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal {...props}>
 | 
			
		||||
      <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
 | 
			
		||||
        <p className={styles.descriptionText}>{descriptionText}</p>
 | 
			
		||||
      <div className="confirmation-modal">
 | 
			
		||||
        <p className="confirmation-modal__description">{descriptionText}</p>
 | 
			
		||||
 | 
			
		||||
        <div className={styles.actions}>
 | 
			
		||||
        <div className="confirmation-modal__actions">
 | 
			
		||||
          <Button theme="outline" onClick={handleCancelClick}>
 | 
			
		||||
            {cancelButtonLabel}
 | 
			
		||||
          </Button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,106 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const card = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "180px",
 | 
			
		||||
  boxShadow: "0px 0px 15px 0px #000000",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  borderRadius: "4px",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  zIndex: "1",
 | 
			
		||||
  ":active": {
 | 
			
		||||
    opacity: vars.opacity.active,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const backdrop = style({
 | 
			
		||||
  background: "linear-gradient(0deg, rgba(0, 0, 0, 0.7) 50%, transparent 100%)",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  justifyContent: "flex-end",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const cover = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  objectFit: "cover",
 | 
			
		||||
  objectPosition: "center",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  zIndex: "-1",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  selectors: {
 | 
			
		||||
    [`${card}:hover &`]: {
 | 
			
		||||
      transform: "scale(1.05)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const content = style({
 | 
			
		||||
  color: "#DADBE1",
 | 
			
		||||
  padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "flex-start",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  transform: "translateY(24px)",
 | 
			
		||||
  selectors: {
 | 
			
		||||
    [`${card}:hover &`]: {
 | 
			
		||||
      transform: "translateY(0px)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const title = style({
 | 
			
		||||
  fontSize: "16px",
 | 
			
		||||
  fontWeight: "bold",
 | 
			
		||||
  textAlign: "left",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadOptions = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  margin: "0",
 | 
			
		||||
  padding: "0",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  flexWrap: "wrap",
 | 
			
		||||
  listStyle: "none",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const specifics = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const specificsItem = style({
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  color: vars.color.muted,
 | 
			
		||||
  fontSize: "12px",
 | 
			
		||||
  alignItems: "flex-end",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const titleContainer = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  color: vars.color.muted,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const shopIcon = style({
 | 
			
		||||
  width: "20px",
 | 
			
		||||
  height: "20px",
 | 
			
		||||
  minWidth: "20px",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const noDownloadsLabel = style({
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  fontWeight: "bold",
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										102
									
								
								src/renderer/src/components/game-card/game-card.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/renderer/src/components/game-card/game-card.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,102 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.game-card {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 180px;
 | 
			
		||||
  box-shadow: 0px 0px 15px 0px #000000;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  transition: all ease 0.2s;
 | 
			
		||||
  border: solid 1px globals.$border-color;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
 | 
			
		||||
  &:active {
 | 
			
		||||
    opacity: globals.$active-opacity;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__backdrop {
 | 
			
		||||
    background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 50%, transparent 100%);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__cover {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    object-fit: cover;
 | 
			
		||||
    object-position: center;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: -1;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    color: #dadbe1;
 | 
			
		||||
    padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    transform: translateY(24px);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__title {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__download-options {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    list-style: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__specifics {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__specifics-item {
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    align-items: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__title-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__shop-icon {
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    min-width: 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__no-download-label {
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:hover &__cover {
 | 
			
		||||
    transform: scale(1.05);
 | 
			
		||||
  }
 | 
			
		||||
  &:hover &__content {
 | 
			
		||||
    transform: translateY(0px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,8 @@ import type { GameStats } from "@types";
 | 
			
		|||
 | 
			
		||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./game-card.css";
 | 
			
		||||
import "./game-card.scss";
 | 
			
		||||
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { Badge } from "../badge/badge";
 | 
			
		||||
import { useCallback, useState } from "react";
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +20,7 @@ export interface GameCardProps
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
const shopIcon = {
 | 
			
		||||
  steam: <SteamLogo className={styles.shopIcon} />,
 | 
			
		||||
  steam: <SteamLogo className="game-card__shop-icon" />,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function GameCard({ game, ...props }: GameCardProps) {
 | 
			
		||||
| 
						 | 
				
			
			@ -48,25 +49,25 @@ export function GameCard({ game, ...props }: GameCardProps) {
 | 
			
		|||
    <button
 | 
			
		||||
      {...props}
 | 
			
		||||
      type="button"
 | 
			
		||||
      className={styles.card}
 | 
			
		||||
      className="game-card"
 | 
			
		||||
      onMouseEnter={handleHover}
 | 
			
		||||
    >
 | 
			
		||||
      <div className={styles.backdrop}>
 | 
			
		||||
      <div className="game-card__backdrop">
 | 
			
		||||
        <img
 | 
			
		||||
          src={steamUrlBuilder.library(game.objectId)}
 | 
			
		||||
          alt={game.title}
 | 
			
		||||
          className={styles.cover}
 | 
			
		||||
          className="game-card__cover"
 | 
			
		||||
          loading="lazy"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <div className={styles.content}>
 | 
			
		||||
          <div className={styles.titleContainer}>
 | 
			
		||||
        <div className="game-card__content">
 | 
			
		||||
          <div className="game-card__title-container">
 | 
			
		||||
            {shopIcon[game.shop]}
 | 
			
		||||
            <p className={styles.title}>{game.title}</p>
 | 
			
		||||
            <p className="game-card__title">{game.title}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {uniqueRepackers.length > 0 ? (
 | 
			
		||||
            <ul className={styles.downloadOptions}>
 | 
			
		||||
            <ul className="game-card__download-options">
 | 
			
		||||
              {uniqueRepackers.map((repacker) => (
 | 
			
		||||
                <li key={repacker}>
 | 
			
		||||
                  <Badge>{repacker}</Badge>
 | 
			
		||||
| 
						 | 
				
			
			@ -74,17 +75,17 @@ export function GameCard({ game, ...props }: GameCardProps) {
 | 
			
		|||
              ))}
 | 
			
		||||
            </ul>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <p className={styles.noDownloadsLabel}>{t("no_downloads")}</p>
 | 
			
		||||
            <p className="game-card__no-download-label">{t("no_downloads")}</p>
 | 
			
		||||
          )}
 | 
			
		||||
          <div className={styles.specifics}>
 | 
			
		||||
            <div className={styles.specificsItem}>
 | 
			
		||||
          <div className="game-card__specifics">
 | 
			
		||||
            <div className="game-card__specifics-item">
 | 
			
		||||
              <DownloadIcon />
 | 
			
		||||
              <span>
 | 
			
		||||
                {stats ? numberFormatter.format(stats.downloadCount) : "…"}
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className={styles.specificsItem}>
 | 
			
		||||
            <div className="game-card__specifics-item">
 | 
			
		||||
              <PeopleIcon />
 | 
			
		||||
              <span>
 | 
			
		||||
                {stats ? numberFormatter.format(stats?.playerCount) : "…"}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										32
									
								
								src/renderer/src/components/header/auto-update-header.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/renderer/src/components/header/auto-update-header.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.auto-update-sub-header {
 | 
			
		||||
  border-bottom: solid 1px globals.$body-color;
 | 
			
		||||
  padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 3);
 | 
			
		||||
 | 
			
		||||
  &__new-version-link {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    color: #8e919b;
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__new-version-icon {
 | 
			
		||||
    color: globals.$success-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__new-version-button {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      text-decoration: underline;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
 | 
			
		|||
import { useEffect, useState } from "react";
 | 
			
		||||
import { SyncIcon } from "@primer/octicons-react";
 | 
			
		||||
import { Link } from "../link/link";
 | 
			
		||||
import * as styles from "./header.css";
 | 
			
		||||
import "./auto-update-header.scss";
 | 
			
		||||
import type { AppUpdaterEvent } from "@types";
 | 
			
		||||
 | 
			
		||||
export const releasesPageUrl =
 | 
			
		||||
| 
						 | 
				
			
			@ -45,9 +45,15 @@ export function AutoUpdateSubHeader() {
 | 
			
		|||
 | 
			
		||||
  if (!isAutoInstallAvailable) {
 | 
			
		||||
    return (
 | 
			
		||||
      <header className={styles.subheader}>
 | 
			
		||||
        <Link to={releasesPageUrl} className={styles.newVersionLink}>
 | 
			
		||||
          <SyncIcon className={styles.newVersionIcon} size={12} />
 | 
			
		||||
      <header className="auto-update-sub-header">
 | 
			
		||||
        <Link
 | 
			
		||||
          to={releasesPageUrl}
 | 
			
		||||
          className="auto-update-sub-header__new-version-link"
 | 
			
		||||
        >
 | 
			
		||||
          <SyncIcon
 | 
			
		||||
            className="auto-update-sub-header__new-version-icon"
 | 
			
		||||
            size={12}
 | 
			
		||||
          />
 | 
			
		||||
          {t("version_available_download", { version: newVersion })}
 | 
			
		||||
        </Link>
 | 
			
		||||
      </header>
 | 
			
		||||
| 
						 | 
				
			
			@ -56,13 +62,16 @@ export function AutoUpdateSubHeader() {
 | 
			
		|||
 | 
			
		||||
  if (isReadyToInstall) {
 | 
			
		||||
    return (
 | 
			
		||||
      <header className={styles.subheader}>
 | 
			
		||||
      <header className="auto-update-sub-header">
 | 
			
		||||
        <button
 | 
			
		||||
          type="button"
 | 
			
		||||
          className={styles.newVersionButton}
 | 
			
		||||
          className="auto-update-sub-header__new-version-button"
 | 
			
		||||
          onClick={handleClickInstallUpdate}
 | 
			
		||||
        >
 | 
			
		||||
          <SyncIcon className={styles.newVersionIcon} size={12} />
 | 
			
		||||
          <SyncIcon
 | 
			
		||||
            className="auto-update-sub-header__new-version-icon"
 | 
			
		||||
            size={12}
 | 
			
		||||
          />
 | 
			
		||||
          {t("version_available_install", { version: newVersion })}
 | 
			
		||||
        </button>
 | 
			
		||||
      </header>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,182 +0,0 @@
 | 
			
		|||
import type { ComplexStyleRule } from "@vanilla-extract/css";
 | 
			
		||||
import { keyframes, style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const slideIn = keyframes({
 | 
			
		||||
  "0%": { transform: "translateX(20px)", opacity: "0" },
 | 
			
		||||
  "100%": {
 | 
			
		||||
    transform: "translateX(0)",
 | 
			
		||||
    opacity: "1",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const slideOut = keyframes({
 | 
			
		||||
  "0%": { transform: "translateX(0px)", opacity: "1" },
 | 
			
		||||
  "100%": {
 | 
			
		||||
    transform: "translateX(20px)",
 | 
			
		||||
    opacity: "0",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const header = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    justifyContent: "space-between",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
    gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
    WebkitAppRegion: "drag",
 | 
			
		||||
    width: "100%",
 | 
			
		||||
    padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
 | 
			
		||||
    color: vars.color.muted,
 | 
			
		||||
    borderBottom: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    backgroundColor: vars.color.darkBackground,
 | 
			
		||||
  } as ComplexStyleRule,
 | 
			
		||||
  variants: {
 | 
			
		||||
    draggingDisabled: {
 | 
			
		||||
      true: {
 | 
			
		||||
        WebkitAppRegion: "no-drag",
 | 
			
		||||
      } as ComplexStyleRule,
 | 
			
		||||
    },
 | 
			
		||||
    isWindows: {
 | 
			
		||||
      true: {
 | 
			
		||||
        WebkitAppRegion: "no-drag",
 | 
			
		||||
      } as ComplexStyleRule,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const search = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    backgroundColor: vars.color.background,
 | 
			
		||||
    display: "inline-flex",
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    width: "200px",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
    borderRadius: "8px",
 | 
			
		||||
    border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    height: "40px",
 | 
			
		||||
    WebkitAppRegion: "no-drag",
 | 
			
		||||
  } as ComplexStyleRule,
 | 
			
		||||
  variants: {
 | 
			
		||||
    focused: {
 | 
			
		||||
      true: {
 | 
			
		||||
        width: "250px",
 | 
			
		||||
        borderColor: "#DADBE1",
 | 
			
		||||
      },
 | 
			
		||||
      false: {
 | 
			
		||||
        ":hover": {
 | 
			
		||||
          borderColor: "rgba(255, 255, 255, 0.5)",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const searchInput = style({
 | 
			
		||||
  backgroundColor: "transparent",
 | 
			
		||||
  border: "none",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  outline: "none",
 | 
			
		||||
  color: "#DADBE1",
 | 
			
		||||
  cursor: "default",
 | 
			
		||||
  fontFamily: "inherit",
 | 
			
		||||
  textOverflow: "ellipsis",
 | 
			
		||||
  ":focus": {
 | 
			
		||||
    cursor: "text",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const actionButton = style({
 | 
			
		||||
  color: "inherit",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  padding: `${SPACING_UNIT}px`,
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    color: "#DADBE1",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const section = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const backButton = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    color: vars.color.body,
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    WebkitAppRegion: "no-drag",
 | 
			
		||||
    position: "absolute",
 | 
			
		||||
    transition: "transform ease 0.2s",
 | 
			
		||||
    animationDuration: "0.2s",
 | 
			
		||||
    width: "16px",
 | 
			
		||||
    height: "16px",
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
  } as ComplexStyleRule,
 | 
			
		||||
  variants: {
 | 
			
		||||
    enabled: {
 | 
			
		||||
      true: {
 | 
			
		||||
        animationName: slideIn,
 | 
			
		||||
      },
 | 
			
		||||
      false: {
 | 
			
		||||
        opacity: "0",
 | 
			
		||||
        pointerEvents: "none",
 | 
			
		||||
        animationName: slideOut,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const title = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    overflow: "hidden",
 | 
			
		||||
    textOverflow: "ellipsis",
 | 
			
		||||
    width: "100%",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    hasBackButton: {
 | 
			
		||||
      true: {
 | 
			
		||||
        transform: "translateX(28px)",
 | 
			
		||||
        width: "calc(100% - 28px)",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const subheader = style({
 | 
			
		||||
  borderBottom: `solid 1px ${vars.color.border}`,
 | 
			
		||||
  padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 3}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const newVersionButton = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  fontSize: "12px",
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    textDecoration: "underline",
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const newVersionLink = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  color: "#8e919b",
 | 
			
		||||
  fontSize: "12px",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const newVersionIcon = style({
 | 
			
		||||
  color: vars.color.success,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										133
									
								
								src/renderer/src/components/header/header.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/renderer/src/components/header/header.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  -webkit-app-region: drag;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
 | 
			
		||||
  color: globals.$muted-color;
 | 
			
		||||
  border-bottom: solid 1px globals.$border-color;
 | 
			
		||||
  background-color: globals.$dark-background-color;
 | 
			
		||||
 | 
			
		||||
  &--dragging-disabled {
 | 
			
		||||
    -webkit-app-region: no-drag;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--is-windows {
 | 
			
		||||
    -webkit-app-region: no-drag;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__search {
 | 
			
		||||
    background-color: globals.$background-color;
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    width: 200px;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    border: solid 1px globals.$border-color;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    -webkit-app-region: no-drag;
 | 
			
		||||
    &:hover {
 | 
			
		||||
      border-color: rgba(255, 255, 255, 0.5);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--focused {
 | 
			
		||||
      width: 250px;
 | 
			
		||||
      border-color: #dadbe1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__search-input {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    border: none;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    color: #dadbe1;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    font-family: inherit;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
 | 
			
		||||
    &:focus {
 | 
			
		||||
      cursor: text;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__action-button {
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    padding: globals.$spacing-unit;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      color: #dadbe1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__section {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__back-button {
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    -webkit-app-region: no-drag;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    transition: transform ease 0.2s;
 | 
			
		||||
    animation-duration: 0.2s;
 | 
			
		||||
    width: 16px;
 | 
			
		||||
    height: 16px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    animation-name: slide-out;
 | 
			
		||||
 | 
			
		||||
    &--enabled {
 | 
			
		||||
      animation: slide-in;
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
      pointer-events: all;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__title {
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    &--has-back-button {
 | 
			
		||||
      transform: translateX(28px);
 | 
			
		||||
      width: calc(100% - 28px);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slide-in {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: translateX(20px);
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateX(0);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slide-out {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: translateX(0px);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateX(20px);
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,9 +5,10 @@ import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
 | 
			
		|||
 | 
			
		||||
import { useAppDispatch, useAppSelector } from "@renderer/hooks";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./header.css";
 | 
			
		||||
import "./header.scss";
 | 
			
		||||
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
 | 
			
		||||
import { setFilters } from "@renderer/features";
 | 
			
		||||
import cn from "classnames";
 | 
			
		||||
 | 
			
		||||
const pathTitle: Record<string, string> = {
 | 
			
		||||
  "/": "home",
 | 
			
		||||
| 
						 | 
				
			
			@ -75,16 +76,16 @@ export function Header() {
 | 
			
		|||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <header
 | 
			
		||||
        className={styles.header({
 | 
			
		||||
          draggingDisabled,
 | 
			
		||||
          isWindows: window.electron.platform === "win32",
 | 
			
		||||
        className={cn("header", {
 | 
			
		||||
          "header--dragging-disabled": draggingDisabled,
 | 
			
		||||
          "header--is-windows": window.electron.platform === "win32",
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        <section className={styles.section} style={{ flex: 1 }}>
 | 
			
		||||
        <section className="header__section" style={{ flex: 1 }}>
 | 
			
		||||
          <button
 | 
			
		||||
            type="button"
 | 
			
		||||
            className={styles.backButton({
 | 
			
		||||
              enabled: location.key !== "default",
 | 
			
		||||
            className={cn("header__back-button", {
 | 
			
		||||
              "header__back-button--enabled": location.key !== "default",
 | 
			
		||||
            })}
 | 
			
		||||
            onClick={handleBackButtonClick}
 | 
			
		||||
            disabled={location.key === "default"}
 | 
			
		||||
| 
						 | 
				
			
			@ -93,19 +94,23 @@ export function Header() {
 | 
			
		|||
          </button>
 | 
			
		||||
 | 
			
		||||
          <h3
 | 
			
		||||
            className={styles.title({
 | 
			
		||||
              hasBackButton: location.key !== "default",
 | 
			
		||||
            className={cn("header__title", {
 | 
			
		||||
              "header__title--has-back-button": location.key !== "default",
 | 
			
		||||
            })}
 | 
			
		||||
          >
 | 
			
		||||
            {title}
 | 
			
		||||
          </h3>
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
        <section className={styles.section}>
 | 
			
		||||
          <div className={styles.search({ focused: isFocused })}>
 | 
			
		||||
        <section className="header__section">
 | 
			
		||||
          <div
 | 
			
		||||
            className={cn("header__search", {
 | 
			
		||||
              "header__search--focused": isFocused,
 | 
			
		||||
            })}
 | 
			
		||||
          >
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className={styles.actionButton}
 | 
			
		||||
              className="header__action-button"
 | 
			
		||||
              onClick={focusInput}
 | 
			
		||||
            >
 | 
			
		||||
              <SearchIcon />
 | 
			
		||||
| 
						 | 
				
			
			@ -117,7 +122,7 @@ export function Header() {
 | 
			
		|||
              name="search"
 | 
			
		||||
              placeholder={t("search")}
 | 
			
		||||
              value={searchValue}
 | 
			
		||||
              className={styles.searchInput}
 | 
			
		||||
              className="header__search-input"
 | 
			
		||||
              onChange={(event) => handleSearch(event.target.value)}
 | 
			
		||||
              onFocus={() => setIsFocused(true)}
 | 
			
		||||
              onBlur={handleBlur}
 | 
			
		||||
| 
						 | 
				
			
			@ -127,7 +132,7 @@ export function Header() {
 | 
			
		|||
              <button
 | 
			
		||||
                type="button"
 | 
			
		||||
                onClick={() => dispatch(setFilters({ title: "" }))}
 | 
			
		||||
                className={styles.actionButton}
 | 
			
		||||
                className="header__action-button"
 | 
			
		||||
              >
 | 
			
		||||
                <XIcon />
 | 
			
		||||
              </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,60 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const hero = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "280px",
 | 
			
		||||
  minHeight: "280px",
 | 
			
		||||
  maxHeight: "280px",
 | 
			
		||||
  borderRadius: "4px",
 | 
			
		||||
  color: "#DADBE1",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  boxShadow: "0px 0px 15px 0px #000000",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
  zIndex: "1",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const heroMedia = style({
 | 
			
		||||
  objectFit: "cover",
 | 
			
		||||
  objectPosition: "center",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  zIndex: "-1",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  imageRendering: "revert",
 | 
			
		||||
  selectors: {
 | 
			
		||||
    [`${hero}:hover &`]: {
 | 
			
		||||
      transform: "scale(1.02)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const backdrop = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%)",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const description = style({
 | 
			
		||||
  maxWidth: "700px",
 | 
			
		||||
  color: vars.color.muted,
 | 
			
		||||
  textAlign: "left",
 | 
			
		||||
  lineHeight: "20px",
 | 
			
		||||
  marginTop: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const content = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  justifyContent: "flex-end",
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										56
									
								
								src/renderer/src/components/hero/hero.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/renderer/src/components/hero/hero.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.hero {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 280px;
 | 
			
		||||
  min-height: 280px;
 | 
			
		||||
  max-height: 280px;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  color: #dadbe1;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  box-shadow: 0px 0px 15px 0px #000000;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  border: solid 1px globals.$border-color;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
 | 
			
		||||
  &__media {
 | 
			
		||||
    object-fit: cover;
 | 
			
		||||
    object-position: center;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: -1;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    image-rendering: revert;
 | 
			
		||||
  }
 | 
			
		||||
  &:hover &__media {
 | 
			
		||||
    transform: scale(1.02);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__backdrop {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%);
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__description {
 | 
			
		||||
    max-width: 700px;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    line-height: 20px;
 | 
			
		||||
    margin-top: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3);
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import * as styles from "./hero.css";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import type { TrendingGame } from "@types";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import Skeleton from "react-loading-skeleton";
 | 
			
		||||
import "./hero.scss";
 | 
			
		||||
 | 
			
		||||
export function Hero() {
 | 
			
		||||
  const [featuredGameDetails, setFeaturedGameDetails] = useState<
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ export function Hero() {
 | 
			
		|||
  }, [i18n.language]);
 | 
			
		||||
 | 
			
		||||
  if (isLoading) {
 | 
			
		||||
    return <Skeleton className={styles.hero} />;
 | 
			
		||||
    return <Skeleton className="hero" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (featuredGameDetails?.length) {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,17 +37,17 @@ export function Hero() {
 | 
			
		|||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        onClick={() => navigate(game.uri)}
 | 
			
		||||
        className={styles.hero}
 | 
			
		||||
        className="hero"
 | 
			
		||||
        key={index}
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles.backdrop}>
 | 
			
		||||
        <div className="hero__backdrop">
 | 
			
		||||
          <img
 | 
			
		||||
            src={game.background}
 | 
			
		||||
            alt={game.description}
 | 
			
		||||
            className={styles.heroMedia}
 | 
			
		||||
            className="hero__media"
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <div className={styles.content}>
 | 
			
		||||
          <div className="hero__content">
 | 
			
		||||
            {game.logo && (
 | 
			
		||||
              <img
 | 
			
		||||
                src={game.logo}
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +56,7 @@ export function Hero() {
 | 
			
		|||
                loading="eager"
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <p className={styles.description}>{game.description}</p>
 | 
			
		||||
            <p className="hero__description">{game.description}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
export const link = style({
 | 
			
		||||
  textDecoration: "none",
 | 
			
		||||
  color: "#C0C1C7",
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    textDecoration: "underline",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								src/renderer/src/components/link/link.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/renderer/src/components/link/link.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
.link {
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  color: #c0c1c7;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
 | 
			
		||||
import cn from "classnames";
 | 
			
		||||
import * as styles from "./link.css";
 | 
			
		||||
import "./link.scss";
 | 
			
		||||
 | 
			
		||||
export function Link({ children, to, className, ...props }: LinkProps) {
 | 
			
		||||
  const openExternal = (event: React.MouseEvent) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
 | 
			
		|||
    return (
 | 
			
		||||
      <a
 | 
			
		||||
        href={to}
 | 
			
		||||
        className={cn(styles.link, className)}
 | 
			
		||||
        className={cn("link", className)}
 | 
			
		||||
        onClick={openExternal}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
| 
						 | 
				
			
			@ -22,11 +22,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ReactRouterDomLink
 | 
			
		||||
      className={cn(styles.link, className)}
 | 
			
		||||
      to={to}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
    <ReactRouterDomLink className={cn("link", className)} to={to} {...props}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </ReactRouterDomLink>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,78 +0,0 @@
 | 
			
		|||
import { keyframes, style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const scaleFadeIn = keyframes({
 | 
			
		||||
  "0%": { opacity: "0", scale: "0.5" },
 | 
			
		||||
  "100%": {
 | 
			
		||||
    opacity: "1",
 | 
			
		||||
    scale: "1",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const scaleFadeOut = keyframes({
 | 
			
		||||
  "0%": { opacity: "1", scale: "1" },
 | 
			
		||||
  "100%": {
 | 
			
		||||
    opacity: "0",
 | 
			
		||||
    scale: "0.5",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const modal = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
 | 
			
		||||
    backgroundColor: vars.color.background,
 | 
			
		||||
    borderRadius: "4px",
 | 
			
		||||
    minWidth: "400px",
 | 
			
		||||
    maxWidth: "600px",
 | 
			
		||||
    color: vars.color.body,
 | 
			
		||||
    maxHeight: "100%",
 | 
			
		||||
    border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    overflow: "hidden",
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    flexDirection: "column",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    closing: {
 | 
			
		||||
      true: {
 | 
			
		||||
        animationName: scaleFadeOut,
 | 
			
		||||
        opacity: "0",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    large: {
 | 
			
		||||
      true: {
 | 
			
		||||
        width: "800px",
 | 
			
		||||
        maxWidth: "800px",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const modalContent = style({
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  overflow: "auto",
 | 
			
		||||
  padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const modalHeader = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  borderBottom: `solid 1px ${vars.color.border}`,
 | 
			
		||||
  justifyContent: "space-between",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const closeModalButton = style({
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  alignSelf: "flex-start",
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    opacity: "0.75",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const closeModalButtonIcon = style({
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										77
									
								
								src/renderer/src/components/modal/modal.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/renderer/src/components/modal/modal.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,77 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.modal {
 | 
			
		||||
  animation: scale-fade-in 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none
 | 
			
		||||
    running;
 | 
			
		||||
  background-color: globals.$background-color;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  min-width: 400px;
 | 
			
		||||
  max-width: 600px;
 | 
			
		||||
  color: globals.$body-color;
 | 
			
		||||
  max-height: 100%;
 | 
			
		||||
  border: solid 1px globals.$border-color;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
 | 
			
		||||
  &--closing {
 | 
			
		||||
    animation-name: scale-fade-out;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--large {
 | 
			
		||||
    width: 800px;
 | 
			
		||||
    max-width: 800px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__header {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    border-bottom: solid 1px globals.$border-color;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__close-button {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    align-self: flex-start;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      opacity: 0.75;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__close-button-icon {
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes scale-fade-in {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    scale: 0.5;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    scale: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes scale-fade-out {
 | 
			
		||||
  0% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    scale: 1;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    scale: 0.5;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,10 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		|||
import { createPortal } from "react-dom";
 | 
			
		||||
import { XIcon } from "@primer/octicons-react";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./modal.css";
 | 
			
		||||
import "./modal.scss";
 | 
			
		||||
 | 
			
		||||
import { Backdrop } from "../backdrop/backdrop";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import cn from "classnames";
 | 
			
		||||
 | 
			
		||||
export interface ModalProps {
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -109,14 +110,17 @@ export function Modal({
 | 
			
		|||
  return createPortal(
 | 
			
		||||
    <Backdrop isClosing={isClosing}>
 | 
			
		||||
      <div
 | 
			
		||||
        className={styles.modal({ closing: isClosing, large })}
 | 
			
		||||
        className={cn("modal", {
 | 
			
		||||
          "modal--closing": isClosing,
 | 
			
		||||
          "modal--large": large,
 | 
			
		||||
        })}
 | 
			
		||||
        role="dialog"
 | 
			
		||||
        aria-labelledby={title}
 | 
			
		||||
        aria-describedby={description}
 | 
			
		||||
        ref={modalContentRef}
 | 
			
		||||
        data-hydra-dialog
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles.modalHeader}>
 | 
			
		||||
        <div className="modal__header">
 | 
			
		||||
          <div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
 | 
			
		||||
            <h3>{title}</h3>
 | 
			
		||||
            {description && <p>{description}</p>}
 | 
			
		||||
| 
						 | 
				
			
			@ -125,13 +129,13 @@ export function Modal({
 | 
			
		|||
          <button
 | 
			
		||||
            type="button"
 | 
			
		||||
            onClick={handleCloseClick}
 | 
			
		||||
            className={styles.closeModalButton}
 | 
			
		||||
            className="modal__close-button"
 | 
			
		||||
            aria-label={t("close")}
 | 
			
		||||
          >
 | 
			
		||||
            <XIcon className={styles.closeModalButtonIcon} size={24} />
 | 
			
		||||
            <XIcon className="modal__close-button-icon" size={24} />
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className={styles.modalContent}>{children}</div>
 | 
			
		||||
        <div className="modal__content">{children}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </Backdrop>,
 | 
			
		||||
    document.body
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,59 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const select = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    display: "inline-flex",
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    width: "fit-content",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
    borderRadius: "8px",
 | 
			
		||||
    border: `1px solid ${vars.color.border}`,
 | 
			
		||||
    height: "40px",
 | 
			
		||||
    minHeight: "40px",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    focused: {
 | 
			
		||||
      true: {
 | 
			
		||||
        borderColor: "#DADBE1",
 | 
			
		||||
      },
 | 
			
		||||
      false: {
 | 
			
		||||
        ":hover": {
 | 
			
		||||
          borderColor: "rgba(255, 255, 255, 0.5)",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    theme: {
 | 
			
		||||
      primary: {
 | 
			
		||||
        backgroundColor: vars.color.darkBackground,
 | 
			
		||||
      },
 | 
			
		||||
      dark: {
 | 
			
		||||
        backgroundColor: vars.color.background,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const option = style({
 | 
			
		||||
  backgroundColor: vars.color.darkBackground,
 | 
			
		||||
  borderRight: "4px solid",
 | 
			
		||||
  borderColor: "transparent",
 | 
			
		||||
  borderRadius: "8px",
 | 
			
		||||
  width: "fit-content",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  outline: "none",
 | 
			
		||||
  color: "#DADBE1",
 | 
			
		||||
  cursor: "default",
 | 
			
		||||
  fontFamily: "inherit",
 | 
			
		||||
  fontSize: vars.size.body,
 | 
			
		||||
  textOverflow: "ellipsis",
 | 
			
		||||
  padding: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const label = style({
 | 
			
		||||
  marginBottom: `${SPACING_UNIT}px`,
 | 
			
		||||
  display: "block",
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										49
									
								
								src/renderer/src/components/select-field/select-field.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/renderer/src/components/select-field/select-field.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.select-field {
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  transition: all ease 0.2s;
 | 
			
		||||
  width: fit-content;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  border: 1px solid globals.$border-color;
 | 
			
		||||
  height: 40px;
 | 
			
		||||
  min-height: 40px;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    border-color: rgba(255, 255, 255, 0.5);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--focused {
 | 
			
		||||
    border-color: #dadbe1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--primary {
 | 
			
		||||
    background-color: globals.$dark-background-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--dark {
 | 
			
		||||
    background-color: globals.$background-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__option {
 | 
			
		||||
    background-color: globals.$dark-background-color;
 | 
			
		||||
    border-right: 4px solid;
 | 
			
		||||
    border-color: transparent;
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    width: fit-content;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    color: #dadbe1;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    font-family: inherit;
 | 
			
		||||
    font-size: globals.$body-font-size;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    padding: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__label {
 | 
			
		||||
    margin-bottom: globals.$spacing-unit;
 | 
			
		||||
    display: block;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +1,13 @@
 | 
			
		|||
import { useId, useState } from "react";
 | 
			
		||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
 | 
			
		||||
import * as styles from "./select-field.css";
 | 
			
		||||
import "./select-field.scss";
 | 
			
		||||
import cn from "classnames";
 | 
			
		||||
 | 
			
		||||
export interface SelectProps
 | 
			
		||||
  extends React.DetailedHTMLProps<
 | 
			
		||||
    React.SelectHTMLAttributes<HTMLSelectElement>,
 | 
			
		||||
    HTMLSelectElement
 | 
			
		||||
  > {
 | 
			
		||||
  theme?: NonNullable<RecipeVariants<typeof styles.select>>["theme"];
 | 
			
		||||
  theme?: "primary" | "dark";
 | 
			
		||||
  label?: string;
 | 
			
		||||
  options?: { key: string; value: string; label: string }[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -25,16 +25,20 @@ export function SelectField({
 | 
			
		|||
  return (
 | 
			
		||||
    <div style={{ flex: 1 }}>
 | 
			
		||||
      {label && (
 | 
			
		||||
        <label htmlFor={id} className={styles.label}>
 | 
			
		||||
        <label htmlFor={id} className="select-field__label">
 | 
			
		||||
          {label}
 | 
			
		||||
        </label>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <div className={styles.select({ focused: isFocused, theme })}>
 | 
			
		||||
      <div
 | 
			
		||||
        className={cn("select-field", `select-field--${theme}`, {
 | 
			
		||||
          "select-field--focused": isFocused,
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        <select
 | 
			
		||||
          id={id}
 | 
			
		||||
          value={value}
 | 
			
		||||
          className={styles.option}
 | 
			
		||||
          className="select-field__option"
 | 
			
		||||
          onFocus={() => setIsFocused(true)}
 | 
			
		||||
          onBlur={() => setIsFocused(false)}
 | 
			
		||||
          onChange={onChange}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,79 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const profileContainer = style({
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const profileButton = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  transition: "all ease 0.1s",
 | 
			
		||||
  color: vars.color.muted,
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  borderRadius: "4px",
 | 
			
		||||
  padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    backgroundColor: "rgba(255, 255, 255, 0.15)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const profileButtonContent = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
 | 
			
		||||
  width: "100%",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const profileButtonInformation = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  alignItems: "flex-start",
 | 
			
		||||
  flex: "1",
 | 
			
		||||
  minWidth: 0,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const profileButtonTitle = style({
 | 
			
		||||
  fontWeight: "bold",
 | 
			
		||||
  fontSize: vars.size.body,
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  textAlign: "left",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  textOverflow: "ellipsis",
 | 
			
		||||
  whiteSpace: "nowrap",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const friendsButton = style({
 | 
			
		||||
  color: vars.color.muted,
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  borderRadius: "50%",
 | 
			
		||||
  width: "40px",
 | 
			
		||||
  minWidth: "40px",
 | 
			
		||||
  minHeight: "40px",
 | 
			
		||||
  height: "40px",
 | 
			
		||||
  backgroundColor: vars.color.background,
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  transition: "all ease 0.3s",
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    backgroundColor: "rgba(255, 255, 255, 0.15)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const friendsButtonBadge = style({
 | 
			
		||||
  backgroundColor: vars.color.success,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  width: "20px",
 | 
			
		||||
  height: "20px",
 | 
			
		||||
  borderRadius: "50%",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  top: "-5px",
 | 
			
		||||
  right: "-5px",
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										77
									
								
								src/renderer/src/components/sidebar/sidebar-profile.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/renderer/src/components/sidebar/sidebar-profile.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,77 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.sidebar-profile {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: globals.$spacing-unit;
 | 
			
		||||
  padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
 | 
			
		||||
 | 
			
		||||
  &__button {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    transition: all ease 0.1s;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    padding: globals.$spacing-unit globals.$spacing-unit;
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: rgba(255, 255, 255, 0.15);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__button-content {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: calc(globals.$spacing-unit + globals.$spacing-unit / 2);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__button-information {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__button-title {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    font-size: globals.$body-font-size;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__friends-button {
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    width: 40px;
 | 
			
		||||
    min-width: 40px;
 | 
			
		||||
    min-height: 40px;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    background-color: globals.$background-color;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    transition: all ease 0.3s;
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: rgba(255, 255, 255, 0.15);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__friends-button-badge {
 | 
			
		||||
    background-color: globals.$success-color;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: -5px;
 | 
			
		||||
    right: -5px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { PeopleIcon } from "@primer/octicons-react";
 | 
			
		||||
import * as styles from "./sidebar-profile.css";
 | 
			
		||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
 | 
			
		||||
import { useEffect, useMemo } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
| 
						 | 
				
			
			@ -8,6 +7,7 @@ import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-mo
 | 
			
		|||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
 | 
			
		||||
import { Avatar } from "../avatar/avatar";
 | 
			
		||||
import { AuthPage } from "@shared";
 | 
			
		||||
import "./sidebar-profile.scss";
 | 
			
		||||
 | 
			
		||||
const LONG_POLLING_INTERVAL = 120_000;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -50,14 +50,14 @@ export function SidebarProfile() {
 | 
			
		|||
    return (
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        className={styles.friendsButton}
 | 
			
		||||
        className="sidebar-profile__friends-button"
 | 
			
		||||
        onClick={() =>
 | 
			
		||||
          showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
 | 
			
		||||
        }
 | 
			
		||||
        title={t("friends")}
 | 
			
		||||
      >
 | 
			
		||||
        {friendRequestCount > 0 && (
 | 
			
		||||
          <small className={styles.friendsButtonBadge}>
 | 
			
		||||
          <small className="sidebar-profile__friends-button-badge">
 | 
			
		||||
            {friendRequestCount > 99 ? "99+" : friendRequestCount}
 | 
			
		||||
          </small>
 | 
			
		||||
        )}
 | 
			
		||||
| 
						 | 
				
			
			@ -85,21 +85,21 @@ export function SidebarProfile() {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.profileContainer}>
 | 
			
		||||
    <div className="sidebar-profile">
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        className={styles.profileButton}
 | 
			
		||||
        className="sidebar-profile__button"
 | 
			
		||||
        onClick={handleProfileClick}
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles.profileButtonContent}>
 | 
			
		||||
        <div className="sidebar-profile__button-content">
 | 
			
		||||
          <Avatar
 | 
			
		||||
            size={35}
 | 
			
		||||
            src={userDetails?.profileImageUrl}
 | 
			
		||||
            alt={userDetails?.displayName}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <div className={styles.profileButtonInformation}>
 | 
			
		||||
            <p className={styles.profileButtonTitle}>
 | 
			
		||||
          <div className="sidebar-profile__button-information">
 | 
			
		||||
            <p className="sidebar-profile__button-title">
 | 
			
		||||
              {userDetails ? userDetails.displayName : t("sign_in")}
 | 
			
		||||
            </p>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,152 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const sidebar = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    backgroundColor: vars.color.darkBackground,
 | 
			
		||||
    color: vars.color.muted,
 | 
			
		||||
    flexDirection: "column",
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    transition: "opacity ease 0.2s",
 | 
			
		||||
    borderRight: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    position: "relative",
 | 
			
		||||
    overflow: "hidden",
 | 
			
		||||
    justifyContent: "space-between",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    resizing: {
 | 
			
		||||
      true: {
 | 
			
		||||
        opacity: vars.opacity.active,
 | 
			
		||||
        pointerEvents: "none",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    darwin: {
 | 
			
		||||
      true: {
 | 
			
		||||
        paddingTop: `${SPACING_UNIT * 6}px`,
 | 
			
		||||
      },
 | 
			
		||||
      false: {
 | 
			
		||||
        paddingTop: `${SPACING_UNIT}px`,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const content = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  overflow: "auto",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const handle = style({
 | 
			
		||||
  width: "5px",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  cursor: "col-resize",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  right: "0",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const menu = style({
 | 
			
		||||
  listStyle: "none",
 | 
			
		||||
  padding: "0",
 | 
			
		||||
  margin: "0",
 | 
			
		||||
  gap: `${SPACING_UNIT / 2}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const menuItem = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    transition: "all ease 0.1s",
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    textWrap: "nowrap",
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    color: vars.color.muted,
 | 
			
		||||
    borderRadius: "4px",
 | 
			
		||||
    ":hover": {
 | 
			
		||||
      backgroundColor: "rgba(255, 255, 255, 0.15)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    active: {
 | 
			
		||||
      true: {
 | 
			
		||||
        backgroundColor: "rgba(255, 255, 255, 0.1)",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    muted: {
 | 
			
		||||
      true: {
 | 
			
		||||
        opacity: vars.opacity.disabled,
 | 
			
		||||
        ":hover": {
 | 
			
		||||
          opacity: "1",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const menuItemButton = style({
 | 
			
		||||
  color: "inherit",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  padding: `9px ${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const menuItemButtonLabel = style({
 | 
			
		||||
  textOverflow: "ellipsis",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gameIcon = style({
 | 
			
		||||
  width: "20px",
 | 
			
		||||
  height: "20px",
 | 
			
		||||
  minWidth: "20px",
 | 
			
		||||
  minHeight: "20px",
 | 
			
		||||
  borderRadius: "4px",
 | 
			
		||||
  backgroundSize: "cover",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const sectionTitle = style({
 | 
			
		||||
  textTransform: "uppercase",
 | 
			
		||||
  fontWeight: "bold",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const section = style({
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  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%",
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										136
									
								
								src/renderer/src/components/sidebar/sidebar.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/renderer/src/components/sidebar/sidebar.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,136 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.sidebar {
 | 
			
		||||
  background-color: globals.$dark-background-color;
 | 
			
		||||
  color: globals.$muted-color;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  transition: opacity ease 0.2s;
 | 
			
		||||
  border-right: solid 1px globals.$border-color;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  padding-top: globals.$spacing-unit;
 | 
			
		||||
 | 
			
		||||
  &--resizing {
 | 
			
		||||
    opacity: globals.$active-opacity;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &--darwin {
 | 
			
		||||
    padding-top: calc(globals.$spacing-unit * 6);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__handle {
 | 
			
		||||
    width: 5px;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    cursor: col-resize;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__menu {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    gap: calc(globals.$spacing-unit / 2);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__menu-item {
 | 
			
		||||
    transition: all ease 0.1s;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    text-wrap: nowrap;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: rgba(255, 255, 255, 0.15);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--active {
 | 
			
		||||
      background-color: rgba(255, 255, 255, 0.1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--muted {
 | 
			
		||||
      opacity: globals.$disabled-opacity;
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__menu-item-button {
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 9px globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__menu-item-button-label {
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__game-icon {
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    min-width: 20px;
 | 
			
		||||
    min-height: 20px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    background-size: cover;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__section-title {
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__section {
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    padding-bottom: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__help-button {
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    padding: globals.$spacing-unit calc(globals.$spacing-unit * 2);
 | 
			
		||||
    gap: 9px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    border-top: solid 1px globals.$border-color;
 | 
			
		||||
    transition: background-color ease 0.1s;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: rgba(255, 255, 255, 0.15);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__help-button-icon {
 | 
			
		||||
    background: linear-gradient(0deg, #16b195 50%, #3e62c0 100%);
 | 
			
		||||
    width: 24px;
 | 
			
		||||
    height: 24px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -14,12 +14,14 @@ import {
 | 
			
		|||
 | 
			
		||||
import { routes } from "./routes";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./sidebar.css";
 | 
			
		||||
import "./sidebar.scss";
 | 
			
		||||
 | 
			
		||||
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 cn from "classnames";
 | 
			
		||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
 | 
			
		||||
 | 
			
		||||
const SIDEBAR_MIN_WIDTH = 200;
 | 
			
		||||
| 
						 | 
				
			
			@ -170,9 +172,9 @@ export function Sidebar() {
 | 
			
		|||
  return (
 | 
			
		||||
    <aside
 | 
			
		||||
      ref={sidebarRef}
 | 
			
		||||
      className={styles.sidebar({
 | 
			
		||||
        resizing: isResizing,
 | 
			
		||||
        darwin: window.electron.platform === "darwin",
 | 
			
		||||
      className={cn("sidebar", {
 | 
			
		||||
        "sidebar--resizing": isResizing,
 | 
			
		||||
        "sidebar--darwin": window.electron.platform === "darwin",
 | 
			
		||||
      })}
 | 
			
		||||
      style={{
 | 
			
		||||
        width: sidebarWidth,
 | 
			
		||||
| 
						 | 
				
			
			@ -185,19 +187,19 @@ export function Sidebar() {
 | 
			
		|||
      >
 | 
			
		||||
        <SidebarProfile />
 | 
			
		||||
 | 
			
		||||
        <div className={styles.content}>
 | 
			
		||||
          <section className={styles.section}>
 | 
			
		||||
            <ul className={styles.menu}>
 | 
			
		||||
        <div className="sidebar__content">
 | 
			
		||||
          <section className="sidebar__section">
 | 
			
		||||
            <ul className="sidebar__menu">
 | 
			
		||||
              {routes.map(({ nameKey, path, render }) => (
 | 
			
		||||
                <li
 | 
			
		||||
                  key={nameKey}
 | 
			
		||||
                  className={styles.menuItem({
 | 
			
		||||
                    active: location.pathname === path,
 | 
			
		||||
                  className={cn("sidebar__menu-item", {
 | 
			
		||||
                    "sidebar__menu-item--active": location.pathname === path,
 | 
			
		||||
                  })}
 | 
			
		||||
                >
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    className={styles.menuItemButton}
 | 
			
		||||
                    className="sidebar__menu-item-button"
 | 
			
		||||
                    onClick={() => handleSidebarItemClick(path)}
 | 
			
		||||
                  >
 | 
			
		||||
                    {render()}
 | 
			
		||||
| 
						 | 
				
			
			@ -208,8 +210,8 @@ export function Sidebar() {
 | 
			
		|||
            </ul>
 | 
			
		||||
          </section>
 | 
			
		||||
 | 
			
		||||
          <section className={styles.section}>
 | 
			
		||||
            <small className={styles.sectionTitle}>{t("my_library")}</small>
 | 
			
		||||
          <section className="sidebar__section">
 | 
			
		||||
            <small className="sidebar__section-title">{t("my_library")}</small>
 | 
			
		||||
 | 
			
		||||
            <TextField
 | 
			
		||||
              ref={filterRef}
 | 
			
		||||
| 
						 | 
				
			
			@ -218,34 +220,35 @@ export function Sidebar() {
 | 
			
		|||
              theme="dark"
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <ul className={styles.menu}>
 | 
			
		||||
            <ul className="sidebar__menu">
 | 
			
		||||
              {filteredLibrary.map((game) => (
 | 
			
		||||
                <li
 | 
			
		||||
                  key={`${game.shop}-${game.objectId}`}
 | 
			
		||||
                  className={styles.menuItem({
 | 
			
		||||
                    active:
 | 
			
		||||
                  className={cn("sidebar__menu-item", {
 | 
			
		||||
                    "sidebar__menu-item--active":
 | 
			
		||||
                      location.pathname ===
 | 
			
		||||
                      `/game/${game.shop}/${game.objectId}`,
 | 
			
		||||
                    muted: game.download?.status === "removed",
 | 
			
		||||
                    "sidebar__menu-item--muted":
 | 
			
		||||
                      game.download?.status === "removed",
 | 
			
		||||
                  })}
 | 
			
		||||
                >
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    className={styles.menuItemButton}
 | 
			
		||||
                    className="sidebar__menu-item-button"
 | 
			
		||||
                    onClick={(event) => handleSidebarGameClick(event, game)}
 | 
			
		||||
                  >
 | 
			
		||||
                    {game.iconUrl ? (
 | 
			
		||||
                      <img
 | 
			
		||||
                        className={styles.gameIcon}
 | 
			
		||||
                        className="sidebar__game-icon"
 | 
			
		||||
                        src={game.iconUrl}
 | 
			
		||||
                        alt={game.title}
 | 
			
		||||
                        loading="lazy"
 | 
			
		||||
                      />
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      <SteamLogo className={styles.gameIcon} />
 | 
			
		||||
                      <SteamLogo className="sidebar__game-icon" />
 | 
			
		||||
                    )}
 | 
			
		||||
 | 
			
		||||
                    <span className={styles.menuItemButtonLabel}>
 | 
			
		||||
                    <span className="sidebar__menu-item-button-label">
 | 
			
		||||
                      {getGameTitle(game)}
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </button>
 | 
			
		||||
| 
						 | 
				
			
			@ -259,10 +262,10 @@ export function Sidebar() {
 | 
			
		|||
      {hasActiveSubscription && (
 | 
			
		||||
        <button
 | 
			
		||||
          type="button"
 | 
			
		||||
          className={styles.helpButton}
 | 
			
		||||
          className="sidebar__help-button"
 | 
			
		||||
          data-open-support-chat
 | 
			
		||||
        >
 | 
			
		||||
          <div className={styles.helpButtonIcon}>
 | 
			
		||||
          <div className="sidebar__help-button-icon">
 | 
			
		||||
            <CommentDiscussionIcon size={14} />
 | 
			
		||||
          </div>
 | 
			
		||||
          <span>{t("need_help")}</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -271,7 +274,7 @@ export function Sidebar() {
 | 
			
		|||
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        className={styles.handle}
 | 
			
		||||
        className="sidebar__handle"
 | 
			
		||||
        onMouseDown={handleMouseDown}
 | 
			
		||||
      />
 | 
			
		||||
    </aside>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,89 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const textFieldContainer = style({
 | 
			
		||||
  flex: "1",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const textField = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    display: "inline-flex",
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    width: "100%",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
    borderRadius: "8px",
 | 
			
		||||
    border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    height: "40px",
 | 
			
		||||
    minHeight: "40px",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    theme: {
 | 
			
		||||
      primary: {
 | 
			
		||||
        backgroundColor: vars.color.darkBackground,
 | 
			
		||||
      },
 | 
			
		||||
      dark: {
 | 
			
		||||
        backgroundColor: vars.color.background,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    hasError: {
 | 
			
		||||
      true: {
 | 
			
		||||
        borderColor: vars.color.danger,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    focused: {
 | 
			
		||||
      true: {
 | 
			
		||||
        borderColor: "#DADBE1",
 | 
			
		||||
      },
 | 
			
		||||
      false: {
 | 
			
		||||
        ":hover": {
 | 
			
		||||
          borderColor: "rgba(255, 255, 255, 0.5)",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const textFieldInput = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    backgroundColor: "transparent",
 | 
			
		||||
    border: "none",
 | 
			
		||||
    width: "100%",
 | 
			
		||||
    height: "100%",
 | 
			
		||||
    outline: "none",
 | 
			
		||||
    color: "#DADBE1",
 | 
			
		||||
    cursor: "default",
 | 
			
		||||
    fontFamily: "inherit",
 | 
			
		||||
    textOverflow: "ellipsis",
 | 
			
		||||
    padding: `${SPACING_UNIT}px`,
 | 
			
		||||
    ":focus": {
 | 
			
		||||
      cursor: "text",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    readOnly: {
 | 
			
		||||
      true: {
 | 
			
		||||
        textOverflow: "inherit",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const togglePasswordButton = style({
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  color: vars.color.muted,
 | 
			
		||||
  padding: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const textFieldWrapper = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const errorLabel = style({
 | 
			
		||||
  color: vars.color.danger,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										79
									
								
								src/renderer/src/components/text-field/text-field.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/renderer/src/components/text-field/text-field.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,79 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.text-field-container {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  gap: globals.$spacing-unit;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
 | 
			
		||||
  &__text-field {
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    border: solid 1px globals.$border-color;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    min-height: 40px;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      border-color: rgba(255, 255, 255, 0.5);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--primary {
 | 
			
		||||
      background-color: globals.$dark-background-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--dark {
 | 
			
		||||
      background-color: globals.$background-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--has-error {
 | 
			
		||||
      border-color: globals.$danger-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--focused {
 | 
			
		||||
      border-color: #dadbe1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__text-field-input {
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    border: none;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    color: #dadbe1;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    font-family: inherit;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    padding: globals.$spacing-unit;
 | 
			
		||||
 | 
			
		||||
    &:focus {
 | 
			
		||||
      cursor: text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--read-only {
 | 
			
		||||
      text-overflow: inherit;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__toggle-password-button {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    padding: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__text-field-wrapper {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__error-label {
 | 
			
		||||
    color: globals.$danger-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +1,17 @@
 | 
			
		|||
import React, { useId, useMemo, useState } from "react";
 | 
			
		||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
 | 
			
		||||
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./text-field.css";
 | 
			
		||||
import cn from "classnames";
 | 
			
		||||
 | 
			
		||||
import "./text-field.scss";
 | 
			
		||||
 | 
			
		||||
export interface TextFieldProps
 | 
			
		||||
  extends React.DetailedHTMLProps<
 | 
			
		||||
    React.InputHTMLAttributes<HTMLInputElement>,
 | 
			
		||||
    HTMLInputElement
 | 
			
		||||
  > {
 | 
			
		||||
  theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
 | 
			
		||||
  theme?: "primary" | "dark";
 | 
			
		||||
  label?: string | React.ReactNode;
 | 
			
		||||
  hint?: string | React.ReactNode;
 | 
			
		||||
  textFieldProps?: React.DetailedHTMLProps<
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +55,12 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
 | 
			
		|||
    }, [props.type, isPasswordVisible]);
 | 
			
		||||
 | 
			
		||||
    const hintContent = useMemo(() => {
 | 
			
		||||
      if (error) return <small className={styles.errorLabel}>{error}</small>;
 | 
			
		||||
      if (error && typeof error === "object" && "message" in error)
 | 
			
		||||
        return (
 | 
			
		||||
          <small className="text-field-container__error-label">
 | 
			
		||||
            {error.message as string}
 | 
			
		||||
          </small>
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
      if (hint) return <small>{hint}</small>;
 | 
			
		||||
      return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -73,22 +79,28 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
 | 
			
		|||
    const hasError = !!error;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={styles.textFieldContainer} {...containerProps}>
 | 
			
		||||
      <div className="text-field-container" {...containerProps}>
 | 
			
		||||
        {label && <label htmlFor={id}>{label}</label>}
 | 
			
		||||
 | 
			
		||||
        <div className={styles.textFieldWrapper}>
 | 
			
		||||
        <div className="text-field-container__text-field-wrapper">
 | 
			
		||||
          <div
 | 
			
		||||
            className={styles.textField({
 | 
			
		||||
              theme,
 | 
			
		||||
              hasError,
 | 
			
		||||
              focused: isFocused,
 | 
			
		||||
            })}
 | 
			
		||||
            className={cn(
 | 
			
		||||
              "text-field-container__text-field",
 | 
			
		||||
              `text-field-container__text-field--${theme}`,
 | 
			
		||||
              {
 | 
			
		||||
                "text-field-container__text-field--has-error": hasError,
 | 
			
		||||
                "text-field-container__text-field--focused": isFocused,
 | 
			
		||||
              }
 | 
			
		||||
            )}
 | 
			
		||||
            {...textFieldProps}
 | 
			
		||||
          >
 | 
			
		||||
            <input
 | 
			
		||||
              ref={ref}
 | 
			
		||||
              id={id}
 | 
			
		||||
              className={styles.textFieldInput({ readOnly: props.readOnly })}
 | 
			
		||||
              className={cn("text-field-container__text-field-input", {
 | 
			
		||||
                "text-field-container__text-field-input--read-only":
 | 
			
		||||
                  props.readOnly,
 | 
			
		||||
              })}
 | 
			
		||||
              {...props}
 | 
			
		||||
              onFocus={handleFocus}
 | 
			
		||||
              onBlur={handleBlur}
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +110,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
 | 
			
		|||
            {showPasswordToggleButton && (
 | 
			
		||||
              <button
 | 
			
		||||
                type="button"
 | 
			
		||||
                className={styles.togglePasswordButton}
 | 
			
		||||
                className="text-field-container__toggle-password-button"
 | 
			
		||||
                onClick={() => setIsPasswordVisible(!isPasswordVisible)}
 | 
			
		||||
                aria-label={t("toggle_password_visibility")}
 | 
			
		||||
              >
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,87 +0,0 @@
 | 
			
		|||
import { keyframes, style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
const TOAST_HEIGHT = 80;
 | 
			
		||||
 | 
			
		||||
export const slideIn = keyframes({
 | 
			
		||||
  "0%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
 | 
			
		||||
  "100%": { transform: "translateY(0)" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const slideOut = keyframes({
 | 
			
		||||
  "0%": { transform: `translateY(0)` },
 | 
			
		||||
  "100%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const toast = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    animationDuration: "0.2s",
 | 
			
		||||
    animationTimingFunction: "ease-in-out",
 | 
			
		||||
    maxHeight: TOAST_HEIGHT,
 | 
			
		||||
    position: "fixed",
 | 
			
		||||
    backgroundColor: vars.color.background,
 | 
			
		||||
    borderRadius: "4px",
 | 
			
		||||
    border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    right: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
    /* Bottom panel height + 16px */
 | 
			
		||||
    bottom: `${26 + SPACING_UNIT * 2}px`,
 | 
			
		||||
    overflow: "hidden",
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    flexDirection: "column",
 | 
			
		||||
    justifyContent: "space-between",
 | 
			
		||||
    zIndex: vars.zIndex.toast,
 | 
			
		||||
    maxWidth: "500px",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    closing: {
 | 
			
		||||
      true: {
 | 
			
		||||
        animationName: slideOut,
 | 
			
		||||
        transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)`,
 | 
			
		||||
      },
 | 
			
		||||
      false: {
 | 
			
		||||
        animationName: slideIn,
 | 
			
		||||
        transform: `translateY(0)`,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const toastContent = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const progress = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "5px",
 | 
			
		||||
  "::-webkit-progress-bar": {
 | 
			
		||||
    backgroundColor: vars.color.darkBackground,
 | 
			
		||||
  },
 | 
			
		||||
  "::-webkit-progress-value": {
 | 
			
		||||
    backgroundColor: vars.color.muted,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const closeButton = style({
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  padding: "0",
 | 
			
		||||
  margin: "0",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const successIcon = style({
 | 
			
		||||
  color: vars.color.success,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const errorIcon = style({
 | 
			
		||||
  color: vars.color.danger,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const warningIcon = style({
 | 
			
		||||
  color: vars.color.warning,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										87
									
								
								src/renderer/src/components/toast/toast.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/renderer/src/components/toast/toast.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,87 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
$toast-height: 80px;
 | 
			
		||||
 | 
			
		||||
.toast {
 | 
			
		||||
  animation-duration: 0.2s;
 | 
			
		||||
  animation-timing-function: ease-in-out;
 | 
			
		||||
  max-height: $toast-height;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  background-color: globals.$background-color;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  border: solid 1px globals.$border-color;
 | 
			
		||||
  right: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  //bottom panel height + 16px
 | 
			
		||||
  bottom: calc(26px + #{globals.$spacing-unit * 2});
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  z-index: globals.$toast-z-index;
 | 
			
		||||
  max-width: 500px;
 | 
			
		||||
  animation-name: slide-in;
 | 
			
		||||
  transform: translateY(0);
 | 
			
		||||
 | 
			
		||||
  &--closing {
 | 
			
		||||
    animation-name: slide-out;
 | 
			
		||||
    transform: translateY(calc($toast-height + #{globals.$spacing-unit * 2}));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__progress {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 5px;
 | 
			
		||||
    &::-webkit-progress-bar {
 | 
			
		||||
      background-color: globals.$dark-background-color;
 | 
			
		||||
    }
 | 
			
		||||
    &::-webkit-progress-value {
 | 
			
		||||
      background-color: globals.$muted-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__close-button {
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__success-icon {
 | 
			
		||||
    color: globals.$success-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__error-icon {
 | 
			
		||||
    color: globals.$danger-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__warning-icon {
 | 
			
		||||
    color: globals.$warning-color;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slide-in {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: translateY(calc($toast-height + #{globals.$spacing-unit * 2}));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateY(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slide-out {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: translateY(0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateY(calc($toast-height + #{globals.$spacing-unit * 2}));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,8 +6,9 @@ import {
 | 
			
		|||
  XIcon,
 | 
			
		||||
} from "@primer/octicons-react";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./toast.css";
 | 
			
		||||
import "./toast.scss";
 | 
			
		||||
import { SPACING_UNIT } from "@renderer/theme.css";
 | 
			
		||||
import cn from "classnames";
 | 
			
		||||
 | 
			
		||||
export interface ToastProps {
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -77,22 +78,28 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
 | 
			
		|||
  if (!visible) return null;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.toast({ closing: isClosing })}>
 | 
			
		||||
      <div className={styles.toastContent}>
 | 
			
		||||
    <div
 | 
			
		||||
      className={cn("toast", {
 | 
			
		||||
        "toast--closing": isClosing,
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="toast__content">
 | 
			
		||||
        <div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
 | 
			
		||||
          {type === "success" && (
 | 
			
		||||
            <CheckCircleFillIcon className={styles.successIcon} />
 | 
			
		||||
            <CheckCircleFillIcon className="toast__success-icon" />
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
 | 
			
		||||
          {type === "error" && (
 | 
			
		||||
            <XCircleFillIcon className="toast__error-icon" />
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {type === "warning" && <AlertIcon className={styles.warningIcon} />}
 | 
			
		||||
          {type === "warning" && <AlertIcon className="toast__warning-icon" />}
 | 
			
		||||
          <span style={{ fontWeight: "bold" }}>{message}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <button
 | 
			
		||||
          type="button"
 | 
			
		||||
          className={styles.closeButton}
 | 
			
		||||
          className="toast__close-button"
 | 
			
		||||
          onClick={startAnimateClosing}
 | 
			
		||||
          aria-label="Close toast"
 | 
			
		||||
        >
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +107,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
 | 
			
		|||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <progress className={styles.progress} value={progress} max={100} />
 | 
			
		||||
      <progress className="toast__progress" value={progress} max={100} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,10 @@
 | 
			
		|||
import { useDate } from "@renderer/hooks";
 | 
			
		||||
import type { UserAchievement } from "@types";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import * as styles from "./achievements.css";
 | 
			
		||||
import "./achievements.scss";
 | 
			
		||||
import { EyeClosedIcon } from "@primer/octicons-react";
 | 
			
		||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
 | 
			
		||||
import { useSubscription } from "@renderer/hooks/use-subscription";
 | 
			
		||||
import { vars } from "@renderer/theme.css";
 | 
			
		||||
 | 
			
		||||
interface AchievementListProps {
 | 
			
		||||
  achievements: UserAchievement[];
 | 
			
		||||
| 
						 | 
				
			
			@ -17,27 +16,21 @@ export function AchievementList({ achievements }: AchievementListProps) {
 | 
			
		|||
  const { formatDateTime } = useDate();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ul className={styles.list}>
 | 
			
		||||
    <ul className="achievements__list">
 | 
			
		||||
      {achievements.map((achievement) => (
 | 
			
		||||
        <li
 | 
			
		||||
          key={achievement.name}
 | 
			
		||||
          className={styles.listItem}
 | 
			
		||||
          style={{ display: "flex" }}
 | 
			
		||||
        >
 | 
			
		||||
        <li key={achievement.name} className="achievements__item">
 | 
			
		||||
          <img
 | 
			
		||||
            className={styles.listItemImage({
 | 
			
		||||
              unlocked: achievement.unlocked,
 | 
			
		||||
            })}
 | 
			
		||||
            className={`achievements__item-image ${!achievement.unlocked ? "achievements__item-image--locked" : ""}`}
 | 
			
		||||
            src={achievement.icon}
 | 
			
		||||
            alt={achievement.displayName}
 | 
			
		||||
            loading="lazy"
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <div style={{ flex: 1 }}>
 | 
			
		||||
            <h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
 | 
			
		||||
          <div className="achievements__item-content">
 | 
			
		||||
            <h4 className="achievements__item-title">
 | 
			
		||||
              {achievement.hidden && (
 | 
			
		||||
                <span
 | 
			
		||||
                  style={{ display: "flex" }}
 | 
			
		||||
                  className="achievements__item-hidden-icon"
 | 
			
		||||
                  title={t("hidden_achievement_tooltip")}
 | 
			
		||||
                >
 | 
			
		||||
                  <EyeClosedIcon size={12} />
 | 
			
		||||
| 
						 | 
				
			
			@ -47,48 +40,36 @@ export function AchievementList({ achievements }: AchievementListProps) {
 | 
			
		|||
            </h4>
 | 
			
		||||
            <p>{achievement.description}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div
 | 
			
		||||
            style={{
 | 
			
		||||
              display: "flex",
 | 
			
		||||
              flexDirection: "column",
 | 
			
		||||
              gap: "8px",
 | 
			
		||||
              alignItems: "flex-end",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
 | 
			
		||||
          <div className="achievements__item-meta">
 | 
			
		||||
            {achievement.points != undefined ? (
 | 
			
		||||
              <div
 | 
			
		||||
                style={{ display: "flex", alignItems: "center", gap: "4px" }}
 | 
			
		||||
                className="achievements__item-points"
 | 
			
		||||
                title={t("achievement_earn_points", {
 | 
			
		||||
                  points: achievement.points,
 | 
			
		||||
                })}
 | 
			
		||||
              >
 | 
			
		||||
                <HydraIcon width={20} height={20} />
 | 
			
		||||
                <p style={{ fontSize: "1.1em" }}>{achievement.points}</p>
 | 
			
		||||
                <HydraIcon className="achievements__item-points-icon" />
 | 
			
		||||
                <p className="achievements__item-points-value">
 | 
			
		||||
                  {achievement.points}
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <button
 | 
			
		||||
                onClick={() => showHydraCloudModal("achievements")}
 | 
			
		||||
                style={{
 | 
			
		||||
                  display: "flex",
 | 
			
		||||
                  alignItems: "center",
 | 
			
		||||
                  gap: "4px",
 | 
			
		||||
                  cursor: "pointer",
 | 
			
		||||
                  color: vars.color.warning,
 | 
			
		||||
                }}
 | 
			
		||||
                title={t("achievement_earn_points", {
 | 
			
		||||
                  points: "???",
 | 
			
		||||
                })}
 | 
			
		||||
                className="achievements__item-points achievements__item-points--locked"
 | 
			
		||||
                title={t("achievement_earn_points", { points: "???" })}
 | 
			
		||||
              >
 | 
			
		||||
                <HydraIcon width={20} height={20} />
 | 
			
		||||
                <p style={{ fontSize: "1.1em" }}>???</p>
 | 
			
		||||
                <HydraIcon className="achievements__item-points-icon" />
 | 
			
		||||
                <p className="achievements__item-points-value">???</p>
 | 
			
		||||
              </button>
 | 
			
		||||
            )}
 | 
			
		||||
            {achievement.unlockTime != null && (
 | 
			
		||||
              <div
 | 
			
		||||
                className="achievements__item-unlock-time"
 | 
			
		||||
                title={t("unlocked_at", {
 | 
			
		||||
                  date: formatDateTime(achievement.unlockTime),
 | 
			
		||||
                })}
 | 
			
		||||
                style={{ whiteSpace: "nowrap", gap: "4px", display: "flex" }}
 | 
			
		||||
              >
 | 
			
		||||
                <small>{formatDateTime(achievement.unlockTime)}</small>
 | 
			
		||||
              </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,71 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const panel = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
 | 
			
		||||
  backgroundColor: vars.color.background,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  alignItems: "start",
 | 
			
		||||
  justifyContent: "space-between",
 | 
			
		||||
  borderBottom: `solid 1px ${vars.color.border}`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const content = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const actions = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadDetailsRow = style({
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadsLink = style({
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  textDecoration: "underline",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const progressBar = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    position: "absolute",
 | 
			
		||||
    bottom: "0",
 | 
			
		||||
    left: "0",
 | 
			
		||||
    width: "100%",
 | 
			
		||||
    height: "3px",
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    "::-webkit-progress-bar": {
 | 
			
		||||
      backgroundColor: "transparent",
 | 
			
		||||
    },
 | 
			
		||||
    "::-webkit-progress-value": {
 | 
			
		||||
      backgroundColor: vars.color.muted,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    disabled: {
 | 
			
		||||
      true: {
 | 
			
		||||
        opacity: vars.opacity.disabled,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const link = style({
 | 
			
		||||
  textAlign: "start",
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    textDecoration: "underline",
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										97
									
								
								src/renderer/src/pages/achievements/achievement-panel.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/renderer/src/pages/achievements/achievement-panel.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,97 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.achievement-panel {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: globals.$spacing-unit * 2 globals.$spacing-unit * 3;
 | 
			
		||||
  background-color: globals.$dark-background-color;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: start;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  border-bottom: solid 1px globals.$border-color;
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__actions {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__download-details-row {
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloads-link {
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__progress-bar {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 3px;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
 | 
			
		||||
    &::-webkit-progress-bar {
 | 
			
		||||
      background-color: transparent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::-webkit-progress-value {
 | 
			
		||||
      background-color: globals.$muted-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--disabled {
 | 
			
		||||
      opacity: globals.$disabled-opacity;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__link {
 | 
			
		||||
    text-align: start;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    background: none;
 | 
			
		||||
    border: none;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      text-decoration: underline;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--warning {
 | 
			
		||||
      color: globals.$warning-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__grid {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    gap: globals.$spacing-unit * 2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__grid--with-subscription {
 | 
			
		||||
    grid-template-columns: 3fr 1fr 1fr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__grid--without-subscription {
 | 
			
		||||
    grid-template-columns: 3fr 2fr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__points-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content-icon {
 | 
			
		||||
    width: 18px;
 | 
			
		||||
    height: 18px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,8 +3,7 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
 | 
			
		|||
import { UserAchievement } from "@types";
 | 
			
		||||
import { useSubscription } from "@renderer/hooks/use-subscription";
 | 
			
		||||
import { useUserDetails } from "@renderer/hooks";
 | 
			
		||||
import { vars } from "@renderer/theme.css";
 | 
			
		||||
import * as styles from "./achievement-panel.css";
 | 
			
		||||
import "./achievement-panel.scss";
 | 
			
		||||
 | 
			
		||||
export interface AchievementPanelProps {
 | 
			
		||||
  achievements: UserAchievement[];
 | 
			
		||||
| 
						 | 
				
			
			@ -28,17 +27,18 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
 | 
			
		|||
 | 
			
		||||
  if (!hasActiveSubscription) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={styles.panel}>
 | 
			
		||||
        <div className={styles.content}>
 | 
			
		||||
          {t("earned_points")} <HydraIcon width={20} height={20} />
 | 
			
		||||
      <div className="achievement-panel">
 | 
			
		||||
        <div className="achievement-panel__content">
 | 
			
		||||
          {t("earned_points")}{" "}
 | 
			
		||||
          <HydraIcon className="achievement-panel__content-icon" />
 | 
			
		||||
          ??? / ???
 | 
			
		||||
        </div>
 | 
			
		||||
        <button
 | 
			
		||||
          type="button"
 | 
			
		||||
          onClick={() => showHydraCloudModal("achievements-points")}
 | 
			
		||||
          className={styles.link}
 | 
			
		||||
          className="achievement-panel__link"
 | 
			
		||||
        >
 | 
			
		||||
          <small style={{ color: vars.color.warning }}>
 | 
			
		||||
          <small className="achievement-panel__link--warning">
 | 
			
		||||
            {t("how_to_earn_achievements_points")}
 | 
			
		||||
          </small>
 | 
			
		||||
        </button>
 | 
			
		||||
| 
						 | 
				
			
			@ -47,9 +47,10 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.panel}>
 | 
			
		||||
      <div className={styles.content}>
 | 
			
		||||
        {t("earned_points")} <HydraIcon width={20} height={20} />
 | 
			
		||||
    <div className="achievement-panel">
 | 
			
		||||
      <div className="achievement-panel__content">
 | 
			
		||||
        {t("earned_points")}{" "}
 | 
			
		||||
        <HydraIcon className="achievement-panel__content-icon" />
 | 
			
		||||
        {achievementsPointsEarnedSum} / {achievementsPointsTotal}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,13 @@
 | 
			
		|||
import Skeleton from "react-loading-skeleton";
 | 
			
		||||
import * as styles from "./achievements.css";
 | 
			
		||||
import "./achievements.scss";
 | 
			
		||||
 | 
			
		||||
export function AchievementsSkeleton() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.container}>
 | 
			
		||||
      <div className={styles.hero}>
 | 
			
		||||
        <Skeleton className={styles.heroImageSkeleton} />
 | 
			
		||||
    <div className="achievements__container">
 | 
			
		||||
      <div className="achievements__hero">
 | 
			
		||||
        <Skeleton className="achievements__hero-image-skeleton" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className={styles.heroPanelSkeleton}></div>
 | 
			
		||||
      <div className="achievements__hero-panel-skeleton"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										262
									
								
								src/renderer/src/pages/achievements/achievements.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								src/renderer/src/pages/achievements/achievements.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,262 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
@use "sass:math";
 | 
			
		||||
 | 
			
		||||
$hero-height: 150px;
 | 
			
		||||
$logo-height: 100px;
 | 
			
		||||
$logo-max-width: 200px;
 | 
			
		||||
 | 
			
		||||
.achievements {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  transition: all ease 0.3s;
 | 
			
		||||
 | 
			
		||||
  &__hero {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: $hero-height;
 | 
			
		||||
    min-height: $hero-height;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
 | 
			
		||||
    &-content {
 | 
			
		||||
      padding: globals.$spacing-unit * 2;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: space-between;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-logo-backdrop {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      justify-content: flex-end;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-image-skeleton {
 | 
			
		||||
      height: 150px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__game-logo {
 | 
			
		||||
    width: $logo-max-width;
 | 
			
		||||
    height: $logo-height;
 | 
			
		||||
    object-fit: contain;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      transform: scale(1.05);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__table-header {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    background-color: var(--color-dark-background);
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    border-bottom: solid 1px var(--color-border);
 | 
			
		||||
    position: sticky;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
 | 
			
		||||
    &--stuck {
 | 
			
		||||
      box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__list {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit * 2;
 | 
			
		||||
    padding: globals.$spacing-unit * 2;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    background-color: var(--color-background);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__item {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    transition: all ease 0.1s;
 | 
			
		||||
    color: var(--color-muted);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    padding: globals.$spacing-unit globals.$spacing-unit;
 | 
			
		||||
    gap: globals.$spacing-unit * 2;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: rgba(255, 255, 255, 0.15);
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-image {
 | 
			
		||||
      width: 54px;
 | 
			
		||||
      height: 54px;
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
      object-fit: cover;
 | 
			
		||||
 | 
			
		||||
      &--locked {
 | 
			
		||||
        filter: grayscale(100%);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-content {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-title {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-hidden-icon {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      color: var(--color-warning);
 | 
			
		||||
      opacity: 0.8;
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        width: 12px;
 | 
			
		||||
        height: 12px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-eye-closed {
 | 
			
		||||
      width: 12px;
 | 
			
		||||
      height: 12px;
 | 
			
		||||
      color: globals.$warning-color;
 | 
			
		||||
      scale: 4;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-meta {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      gap: 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-points {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: 4px;
 | 
			
		||||
      margin-right: 4px;
 | 
			
		||||
      font-weight: 600;
 | 
			
		||||
 | 
			
		||||
      &--locked {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        color: var(--color-warning);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &-icon {
 | 
			
		||||
        width: 18px;
 | 
			
		||||
        height: 18px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &-value {
 | 
			
		||||
        font-size: 1.1em;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-unlock-time {
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
      gap: 4px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-compared {
 | 
			
		||||
      display: grid;
 | 
			
		||||
      grid-template-columns: 3fr 1fr 1fr;
 | 
			
		||||
 | 
			
		||||
      &--no-owner {
 | 
			
		||||
        grid-template-columns: 3fr 2fr;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-main {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: row;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: globals.$spacing-unit;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-status {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      padding: globals.$spacing-unit;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
 | 
			
		||||
      &--unlocked {
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
        flex-direction: row;
 | 
			
		||||
        gap: globals.$spacing-unit;
 | 
			
		||||
        padding: 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__progress-bar {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 8px;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
 | 
			
		||||
    &::-webkit-progress-bar {
 | 
			
		||||
      background-color: rgba(255, 255, 255, 0.15);
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::-webkit-progress-value {
 | 
			
		||||
      background-color: var(--color-muted);
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__profile-avatar {
 | 
			
		||||
    height: 54px;
 | 
			
		||||
    width: 54px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    background-color: var(--color-background);
 | 
			
		||||
    position: relative;
 | 
			
		||||
    object-fit: cover;
 | 
			
		||||
 | 
			
		||||
    &--small {
 | 
			
		||||
      height: 32px;
 | 
			
		||||
      width: 32px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__subscription-button {
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    gap: math.div(globals.$spacing-unit, 2);
 | 
			
		||||
    color: var(--color-body);
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      text-decoration: underline;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +1,11 @@
 | 
			
		|||
import type { ComparedAchievements } from "@types";
 | 
			
		||||
import * as styles from "./achievements.css";
 | 
			
		||||
import "./achievements.scss";
 | 
			
		||||
import {
 | 
			
		||||
  CheckCircleIcon,
 | 
			
		||||
  EyeClosedIcon,
 | 
			
		||||
  LockIcon,
 | 
			
		||||
} from "@primer/octicons-react";
 | 
			
		||||
import { useDate } from "@renderer/hooks";
 | 
			
		||||
import { SPACING_UNIT } from "@renderer/theme.css";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export interface ComparedAchievementListProps {
 | 
			
		||||
| 
						 | 
				
			
			@ -20,39 +19,26 @@ export function ComparedAchievementList({
 | 
			
		|||
  const { formatDateTime } = useDate();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ul className={styles.list}>
 | 
			
		||||
    <ul className="achievements__list">
 | 
			
		||||
      {achievements.achievements.map((achievement, index) => (
 | 
			
		||||
        <li
 | 
			
		||||
          key={index}
 | 
			
		||||
          className={styles.listItem}
 | 
			
		||||
          style={{
 | 
			
		||||
            display: "grid",
 | 
			
		||||
            gridTemplateColumns: achievement.ownerStat
 | 
			
		||||
              ? "3fr 1fr 1fr"
 | 
			
		||||
              : "3fr 2fr",
 | 
			
		||||
          }}
 | 
			
		||||
          className={`achievements__item achievements__item-compared ${
 | 
			
		||||
            !achievement.ownerStat && "achievements__item-compared--no-owner"
 | 
			
		||||
          }`}
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            style={{
 | 
			
		||||
              display: "flex",
 | 
			
		||||
              flexDirection: "row",
 | 
			
		||||
              alignItems: "center",
 | 
			
		||||
              gap: `${SPACING_UNIT}px`,
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
          <div className="achievements__item-main">
 | 
			
		||||
            <img
 | 
			
		||||
              className={styles.listItemImage({
 | 
			
		||||
                unlocked: true,
 | 
			
		||||
              })}
 | 
			
		||||
              className="achievements__item-image"
 | 
			
		||||
              src={achievement.icon}
 | 
			
		||||
              alt={achievement.displayName}
 | 
			
		||||
              loading="lazy"
 | 
			
		||||
            />
 | 
			
		||||
            <div>
 | 
			
		||||
              <h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
 | 
			
		||||
            <div className="achievements__item-content">
 | 
			
		||||
              <h4 className="achievements__item-title">
 | 
			
		||||
                {achievement.hidden && (
 | 
			
		||||
                  <span
 | 
			
		||||
                    style={{ display: "flex" }}
 | 
			
		||||
                    className="achievements__item-hidden-icon"
 | 
			
		||||
                    title={t("hidden_achievement_tooltip")}
 | 
			
		||||
                  >
 | 
			
		||||
                    <EyeClosedIcon size={12} />
 | 
			
		||||
| 
						 | 
				
			
			@ -67,25 +53,13 @@ export function ComparedAchievementList({
 | 
			
		|||
          {achievement.ownerStat ? (
 | 
			
		||||
            achievement.ownerStat.unlocked ? (
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
                  whiteSpace: "nowrap",
 | 
			
		||||
                  display: "flex",
 | 
			
		||||
                  flexDirection: "row",
 | 
			
		||||
                  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
                  justifyContent: "center",
 | 
			
		||||
                }}
 | 
			
		||||
                className="achievements__item-status achievements__item-status--unlocked"
 | 
			
		||||
                title={formatDateTime(achievement.ownerStat.unlockTime!)}
 | 
			
		||||
              >
 | 
			
		||||
                <CheckCircleIcon />
 | 
			
		||||
              </div>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
                  display: "flex",
 | 
			
		||||
                  padding: `${SPACING_UNIT}px`,
 | 
			
		||||
                  justifyContent: "center",
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
              <div className="achievements__item-status">
 | 
			
		||||
                <LockIcon />
 | 
			
		||||
              </div>
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			@ -93,25 +67,13 @@ export function ComparedAchievementList({
 | 
			
		|||
 | 
			
		||||
          {achievement.targetStat.unlocked ? (
 | 
			
		||||
            <div
 | 
			
		||||
              style={{
 | 
			
		||||
                whiteSpace: "nowrap",
 | 
			
		||||
                display: "flex",
 | 
			
		||||
                flexDirection: "row",
 | 
			
		||||
                gap: `${SPACING_UNIT}px`,
 | 
			
		||||
                justifyContent: "center",
 | 
			
		||||
              }}
 | 
			
		||||
              className="achievements__item-status achievements__item-status--unlocked"
 | 
			
		||||
              title={formatDateTime(achievement.targetStat.unlockTime!)}
 | 
			
		||||
            >
 | 
			
		||||
              <CheckCircleIcon />
 | 
			
		||||
            </div>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div
 | 
			
		||||
              style={{
 | 
			
		||||
                display: "flex",
 | 
			
		||||
                padding: `${SPACING_UNIT}px`,
 | 
			
		||||
                justifyContent: "center",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
            <div className="achievements__item-status">
 | 
			
		||||
              <LockIcon />
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,8 @@
 | 
			
		|||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import * as styles from "./achievement-panel.css";
 | 
			
		||||
 | 
			
		||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
 | 
			
		||||
import { ComparedAchievements } from "@types";
 | 
			
		||||
import { SPACING_UNIT } from "@renderer/theme.css";
 | 
			
		||||
import { useUserDetails } from "@renderer/hooks";
 | 
			
		||||
import "./achievement-panel.scss";
 | 
			
		||||
 | 
			
		||||
export interface ComparedAchievementPanelProps {
 | 
			
		||||
  achievements: ComparedAchievements;
 | 
			
		||||
| 
						 | 
				
			
			@ -18,25 +16,25 @@ export function ComparedAchievementPanel({
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={styles.panel}
 | 
			
		||||
      style={{
 | 
			
		||||
        display: "grid",
 | 
			
		||||
        gridTemplateColumns: hasActiveSubscription ? "3fr 1fr 1fr" : "3fr 2fr",
 | 
			
		||||
        gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
      }}
 | 
			
		||||
      className={`achievement-panel achievement-panel__grid ${
 | 
			
		||||
        hasActiveSubscription
 | 
			
		||||
          ? "achievement-panel__grid--with-subscription"
 | 
			
		||||
          : "achievement-panel__grid--without-subscription"
 | 
			
		||||
      }`}
 | 
			
		||||
    >
 | 
			
		||||
      <div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
 | 
			
		||||
        {t("available_points")} <HydraIcon width={20} height={20} />{" "}
 | 
			
		||||
      <div className="achievement-panel__points-container">
 | 
			
		||||
        {t("available_points")}{" "}
 | 
			
		||||
        <HydraIcon className="achievement-panel__content-icon" />{" "}
 | 
			
		||||
        {achievements.achievementsPointsTotal}
 | 
			
		||||
      </div>
 | 
			
		||||
      {hasActiveSubscription && (
 | 
			
		||||
        <div className={styles.content}>
 | 
			
		||||
          <HydraIcon width={20} height={20} />
 | 
			
		||||
        <div className="achievement-panel__content">
 | 
			
		||||
          <HydraIcon className="achievement-panel__content-icon" />
 | 
			
		||||
          {achievements.owner.achievementsPointsEarnedSum ?? 0}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      <div className={styles.content}>
 | 
			
		||||
        <HydraIcon width={20} height={20} />
 | 
			
		||||
      <div className="achievement-panel__content">
 | 
			
		||||
        <HydraIcon className="achievement-panel__content-icon" />
 | 
			
		||||
        {achievements.target.achievementsPointsEarnedSum}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { vars } from "@renderer/theme.css";
 | 
			
		||||
import { XIcon } from "@primer/octicons-react";
 | 
			
		||||
import "./filter.scss";
 | 
			
		||||
 | 
			
		||||
interface FilterItemProps {
 | 
			
		||||
  filter: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -9,39 +9,13 @@ interface FilterItemProps {
 | 
			
		|||
 | 
			
		||||
export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      style={{
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        alignItems: "center",
 | 
			
		||||
        color: vars.color.body,
 | 
			
		||||
        backgroundColor: vars.color.darkBackground,
 | 
			
		||||
        padding: "6px 12px",
 | 
			
		||||
        borderRadius: 4,
 | 
			
		||||
        border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
        fontSize: 12,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          width: 10,
 | 
			
		||||
          height: 10,
 | 
			
		||||
          backgroundColor: orbColor,
 | 
			
		||||
          borderRadius: "50%",
 | 
			
		||||
          marginRight: 8,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    <div className="filter-item">
 | 
			
		||||
      <div className="filter-item__orb" style={{ backgroundColor: orbColor }} />
 | 
			
		||||
      {filter}
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        onClick={onRemove}
 | 
			
		||||
        style={{
 | 
			
		||||
          color: vars.color.body,
 | 
			
		||||
          marginLeft: 4,
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          justifyContent: "center",
 | 
			
		||||
          cursor: "pointer",
 | 
			
		||||
        }}
 | 
			
		||||
        className="filter-item__remove-button"
 | 
			
		||||
      >
 | 
			
		||||
        <XIcon size={13} />
 | 
			
		||||
      </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,8 @@
 | 
			
		|||
import { CheckboxField, TextField } from "@renderer/components";
 | 
			
		||||
import { useFormat } from "@renderer/hooks";
 | 
			
		||||
import { useCallback, useMemo, useState } from "react";
 | 
			
		||||
 | 
			
		||||
import "./filter.scss";
 | 
			
		||||
import List from "rc-virtual-list";
 | 
			
		||||
import { vars } from "@renderer/theme.css";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
export interface FilterSectionProps {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,36 +53,18 @@ export function FilterSection({
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
 | 
			
		||||
      <div className="filter-section__header">
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            width: 10,
 | 
			
		||||
            height: 10,
 | 
			
		||||
            backgroundColor: color,
 | 
			
		||||
            borderRadius: "50%",
 | 
			
		||||
          }}
 | 
			
		||||
          className="filter-section__orb"
 | 
			
		||||
          style={{ backgroundColor: color }}
 | 
			
		||||
        />
 | 
			
		||||
        <h3
 | 
			
		||||
          style={{
 | 
			
		||||
            fontSize: 16,
 | 
			
		||||
            fontWeight: 500,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {title}
 | 
			
		||||
        </h3>
 | 
			
		||||
        <h3 className="filter-section__title">{title}</h3>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {selectedItemsCount > 0 ? (
 | 
			
		||||
        <button
 | 
			
		||||
          type="button"
 | 
			
		||||
          style={{
 | 
			
		||||
            fontSize: 12,
 | 
			
		||||
            marginBottom: 12,
 | 
			
		||||
            display: "block",
 | 
			
		||||
            color: vars.color.body,
 | 
			
		||||
            cursor: "pointer",
 | 
			
		||||
            textDecoration: "underline",
 | 
			
		||||
          }}
 | 
			
		||||
          className="filter-section__clear-button"
 | 
			
		||||
          onClick={onClear}
 | 
			
		||||
        >
 | 
			
		||||
          {t("clear_filters", {
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +72,7 @@ export function FilterSection({
 | 
			
		|||
          })}
 | 
			
		||||
        </button>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <span style={{ fontSize: 12, marginBottom: 12, display: "block" }}>
 | 
			
		||||
        <span className="filter-section__count">
 | 
			
		||||
          {t("filter_count", {
 | 
			
		||||
            filterCount: formatNumber(items.length),
 | 
			
		||||
          })}
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +83,7 @@ export function FilterSection({
 | 
			
		|||
        placeholder={t("search")}
 | 
			
		||||
        onChange={(e) => onSearch(e.target.value)}
 | 
			
		||||
        value={search}
 | 
			
		||||
        containerProps={{ style: { marginBottom: 16 } }}
 | 
			
		||||
        containerProps={{ className: "filter-section__search" }}
 | 
			
		||||
        theme="dark"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +103,7 @@ export function FilterSection({
 | 
			
		|||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {(item) => (
 | 
			
		||||
          <div key={item.value} style={{ height: 28, maxHeight: 28 }}>
 | 
			
		||||
          <div key={item.value} className="filter-section__item">
 | 
			
		||||
            <CheckboxField
 | 
			
		||||
              label={item.label}
 | 
			
		||||
              checked={item.checked}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										71
									
								
								src/renderer/src/pages/catalogue/filter.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/renderer/src/pages/catalogue/filter.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,71 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.filter-item {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  color: globals.$body-color;
 | 
			
		||||
  background-color: globals.$dark-background-color;
 | 
			
		||||
  padding: 6px 12px;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  border: solid 1px globals.$border-color;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
 | 
			
		||||
  &__orb {
 | 
			
		||||
    width: 10px;
 | 
			
		||||
    height: 10px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    margin-right: 8px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__remove-button {
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    margin-left: 4px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.filter-section {
 | 
			
		||||
  &__header {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: 8px;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__orb {
 | 
			
		||||
    width: 10px;
 | 
			
		||||
    height: 10px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__title {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__count {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    margin-bottom: 12px;
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__clear-button {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    margin-bottom: 12px;
 | 
			
		||||
    display: block;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__search {
 | 
			
		||||
    margin-bottom: 16px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__item {
 | 
			
		||||
    height: 28px;
 | 
			
		||||
    max-height: 28px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/renderer/src/pages/catalogue/pagination.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/renderer/src/pages/catalogue/pagination.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
.pagination {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 4px;
 | 
			
		||||
 | 
			
		||||
  &__button {
 | 
			
		||||
    width: 40px;
 | 
			
		||||
    max-width: 40px;
 | 
			
		||||
    max-height: 40px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__ellipsis {
 | 
			
		||||
    width: 40px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    &-text {
 | 
			
		||||
      font-size: 16px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import { Button } from "@renderer/components/button/button";
 | 
			
		||||
import { ChevronLeftIcon, ChevronRightIcon } from "@primer/octicons-react";
 | 
			
		||||
import { useFormat } from "@renderer/hooks/use-format";
 | 
			
		||||
import "./pagination.scss";
 | 
			
		||||
 | 
			
		||||
interface PaginationProps {
 | 
			
		||||
  page: number;
 | 
			
		||||
| 
						 | 
				
			
			@ -31,17 +32,12 @@ export function Pagination({
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      style={{
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        gap: 4,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
    <div className="pagination">
 | 
			
		||||
      {/* Previous Button */}
 | 
			
		||||
      <Button
 | 
			
		||||
        theme="outline"
 | 
			
		||||
        onClick={() => onPageChange(page - 1)}
 | 
			
		||||
        style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
 | 
			
		||||
        className="pagination__button"
 | 
			
		||||
        disabled={page === 1}
 | 
			
		||||
      >
 | 
			
		||||
        <ChevronLeftIcon />
 | 
			
		||||
| 
						 | 
				
			
			@ -53,22 +49,15 @@ export function Pagination({
 | 
			
		|||
          <Button
 | 
			
		||||
            theme="outline"
 | 
			
		||||
            onClick={() => onPageChange(1)}
 | 
			
		||||
            style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
 | 
			
		||||
            className="pagination__button"
 | 
			
		||||
            disabled={page === 1}
 | 
			
		||||
          >
 | 
			
		||||
            {1}
 | 
			
		||||
          </Button>
 | 
			
		||||
 | 
			
		||||
          {/* ellipsis */}
 | 
			
		||||
          <div
 | 
			
		||||
            style={{
 | 
			
		||||
              width: 40,
 | 
			
		||||
              justifyContent: "center",
 | 
			
		||||
              display: "flex",
 | 
			
		||||
              alignItems: "center",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <span style={{ fontSize: 16 }}>...</span>
 | 
			
		||||
          <div className="pagination__ellipsis">
 | 
			
		||||
            <span className="pagination__ellipsis-text">...</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +70,7 @@ export function Pagination({
 | 
			
		|||
        <Button
 | 
			
		||||
          theme={page === pageNumber ? "primary" : "outline"}
 | 
			
		||||
          key={pageNumber}
 | 
			
		||||
          style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
 | 
			
		||||
          className="pagination__button"
 | 
			
		||||
          onClick={() => onPageChange(pageNumber)}
 | 
			
		||||
        >
 | 
			
		||||
          {formatNumber(pageNumber)}
 | 
			
		||||
| 
						 | 
				
			
			@ -91,22 +80,15 @@ export function Pagination({
 | 
			
		|||
      {page < totalPages - 1 && (
 | 
			
		||||
        <>
 | 
			
		||||
          {/* ellipsis */}
 | 
			
		||||
          <div
 | 
			
		||||
            style={{
 | 
			
		||||
              width: 40,
 | 
			
		||||
              justifyContent: "center",
 | 
			
		||||
              display: "flex",
 | 
			
		||||
              alignItems: "center",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <span style={{ fontSize: 16 }}>...</span>
 | 
			
		||||
          <div className="pagination__ellipsis">
 | 
			
		||||
            <span className="pagination__ellipsis-text">...</span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {/* last page */}
 | 
			
		||||
          <Button
 | 
			
		||||
            theme="outline"
 | 
			
		||||
            onClick={() => onPageChange(totalPages)}
 | 
			
		||||
            style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
 | 
			
		||||
            className="pagination__button"
 | 
			
		||||
            disabled={page === totalPages}
 | 
			
		||||
          >
 | 
			
		||||
            {formatNumber(totalPages)}
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +100,7 @@ export function Pagination({
 | 
			
		|||
      <Button
 | 
			
		||||
        theme="outline"
 | 
			
		||||
        onClick={() => onPageChange(page + 1)}
 | 
			
		||||
        style={{ width: 40, maxWidth: 40, maxHeight: 40 }}
 | 
			
		||||
        className="pagination__button"
 | 
			
		||||
        disabled={page === totalPages}
 | 
			
		||||
      >
 | 
			
		||||
        <ChevronRightIcon />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const deleteActionsButtonsCtn = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  justifyContent: "end",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										11
									
								
								src/renderer/src/pages/downloads/delete-game-modal.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/renderer/src/pages/downloads/delete-game-modal.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.delete-game-modal {
 | 
			
		||||
  &__actions {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,6 @@
 | 
			
		|||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
import { Button, Modal } from "@renderer/components";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./delete-game-modal.css";
 | 
			
		||||
import "./delete-game-modal.scss";
 | 
			
		||||
 | 
			
		||||
interface DeleteGameModalProps {
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +27,7 @@ export function DeleteGameModal({
 | 
			
		|||
      description={t("delete_modal_description")}
 | 
			
		||||
      onClose={onClose}
 | 
			
		||||
    >
 | 
			
		||||
      <div className={styles.deleteActionsButtonsCtn}>
 | 
			
		||||
      <div className="delete-game-modal__actions">
 | 
			
		||||
        <Button onClick={handleDeleteGame} theme="outline">
 | 
			
		||||
          {t("delete")}
 | 
			
		||||
        </Button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,109 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const downloadTitleWrapper = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  marginBottom: `${SPACING_UNIT}px`,
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadTitle = style({
 | 
			
		||||
  fontWeight: "bold",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  textAlign: "left",
 | 
			
		||||
  fontSize: "16px",
 | 
			
		||||
  display: "block",
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    textDecoration: "underline",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloads = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  margin: "0",
 | 
			
		||||
  padding: "0",
 | 
			
		||||
  marginTop: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadCover = style({
 | 
			
		||||
  width: "280px",
 | 
			
		||||
  minWidth: "280px",
 | 
			
		||||
  height: "auto",
 | 
			
		||||
  borderRight: `solid 1px ${vars.color.border}`,
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  zIndex: "1",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadCoverContent = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  padding: `${SPACING_UNIT}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "flex-end",
 | 
			
		||||
  justifyContent: "flex-end",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadCoverBackdrop = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  zIndex: "1",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadCoverImage = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  zIndex: "-1",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const download = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  backgroundColor: vars.color.background,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  borderRadius: "8px",
 | 
			
		||||
  border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  boxShadow: "0px 0px 5px 0px #000000",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  height: "140px",
 | 
			
		||||
  minHeight: "140px",
 | 
			
		||||
  maxHeight: "140px",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadDetails = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  flex: "1",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT / 2}px`,
 | 
			
		||||
  fontSize: "14px",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadRightContent = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  flex: "1",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  background: "linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%)",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadActions = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadGroup = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										140
									
								
								src/renderer/src/pages/downloads/download-group.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/renderer/src/pages/downloads/download-group.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,140 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.download-group {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
 | 
			
		||||
  &__header {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
 | 
			
		||||
    &-divider {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      background-color: globals.$border-color;
 | 
			
		||||
      height: 1px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-count {
 | 
			
		||||
      font-weight: 400;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__title-wrapper {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    margin-bottom: globals.$spacing-unit;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__title {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    display: block;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      text-decoration: underline;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloads {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin-top: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__item {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    background-color: globals.$background-color;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    border: solid 1px globals.$border-color;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    box-shadow: 0px 0px 5px 0px #000000;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    height: 140px;
 | 
			
		||||
    min-height: 140px;
 | 
			
		||||
    max-height: 140px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__cover {
 | 
			
		||||
    width: 280px;
 | 
			
		||||
    min-width: 280px;
 | 
			
		||||
    height: auto;
 | 
			
		||||
    border-right: solid 1px globals.$border-color;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
 | 
			
		||||
    &-content {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      padding: globals.$spacing-unit;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: flex-end;
 | 
			
		||||
      justify-content: flex-end;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-backdrop {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      background: linear-gradient(
 | 
			
		||||
        0deg,
 | 
			
		||||
        rgba(0, 0, 0, 0.8) 5%,
 | 
			
		||||
        transparent 100%
 | 
			
		||||
      );
 | 
			
		||||
      display: flex;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      z-index: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-image {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      z-index: -1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__right-content {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__details {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    gap: calc(globals.$spacing-unit / 2);
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__actions {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__menu-button {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 12px;
 | 
			
		||||
    right: 12px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    border: none;
 | 
			
		||||
    padding: 8px;
 | 
			
		||||
    min-height: unset;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -12,9 +12,8 @@ import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
 | 
			
		|||
import { DOWNLOADER_NAME } from "@renderer/constants";
 | 
			
		||||
import { useAppSelector, useDownload } from "@renderer/hooks";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./download-group.css";
 | 
			
		||||
import "./download-group.scss";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
| 
						 | 
				
			
			@ -246,44 +245,26 @@ export function DownloadGroup({
 | 
			
		|||
  if (!library.length) return null;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.downloadGroup}>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
    <div className="download-group">
 | 
			
		||||
      <div className="download-group__header">
 | 
			
		||||
        <h2>{title}</h2>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            flex: 1,
 | 
			
		||||
            backgroundColor: vars.color.border,
 | 
			
		||||
            height: "1px",
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <h3 style={{ fontWeight: "400" }}>{library.length}</h3>
 | 
			
		||||
        <div className="download-group__header-divider" />
 | 
			
		||||
        <h3 className="download-group__header-count">{library.length}</h3>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <ul className={styles.downloads}>
 | 
			
		||||
      <ul className="download-group__downloads">
 | 
			
		||||
        {library.map((game) => {
 | 
			
		||||
          return (
 | 
			
		||||
            <li
 | 
			
		||||
              key={game.id}
 | 
			
		||||
              className={styles.download}
 | 
			
		||||
              style={{ position: "relative" }}
 | 
			
		||||
            >
 | 
			
		||||
              <div className={styles.downloadCover}>
 | 
			
		||||
                <div className={styles.downloadCoverBackdrop}>
 | 
			
		||||
            <li key={game.id} className="download-group__item">
 | 
			
		||||
              <div className="download-group__cover">
 | 
			
		||||
                <div className="download-group__cover-backdrop">
 | 
			
		||||
                  <img
 | 
			
		||||
                    src={steamUrlBuilder.library(game.objectId)}
 | 
			
		||||
                    className={styles.downloadCoverImage}
 | 
			
		||||
                    className="download-group__cover-image"
 | 
			
		||||
                    alt={game.title}
 | 
			
		||||
                  />
 | 
			
		||||
 | 
			
		||||
                  <div className={styles.downloadCoverContent}>
 | 
			
		||||
                  <div className="download-group__cover-content">
 | 
			
		||||
                    <Badge>
 | 
			
		||||
                      {
 | 
			
		||||
                        DOWNLOADER_NAME[
 | 
			
		||||
| 
						 | 
				
			
			@ -294,12 +275,12 @@ export function DownloadGroup({
 | 
			
		|||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className={styles.downloadRightContent}>
 | 
			
		||||
                <div className={styles.downloadDetails}>
 | 
			
		||||
                  <div className={styles.downloadTitleWrapper}>
 | 
			
		||||
              <div className="download-group__right-content">
 | 
			
		||||
                <div className="download-group__details">
 | 
			
		||||
                  <div className="download-group__title-wrapper">
 | 
			
		||||
                    <button
 | 
			
		||||
                      type="button"
 | 
			
		||||
                      className={styles.downloadTitle}
 | 
			
		||||
                      className="download-group__title"
 | 
			
		||||
                      onClick={() =>
 | 
			
		||||
                        navigate(
 | 
			
		||||
                          buildGameDetailsPath({
 | 
			
		||||
| 
						 | 
				
			
			@ -323,15 +304,7 @@ export function DownloadGroup({
 | 
			
		|||
                    sideOffset={-75}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Button
 | 
			
		||||
                      style={{
 | 
			
		||||
                        position: "absolute",
 | 
			
		||||
                        top: "12px",
 | 
			
		||||
                        right: "12px",
 | 
			
		||||
                        borderRadius: "50%",
 | 
			
		||||
                        border: "none",
 | 
			
		||||
                        padding: "8px",
 | 
			
		||||
                        minHeight: "unset",
 | 
			
		||||
                      }}
 | 
			
		||||
                      className="download-group__menu-button"
 | 
			
		||||
                      theme="outline"
 | 
			
		||||
                    >
 | 
			
		||||
                      <ThreeBarsIcon />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,37 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT } from "../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const downloadsContainer = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  padding: `${SPACING_UNIT * 3}px`,
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadGroups = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT * 3}px`,
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const arrowIcon = style({
 | 
			
		||||
  width: "60px",
 | 
			
		||||
  height: "60px",
 | 
			
		||||
  borderRadius: "50%",
 | 
			
		||||
  backgroundColor: "rgba(255, 255, 255, 0.06)",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  marginBottom: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const noDownloads = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										37
									
								
								src/renderer/src/pages/downloads/downloads.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/renderer/src/pages/downloads/downloads.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.downloads {
 | 
			
		||||
  &__container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 3);
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__groups {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 3);
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__arrow-icon {
 | 
			
		||||
    width: 60px;
 | 
			
		||||
    height: 60px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    background-color: rgba(255, 255, 255, 0.06);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    margin-bottom: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__no-downloads {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ import { useDownload, useLibrary } from "@renderer/hooks";
 | 
			
		|||
 | 
			
		||||
import { useEffect, useMemo, useRef, useState } from "react";
 | 
			
		||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
 | 
			
		||||
import * as styles from "./downloads.css";
 | 
			
		||||
import "./downloads.scss";
 | 
			
		||||
import { DeleteGameModal } from "./delete-game-modal";
 | 
			
		||||
import { DownloadGroup } from "./download-group";
 | 
			
		||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
 | 
			
		||||
| 
						 | 
				
			
			@ -122,8 +122,8 @@ export default function Downloads() {
 | 
			
		|||
      />
 | 
			
		||||
 | 
			
		||||
      {hasItemsInLibrary ? (
 | 
			
		||||
        <section className={styles.downloadsContainer}>
 | 
			
		||||
          <div className={styles.downloadGroups}>
 | 
			
		||||
        <section className="downloads__container">
 | 
			
		||||
          <div className="downloads__groups">
 | 
			
		||||
            {downloadGroups.map((group) => (
 | 
			
		||||
              <DownloadGroup
 | 
			
		||||
                key={group.title}
 | 
			
		||||
| 
						 | 
				
			
			@ -137,8 +137,8 @@ export default function Downloads() {
 | 
			
		|||
          </div>
 | 
			
		||||
        </section>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div className={styles.noDownloads}>
 | 
			
		||||
          <div className={styles.arrowIcon}>
 | 
			
		||||
        <div className="downloads__no-downloads">
 | 
			
		||||
          <div className="downloads__arrow-icon">
 | 
			
		||||
            <ArrowDownIcon size={24} />
 | 
			
		||||
          </div>
 | 
			
		||||
          <h2>{t("no_downloads_title")}</h2>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,27 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const mappingMethods = style({
 | 
			
		||||
  display: "grid",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  gridTemplateColumns: "repeat(2, 1fr)",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const fileList = style({
 | 
			
		||||
  listStyle: "none",
 | 
			
		||||
  margin: "0",
 | 
			
		||||
  padding: "0",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  marginTop: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const fileItem = style({
 | 
			
		||||
  flex: 1,
 | 
			
		||||
  color: vars.color.muted,
 | 
			
		||||
  textDecoration: "underline",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.cloud-sync-files-modal {
 | 
			
		||||
  &__mapping-methods {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    grid-template-columns: repeat(2, 1fr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__file-list {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    margin-top: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__file-item {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__mapping-label {
 | 
			
		||||
    margin-bottom: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__custom-path {
 | 
			
		||||
    margin-top: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
 | 
			
		|||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { CheckCircleFillIcon, FileDirectoryIcon } from "@primer/octicons-react";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./cloud-sync-files-modal.css";
 | 
			
		||||
import "./cloud-sync-files-modal.scss";
 | 
			
		||||
import { formatBytes } from "@shared";
 | 
			
		||||
import { useToast } from "@renderer/hooks";
 | 
			
		||||
import { useForm } from "react-hook-form";
 | 
			
		||||
| 
						 | 
				
			
			@ -96,10 +96,12 @@ export function CloudSyncFilesModal({
 | 
			
		|||
      description={t("manage_files_description")}
 | 
			
		||||
      onClose={onClose}
 | 
			
		||||
    >
 | 
			
		||||
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
 | 
			
		||||
        <span style={{ marginBottom: 8 }}>{t("mapping_method_label")}</span>
 | 
			
		||||
      <div className="cloud-sync-files-modal__container">
 | 
			
		||||
        <span className="cloud-sync-files-modal__mapping-label">
 | 
			
		||||
          {t("mapping_method_label")}
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <div className={styles.mappingMethods}>
 | 
			
		||||
        <div className="cloud-sync-files-modal__mapping-methods">
 | 
			
		||||
          {Object.values(FileMappingMethod).map((mappingMethod) => (
 | 
			
		||||
            <Button
 | 
			
		||||
              key={mappingMethod}
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +121,7 @@ export function CloudSyncFilesModal({
 | 
			
		|||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div style={{ marginTop: 16 }}>
 | 
			
		||||
      <div className="cloud-sync-files-modal__custom-path">
 | 
			
		||||
        {selectedFileMappingMethod === FileMappingMethod.Automatic ? (
 | 
			
		||||
          <p>{t("files_automatically_mapped")}</p>
 | 
			
		||||
        ) : (
 | 
			
		||||
| 
						 | 
				
			
			@ -142,11 +144,11 @@ export function CloudSyncFilesModal({
 | 
			
		|||
          />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <ul className={styles.fileList}>
 | 
			
		||||
        <ul className="cloud-sync-files-modal__file-list">
 | 
			
		||||
          {files.map((file) => (
 | 
			
		||||
            <li key={file.path} style={{ display: "flex" }}>
 | 
			
		||||
            <li key={file.path} className="cloud-sync-files-modal__file-item">
 | 
			
		||||
              <button
 | 
			
		||||
                className={styles.fileItem}
 | 
			
		||||
                className="cloud-sync-files-modal__file-item"
 | 
			
		||||
                onClick={() => window.electron.showItemInFolder(file.path)}
 | 
			
		||||
              >
 | 
			
		||||
                {file.path.split("/").at(-1)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,65 +0,0 @@
 | 
			
		|||
import { keyframes, style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const rotate = keyframes({
 | 
			
		||||
  "0%": { transform: "rotate(0deg)" },
 | 
			
		||||
  "100%": {
 | 
			
		||||
    transform: "rotate(360deg)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const artifacts = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  listStyle: "none",
 | 
			
		||||
  margin: "0",
 | 
			
		||||
  padding: "0",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const artifactButton = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  textAlign: "left",
 | 
			
		||||
  flexDirection: "row",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  backgroundColor: vars.color.darkBackground,
 | 
			
		||||
  border: `1px solid ${vars.color.border}`,
 | 
			
		||||
  borderRadius: "4px",
 | 
			
		||||
  justifyContent: "space-between",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const syncIcon = style({
 | 
			
		||||
  animationName: rotate,
 | 
			
		||||
  animationDuration: "1s",
 | 
			
		||||
  animationIterationCount: "infinite",
 | 
			
		||||
  animationTimingFunction: "linear",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const progress = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "5px",
 | 
			
		||||
  "::-webkit-progress-bar": {
 | 
			
		||||
    backgroundColor: vars.color.darkBackground,
 | 
			
		||||
  },
 | 
			
		||||
  "::-webkit-progress-value": {
 | 
			
		||||
    backgroundColor: vars.color.muted,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const manageFilesButton = style({
 | 
			
		||||
  margin: "0",
 | 
			
		||||
  padding: "0",
 | 
			
		||||
  alignSelf: "flex-start",
 | 
			
		||||
  fontSize: 14,
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  textDecoration: "underline",
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  ":disabled": {
 | 
			
		||||
    cursor: "not-allowed",
 | 
			
		||||
    opacity: vars.opacity.disabled,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,113 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
@keyframes rotate {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: rotate(0deg);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: rotate(360deg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cloud-sync-modal {
 | 
			
		||||
  &__header {
 | 
			
		||||
    margin-bottom: 24px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__title-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: 4px;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__backups-header {
 | 
			
		||||
    margin-bottom: 16px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__artifacts {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__artifact {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    background-color: globals.$dark-background-color;
 | 
			
		||||
    border: 1px solid globals.$border-color;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
 | 
			
		||||
    &-info {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      gap: 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-header {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: 8px;
 | 
			
		||||
      margin-bottom: 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-meta {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-actions {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      gap: 8px;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__sync-icon {
 | 
			
		||||
    animation: rotate 1s linear infinite;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__progress {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 5px;
 | 
			
		||||
 | 
			
		||||
    &::-webkit-progress-bar {
 | 
			
		||||
      background-color: globals.$dark-background-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::-webkit-progress-value {
 | 
			
		||||
      background-color: globals.$muted-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__manage-files-button {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    align-self: flex-start;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
 | 
			
		||||
    &:disabled {
 | 
			
		||||
      cursor: not-allowed;
 | 
			
		||||
      opacity: globals.$disabled-opacity;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import { Button, Modal, ModalProps } from "@renderer/components";
 | 
			
		|||
import { useContext, useEffect, useMemo, useState } from "react";
 | 
			
		||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./cloud-sync-modal.css";
 | 
			
		||||
import "./cloud-sync-modal.scss";
 | 
			
		||||
import { formatBytes } from "@shared";
 | 
			
		||||
import { format } from "date-fns";
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +18,6 @@ import { useAppSelector, useToast } from "@renderer/hooks";
 | 
			
		|||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { AxiosProgressEvent } from "axios";
 | 
			
		||||
import { formatDownloadProgress } from "@renderer/helpers";
 | 
			
		||||
import { SPACING_UNIT } from "@renderer/theme.css";
 | 
			
		||||
 | 
			
		||||
export interface CloudSyncModalProps
 | 
			
		||||
  extends Omit<ModalProps, "children" | "title"> {}
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +94,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
    if (uploadingBackup) {
 | 
			
		||||
      return (
 | 
			
		||||
        <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
 | 
			
		||||
          <SyncIcon className={styles.syncIcon} />
 | 
			
		||||
          <SyncIcon className="cloud-sync-modal__sync-icon" />
 | 
			
		||||
          {t("uploading_backup")}
 | 
			
		||||
        </span>
 | 
			
		||||
      );
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +103,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
    if (restoringBackup) {
 | 
			
		||||
      return (
 | 
			
		||||
        <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
 | 
			
		||||
          <SyncIcon className={styles.syncIcon} />
 | 
			
		||||
          <SyncIcon className="cloud-sync-modal__sync-icon" />
 | 
			
		||||
          {t("restoring_backup", {
 | 
			
		||||
            progress: formatDownloadProgress(
 | 
			
		||||
              backupDownloadProgress?.progress ?? 0
 | 
			
		||||
| 
						 | 
				
			
			@ -117,7 +116,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
    if (loadingPreview) {
 | 
			
		||||
      return (
 | 
			
		||||
        <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
 | 
			
		||||
          <SyncIcon className={styles.syncIcon} />
 | 
			
		||||
          <SyncIcon className="cloud-sync-modal__sync-icon" />
 | 
			
		||||
          {t("loading_save_preview")}
 | 
			
		||||
        </span>
 | 
			
		||||
      );
 | 
			
		||||
| 
						 | 
				
			
			@ -157,21 +156,14 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
      onClose={onClose}
 | 
			
		||||
      large
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          marginBottom: 24,
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
 | 
			
		||||
      <div className="cloud-sync-modal__header">
 | 
			
		||||
        <div className="cloud-sync-modal__title-container">
 | 
			
		||||
          <h2>{gameTitle}</h2>
 | 
			
		||||
          <p>{backupStateLabel}</p>
 | 
			
		||||
 | 
			
		||||
          <button
 | 
			
		||||
            type="button"
 | 
			
		||||
            className={styles.manageFilesButton}
 | 
			
		||||
            className="cloud-sync-modal__manage-files-button"
 | 
			
		||||
            onClick={() => setShowCloudSyncFilesModal(true)}
 | 
			
		||||
            disabled={disableActions}
 | 
			
		||||
          >
 | 
			
		||||
| 
						 | 
				
			
			@ -188,40 +180,36 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
            artifacts.length >= backupsPerGameLimit
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          <UploadIcon />
 | 
			
		||||
          {uploadingBackup ? (
 | 
			
		||||
            <SyncIcon className="cloud-sync-modal__sync-icon" />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <UploadIcon />
 | 
			
		||||
          )}
 | 
			
		||||
          {t("create_backup")}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div style={{ display: "flex", justifyContent: "space-between" }}>
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            marginBottom: 16,
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
            gap: SPACING_UNIT,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <h2>{t("backups")}</h2>
 | 
			
		||||
          <small>
 | 
			
		||||
            {artifacts.length} / {backupsPerGameLimit}
 | 
			
		||||
          </small>
 | 
			
		||||
        </div>
 | 
			
		||||
      {uploadingBackup && (
 | 
			
		||||
        <progress
 | 
			
		||||
          className="cloud-sync-modal__progress"
 | 
			
		||||
          value={backupDownloadProgress?.progress ?? 0}
 | 
			
		||||
          max={100}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <div className="cloud-sync-modal__backups-header">
 | 
			
		||||
        <h2>{t("backups")}</h2>
 | 
			
		||||
        <small>
 | 
			
		||||
          {artifacts.length} / {backupsPerGameLimit}
 | 
			
		||||
        </small>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {artifacts.length > 0 ? (
 | 
			
		||||
        <ul className={styles.artifacts}>
 | 
			
		||||
        <ul className="cloud-sync-modal__artifacts">
 | 
			
		||||
          {artifacts.map((artifact) => (
 | 
			
		||||
            <li key={artifact.id} className={styles.artifactButton}>
 | 
			
		||||
              <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: "flex",
 | 
			
		||||
                    alignItems: "center",
 | 
			
		||||
                    gap: 8,
 | 
			
		||||
                    marginBottom: 4,
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
            <li key={artifact.id} className="cloud-sync-modal__artifact">
 | 
			
		||||
              <div className="cloud-sync-modal__artifact-info">
 | 
			
		||||
                <div className="cloud-sync-modal__artifact-header">
 | 
			
		||||
                  <h3>
 | 
			
		||||
                    {t("backup_from", {
 | 
			
		||||
                      date: format(artifact.createdAt, "dd/MM/yyyy"),
 | 
			
		||||
| 
						 | 
				
			
			@ -230,29 +218,33 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
                  <small>{formatBytes(artifact.artifactLengthInBytes)}</small>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
 | 
			
		||||
                <span className="cloud-sync-modal__artifact-meta">
 | 
			
		||||
                  <DeviceDesktopIcon size={14} />
 | 
			
		||||
                  {artifact.hostname}
 | 
			
		||||
                </span>
 | 
			
		||||
 | 
			
		||||
                <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
 | 
			
		||||
                <span className="cloud-sync-modal__artifact-meta">
 | 
			
		||||
                  <InfoIcon size={14} />
 | 
			
		||||
                  {artifact.downloadOptionTitle ?? t("no_download_option_info")}
 | 
			
		||||
                </span>
 | 
			
		||||
 | 
			
		||||
                <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
 | 
			
		||||
                <span className="cloud-sync-modal__artifact-meta">
 | 
			
		||||
                  <ClockIcon size={14} />
 | 
			
		||||
                  {format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")}
 | 
			
		||||
                </span>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
 | 
			
		||||
              <div className="cloud-sync-modal__artifact-actions">
 | 
			
		||||
                <Button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  onClick={() => handleBackupInstallClick(artifact.id)}
 | 
			
		||||
                  disabled={disableActions}
 | 
			
		||||
                >
 | 
			
		||||
                  <HistoryIcon />
 | 
			
		||||
                  {restoringBackup ? (
 | 
			
		||||
                    <SyncIcon className="cloud-sync-modal__sync-icon" />
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <HistoryIcon />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {t("install_backup")}
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const descriptionHeader = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  justifyContent: "space-between",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  backgroundColor: vars.color.background,
 | 
			
		||||
  height: "72px",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const descriptionHeaderInfo = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.description-header {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  background-color: globals.$background-color;
 | 
			
		||||
  height: 72px;
 | 
			
		||||
 | 
			
		||||
  &__info {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,19 +1,17 @@
 | 
			
		|||
import { useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./description-header.css";
 | 
			
		||||
import { useContext } from "react";
 | 
			
		||||
import { gameDetailsContext } from "@renderer/context";
 | 
			
		||||
import "./description-header.scss";
 | 
			
		||||
 | 
			
		||||
export function DescriptionHeader() {
 | 
			
		||||
  const { shopDetails } = useContext(gameDetailsContext);
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation("game_details");
 | 
			
		||||
 | 
			
		||||
  if (!shopDetails) return null;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.descriptionHeader}>
 | 
			
		||||
      <section className={styles.descriptionHeaderInfo}>
 | 
			
		||||
    <div className="description-header">
 | 
			
		||||
      <section className="description-header__info">
 | 
			
		||||
        <p>
 | 
			
		||||
          {t("release_date", {
 | 
			
		||||
            date: shopDetails?.release_date.date,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,131 +0,0 @@
 | 
			
		|||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const gallerySliderContainer = style({
 | 
			
		||||
  padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gallerySliderMedia = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  display: "block",
 | 
			
		||||
  flexShrink: "0",
 | 
			
		||||
  flexGrow: "0",
 | 
			
		||||
  transition: "translate 0.3s ease-in-out",
 | 
			
		||||
  borderRadius: "4px",
 | 
			
		||||
  alignSelf: "center",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gallerySliderAnimationContainer = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  overflow: "hidden",
 | 
			
		||||
  "@media": {
 | 
			
		||||
    "(min-width: 1280px)": {
 | 
			
		||||
      width: "60%",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gallerySliderPreview = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  padding: `${SPACING_UNIT}px 0`,
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  overflowX: "auto",
 | 
			
		||||
  overflowY: "hidden",
 | 
			
		||||
  gap: `${SPACING_UNIT / 2}px`,
 | 
			
		||||
  "@media": {
 | 
			
		||||
    "(min-width: 1280px)": {
 | 
			
		||||
      width: "60%",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  "::-webkit-scrollbar-thumb": {
 | 
			
		||||
    width: "20%",
 | 
			
		||||
  },
 | 
			
		||||
  "::-webkit-scrollbar": {
 | 
			
		||||
    height: "10px",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const mediaPreviewButton = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    width: "20%",
 | 
			
		||||
    display: "block",
 | 
			
		||||
    flexShrink: "0",
 | 
			
		||||
    flexGrow: "0",
 | 
			
		||||
    opacity: "0.3",
 | 
			
		||||
    transition: "translate 0.3s ease-in-out, opacity 0.2s ease",
 | 
			
		||||
    borderRadius: "4px",
 | 
			
		||||
    border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    overflow: "hidden",
 | 
			
		||||
    ":hover": {
 | 
			
		||||
      opacity: "0.8",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    active: {
 | 
			
		||||
      true: {
 | 
			
		||||
        opacity: "1",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const mediaPreview = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gallerySliderButton = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    position: "absolute",
 | 
			
		||||
    alignSelf: "center",
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    backgroundColor: "rgba(0, 0, 0, 0.4)",
 | 
			
		||||
    transition: "all 0.2s ease-in-out",
 | 
			
		||||
    borderRadius: "50%",
 | 
			
		||||
    color: vars.color.muted,
 | 
			
		||||
    width: "48px",
 | 
			
		||||
    height: "48px",
 | 
			
		||||
    ":hover": {
 | 
			
		||||
      backgroundColor: "rgba(0, 0, 0, 0.6)",
 | 
			
		||||
    },
 | 
			
		||||
    ":active": {
 | 
			
		||||
      transform: "scale(0.95)",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    direction: {
 | 
			
		||||
      left: {
 | 
			
		||||
        left: "0",
 | 
			
		||||
        marginLeft: `${SPACING_UNIT}px`,
 | 
			
		||||
        transform: `translateX(${-(48 + SPACING_UNIT)}px)`,
 | 
			
		||||
      },
 | 
			
		||||
      right: {
 | 
			
		||||
        right: "0",
 | 
			
		||||
        marginRight: `${SPACING_UNIT}px`,
 | 
			
		||||
        transform: `translateX(${48 + SPACING_UNIT}px)`,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    visible: {
 | 
			
		||||
      true: {
 | 
			
		||||
        transform: "translateX(0)",
 | 
			
		||||
        opacity: "1",
 | 
			
		||||
      },
 | 
			
		||||
      false: {
 | 
			
		||||
        opacity: "0",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,131 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.gallery-slider {
 | 
			
		||||
  &__container {
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__media {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: block;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    flex-grow: 0;
 | 
			
		||||
    transition: translate 0.3s ease-in-out;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    align-self: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__animation-container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    @media (min-width: 1280px) {
 | 
			
		||||
      width: 60%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__preview {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: globals.$spacing-unit 0;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow-x: auto;
 | 
			
		||||
    overflow-y: hidden;
 | 
			
		||||
    gap: calc(globals.$spacing-unit / 2);
 | 
			
		||||
 | 
			
		||||
    @media (min-width: 1280px) {
 | 
			
		||||
      width: 60%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::-webkit-scrollbar-thumb {
 | 
			
		||||
      width: 20%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::-webkit-scrollbar {
 | 
			
		||||
      height: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__preview-button {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    display: block;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    flex-grow: 0;
 | 
			
		||||
    opacity: 0.3;
 | 
			
		||||
    transition:
 | 
			
		||||
      translate 0.3s ease-in-out,
 | 
			
		||||
      opacity 0.2s ease;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    border: solid 1px globals.$border-color;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      opacity: 0.8;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--active {
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__preview-image {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__button {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    align-self: center;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.4);
 | 
			
		||||
    transition: all 0.2s ease-in-out;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    width: 48px;
 | 
			
		||||
    height: 48px;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: rgba(0, 0, 0, 0.6);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:active {
 | 
			
		||||
      transform: scale(0.95);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--left {
 | 
			
		||||
      left: 0;
 | 
			
		||||
      margin-left: globals.$spacing-unit;
 | 
			
		||||
      transform: translateX(calc(-1 * (48px + globals.$spacing-unit)));
 | 
			
		||||
 | 
			
		||||
      &.gallery-slider__button--visible {
 | 
			
		||||
        transform: translateX(0);
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--right {
 | 
			
		||||
      right: 0;
 | 
			
		||||
      margin-right: globals.$spacing-unit;
 | 
			
		||||
      transform: translateX(calc(48px + globals.$spacing-unit));
 | 
			
		||||
 | 
			
		||||
      &.gallery-slider__button--visible {
 | 
			
		||||
        transform: translateX(0);
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--hidden {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,8 @@
 | 
			
		|||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./gallery-slider.css";
 | 
			
		||||
import { gameDetailsContext } from "@renderer/context";
 | 
			
		||||
import "./gallery-slider.scss";
 | 
			
		||||
 | 
			
		||||
export function GallerySlider() {
 | 
			
		||||
  const { shopDetails } = useContext(gameDetailsContext);
 | 
			
		||||
| 
						 | 
				
			
			@ -97,11 +96,11 @@ export function GallerySlider() {
 | 
			
		|||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {hasScreenshots && (
 | 
			
		||||
        <div className={styles.gallerySliderContainer}>
 | 
			
		||||
        <div className="gallery-slider__container">
 | 
			
		||||
          <div
 | 
			
		||||
            onMouseEnter={() => setShowArrows(true)}
 | 
			
		||||
            onMouseLeave={() => setShowArrows(false)}
 | 
			
		||||
            className={styles.gallerySliderAnimationContainer}
 | 
			
		||||
            className="gallery-slider__animation-container"
 | 
			
		||||
            ref={mediaContainerRef}
 | 
			
		||||
          >
 | 
			
		||||
            {shopDetails.movies &&
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +108,7 @@ export function GallerySlider() {
 | 
			
		|||
                <video
 | 
			
		||||
                  key={video.id}
 | 
			
		||||
                  controls
 | 
			
		||||
                  className={styles.gallerySliderMedia}
 | 
			
		||||
                  className="gallery-slider__media"
 | 
			
		||||
                  poster={video.thumbnail}
 | 
			
		||||
                  style={{ translate: `${-100 * mediaIndex}%` }}
 | 
			
		||||
                  loop
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +123,7 @@ export function GallerySlider() {
 | 
			
		|||
              shopDetails.screenshots?.map((image, i) => (
 | 
			
		||||
                <img
 | 
			
		||||
                  key={image.id}
 | 
			
		||||
                  className={styles.gallerySliderMedia}
 | 
			
		||||
                  className="gallery-slider__media"
 | 
			
		||||
                  src={image.path_full}
 | 
			
		||||
                  style={{ translate: `${-100 * mediaIndex}%` }}
 | 
			
		||||
                  alt={t("screenshot", { number: i + 1 })}
 | 
			
		||||
| 
						 | 
				
			
			@ -135,10 +134,11 @@ export function GallerySlider() {
 | 
			
		|||
            <button
 | 
			
		||||
              onClick={showPrevImage}
 | 
			
		||||
              type="button"
 | 
			
		||||
              className={styles.gallerySliderButton({
 | 
			
		||||
                visible: showArrows,
 | 
			
		||||
                direction: "left",
 | 
			
		||||
              })}
 | 
			
		||||
              className={`gallery-slider__button gallery-slider__button--left ${
 | 
			
		||||
                showArrows
 | 
			
		||||
                  ? "gallery-slider__button--visible"
 | 
			
		||||
                  : "gallery-slider__button--hidden"
 | 
			
		||||
              }`}
 | 
			
		||||
              aria-label={t("previous_screenshot")}
 | 
			
		||||
              tabIndex={0}
 | 
			
		||||
            >
 | 
			
		||||
| 
						 | 
				
			
			@ -148,10 +148,11 @@ export function GallerySlider() {
 | 
			
		|||
            <button
 | 
			
		||||
              onClick={showNextImage}
 | 
			
		||||
              type="button"
 | 
			
		||||
              className={styles.gallerySliderButton({
 | 
			
		||||
                visible: showArrows,
 | 
			
		||||
                direction: "right",
 | 
			
		||||
              })}
 | 
			
		||||
              className={`gallery-slider__button gallery-slider__button--right ${
 | 
			
		||||
                showArrows
 | 
			
		||||
                  ? "gallery-slider__button--visible"
 | 
			
		||||
                  : "gallery-slider__button--hidden"
 | 
			
		||||
              }`}
 | 
			
		||||
              aria-label={t("next_screenshot")}
 | 
			
		||||
              tabIndex={0}
 | 
			
		||||
            >
 | 
			
		||||
| 
						 | 
				
			
			@ -159,20 +160,22 @@ export function GallerySlider() {
 | 
			
		|||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className={styles.gallerySliderPreview} ref={scrollContainerRef}>
 | 
			
		||||
          <div className="gallery-slider__preview" ref={scrollContainerRef}>
 | 
			
		||||
            {previews.map((media, i) => (
 | 
			
		||||
              <button
 | 
			
		||||
                key={media.id}
 | 
			
		||||
                type="button"
 | 
			
		||||
                className={styles.mediaPreviewButton({
 | 
			
		||||
                  active: mediaIndex === i,
 | 
			
		||||
                })}
 | 
			
		||||
                className={`gallery-slider__preview-button ${
 | 
			
		||||
                  mediaIndex === i
 | 
			
		||||
                    ? "gallery-slider__preview-button--active"
 | 
			
		||||
                    : ""
 | 
			
		||||
                }`}
 | 
			
		||||
                onClick={() => setMediaIndex(i)}
 | 
			
		||||
                aria-label={t("open_screenshot", { number: i + 1 })}
 | 
			
		||||
              >
 | 
			
		||||
                <img
 | 
			
		||||
                  src={media.thumbnail}
 | 
			
		||||
                  className={styles.mediaPreview}
 | 
			
		||||
                  className="gallery-slider__preview-image"
 | 
			
		||||
                  alt={t("screenshot", { number: i + 1 })}
 | 
			
		||||
                />
 | 
			
		||||
              </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,6 @@ import { DescriptionHeader } from "./description-header/description-header";
 | 
			
		|||
import { GallerySlider } from "./gallery-slider/gallery-slider";
 | 
			
		||||
import { Sidebar } from "./sidebar/sidebar";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./game-details.css";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
 | 
			
		||||
import { AuthPage, steamUrlBuilder } from "@shared";
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +14,9 @@ import { AuthPage, steamUrlBuilder } from "@shared";
 | 
			
		|||
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
 | 
			
		||||
import { useUserDetails } from "@renderer/hooks";
 | 
			
		||||
import { useSubscription } from "@renderer/hooks/use-subscription";
 | 
			
		||||
import "./game-details.scss";
 | 
			
		||||
 | 
			
		||||
const HERO_HEIGHT = 300;
 | 
			
		||||
const HERO_ANIMATION_THRESHOLD = 25;
 | 
			
		||||
 | 
			
		||||
export function GameDetailsContent() {
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +81,7 @@ export function GameDetailsContent() {
 | 
			
		|||
  }, [objectId]);
 | 
			
		||||
 | 
			
		||||
  const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
 | 
			
		||||
    const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT;
 | 
			
		||||
    const heroHeight = heroRef.current?.clientHeight ?? HERO_HEIGHT;
 | 
			
		||||
 | 
			
		||||
    const scrollY = (event.target as HTMLDivElement).scrollTop;
 | 
			
		||||
    const opacity = Math.max(
 | 
			
		||||
| 
						 | 
				
			
			@ -118,10 +119,12 @@ export function GameDetailsContent() {
 | 
			
		|||
  }, [getGameArtifacts]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
 | 
			
		||||
    <div
 | 
			
		||||
      className={`game-details__wrapper ${hasNSFWContentBlocked ? "game-details__wrapper--blurred" : ""}`}
 | 
			
		||||
    >
 | 
			
		||||
      <img
 | 
			
		||||
        src={steamUrlBuilder.libraryHero(objectId!)}
 | 
			
		||||
        className={styles.heroImage}
 | 
			
		||||
        className="game-details__hero-image"
 | 
			
		||||
        alt={game?.title}
 | 
			
		||||
        onLoad={handleHeroLoad}
 | 
			
		||||
      />
 | 
			
		||||
| 
						 | 
				
			
			@ -129,47 +132,38 @@ export function GameDetailsContent() {
 | 
			
		|||
      <section
 | 
			
		||||
        ref={containerRef}
 | 
			
		||||
        onScroll={onScroll}
 | 
			
		||||
        className={styles.container}
 | 
			
		||||
        className="game-details__container"
 | 
			
		||||
      >
 | 
			
		||||
        <div ref={heroRef} className={styles.hero}>
 | 
			
		||||
        <div ref={heroRef} className="game-details__hero">
 | 
			
		||||
          <div
 | 
			
		||||
            className="game-details__hero-backdrop"
 | 
			
		||||
            style={{
 | 
			
		||||
              backgroundColor: gameColor,
 | 
			
		||||
              flex: 1,
 | 
			
		||||
              opacity: Math.min(1, 1 - backdropOpactiy),
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <div
 | 
			
		||||
            className={styles.heroLogoBackdrop}
 | 
			
		||||
            className="game-details__hero-logo-backdrop"
 | 
			
		||||
            style={{ opacity: backdropOpactiy }}
 | 
			
		||||
          >
 | 
			
		||||
            <div className={styles.heroContent}>
 | 
			
		||||
            <div className="game-details__hero-content">
 | 
			
		||||
              <img
 | 
			
		||||
                src={steamUrlBuilder.logo(objectId!)}
 | 
			
		||||
                className={styles.gameLogo}
 | 
			
		||||
                className="game-details__game-logo"
 | 
			
		||||
                alt={game?.title}
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <button
 | 
			
		||||
                type="button"
 | 
			
		||||
                className={styles.cloudSyncButton}
 | 
			
		||||
                className="game-details__cloud-sync-button"
 | 
			
		||||
                onClick={handleCloudSaveButtonClick}
 | 
			
		||||
              >
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{
 | 
			
		||||
                    width: 16 + 4,
 | 
			
		||||
                    height: 16,
 | 
			
		||||
                    display: "flex",
 | 
			
		||||
                    alignItems: "center",
 | 
			
		||||
                    justifyContent: "center",
 | 
			
		||||
                    position: "relative",
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                <div className="game-details__cloud-icon-container">
 | 
			
		||||
                  <img
 | 
			
		||||
                    src={cloudIconAnimated}
 | 
			
		||||
                    alt="Cloud icon"
 | 
			
		||||
                    style={{ width: 26, position: "absolute", top: -3 }}
 | 
			
		||||
                    className="game-details__cloud-icon"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                {t("cloud_save")}
 | 
			
		||||
| 
						 | 
				
			
			@ -180,8 +174,8 @@ export function GameDetailsContent() {
 | 
			
		|||
 | 
			
		||||
        <HeroPanel isHeaderStuck={isHeaderStuck} />
 | 
			
		||||
 | 
			
		||||
        <div className={styles.descriptionContainer}>
 | 
			
		||||
          <div className={styles.descriptionContent}>
 | 
			
		||||
        <div className="game-details__description-container">
 | 
			
		||||
          <div className="game-details__description-content">
 | 
			
		||||
            <DescriptionHeader />
 | 
			
		||||
            <GallerySlider />
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +183,7 @@ export function GameDetailsContent() {
 | 
			
		|||
              dangerouslySetInnerHTML={{
 | 
			
		||||
                __html: aboutTheGame,
 | 
			
		||||
              }}
 | 
			
		||||
              className={styles.description}
 | 
			
		||||
              className="game-details__description"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,65 +1,52 @@
 | 
			
		|||
import Skeleton from "react-loading-skeleton";
 | 
			
		||||
 | 
			
		||||
import { Button } from "@renderer/components";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./game-details.css";
 | 
			
		||||
import * as sidebarStyles from "./sidebar/sidebar.css";
 | 
			
		||||
import * as descriptionHeaderStyles from "./description-header/description-header.css";
 | 
			
		||||
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import "./game-details.scss";
 | 
			
		||||
 | 
			
		||||
export function GameDetailsSkeleton() {
 | 
			
		||||
  const { t } = useTranslation("game_details");
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles.container}>
 | 
			
		||||
      <div className={styles.hero}>
 | 
			
		||||
        <Skeleton className={styles.heroImageSkeleton} />
 | 
			
		||||
    <div className="game-details__container">
 | 
			
		||||
      <div className="game-details__hero">
 | 
			
		||||
        <Skeleton className="game-details__hero-image-skeleton" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className={styles.heroPanelSkeleton}>
 | 
			
		||||
        <section className={descriptionHeaderStyles.descriptionHeaderInfo}>
 | 
			
		||||
      <div className="game-details__hero-panel-skeleton">
 | 
			
		||||
        <section className="description-header__info">
 | 
			
		||||
          <Skeleton width={155} />
 | 
			
		||||
          <Skeleton width={135} />
 | 
			
		||||
        </section>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className={styles.descriptionContainer}>
 | 
			
		||||
        <div className={styles.descriptionContent}>
 | 
			
		||||
          <div className={descriptionHeaderStyles.descriptionHeader}>
 | 
			
		||||
            <section className={descriptionHeaderStyles.descriptionHeaderInfo}>
 | 
			
		||||
      <div className="game-details__description-container">
 | 
			
		||||
        <div className="game-details__description-content">
 | 
			
		||||
          <div className="description-header">
 | 
			
		||||
            <section className="description-header__info">
 | 
			
		||||
              <Skeleton width={145} />
 | 
			
		||||
              <Skeleton width={150} />
 | 
			
		||||
            </section>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className={styles.descriptionSkeleton}>
 | 
			
		||||
          <div className="game-details__description-skeleton">
 | 
			
		||||
            {Array.from({ length: 3 }).map((_, index) => (
 | 
			
		||||
              <Skeleton key={index} />
 | 
			
		||||
            ))}
 | 
			
		||||
            <Skeleton className={styles.heroImageSkeleton} />
 | 
			
		||||
            <Skeleton className="game-details__hero-image-skeleton" />
 | 
			
		||||
            {Array.from({ length: 2 }).map((_, index) => (
 | 
			
		||||
              <Skeleton key={index} />
 | 
			
		||||
            ))}
 | 
			
		||||
            <Skeleton className={styles.heroImageSkeleton} />
 | 
			
		||||
            <Skeleton className="game-details__hero-image-skeleton" />
 | 
			
		||||
            <Skeleton />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className={sidebarStyles.contentSidebar}>
 | 
			
		||||
          <div className={sidebarStyles.requirementButtonContainer}>
 | 
			
		||||
            <Button
 | 
			
		||||
              className={sidebarStyles.requirementButton}
 | 
			
		||||
              theme="primary"
 | 
			
		||||
              disabled
 | 
			
		||||
            >
 | 
			
		||||
        <div className="content-sidebar">
 | 
			
		||||
          <div className="requirement__button-container">
 | 
			
		||||
            <Button className="requirement__button" theme="primary" disabled>
 | 
			
		||||
              {t("minimum")}
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              className={sidebarStyles.requirementButton}
 | 
			
		||||
              theme="outline"
 | 
			
		||||
              disabled
 | 
			
		||||
            >
 | 
			
		||||
            <Button className="requirement__button" theme="outline" disabled>
 | 
			
		||||
              {t("recommended")}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className={sidebarStyles.requirementsDetailsSkeleton}>
 | 
			
		||||
          <div className="requirement__details-skeleton">
 | 
			
		||||
            {Array.from({ length: 6 }).map((_, index) => (
 | 
			
		||||
              <Skeleton key={index} height={20} />
 | 
			
		||||
            ))}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,234 +0,0 @@
 | 
			
		|||
import { globalStyle, keyframes, style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../theme.css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
export const HERO_HEIGHT = 300;
 | 
			
		||||
 | 
			
		||||
export const slideIn = keyframes({
 | 
			
		||||
  "0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)`, opacity: "0px" },
 | 
			
		||||
  "100%": { transform: "translateY(0)", opacity: "1" },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const wrapper = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    flexDirection: "column",
 | 
			
		||||
    overflow: "hidden",
 | 
			
		||||
    width: "100%",
 | 
			
		||||
    height: "100%",
 | 
			
		||||
    transition: "all ease 0.3s",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    blurredContent: {
 | 
			
		||||
      true: {
 | 
			
		||||
        filter: "blur(20px)",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const hero = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: `${HERO_HEIGHT}px`,
 | 
			
		||||
  minHeight: `${HERO_HEIGHT}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  "@media": {
 | 
			
		||||
    "(min-width: 1250px)": {
 | 
			
		||||
      height: "350px",
 | 
			
		||||
      minHeight: "350px",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const heroContent = style({
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  justifyContent: "space-between",
 | 
			
		||||
  alignItems: "flex-end",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const heroLogoBackdrop = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  background: "linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 100%)",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  justifyContent: "space-between",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const heroImage = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: `${HERO_HEIGHT}px`,
 | 
			
		||||
  minHeight: `${HERO_HEIGHT}px`,
 | 
			
		||||
  objectFit: "cover",
 | 
			
		||||
  objectPosition: "top",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  zIndex: "0",
 | 
			
		||||
  "@media": {
 | 
			
		||||
    "(min-width: 1250px)": {
 | 
			
		||||
      objectPosition: "center",
 | 
			
		||||
      height: "350px",
 | 
			
		||||
      minHeight: "350px",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gameLogo = style({
 | 
			
		||||
  width: 300,
 | 
			
		||||
  alignSelf: "flex-end",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const heroImageSkeleton = style({
 | 
			
		||||
  height: "300px",
 | 
			
		||||
  "@media": {
 | 
			
		||||
    "(min-width: 1250px)": {
 | 
			
		||||
      height: "350px",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const container = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  overflow: "auto",
 | 
			
		||||
  zIndex: "1",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const descriptionContainer = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  flex: "1",
 | 
			
		||||
  background: `linear-gradient(0deg, ${vars.color.background} 50%, ${vars.color.darkBackground} 100%)`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const descriptionContent = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "100%",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const description = style({
 | 
			
		||||
  userSelect: "text",
 | 
			
		||||
  lineHeight: "22px",
 | 
			
		||||
  fontSize: "16px",
 | 
			
		||||
  padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
  "@media": {
 | 
			
		||||
    "(min-width: 1280px)": {
 | 
			
		||||
      width: "60%",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  marginLeft: "auto",
 | 
			
		||||
  marginRight: "auto",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const descriptionSkeleton = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  "@media": {
 | 
			
		||||
    "(min-width: 1280px)": {
 | 
			
		||||
      width: "60%",
 | 
			
		||||
      lineHeight: "22px",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  marginLeft: "auto",
 | 
			
		||||
  marginRight: "auto",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const randomizerButton = style({
 | 
			
		||||
  animationName: slideIn,
 | 
			
		||||
  animationDuration: "0.2s",
 | 
			
		||||
  position: "fixed",
 | 
			
		||||
  bottom: `${SPACING_UNIT * 3}px`,
 | 
			
		||||
  /* Scroll bar + spacing */
 | 
			
		||||
  right: `${9 + SPACING_UNIT * 2}px`,
 | 
			
		||||
  boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
 | 
			
		||||
  border: `solid 2px ${vars.color.border}`,
 | 
			
		||||
  zIndex: "1",
 | 
			
		||||
  backgroundColor: vars.color.background,
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    backgroundColor: vars.color.background,
 | 
			
		||||
    boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 15px 5px",
 | 
			
		||||
    opacity: "1",
 | 
			
		||||
  },
 | 
			
		||||
  ":active": {
 | 
			
		||||
    transform: "scale(0.98)",
 | 
			
		||||
  },
 | 
			
		||||
  ":disabled": {
 | 
			
		||||
    boxShadow: "none",
 | 
			
		||||
    transform: "none",
 | 
			
		||||
    opacity: "0.8",
 | 
			
		||||
    backgroundColor: vars.color.background,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const heroPanelSkeleton = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  backgroundColor: vars.color.background,
 | 
			
		||||
  height: "72px",
 | 
			
		||||
  borderBottom: `solid 1px ${vars.color.border}`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
globalStyle(".bb_tag", {
 | 
			
		||||
  marginTop: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  marginBottom: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
globalStyle(`${description} img`, {
 | 
			
		||||
  borderRadius: "5px",
 | 
			
		||||
  marginTop: `${SPACING_UNIT}px`,
 | 
			
		||||
  marginBottom: `${SPACING_UNIT * 3}px`,
 | 
			
		||||
  display: "block",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "auto",
 | 
			
		||||
  objectFit: "cover",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
globalStyle(`${description} a`, {
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const cloudSyncButton = style({
 | 
			
		||||
  padding: `${SPACING_UNIT * 1.5}px ${SPACING_UNIT * 2}px`,
 | 
			
		||||
  backgroundColor: "rgba(0, 0, 0, 0.6)",
 | 
			
		||||
  backdropFilter: "blur(20px)",
 | 
			
		||||
  borderRadius: "8px",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  minHeight: "40px",
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  justifyContent: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  color: vars.color.muted,
 | 
			
		||||
  fontSize: "14px",
 | 
			
		||||
  border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
  boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.8)",
 | 
			
		||||
  animation: `${slideIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
 | 
			
		||||
  animationDuration: "0.3s",
 | 
			
		||||
  ":active": {
 | 
			
		||||
    opacity: "0.9",
 | 
			
		||||
  },
 | 
			
		||||
  ":disabled": {
 | 
			
		||||
    opacity: vars.opacity.disabled,
 | 
			
		||||
    cursor: "not-allowed",
 | 
			
		||||
  },
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    backgroundColor: "rgba(0, 0, 0, 0.5)",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										270
									
								
								src/renderer/src/pages/game-details/game-details.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								src/renderer/src/pages/game-details/game-details.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,270 @@
 | 
			
		|||
@use "../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
$hero-height: 300px;
 | 
			
		||||
 | 
			
		||||
@keyframes slide-in {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: translateY(calc(40px + globals.$spacing-unit * 2));
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateY(0);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.game-details {
 | 
			
		||||
  &__wrapper {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    transition: all ease 0.3s;
 | 
			
		||||
 | 
			
		||||
    &--blurred {
 | 
			
		||||
      filter: blur(20px);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hero {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: $hero-height;
 | 
			
		||||
    min-height: $hero-height;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
 | 
			
		||||
    @media (min-width: 1250px) {
 | 
			
		||||
      height: 350px;
 | 
			
		||||
      min-height: 350px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hero-content {
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hero-logo-backdrop {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 100%);
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hero-image {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: $hero-height;
 | 
			
		||||
    min-height: $hero-height;
 | 
			
		||||
    object-fit: cover;
 | 
			
		||||
    object-position: top;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 0;
 | 
			
		||||
 | 
			
		||||
    @media (min-width: 1250px) {
 | 
			
		||||
      object-position: center;
 | 
			
		||||
      height: 350px;
 | 
			
		||||
      min-height: 350px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__game-logo {
 | 
			
		||||
    width: 300px;
 | 
			
		||||
    align-self: flex-end;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hero-image-skeleton {
 | 
			
		||||
    height: 300px;
 | 
			
		||||
 | 
			
		||||
    @media (min-width: 1250px) {
 | 
			
		||||
      height: 350px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__description-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    background: linear-gradient(
 | 
			
		||||
      0deg,
 | 
			
		||||
      globals.$background-color 50%,
 | 
			
		||||
      globals.$dark-background-color 100%
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__description-content {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__description {
 | 
			
		||||
    user-select: text;
 | 
			
		||||
    line-height: 22px;
 | 
			
		||||
    font-size: globals.$body-font-size;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
 | 
			
		||||
    @media (min-width: 1280px) {
 | 
			
		||||
      width: 60%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    img {
 | 
			
		||||
      border-radius: 5px;
 | 
			
		||||
      margin-top: globals.$spacing-unit;
 | 
			
		||||
      margin-bottom: calc(globals.$spacing-unit * 3);
 | 
			
		||||
      display: block;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: auto;
 | 
			
		||||
      object-fit: cover;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    a {
 | 
			
		||||
      color: globals.$body-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .bb_tag {
 | 
			
		||||
      margin-top: calc(globals.$spacing-unit * 2);
 | 
			
		||||
      margin-bottom: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__description-skeleton {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
 | 
			
		||||
    @media (min-width: 1280px) {
 | 
			
		||||
      width: 60%;
 | 
			
		||||
      line-height: 22px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__randomizer-button {
 | 
			
		||||
    animation: slide-in 0.2s;
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    bottom: calc(globals.$spacing-unit * 3);
 | 
			
		||||
    right: calc(9px + globals.$spacing-unit * 2);
 | 
			
		||||
    box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 10px 1px;
 | 
			
		||||
    border: solid 2px globals.$border-color;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    background-color: globals.$background-color;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: globals.$background-color;
 | 
			
		||||
      box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 15px 5px;
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:active {
 | 
			
		||||
      transform: scale(0.98);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:disabled {
 | 
			
		||||
      box-shadow: none;
 | 
			
		||||
      transform: none;
 | 
			
		||||
      opacity: 0.8;
 | 
			
		||||
      background-color: globals.$background-color;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hero-panel-skeleton {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    background-color: globals.$background-color;
 | 
			
		||||
    height: 72px;
 | 
			
		||||
    border-bottom: solid 1px globals.$border-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__cloud-sync-button {
 | 
			
		||||
    padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2);
 | 
			
		||||
    background-color: rgba(0, 0, 0, 0.6);
 | 
			
		||||
    backdrop-filter: blur(20px);
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    min-height: 40px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    color: globals.$muted-color;
 | 
			
		||||
    font-size: globals.$small-font-size;
 | 
			
		||||
    border: solid 1px globals.$border-color;
 | 
			
		||||
    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
 | 
			
		||||
    animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1);
 | 
			
		||||
 | 
			
		||||
    &:active {
 | 
			
		||||
      opacity: 0.9;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:disabled {
 | 
			
		||||
      opacity: globals.$disabled-opacity;
 | 
			
		||||
      cursor: not-allowed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: rgba(0, 0, 0, 0.5);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__stars-icon-container {
 | 
			
		||||
    width: 16px;
 | 
			
		||||
    height: 16px;
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__stars-icon {
 | 
			
		||||
    width: 70px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: -28px;
 | 
			
		||||
    left: -27px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__cloud-icon-container {
 | 
			
		||||
    width: 20px;
 | 
			
		||||
    height: 16px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__cloud-icon {
 | 
			
		||||
    width: 26px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: -3px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hero-backdrop {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    transition: opacity 0.2s ease;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +11,6 @@ import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif";
 | 
			
		|||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { SkeletonTheme } from "react-loading-skeleton";
 | 
			
		||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
 | 
			
		||||
import * as styles from "./game-details.css";
 | 
			
		||||
 | 
			
		||||
import { vars } from "@renderer/theme.css";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +26,7 @@ import { GameOptionsModal, RepacksModal } from "./modals";
 | 
			
		|||
import { Downloader, getDownloadersForUri } from "@shared";
 | 
			
		||||
import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal";
 | 
			
		||||
import { CloudSyncFilesModal } from "./cloud-sync-files-modal/cloud-sync-files-modal";
 | 
			
		||||
import "./game-details.scss";
 | 
			
		||||
 | 
			
		||||
export default function GameDetails() {
 | 
			
		||||
  const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
 | 
			
		||||
| 
						 | 
				
			
			@ -185,23 +185,16 @@ export default function GameDetails() {
 | 
			
		|||
 | 
			
		||||
                {fromRandomizer && (
 | 
			
		||||
                  <Button
 | 
			
		||||
                    className={styles.randomizerButton}
 | 
			
		||||
                    className="game-details__randomizer-button"
 | 
			
		||||
                    onClick={handleRandomizerClick}
 | 
			
		||||
                    theme="outline"
 | 
			
		||||
                    disabled={!randomGame || randomizerLocked}
 | 
			
		||||
                  >
 | 
			
		||||
                    <div
 | 
			
		||||
                      style={{ width: 16, height: 16, position: "relative" }}
 | 
			
		||||
                    >
 | 
			
		||||
                    <div className="game-details__stars-icon-container">
 | 
			
		||||
                      <img
 | 
			
		||||
                        src={starsIconAnimated}
 | 
			
		||||
                        alt="Stars animation"
 | 
			
		||||
                        style={{
 | 
			
		||||
                          width: 70,
 | 
			
		||||
                          position: "absolute",
 | 
			
		||||
                          top: -28,
 | 
			
		||||
                          left: -27,
 | 
			
		||||
                        }}
 | 
			
		||||
                        className="game-details__stars-icon"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {t("next_suggestion")}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const heroPanelAction = style({
 | 
			
		||||
  border: `solid 1px ${vars.color.muted}`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const actions = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const separator = style({
 | 
			
		||||
  width: "1px",
 | 
			
		||||
  backgroundColor: vars.color.muted,
 | 
			
		||||
  opacity: "0.2",
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.hero-panel-actions {
 | 
			
		||||
  &__action {
 | 
			
		||||
    border: solid 1px globals.$muted-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__separator {
 | 
			
		||||
    width: 1px;
 | 
			
		||||
    background-color: globals.$muted-color;
 | 
			
		||||
    opacity: 0.2;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,9 +8,8 @@ import { Button } from "@renderer/components";
 | 
			
		|||
import { useDownload, useLibrary } from "@renderer/hooks";
 | 
			
		||||
import { useContext, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import * as styles from "./hero-panel-actions.css";
 | 
			
		||||
 | 
			
		||||
import { gameDetailsContext } from "@renderer/context";
 | 
			
		||||
import "./hero-panel-actions.scss";
 | 
			
		||||
 | 
			
		||||
export function HeroPanelActions() {
 | 
			
		||||
  const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +86,7 @@ export function HeroPanelActions() {
 | 
			
		|||
      theme="outline"
 | 
			
		||||
      disabled={toggleLibraryGameDisabled}
 | 
			
		||||
      onClick={addGameToLibrary}
 | 
			
		||||
      className={styles.heroPanelAction}
 | 
			
		||||
      className="hero-panel-actions__action"
 | 
			
		||||
    >
 | 
			
		||||
      <PlusCircleIcon />
 | 
			
		||||
      {t("add_to_library")}
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +98,7 @@ export function HeroPanelActions() {
 | 
			
		|||
      onClick={() => setShowRepacksModal(true)}
 | 
			
		||||
      theme="outline"
 | 
			
		||||
      disabled={deleting}
 | 
			
		||||
      className={styles.heroPanelAction}
 | 
			
		||||
      className="hero-panel-actions__action"
 | 
			
		||||
    >
 | 
			
		||||
      {t("open_download_options")}
 | 
			
		||||
    </Button>
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +111,7 @@ export function HeroPanelActions() {
 | 
			
		|||
          onClick={closeGame}
 | 
			
		||||
          theme="outline"
 | 
			
		||||
          disabled={deleting}
 | 
			
		||||
          className={styles.heroPanelAction}
 | 
			
		||||
          className="hero-panel-actions__action"
 | 
			
		||||
        >
 | 
			
		||||
          {t("close")}
 | 
			
		||||
        </Button>
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +124,7 @@ export function HeroPanelActions() {
 | 
			
		|||
          onClick={openGame}
 | 
			
		||||
          theme="outline"
 | 
			
		||||
          disabled={deleting || isGameRunning}
 | 
			
		||||
          className={styles.heroPanelAction}
 | 
			
		||||
          className="hero-panel-actions__action"
 | 
			
		||||
        >
 | 
			
		||||
          <PlayIcon />
 | 
			
		||||
          {t("play")}
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +137,7 @@ export function HeroPanelActions() {
 | 
			
		|||
        onClick={() => setShowRepacksModal(true)}
 | 
			
		||||
        theme="outline"
 | 
			
		||||
        disabled={isGameDownloading || !repacks.length}
 | 
			
		||||
        className={styles.heroPanelAction}
 | 
			
		||||
        className="hero-panel-actions__action"
 | 
			
		||||
      >
 | 
			
		||||
        <DownloadIcon />
 | 
			
		||||
        {t("download")}
 | 
			
		||||
| 
						 | 
				
			
			@ -157,16 +156,14 @@ export function HeroPanelActions() {
 | 
			
		|||
 | 
			
		||||
  if (game) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={styles.actions}>
 | 
			
		||||
      <div className="hero-panel-actions__container">
 | 
			
		||||
        {gameActionButton()}
 | 
			
		||||
 | 
			
		||||
        <div className={styles.separator} />
 | 
			
		||||
 | 
			
		||||
        <div className="hero-panel-actions__separator" />
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => setShowGameOptionsModal(true)}
 | 
			
		||||
          theme="outline"
 | 
			
		||||
          disabled={deleting}
 | 
			
		||||
          className={styles.heroPanelAction}
 | 
			
		||||
          className="hero-panel-actions__action"
 | 
			
		||||
        >
 | 
			
		||||
          <GearIcon />
 | 
			
		||||
          {t("options")}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.hero-panel-playtime {
 | 
			
		||||
  &__download-details {
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloads-link {
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,24 +1,18 @@
 | 
			
		|||
import { useContext, useEffect, useMemo, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import * as styles from "./hero-panel.css";
 | 
			
		||||
import { formatDownloadProgress } from "@renderer/helpers";
 | 
			
		||||
import { useDate, useDownload, useFormat } from "@renderer/hooks";
 | 
			
		||||
import { Link } from "@renderer/components";
 | 
			
		||||
 | 
			
		||||
import { gameDetailsContext } from "@renderer/context";
 | 
			
		||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
 | 
			
		||||
import "./hero-panel-playtime.scss";
 | 
			
		||||
 | 
			
		||||
export function HeroPanelPlaytime() {
 | 
			
		||||
  const [lastTimePlayed, setLastTimePlayed] = useState("");
 | 
			
		||||
 | 
			
		||||
  const { game, isGameRunning } = useContext(gameDetailsContext);
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation("game_details");
 | 
			
		||||
 | 
			
		||||
  const { numberFormatter } = useFormat();
 | 
			
		||||
 | 
			
		||||
  const { progress, lastPacket } = useDownload();
 | 
			
		||||
 | 
			
		||||
  const { formatDistance } = useDate();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -56,8 +50,8 @@ export function HeroPanelPlaytime() {
 | 
			
		|||
    game.download?.status === "active" && lastPacket?.gameId === game.id;
 | 
			
		||||
 | 
			
		||||
  const downloadInProgressInfo = (
 | 
			
		||||
    <div className={styles.downloadDetailsRow}>
 | 
			
		||||
      <Link to="/downloads" className={styles.downloadsLink}>
 | 
			
		||||
    <div className="hero-panel-playtime__download-details">
 | 
			
		||||
      <Link to="/downloads" className="hero-panel-playtime__downloads-link">
 | 
			
		||||
        {game.download?.status === "active"
 | 
			
		||||
          ? t("download_in_progress")
 | 
			
		||||
          : t("download_paused")}
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +78,6 @@ export function HeroPanelPlaytime() {
 | 
			
		|||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <p>{t("playing_now")}</p>
 | 
			
		||||
 | 
			
		||||
        {hasDownload && downloadInProgressInfo}
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,77 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
import { recipe } from "@vanilla-extract/recipes";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const panel = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    width: "100%",
 | 
			
		||||
    height: "72px",
 | 
			
		||||
    minHeight: "72px",
 | 
			
		||||
    padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
 | 
			
		||||
    backgroundColor: vars.color.darkBackground,
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
    justifyContent: "space-between",
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    borderBottom: `solid 1px ${vars.color.border}`,
 | 
			
		||||
    position: "sticky",
 | 
			
		||||
    overflow: "hidden",
 | 
			
		||||
    top: "0",
 | 
			
		||||
    zIndex: "2",
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    stuck: {
 | 
			
		||||
      true: {
 | 
			
		||||
        boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.8)",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const content = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const actions = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadDetailsRow = style({
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadsLink = style({
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  textDecoration: "underline",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const progressBar = recipe({
 | 
			
		||||
  base: {
 | 
			
		||||
    position: "absolute",
 | 
			
		||||
    bottom: "0",
 | 
			
		||||
    left: "0",
 | 
			
		||||
    width: "100%",
 | 
			
		||||
    height: "3px",
 | 
			
		||||
    transition: "all ease 0.2s",
 | 
			
		||||
    "::-webkit-progress-bar": {
 | 
			
		||||
      backgroundColor: "transparent",
 | 
			
		||||
    },
 | 
			
		||||
    "::-webkit-progress-value": {
 | 
			
		||||
      backgroundColor: vars.color.muted,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  variants: {
 | 
			
		||||
    disabled: {
 | 
			
		||||
      true: {
 | 
			
		||||
        opacity: vars.opacity.disabled,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										66
									
								
								src/renderer/src/pages/game-details/hero/hero-panel.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/renderer/src/pages/game-details/hero/hero-panel.scss
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,66 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.hero-panel {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 72px;
 | 
			
		||||
  min-height: 72px;
 | 
			
		||||
  padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
 | 
			
		||||
  background-color: globals.$dark-background-color;
 | 
			
		||||
  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;
 | 
			
		||||
 | 
			
		||||
  &--stuck {
 | 
			
		||||
    box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__content {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__actions {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__download-details {
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloads-link {
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__progress-bar {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 3px;
 | 
			
		||||
    transition: all ease 0.2s;
 | 
			
		||||
 | 
			
		||||
    &::-webkit-progress-bar {
 | 
			
		||||
      background-color: transparent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::-webkit-progress-value {
 | 
			
		||||
      background-color: globals.$muted-color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--disabled {
 | 
			
		||||
      opacity: globals.$disabled-opacity;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next";
 | 
			
		|||
import { useDate, useDownload } from "@renderer/hooks";
 | 
			
		||||
 | 
			
		||||
import { HeroPanelActions } from "./hero-panel-actions";
 | 
			
		||||
import * as styles from "./hero-panel.css";
 | 
			
		||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
 | 
			
		||||
 | 
			
		||||
import { gameDetailsContext } from "@renderer/context";
 | 
			
		||||
import "./hero-panel.scss";
 | 
			
		||||
 | 
			
		||||
export interface HeroPanelProps {
 | 
			
		||||
  isHeaderStuck: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -54,30 +54,28 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
 | 
			
		|||
    game?.download?.status === "paused";
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{ backgroundColor: gameColor }}
 | 
			
		||||
        className={styles.panel({ stuck: isHeaderStuck })}
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles.content}>{getInfo()}</div>
 | 
			
		||||
        <div className={styles.actions}>
 | 
			
		||||
          <HeroPanelActions />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {showProgressBar && (
 | 
			
		||||
          <progress
 | 
			
		||||
            max={1}
 | 
			
		||||
            value={
 | 
			
		||||
              isGameDownloading
 | 
			
		||||
                ? lastPacket?.progress
 | 
			
		||||
                : game?.download?.progress
 | 
			
		||||
            }
 | 
			
		||||
            className={styles.progressBar({
 | 
			
		||||
              disabled: game?.download?.status === "paused",
 | 
			
		||||
            })}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
    <div
 | 
			
		||||
      style={{ backgroundColor: gameColor }}
 | 
			
		||||
      className={`hero-panel ${isHeaderStuck ? "hero-panel--stuck" : ""}`}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="hero-panel__content">{getInfo()}</div>
 | 
			
		||||
      <div className="hero-panel__actions">
 | 
			
		||||
        <HeroPanelActions />
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
 | 
			
		||||
      {showProgressBar && (
 | 
			
		||||
        <progress
 | 
			
		||||
          max={1}
 | 
			
		||||
          value={
 | 
			
		||||
            isGameDownloading ? lastPacket?.progress : game?.download?.progress
 | 
			
		||||
          }
 | 
			
		||||
          className={`hero-panel__progress-bar ${
 | 
			
		||||
            game?.download?.status === "paused"
 | 
			
		||||
              ? "hero-panel__progress-bar--disabled"
 | 
			
		||||
              : ""
 | 
			
		||||
          }`}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,45 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const container = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  gap: `${SPACING_UNIT * 3}px`,
 | 
			
		||||
  width: "100%",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadsPathField = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const hintText = style({
 | 
			
		||||
  fontSize: "12px",
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloaders = style({
 | 
			
		||||
  display: "grid",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  gridTemplateColumns: "repeat(2, 1fr)",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloaderOption = style({
 | 
			
		||||
  position: "relative",
 | 
			
		||||
  ":only-child": {
 | 
			
		||||
    gridColumn: "1 / -1",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloaderIcon = style({
 | 
			
		||||
  position: "absolute",
 | 
			
		||||
  left: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const pathError = style({
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  ":hover": {
 | 
			
		||||
    textDecoration: "underline",
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.download-settings-modal {
 | 
			
		||||
  &__container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 3);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloads-path-field {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__hint-text {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    color: globals.$body-color;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloaders {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
    grid-template-columns: repeat(2, 1fr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloader-option {
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    &:only-child {
 | 
			
		||||
      grid-column: 1 / -1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloader-icon {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: calc(globals.$spacing-unit * 2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__path-error {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      text-decoration: underline;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +1,12 @@
 | 
			
		|||
import { useCallback, useEffect, useMemo, useState } from "react";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./download-settings-modal.css";
 | 
			
		||||
import { Button, Link, Modal, TextField } from "@renderer/components";
 | 
			
		||||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
 | 
			
		||||
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
 | 
			
		||||
 | 
			
		||||
import type { GameRepack } from "@types";
 | 
			
		||||
import { SPACING_UNIT } from "@renderer/theme.css";
 | 
			
		||||
import { DOWNLOADER_NAME } from "@renderer/constants";
 | 
			
		||||
import { useAppSelector, useFeature, useToast } from "@renderer/hooks";
 | 
			
		||||
import "./download-settings-modal.scss";
 | 
			
		||||
 | 
			
		||||
export interface DownloadSettingsModalProps {
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -145,21 +142,15 @@ export function DownloadSettingsModal({
 | 
			
		|||
      })}
 | 
			
		||||
      onClose={onClose}
 | 
			
		||||
    >
 | 
			
		||||
      <div className={styles.container}>
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            flexDirection: "column",
 | 
			
		||||
            gap: `${SPACING_UNIT}px`,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
      <div className="download-settings-modal__container">
 | 
			
		||||
        <div className="download-settings-modal__downloads-path-field">
 | 
			
		||||
          <span>{t("downloader")}</span>
 | 
			
		||||
 | 
			
		||||
          <div className={styles.downloaders}>
 | 
			
		||||
          <div className="download-settings-modal__downloaders">
 | 
			
		||||
            {downloaders.map((downloader) => (
 | 
			
		||||
              <Button
 | 
			
		||||
                key={downloader}
 | 
			
		||||
                className={styles.downloaderOption}
 | 
			
		||||
                className="download-settings-modal__downloader-option"
 | 
			
		||||
                theme={
 | 
			
		||||
                  selectedDownloader === downloader ? "primary" : "outline"
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -170,7 +161,7 @@ export function DownloadSettingsModal({
 | 
			
		|||
                onClick={() => setSelectedDownloader(downloader)}
 | 
			
		||||
              >
 | 
			
		||||
                {selectedDownloader === downloader && (
 | 
			
		||||
                  <CheckCircleFillIcon className={styles.downloaderIcon} />
 | 
			
		||||
                  <CheckCircleFillIcon className="download-settings-modal__downloader-icon" />
 | 
			
		||||
                )}
 | 
			
		||||
                {DOWNLOADER_NAME[downloader]}
 | 
			
		||||
              </Button>
 | 
			
		||||
| 
						 | 
				
			
			@ -178,13 +169,7 @@ export function DownloadSettingsModal({
 | 
			
		|||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            flexDirection: "column",
 | 
			
		||||
            gap: `${SPACING_UNIT}px`,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
        <div className="download-settings-modal__downloads-path-field">
 | 
			
		||||
          <TextField
 | 
			
		||||
            value={selectedPath}
 | 
			
		||||
            readOnly
 | 
			
		||||
| 
						 | 
				
			
			@ -193,7 +178,7 @@ export function DownloadSettingsModal({
 | 
			
		|||
            error={
 | 
			
		||||
              hasWritePermission === false ? (
 | 
			
		||||
                <span
 | 
			
		||||
                  className={styles.pathError}
 | 
			
		||||
                  className="download-settings-modal__path-error"
 | 
			
		||||
                  data-open-article="cannot-write-directory"
 | 
			
		||||
                >
 | 
			
		||||
                  {t("no_write_permission")}
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +197,7 @@ export function DownloadSettingsModal({
 | 
			
		|||
            }
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <p className={styles.hintText}>
 | 
			
		||||
          <p className="download-settings-modal__hint-text">
 | 
			
		||||
            <Trans i18nKey="select_folder_hint" ns="game_details">
 | 
			
		||||
              <Link to="/settings" />
 | 
			
		||||
            </Trans>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,24 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const optionsContainer = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gameOptionHeader = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gameOptionHeaderDescription = style({
 | 
			
		||||
  fontWeight: "400",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const gameOptionRow = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,86 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.game-options-modal {
 | 
			
		||||
  &__container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__section {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__header {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
 | 
			
		||||
    &-description {
 | 
			
		||||
      font-weight: 400;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__row {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__executable-field {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
 | 
			
		||||
    &-input {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-buttons {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      gap: globals.$spacing-unit;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__wine-prefix {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: calc(globals.$spacing-unit * 2);
 | 
			
		||||
 | 
			
		||||
    &-input {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__launch-options {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
 | 
			
		||||
    &-input {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__downloads {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__danger-zone {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
 | 
			
		||||
    &-description {
 | 
			
		||||
      font-weight: 400;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-buttons {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      gap: globals.$spacing-unit;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ import { useContext, useRef, useState } from "react";
 | 
			
		|||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { Button, Modal, TextField } from "@renderer/components";
 | 
			
		||||
import type { LibraryGame } from "@types";
 | 
			
		||||
import * as styles from "./game-options-modal.css";
 | 
			
		||||
import { gameDetailsContext } from "@renderer/context";
 | 
			
		||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
 | 
			
		||||
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +9,7 @@ import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
 | 
			
		|||
import { ResetAchievementsModal } from "./reset-achievements-modal";
 | 
			
		||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
 | 
			
		||||
import { debounce } from "lodash-es";
 | 
			
		||||
import "./game-options-modal.scss";
 | 
			
		||||
 | 
			
		||||
export interface GameOptionsModalProps {
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -192,14 +192,12 @@ export function GameOptionsModal({
 | 
			
		|||
        onClose={() => setShowDeleteModal(false)}
 | 
			
		||||
        deleteGame={handleDeleteGame}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <RemoveGameFromLibraryModal
 | 
			
		||||
        visible={showRemoveGameModal}
 | 
			
		||||
        onClose={() => setShowRemoveGameModal(false)}
 | 
			
		||||
        removeGameFromLibrary={handleRemoveGameFromLibrary}
 | 
			
		||||
        game={game}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <ResetAchievementsModal
 | 
			
		||||
        visible={showResetAchievementsModal}
 | 
			
		||||
        onClose={() => setShowResetAchievementsModal(false)}
 | 
			
		||||
| 
						 | 
				
			
			@ -213,59 +211,66 @@ export function GameOptionsModal({
 | 
			
		|||
        onClose={onClose}
 | 
			
		||||
        large={true}
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles.optionsContainer}>
 | 
			
		||||
          <div className={styles.gameOptionHeader}>
 | 
			
		||||
            <h2>{t("executable_section_title")}</h2>
 | 
			
		||||
            <h4 className={styles.gameOptionHeaderDescription}>
 | 
			
		||||
              {t("executable_section_description")}
 | 
			
		||||
            </h4>
 | 
			
		||||
        <div className="game-options-modal__container">
 | 
			
		||||
          <div className="game-options-modal__section">
 | 
			
		||||
            <div className="game-options-modal__header">
 | 
			
		||||
              <h2>{t("executable_section_title")}</h2>
 | 
			
		||||
              <h4 className="game-options-modal__header-description">
 | 
			
		||||
                {t("executable_section_description")}
 | 
			
		||||
              </h4>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className="game-options-modal__executable-field">
 | 
			
		||||
              <TextField
 | 
			
		||||
                value={game.executablePath || ""}
 | 
			
		||||
                readOnly
 | 
			
		||||
                theme="dark"
 | 
			
		||||
                disabled
 | 
			
		||||
                placeholder={t("no_executable_selected")}
 | 
			
		||||
                rightContent={
 | 
			
		||||
                  <>
 | 
			
		||||
                    <Button
 | 
			
		||||
                      type="button"
 | 
			
		||||
                      theme="outline"
 | 
			
		||||
                      onClick={handleChangeExecutableLocation}
 | 
			
		||||
                    >
 | 
			
		||||
                      <FileIcon />
 | 
			
		||||
                      {t("select_executable")}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                    {game.executablePath && (
 | 
			
		||||
                      <Button
 | 
			
		||||
                        onClick={handleClearExecutablePath}
 | 
			
		||||
                        theme="outline"
 | 
			
		||||
                      >
 | 
			
		||||
                        {t("clear")}
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </>
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              {game.executablePath && (
 | 
			
		||||
                <div className="game-options-modal__executable-field-buttons">
 | 
			
		||||
                  <Button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    theme="outline"
 | 
			
		||||
                    onClick={handleOpenGameExecutablePath}
 | 
			
		||||
                  >
 | 
			
		||||
                    {t("open_folder")}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                  <Button onClick={handleCreateShortcut} theme="outline">
 | 
			
		||||
                    {t("create_shortcut")}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <TextField
 | 
			
		||||
            value={game.executablePath || ""}
 | 
			
		||||
            readOnly
 | 
			
		||||
            theme="dark"
 | 
			
		||||
            disabled
 | 
			
		||||
            placeholder={t("no_executable_selected")}
 | 
			
		||||
            rightContent={
 | 
			
		||||
              <>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type="button"
 | 
			
		||||
                  theme="outline"
 | 
			
		||||
                  onClick={handleChangeExecutableLocation}
 | 
			
		||||
                >
 | 
			
		||||
                  <FileIcon />
 | 
			
		||||
                  {t("select_executable")}
 | 
			
		||||
                </Button>
 | 
			
		||||
                {game.executablePath && (
 | 
			
		||||
                  <Button onClick={handleClearExecutablePath} theme="outline">
 | 
			
		||||
                    {t("clear")}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                )}
 | 
			
		||||
              </>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          {game.executablePath && (
 | 
			
		||||
            <div className={styles.gameOptionRow}>
 | 
			
		||||
              <Button
 | 
			
		||||
                type="button"
 | 
			
		||||
                theme="outline"
 | 
			
		||||
                onClick={handleOpenGameExecutablePath}
 | 
			
		||||
              >
 | 
			
		||||
                {t("open_folder")}
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button onClick={handleCreateShortcut} theme="outline">
 | 
			
		||||
                {t("create_shortcut")}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {shouldShowWinePrefixConfiguration && (
 | 
			
		||||
            <div className={styles.optionsContainer}>
 | 
			
		||||
              <div className={styles.gameOptionHeader}>
 | 
			
		||||
            <div className="game-options-modal__wine-prefix">
 | 
			
		||||
              <div className="game-options-modal__header">
 | 
			
		||||
                <h2>{t("wine_prefix")}</h2>
 | 
			
		||||
                <h4 className={styles.gameOptionHeaderDescription}>
 | 
			
		||||
                <h4 className="game-options-modal__header-description">
 | 
			
		||||
                  {t("wine_prefix_description")}
 | 
			
		||||
                </h4>
 | 
			
		||||
              </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -300,11 +305,13 @@ export function GameOptionsModal({
 | 
			
		|||
          )}
 | 
			
		||||
 | 
			
		||||
          {shouldShowLaunchOptionsConfiguration && (
 | 
			
		||||
            <div className={styles.gameOptionHeader}>
 | 
			
		||||
              <h2>{t("launch_options")}</h2>
 | 
			
		||||
              <h4 className={styles.gameOptionHeaderDescription}>
 | 
			
		||||
                {t("launch_options_description")}
 | 
			
		||||
              </h4>
 | 
			
		||||
            <div className="game-options-modal__launch-options">
 | 
			
		||||
              <div className="game-options-modal__header">
 | 
			
		||||
                <h2>{t("launch_options")}</h2>
 | 
			
		||||
                <h4 className="game-options-modal__header-description">
 | 
			
		||||
                  {t("launch_options_description")}
 | 
			
		||||
                </h4>
 | 
			
		||||
              </div>
 | 
			
		||||
              <TextField
 | 
			
		||||
                value={launchOptions}
 | 
			
		||||
                theme="dark"
 | 
			
		||||
| 
						 | 
				
			
			@ -321,72 +328,76 @@ export function GameOptionsModal({
 | 
			
		|||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <div className={styles.gameOptionHeader}>
 | 
			
		||||
            <h2>{t("downloads_secion_title")}</h2>
 | 
			
		||||
            <h4 className={styles.gameOptionHeaderDescription}>
 | 
			
		||||
              {t("downloads_section_description")}
 | 
			
		||||
            </h4>
 | 
			
		||||
          <div className="game-options-modal__downloads">
 | 
			
		||||
            <div className="game-options-modal__header">
 | 
			
		||||
              <h2>{t("downloads_secion_title")}</h2>
 | 
			
		||||
              <h4 className="game-options-modal__header-description">
 | 
			
		||||
                {t("downloads_section_description")}
 | 
			
		||||
              </h4>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className="game-options-modal__row">
 | 
			
		||||
              <Button
 | 
			
		||||
                onClick={() => setShowRepacksModal(true)}
 | 
			
		||||
                theme="outline"
 | 
			
		||||
                disabled={deleting || isGameDownloading || !repacks.length}
 | 
			
		||||
              >
 | 
			
		||||
                {t("open_download_options")}
 | 
			
		||||
              </Button>
 | 
			
		||||
              {game.download?.downloadPath && (
 | 
			
		||||
                <Button
 | 
			
		||||
                  onClick={handleOpenDownloadFolder}
 | 
			
		||||
                  theme="outline"
 | 
			
		||||
                  disabled={deleting}
 | 
			
		||||
                >
 | 
			
		||||
                  {t("open_download_location")}
 | 
			
		||||
                </Button>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className={styles.gameOptionRow}>
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={() => setShowRepacksModal(true)}
 | 
			
		||||
              theme="outline"
 | 
			
		||||
              disabled={deleting || isGameDownloading || !repacks.length}
 | 
			
		||||
            >
 | 
			
		||||
              {t("open_download_options")}
 | 
			
		||||
            </Button>
 | 
			
		||||
            {game.download?.downloadPath && (
 | 
			
		||||
          <div className="game-options-modal__danger-zone">
 | 
			
		||||
            <div className="game-options-modal__header">
 | 
			
		||||
              <h2>{t("danger_zone_section_title")}</h2>
 | 
			
		||||
              <h4 className="game-options-modal__danger-zone-description">
 | 
			
		||||
                {t("danger_zone_section_description")}
 | 
			
		||||
              </h4>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className="game-options-modal__danger-zone-buttons">
 | 
			
		||||
              <Button
 | 
			
		||||
                onClick={handleOpenDownloadFolder}
 | 
			
		||||
                theme="outline"
 | 
			
		||||
                onClick={() => setShowRemoveGameModal(true)}
 | 
			
		||||
                theme="danger"
 | 
			
		||||
                disabled={deleting}
 | 
			
		||||
              >
 | 
			
		||||
                {t("open_download_location")}
 | 
			
		||||
                {t("remove_from_library")}
 | 
			
		||||
              </Button>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className={styles.gameOptionHeader}>
 | 
			
		||||
            <h2>{t("danger_zone_section_title")}</h2>
 | 
			
		||||
            <h4 className={styles.gameOptionHeaderDescription}>
 | 
			
		||||
              {t("danger_zone_section_description")}
 | 
			
		||||
            </h4>
 | 
			
		||||
          </div>
 | 
			
		||||
              <Button
 | 
			
		||||
                onClick={() => setShowResetAchievementsModal(true)}
 | 
			
		||||
                theme="danger"
 | 
			
		||||
                disabled={
 | 
			
		||||
                  deleting ||
 | 
			
		||||
                  isDeletingAchievements ||
 | 
			
		||||
                  !hasAchievements ||
 | 
			
		||||
                  !userDetails
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                {t("reset_achievements")}
 | 
			
		||||
              </Button>
 | 
			
		||||
 | 
			
		||||
          <div className={styles.gameOptionRow}>
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={() => setShowRemoveGameModal(true)}
 | 
			
		||||
              theme="danger"
 | 
			
		||||
              disabled={deleting}
 | 
			
		||||
            >
 | 
			
		||||
              {t("remove_from_library")}
 | 
			
		||||
            </Button>
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={() => setShowResetAchievementsModal(true)}
 | 
			
		||||
              theme="danger"
 | 
			
		||||
              disabled={
 | 
			
		||||
                deleting ||
 | 
			
		||||
                isDeletingAchievements ||
 | 
			
		||||
                !hasAchievements ||
 | 
			
		||||
                !userDetails
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              {t("reset_achievements")}
 | 
			
		||||
            </Button>
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setShowDeleteModal(true);
 | 
			
		||||
              }}
 | 
			
		||||
              theme="danger"
 | 
			
		||||
              disabled={
 | 
			
		||||
                isGameDownloading || deleting || !game.download?.downloadPath
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              {t("remove_files")}
 | 
			
		||||
            </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setShowDeleteModal(true);
 | 
			
		||||
                }}
 | 
			
		||||
                theme="danger"
 | 
			
		||||
                disabled={
 | 
			
		||||
                  isGameDownloading || deleting || !game.download?.downloadPath
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                {t("remove_files")}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Modal>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const deleteActionsButtonsCtn = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  justifyContent: "end",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
@use "../../../scss/globals.scss";
 | 
			
		||||
 | 
			
		||||
.remove-from-library-modal {
 | 
			
		||||
  &__actions {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    gap: globals.$spacing-unit;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { Button, Modal } from "@renderer/components";
 | 
			
		||||
import * as styles from "./remove-from-library-modal.css";
 | 
			
		||||
import type { Game } from "@types";
 | 
			
		||||
import "./remove-from-library-modal.scss";
 | 
			
		||||
 | 
			
		||||
interface RemoveGameFromLibraryModalProps {
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ export function RemoveGameFromLibraryModal({
 | 
			
		|||
      description={t("remove_from_library_description", { game: game.title })}
 | 
			
		||||
      onClose={onClose}
 | 
			
		||||
    >
 | 
			
		||||
      <div className={styles.deleteActionsButtonsCtn}>
 | 
			
		||||
      <div className="remove-from-library-modal__actions">
 | 
			
		||||
        <Button onClick={handleRemoveGame} theme="outline">
 | 
			
		||||
          {t("remove")}
 | 
			
		||||
        </Button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
import { style } from "@vanilla-extract/css";
 | 
			
		||||
 | 
			
		||||
import { SPACING_UNIT, vars } from "../../../theme.css";
 | 
			
		||||
 | 
			
		||||
export const repacks = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const repackButton = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  textAlign: "left",
 | 
			
		||||
  flexDirection: "column",
 | 
			
		||||
  alignItems: "flex-start",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  color: vars.color.body,
 | 
			
		||||
  padding: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue