mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-12 14:26:16 +00:00
Compare commits
34 Commits
fix/using-
...
feature/mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee0e314b29 | ||
|
|
d50bb137e6 | ||
|
|
1bbf3b27bf | ||
|
|
3c9d036efd | ||
|
|
e97a6fe51a | ||
|
|
ead094de01 | ||
|
|
855a646d23 | ||
|
|
8192e5d8f1 | ||
|
|
2bd4b69926 | ||
|
|
62c6071395 | ||
|
|
d1750fff59 | ||
|
|
138244d5aa | ||
|
|
d038398750 | ||
|
|
b0eb7c16cd | ||
|
|
691dba26af | ||
|
|
ad330bd7a3 | ||
|
|
fe8f1b44db | ||
|
|
b9c072e7ac | ||
|
|
50df38856d | ||
|
|
f5395305eb | ||
|
|
3f29a78593 | ||
|
|
71f3409275 | ||
|
|
c4f5d17b40 | ||
|
|
686ec61a99 | ||
|
|
fb63ec864c | ||
|
|
e07297fc53 | ||
|
|
c17839ae97 | ||
|
|
395f77e17c | ||
|
|
f6707a5c84 | ||
|
|
ee4c564698 | ||
|
|
cedb61cb38 | ||
|
|
a292164a55 | ||
|
|
4b59a007f4 | ||
|
|
c9e99d3852 |
@@ -36,6 +36,7 @@
|
|||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@fontsource/noto-sans": "^5.1.0",
|
"@fontsource/noto-sans": "^5.1.0",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"featured": "Destaques",
|
"featured": "Destaques",
|
||||||
"hot": "Populares",
|
"hot": "Populares",
|
||||||
"weekly": "📅 Mais baixados da semana",
|
"weekly": "📅 Mais baixados da semana",
|
||||||
"achievements": "🏆 Pra platinar",
|
"achievements": "🏆 Para platinar",
|
||||||
"surprise_me": "Surpreenda-me",
|
"surprise_me": "Surpreenda-me",
|
||||||
"no_results": "Nenhum resultado encontrado",
|
"no_results": "Nenhum resultado encontrado",
|
||||||
"start_typing": "Comece a digitar para pesquisar…"
|
"start_typing": "Comece a digitar para pesquisar…"
|
||||||
|
|||||||
7
src/main/events/aparence/open-editor-window.ts
Normal file
7
src/main/events/aparence/open-editor-window.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { WindowManager } from "@main/services";
|
||||||
|
|
||||||
|
const openEditorWindow = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||||
|
WindowManager.openEditorWindow();
|
||||||
|
|
||||||
|
registerEvent("openEditorWindow", openEditorWindow);
|
||||||
@@ -49,6 +49,7 @@ import "./user-preferences/authenticate-real-debrid";
|
|||||||
import "./download-sources/put-download-source";
|
import "./download-sources/put-download-source";
|
||||||
import "./auth/sign-out";
|
import "./auth/sign-out";
|
||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
|
import "./aparence/open-editor-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
import "./user/get-user";
|
import "./user/get-user";
|
||||||
import "./user/get-blocked-users";
|
import "./user/get-blocked-users";
|
||||||
|
|||||||
@@ -190,6 +190,52 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static openEditorWindow() {
|
||||||
|
if (this.mainWindow) {
|
||||||
|
const editorWindow = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 720,
|
||||||
|
minWidth: 600,
|
||||||
|
minHeight: 720,
|
||||||
|
backgroundColor: "#1c1c1c",
|
||||||
|
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||||
|
...(process.platform === "linux" ? { icon } : {}),
|
||||||
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
|
titleBarOverlay: {
|
||||||
|
symbolColor: "#DADBE1",
|
||||||
|
color: "#151515",
|
||||||
|
height: 34,
|
||||||
|
},
|
||||||
|
parent: this.mainWindow,
|
||||||
|
modal: true,
|
||||||
|
show: false,
|
||||||
|
maximizable: true,
|
||||||
|
resizable: true,
|
||||||
|
minimizable: true,
|
||||||
|
webPreferences: {
|
||||||
|
sandbox: false,
|
||||||
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
editorWindow.removeMenu();
|
||||||
|
|
||||||
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
|
editorWindow.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}#/editor`);
|
||||||
|
} else {
|
||||||
|
editorWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
||||||
|
hash: "editor",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editorWindow.once("ready-to-show", () => {
|
||||||
|
editorWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app.isPackaged) editorWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static redirect(hash: string) {
|
public static redirect(hash: string) {
|
||||||
if (!this.mainWindow) this.createMainWindow();
|
if (!this.mainWindow) this.createMainWindow();
|
||||||
this.loadMainWindowURL(hash);
|
this.loadMainWindowURL(hash);
|
||||||
|
|||||||
@@ -313,4 +313,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
/* Notifications */
|
/* Notifications */
|
||||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||||
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
openEditorWindow: () => ipcRenderer.invoke("openEditorWindow"),
|
||||||
});
|
});
|
||||||
|
|||||||
21
src/renderer/src/_theme.scss
Normal file
21
src/renderer/src/_theme.scss
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
$spacing-unit: 8px;
|
||||||
|
|
||||||
|
$color-background: #1c1c1c;
|
||||||
|
$color-dark-background: #151515;
|
||||||
|
$color-muted: #c0c1c7;
|
||||||
|
$color-body: #8e919b;
|
||||||
|
$color-border: rgba(255, 255, 255, 0.15);
|
||||||
|
$color-success: #1c9749;
|
||||||
|
$color-danger: #e11d48;
|
||||||
|
$color-warning: #ffc107;
|
||||||
|
|
||||||
|
$opacity-disabled: 0.5;
|
||||||
|
$opacity-active: 0.7;
|
||||||
|
|
||||||
|
$size-body: 14px;
|
||||||
|
$size-small: 12px;
|
||||||
|
|
||||||
|
$z-index-toast: 5;
|
||||||
|
$z-index-bottom-panel: 3;
|
||||||
|
$z-index-title-bar: 4;
|
||||||
|
$z-index-backdrop: 4;
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import {
|
|
||||||
ComplexStyleRule,
|
|
||||||
createContainer,
|
|
||||||
globalStyle,
|
|
||||||
style,
|
|
||||||
} from "@vanilla-extract/css";
|
|
||||||
import { SPACING_UNIT, vars } from "./theme.css";
|
|
||||||
|
|
||||||
export const appContainer = createContainer();
|
|
||||||
|
|
||||||
globalStyle("*", {
|
|
||||||
boxSizing: "border-box",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("::-webkit-scrollbar", {
|
|
||||||
width: "9px",
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("::-webkit-scrollbar-track", {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.03)",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("::-webkit-scrollbar-thumb", {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
|
||||||
borderRadius: "24px",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("::-webkit-scrollbar-thumb:hover", {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.16)",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("html, body, #root, main", {
|
|
||||||
height: "100%",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("body", {
|
|
||||||
overflow: "hidden",
|
|
||||||
userSelect: "none",
|
|
||||||
fontFamily: "Noto Sans, sans-serif",
|
|
||||||
fontSize: vars.size.body,
|
|
||||||
color: vars.color.body,
|
|
||||||
margin: "0",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("button", {
|
|
||||||
padding: "0",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
border: "none",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("h1, h2, h3, h4, h5, h6, p", {
|
|
||||||
margin: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("p", {
|
|
||||||
lineHeight: "20px",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("#root, main", {
|
|
||||||
display: "flex",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("#root", {
|
|
||||||
flexDirection: "column",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("main", {
|
|
||||||
overflow: "hidden",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle(
|
|
||||||
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button",
|
|
||||||
{
|
|
||||||
WebkitAppearance: "none",
|
|
||||||
margin: "0",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
globalStyle("label", {
|
|
||||||
fontSize: vars.size.body,
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("input[type=number]", {
|
|
||||||
MozAppearance: "textfield",
|
|
||||||
});
|
|
||||||
|
|
||||||
globalStyle("img", {
|
|
||||||
WebkitUserDrag: "none",
|
|
||||||
} as Record<string, string>);
|
|
||||||
|
|
||||||
globalStyle("progress[value]", {
|
|
||||||
WebkitAppearance: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const container = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
containerName: appContainer,
|
|
||||||
containerType: "inline-size",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const content = style({
|
|
||||||
overflowY: "auto",
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
position: "relative",
|
|
||||||
height: "100%",
|
|
||||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 50%, ${vars.color.background} 100%)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const titleBar = style({
|
|
||||||
display: "flex",
|
|
||||||
width: "100%",
|
|
||||||
height: "35px",
|
|
||||||
minHeight: "35px",
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
alignItems: "center",
|
|
||||||
padding: `0 ${SPACING_UNIT * 2}px`,
|
|
||||||
WebkitAppRegion: "drag",
|
|
||||||
zIndex: "4",
|
|
||||||
borderBottom: `1px solid ${vars.color.border}`,
|
|
||||||
} as ComplexStyleRule);
|
|
||||||
|
|
||||||
export const cloudText = style({
|
|
||||||
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
|
|
||||||
backgroundClip: "text",
|
|
||||||
color: "transparent",
|
|
||||||
});
|
|
||||||
136
src/renderer/src/app.scss
Normal file
136
src/renderer/src/app.scss
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
@use "./scss/variables" as vars;
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 9px;
|
||||||
|
background-color: vars.$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: vars.$body-font-size;
|
||||||
|
color: vars.$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: vars.$body-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress[value] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container__content {
|
||||||
|
overflow-y: auto;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
vars.$dark-background-color 50%,
|
||||||
|
vars.$background-color 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container__title-bar {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 35px;
|
||||||
|
min-height: 35px;
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 vars.$spacing-unit * 2;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
z-index: vars.$title-bar-z-index;
|
||||||
|
border-bottom: 1px solid vars.$border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container__cloud-text {
|
||||||
|
background: linear-gradient(270deg, #16b195 50%, #3e62c0 100%);
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
@@ -12,8 +12,6 @@ import {
|
|||||||
useUserDetails,
|
useUserDetails,
|
||||||
} from "@renderer/hooks";
|
} from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./app.css";
|
|
||||||
|
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
setUserPreferences,
|
setUserPreferences,
|
||||||
@@ -30,6 +28,8 @@ import { downloadSourcesTable } from "./dexie";
|
|||||||
import { useSubscription } from "./hooks/use-subscription";
|
import { useSubscription } from "./hooks/use-subscription";
|
||||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||||
|
|
||||||
|
import "./app.scss";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -240,11 +240,11 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{window.electron.platform === "win32" && (
|
{window.electron.platform === "win32" && (
|
||||||
<div className={styles.titleBar}>
|
<div className="title-bar">
|
||||||
<h4>
|
<h4>
|
||||||
Hydra
|
Hydra
|
||||||
{hasActiveSubscription && (
|
{hasActiveSubscription && (
|
||||||
<span className={styles.cloudText}> Cloud</span>
|
<span className="title-bar__cloud-text"> Cloud</span>
|
||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,10 +275,10 @@ export function App() {
|
|||||||
<main>
|
<main>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
<article className={styles.container}>
|
<article className="app-container">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<section ref={contentRef} className={styles.content}>
|
<section ref={contentRef} className="app-container__content">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
@use "../../scss/globals.scss";
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
.profile-avatar {
|
.profile-avatar {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: globals.$background-color;
|
background-color: vars.$background-color;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px vars.$border-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: globals.$muted-color;
|
color: vars.$muted-color;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&__image {
|
&__image {
|
||||||
|
|||||||
@@ -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/variables" as vars;
|
||||||
|
|
||||||
|
.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: vars.$backdrop-z-index;
|
||||||
|
top: 0;
|
||||||
|
padding: calc(vars.$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(#{vars.$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 {
|
export interface BackdropProps {
|
||||||
isClosing?: boolean;
|
isClosing?: boolean;
|
||||||
@@ -8,9 +9,9 @@ export interface BackdropProps {
|
|||||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.backdrop({
|
className={cn("backdrop", {
|
||||||
closing: isClosing,
|
"backdrop--closing": isClosing,
|
||||||
windows: window.electron.platform === "win32",
|
"backdrop--windows": window.electron.platform === "win32",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@use "../../scss/globals.scss";
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
color: globals.$muted-color;
|
color: vars.$muted-color;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
padding: calc(vars.$spacing-unit / 2) vars.$spacing-unit;
|
||||||
border: solid 1px globals.$muted-color;
|
border: solid 1px vars.$muted-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
@use "../../scss/globals.scss";
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
.bottom-panel {
|
.bottom-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: solid 1px globals.$border-color;
|
border-top: solid 1px vars.$border-color;
|
||||||
background-color: globals.$background-color;
|
background-color: vars.$background-color;
|
||||||
padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 2);
|
padding: calc(vars.$spacing-unit / 2) calc(vars.$spacing-unit * 2);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: globals.$bottom-panel-z-index;
|
z-index: vars.$bottom-panel-z-index;
|
||||||
|
|
||||||
&__downloads-button {
|
&__downloads-button {
|
||||||
color: globals.$body-color;
|
color: vars.$body-color;
|
||||||
border-bottom: solid 1px transparent;
|
border-bottom: solid 1px transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-bottom: solid 1px globals.$body-color;
|
border-bottom: solid 1px vars.$body-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@use "../../scss/globals.scss";
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
padding: globals.$spacing-unit globals.$spacing-unit * 2;
|
padding: vars.$spacing-unit vars.$spacing-unit * 2;
|
||||||
background-color: globals.$muted-color;
|
background-color: vars.$muted-color;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: solid 1px transparent;
|
border: solid 1px transparent;
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
@@ -11,14 +11,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: globals.$spacing-unit;
|
gap: vars.$spacing-unit;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
opacity: globals.$active-opacity;
|
opacity: vars.$active-opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: globals.$disabled-opacity;
|
opacity: vars.$disabled-opacity;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,14 +28,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background-color: globals.$muted-color;
|
background-color: vars.$muted-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--outline {
|
&--outline {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px vars.$border-color;
|
||||||
color: globals.$muted-color;
|
color: vars.$muted-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
@@ -47,14 +47,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--dark {
|
&--dark {
|
||||||
background-color: globals.$dark-background-color;
|
background-color: vars.$dark-background-color;
|
||||||
color: globals.$muted-color;
|
color: vars.$muted-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--danger {
|
&--danger {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: globals.$danger-color;
|
background-color: vars.$danger-color;
|
||||||
color: globals.$muted-color;
|
color: vars.$muted-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #b3203f;
|
background-color: #b3203f;
|
||||||
|
|||||||
@@ -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,39 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.checkbox-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&__checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
border: solid 1px vars.$border-color;
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useId } from "react";
|
import { useId } from "react";
|
||||||
import * as styles from "./checkbox-field.css";
|
|
||||||
import { CheckIcon } from "@primer/octicons-react";
|
import { CheckIcon } from "@primer/octicons-react";
|
||||||
|
import "./checkbox-field.scss";
|
||||||
|
|
||||||
export interface CheckboxFieldProps
|
export interface CheckboxFieldProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
@@ -14,17 +14,19 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
|||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.checkboxField}>
|
<div className="checkbox-field">
|
||||||
<div className={styles.checkbox({ checked: props.checked })}>
|
<div
|
||||||
|
className={`checkbox-field__checkbox ${props.checked ? "checked" : ""}`}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className={styles.checkboxInput}
|
className="checkbox-field__input"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{props.checked && <CheckIcon />}
|
{props.checked && <CheckIcon />}
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor={id} className={styles.checkboxLabel}>
|
<label htmlFor={id} className="checkbox-field__label">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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/variables" as vars;
|
||||||
|
|
||||||
|
.confirmation-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(vars.$spacing-unit * 2);
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-self: flex-end;
|
||||||
|
gap: calc(vars.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
&__description {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button } from "../button/button";
|
import { Button } from "../button/button";
|
||||||
import { Modal, type ModalProps } from "../modal/modal";
|
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"> {
|
export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
|
||||||
confirmButtonLabel: string;
|
confirmButtonLabel: string;
|
||||||
@@ -31,10 +31,10 @@ export function ConfirmationModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal {...props}>
|
<Modal {...props}>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
<div className="confirmation-modal">
|
||||||
<p className={styles.descriptionText}>{descriptionText}</p>
|
<p className="confirmation-modal__description">{descriptionText}</p>
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className="confirmation-modal__actions">
|
||||||
<Button theme="outline" onClick={handleCancelClick}>
|
<Button theme="outline" onClick={handleCancelClick}>
|
||||||
{cancelButtonLabel}
|
{cancelButtonLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
@use "../../scss/globals.scss";
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
&__content {
|
&__content {
|
||||||
background-color: globals.$dark-background-color;
|
background-color: vars.$dark-background-color;
|
||||||
border: 1px solid globals.$border-color;
|
border: 1px solid vars.$border-color;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -20,13 +20,13 @@
|
|||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: globals.$muted-color;
|
color: vars.$muted-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__separator {
|
&__separator {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: globals.$border-color;
|
background-color: vars.$border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
@@ -49,12 +49,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:not(&__item--disabled) &__item:hover {
|
&:not(&__item--disabled) &__item:hover {
|
||||||
background-color: globals.$background-color;
|
background-color: vars.$background-color;
|
||||||
color: globals.$muted-color;
|
color: vars.$muted-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item:focus {
|
&__item:focus {
|
||||||
background-color: globals.$background-color;
|
background-color: vars.$background-color;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/variables" as vars;
|
||||||
|
|
||||||
|
.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 vars.$border-color;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: vars.$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: vars.$spacing-unit calc(vars.$spacing-unit * 2);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: vars.$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: vars.$spacing-unit;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__specifics {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(vars.$spacing-unit * 2);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__specifics-item {
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
color: vars.$muted-color;
|
||||||
|
font-size: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
color: vars.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__shop-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-download-label {
|
||||||
|
color: vars.$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 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 { useTranslation } from "react-i18next";
|
||||||
import { Badge } from "../badge/badge";
|
import { Badge } from "../badge/badge";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
@@ -19,7 +20,7 @@ export interface GameCardProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shopIcon = {
|
const shopIcon = {
|
||||||
steam: <SteamLogo className={styles.shopIcon} />,
|
steam: <SteamLogo className="game-card__shop-icon" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GameCard({ game, ...props }: GameCardProps) {
|
export function GameCard({ game, ...props }: GameCardProps) {
|
||||||
@@ -48,25 +49,25 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
|||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.card}
|
className="game-card"
|
||||||
onMouseEnter={handleHover}
|
onMouseEnter={handleHover}
|
||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className="game-card__backdrop">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectId)}
|
src={steamUrlBuilder.library(game.objectId)}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
className={styles.cover}
|
className="game-card__cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className="game-card__content">
|
||||||
<div className={styles.titleContainer}>
|
<div className="game-card__title-container">
|
||||||
{shopIcon[game.shop]}
|
{shopIcon[game.shop]}
|
||||||
<p className={styles.title}>{game.title}</p>
|
<p className="game-card__title">{game.title}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{uniqueRepackers.length > 0 ? (
|
{uniqueRepackers.length > 0 ? (
|
||||||
<ul className={styles.downloadOptions}>
|
<ul className="game-card__download-options">
|
||||||
{uniqueRepackers.map((repacker) => (
|
{uniqueRepackers.map((repacker) => (
|
||||||
<li key={repacker}>
|
<li key={repacker}>
|
||||||
<Badge>{repacker}</Badge>
|
<Badge>{repacker}</Badge>
|
||||||
@@ -74,17 +75,17 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</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="game-card__specifics">
|
||||||
<div className={styles.specificsItem}>
|
<div className="game-card__specifics-item">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
<span>
|
<span>
|
||||||
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
|
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.specificsItem}>
|
<div className="game-card__specifics-item">
|
||||||
<PeopleIcon />
|
<PeopleIcon />
|
||||||
<span>
|
<span>
|
||||||
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
|
{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/variables" as vars;
|
||||||
|
|
||||||
|
.auto-update-sub-header {
|
||||||
|
border-bottom: solid 1px vars.$body-color;
|
||||||
|
padding: calc(vars.$spacing-unit / 2) calc(vars.$spacing-unit * 3);
|
||||||
|
|
||||||
|
&__new-version-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
color: #8e919b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__new-version-icon {
|
||||||
|
color: vars.$success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__new-version-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
color: vars.$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 { useEffect, useState } from "react";
|
||||||
import { SyncIcon } from "@primer/octicons-react";
|
import { SyncIcon } from "@primer/octicons-react";
|
||||||
import { Link } from "../link/link";
|
import { Link } from "../link/link";
|
||||||
import * as styles from "./header.css";
|
import "./auto-update-header.scss";
|
||||||
import type { AppUpdaterEvent } from "@types";
|
import type { AppUpdaterEvent } from "@types";
|
||||||
|
|
||||||
export const releasesPageUrl =
|
export const releasesPageUrl =
|
||||||
@@ -45,9 +45,15 @@ export function AutoUpdateSubHeader() {
|
|||||||
|
|
||||||
if (!isAutoInstallAvailable) {
|
if (!isAutoInstallAvailable) {
|
||||||
return (
|
return (
|
||||||
<header className={styles.subheader}>
|
<header className="auto-update-sub-header">
|
||||||
<Link to={releasesPageUrl} className={styles.newVersionLink}>
|
<Link
|
||||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
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 })}
|
{t("version_available_download", { version: newVersion })}
|
||||||
</Link>
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
@@ -56,13 +62,16 @@ export function AutoUpdateSubHeader() {
|
|||||||
|
|
||||||
if (isReadyToInstall) {
|
if (isReadyToInstall) {
|
||||||
return (
|
return (
|
||||||
<header className={styles.subheader}>
|
<header className="auto-update-sub-header">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.newVersionButton}
|
className="auto-update-sub-header__new-version-button"
|
||||||
onClick={handleClickInstallUpdate}
|
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 })}
|
{t("version_available_install", { version: newVersion })}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</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,
|
|
||||||
});
|
|
||||||
145
src/renderer/src/components/header/header.scss
Normal file
145
src/renderer/src/components/header/header.scss
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(vars.$spacing-unit * 2);
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
width: 100%;
|
||||||
|
padding: calc(vars.$spacing-unit * 2) calc(vars.$spacing-unit * 3);
|
||||||
|
color: vars.$muted-color;
|
||||||
|
border-bottom: solid 1px vars.$border-color;
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
|
||||||
|
&--dragging-disabled {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--is-windows {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search {
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
display: inline-flex;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
width: 200px;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: solid 1px vars.$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: vars.$spacing-unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #dadbe1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(vars.$spacing-unit * 2);
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__back-button {
|
||||||
|
color: vars.$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__new-version-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
color: vars.$body-color;
|
||||||
|
font-size: vars.$new-version-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__new-version-icon {
|
||||||
|
color: vars.$success-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 { useAppDispatch, useAppSelector } from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./header.css";
|
import "./header.scss";
|
||||||
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
||||||
import { setFilters } from "@renderer/features";
|
import { setFilters } from "@renderer/features";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
const pathTitle: Record<string, string> = {
|
const pathTitle: Record<string, string> = {
|
||||||
"/": "home",
|
"/": "home",
|
||||||
@@ -75,16 +76,16 @@ export function Header() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
className={styles.header({
|
className={cn("header", {
|
||||||
draggingDisabled,
|
"header--dragging-disabled": draggingDisabled,
|
||||||
isWindows: window.electron.platform === "win32",
|
"header--is-windows": window.electron.platform === "win32",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<section className={styles.section} style={{ flex: 1 }}>
|
<section className="header__section" style={{ flex: 1 }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.backButton({
|
className={cn("header__back-button", {
|
||||||
enabled: location.key !== "default",
|
"header__back-button--enabled": location.key !== "default",
|
||||||
})}
|
})}
|
||||||
onClick={handleBackButtonClick}
|
onClick={handleBackButtonClick}
|
||||||
disabled={location.key === "default"}
|
disabled={location.key === "default"}
|
||||||
@@ -93,19 +94,23 @@ export function Header() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3
|
<h3
|
||||||
className={styles.title({
|
className={cn("header__title", {
|
||||||
hasBackButton: location.key !== "default",
|
"header__title--has-back-button": location.key !== "default",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className="header__section">
|
||||||
<div className={styles.search({ focused: isFocused })}>
|
<div
|
||||||
|
className={cn("header__search", {
|
||||||
|
"header__search--focused": isFocused,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.actionButton}
|
className="header__action-button"
|
||||||
onClick={focusInput}
|
onClick={focusInput}
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
@@ -117,7 +122,7 @@ export function Header() {
|
|||||||
name="search"
|
name="search"
|
||||||
placeholder={t("search")}
|
placeholder={t("search")}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
className={styles.searchInput}
|
className="header__search-input"
|
||||||
onChange={(event) => handleSearch(event.target.value)}
|
onChange={(event) => handleSearch(event.target.value)}
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
@@ -127,7 +132,7 @@ export function Header() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => dispatch(setFilters({ title: "" }))}
|
onClick={() => dispatch(setFilters({ title: "" }))}
|
||||||
className={styles.actionButton}
|
className="header__action-button"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</button>
|
</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",
|
|
||||||
});
|
|
||||||
57
src/renderer/src/components/hero/hero.scss
Normal file
57
src/renderer/src/components/hero/hero.scss
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.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 vars.$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 {
|
||||||
|
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: vars.$muted-color;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-top: vars.$spacing-unit * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: vars.$spacing-unit * 4 vars.$spacing-unit * 3;
|
||||||
|
gap: vars.$spacing-unit * 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import * as styles from "./hero.css";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { TrendingGame } from "@types";
|
import type { TrendingGame } from "@types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
import "./hero.scss";
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
const [featuredGameDetails, setFeaturedGameDetails] = useState<
|
const [featuredGameDetails, setFeaturedGameDetails] = useState<
|
||||||
@@ -29,7 +29,7 @@ export function Hero() {
|
|||||||
}, [i18n.language]);
|
}, [i18n.language]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Skeleton className={styles.hero} />;
|
return <Skeleton className="hero" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featuredGameDetails?.length) {
|
if (featuredGameDetails?.length) {
|
||||||
@@ -37,17 +37,17 @@ export function Hero() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(game.uri)}
|
onClick={() => navigate(game.uri)}
|
||||||
className={styles.hero}
|
className="hero"
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className="hero__backdrop">
|
||||||
<img
|
<img
|
||||||
src={game.background}
|
src={game.background}
|
||||||
alt={game.description}
|
alt={game.description}
|
||||||
className={styles.heroMedia}
|
className="hero__media"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className="hero__content">
|
||||||
{game.logo && (
|
{game.logo && (
|
||||||
<img
|
<img
|
||||||
src={game.logo}
|
src={game.logo}
|
||||||
@@ -56,7 +56,7 @@ export function Hero() {
|
|||||||
loading="eager"
|
loading="eager"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className={styles.description}>{game.description}</p>
|
<p className="hero__description">{game.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
|
|
||||||
export const link = style({
|
|
||||||
textDecoration: "none",
|
|
||||||
color: "#C0C1C7",
|
|
||||||
":hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
10
src/renderer/src/components/link/link.scss
Normal file
10
src/renderer/src/components/link/link.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: vars.$muted-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
|
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import * as styles from "./link.css";
|
import "./link.scss";
|
||||||
|
|
||||||
export function Link({ children, to, className, ...props }: LinkProps) {
|
export function Link({ children, to, className, ...props }: LinkProps) {
|
||||||
const openExternal = (event: React.MouseEvent) => {
|
const openExternal = (event: React.MouseEvent) => {
|
||||||
@@ -12,7 +12,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={to}
|
href={to}
|
||||||
className={cn(styles.link, className)}
|
className={cn("link", className)}
|
||||||
onClick={openExternal}
|
onClick={openExternal}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -22,11 +22,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactRouterDomLink
|
<ReactRouterDomLink className={cn("link", className)} to={to} {...props}>
|
||||||
className={cn(styles.link, className)}
|
|
||||||
to={to}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</ReactRouterDomLink>
|
</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/variables" as vars;
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
animation: scale-fade-in 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none
|
||||||
|
running;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 600px;
|
||||||
|
color: vars.$body-color;
|
||||||
|
max-height: 100%;
|
||||||
|
border: solid 1px vars.$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(vars.$spacing-unit * 3) calc(vars.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
padding: calc(vars.$spacing-unit * 2);
|
||||||
|
border-bottom: solid 1px vars.$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: vars.$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 { createPortal } from "react-dom";
|
||||||
import { XIcon } from "@primer/octicons-react";
|
import { XIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./modal.css";
|
import "./modal.scss";
|
||||||
|
|
||||||
import { Backdrop } from "../backdrop/backdrop";
|
import { Backdrop } from "../backdrop/backdrop";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -108,14 +109,17 @@ export function Modal({
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<Backdrop isClosing={isClosing}>
|
<Backdrop isClosing={isClosing}>
|
||||||
<div
|
<div
|
||||||
className={styles.modal({ closing: isClosing, large })}
|
className={cn("modal", {
|
||||||
|
modal__closing: isClosing,
|
||||||
|
modal__large: large,
|
||||||
|
})}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-labelledby={title}
|
aria-labelledby={title}
|
||||||
aria-describedby={description}
|
aria-describedby={description}
|
||||||
ref={modalContentRef}
|
ref={modalContentRef}
|
||||||
data-hydra-dialog
|
data-hydra-dialog
|
||||||
>
|
>
|
||||||
<div className={styles.modalHeader}>
|
<div className="modal__header">
|
||||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
@@ -124,13 +128,13 @@ export function Modal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCloseClick}
|
onClick={handleCloseClick}
|
||||||
className={styles.closeModalButton}
|
className="modal__close-button"
|
||||||
aria-label={t("close")}
|
aria-label={t("close")}
|
||||||
>
|
>
|
||||||
<XIcon className={styles.closeModalButtonIcon} size={24} />
|
<XIcon className="modal__close-button-icon" size={24} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.modalContent}>{children}</div>
|
<div className="modal__content">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</Backdrop>,
|
</Backdrop>,
|
||||||
document.body
|
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/variables" as vars;
|
||||||
|
|
||||||
|
.select-field {
|
||||||
|
display: inline-flex;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid vars.$border-color;
|
||||||
|
height: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__focused {
|
||||||
|
border-color: #dadbe1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__primary {
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dark {
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option {
|
||||||
|
background-color: vars.$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: vars.$body-font-size;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
margin-bottom: vars.$spacing-unit;
|
||||||
|
display: block;
|
||||||
|
color: vars.$body-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useId, useState } from "react";
|
import { useId, useState } from "react";
|
||||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
import "./select-field.scss";
|
||||||
import * as styles from "./select-field.css";
|
import cn from "classnames";
|
||||||
|
|
||||||
export interface SelectProps
|
export interface SelectProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
React.SelectHTMLAttributes<HTMLSelectElement>,
|
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
HTMLSelectElement
|
HTMLSelectElement
|
||||||
> {
|
> {
|
||||||
theme?: NonNullable<RecipeVariants<typeof styles.select>>["theme"];
|
theme?: "primary" | "dark";
|
||||||
label?: string;
|
label?: string;
|
||||||
options?: { key: string; value: string; label: string }[];
|
options?: { key: string; value: string; label: string }[];
|
||||||
}
|
}
|
||||||
@@ -25,16 +25,20 @@ export function SelectField({
|
|||||||
return (
|
return (
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={id} className={styles.label}>
|
<label htmlFor={id} className="select-field__label">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.select({ focused: isFocused, theme })}>
|
<div
|
||||||
|
className={cn("select-field", `select-field--${theme}`, {
|
||||||
|
"select-field__focused": isFocused,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
id={id}
|
id={id}
|
||||||
value={value}
|
value={value}
|
||||||
className={styles.option}
|
className="select-field__option"
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
onChange={onChange}
|
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",
|
|
||||||
});
|
|
||||||
79
src/renderer/src/components/sidebar/sidebar-profile.scss
Normal file
79
src/renderer/src/components/sidebar/sidebar-profile.scss
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.sidebar-profile {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
padding: vars.$spacing-unit vars.$spacing-unit * 2;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
color: vars.$muted-color;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: vars.$spacing-unit vars.$spacing-unit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(vars.$spacing-unit + vars.$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: 14px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__friends-button {
|
||||||
|
color: vars.$muted-color;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
position: relative;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__friends-button-badge {
|
||||||
|
background-color: vars.$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 { useNavigate } from "react-router-dom";
|
||||||
import { PeopleIcon } from "@primer/octicons-react";
|
import { PeopleIcon } from "@primer/octicons-react";
|
||||||
import * as styles from "./sidebar-profile.css";
|
|
||||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { Avatar } from "../avatar/avatar";
|
import { Avatar } from "../avatar/avatar";
|
||||||
import { AuthPage } from "@shared";
|
import { AuthPage } from "@shared";
|
||||||
|
import "./sidebar-profile.scss";
|
||||||
|
|
||||||
const LONG_POLLING_INTERVAL = 120_000;
|
const LONG_POLLING_INTERVAL = 120_000;
|
||||||
|
|
||||||
@@ -50,14 +50,14 @@ export function SidebarProfile() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.friendsButton}
|
className="sidebar-profile__friends-button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||||
}
|
}
|
||||||
title={t("friends")}
|
title={t("friends")}
|
||||||
>
|
>
|
||||||
{friendRequestCount > 0 && (
|
{friendRequestCount > 0 && (
|
||||||
<small className={styles.friendsButtonBadge}>
|
<small className="sidebar-profile__friends-button-badge">
|
||||||
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
@@ -85,21 +85,21 @@ export function SidebarProfile() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.profileContainer}>
|
<div className="sidebar-profile">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.profileButton}
|
className="sidebar-profile__button"
|
||||||
onClick={handleProfileClick}
|
onClick={handleProfileClick}
|
||||||
>
|
>
|
||||||
<div className={styles.profileButtonContent}>
|
<div className="sidebar-profile__button-content">
|
||||||
<Avatar
|
<Avatar
|
||||||
size={35}
|
size={35}
|
||||||
src={userDetails?.profileImageUrl}
|
src={userDetails?.profileImageUrl}
|
||||||
alt={userDetails?.displayName}
|
alt={userDetails?.displayName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.profileButtonInformation}>
|
<div className="sidebar-profile__button-information">
|
||||||
<p className={styles.profileButtonTitle}>
|
<p className="sidebar-profile__button-title">
|
||||||
{userDetails ? userDetails.displayName : t("sign_in")}
|
{userDetails ? userDetails.displayName : t("sign_in")}
|
||||||
</p>
|
</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/variables" as vars;
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
color: vars.$muted-color;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
transition: opacity ease 0.2s;
|
||||||
|
border-right: solid 1px vars.$border-color;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: vars.$spacing-unit;
|
||||||
|
|
||||||
|
&__resizing {
|
||||||
|
opacity: vars.$active-opacity;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__darwin {
|
||||||
|
padding-top: calc(vars.$spacing-unit * 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: calc(vars.$spacing-unit * 2);
|
||||||
|
gap: calc(vars.$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(vars.$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: vars.$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: vars.$disabled-opacity;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-item-button {
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px vars.$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(vars.$spacing-unit * 2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-button {
|
||||||
|
color: vars.$muted-color;
|
||||||
|
padding: vars.$spacing-unit calc(vars.$spacing-unit * 2);
|
||||||
|
gap: 9px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-top: solid 1px vars.$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 { routes } from "./routes";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import "./sidebar.scss";
|
||||||
|
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
|
import cn from "classnames";
|
||||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
@@ -168,9 +170,9 @@ export function Sidebar() {
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
className={styles.sidebar({
|
className={cn("sidebar", {
|
||||||
resizing: isResizing,
|
sidebar__resizing: isResizing,
|
||||||
darwin: window.electron.platform === "darwin",
|
sidebar__darwin: window.electron.platform === "darwin",
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
width: sidebarWidth,
|
width: sidebarWidth,
|
||||||
@@ -183,19 +185,19 @@ export function Sidebar() {
|
|||||||
>
|
>
|
||||||
<SidebarProfile />
|
<SidebarProfile />
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className="sidebar__content">
|
||||||
<section className={styles.section}>
|
<section className="sidebar__section">
|
||||||
<ul className={styles.menu}>
|
<ul className="sidebar__menu">
|
||||||
{routes.map(({ nameKey, path, render }) => (
|
{routes.map(({ nameKey, path, render }) => (
|
||||||
<li
|
<li
|
||||||
key={nameKey}
|
key={nameKey}
|
||||||
className={styles.menuItem({
|
className={cn("sidebar__menu-item", {
|
||||||
active: location.pathname === path,
|
"sidebar__menu-item--active": location.pathname === path,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.menuItemButton}
|
className="sidebar__menu-item-button"
|
||||||
onClick={() => handleSidebarItemClick(path)}
|
onClick={() => handleSidebarItemClick(path)}
|
||||||
>
|
>
|
||||||
{render()}
|
{render()}
|
||||||
@@ -206,8 +208,8 @@ export function Sidebar() {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className="sidebar__section">
|
||||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
<small className="sidebar__section-title">{t("my_library")}</small>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
ref={filterRef}
|
ref={filterRef}
|
||||||
@@ -216,34 +218,34 @@ export function Sidebar() {
|
|||||||
theme="dark"
|
theme="dark"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ul className={styles.menu}>
|
<ul className="sidebar__menu">
|
||||||
{filteredLibrary.map((game) => (
|
{filteredLibrary.map((game) => (
|
||||||
<li
|
<li
|
||||||
key={game.id}
|
key={game.id}
|
||||||
className={styles.menuItem({
|
className={cn("sidebar__menu-item", {
|
||||||
active:
|
"sidebar__menu-item--active":
|
||||||
location.pathname ===
|
location.pathname ===
|
||||||
`/game/${game.shop}/${game.objectID}`,
|
`/game/${game.shop}/${game.objectID}`,
|
||||||
muted: game.status === "removed",
|
"sidebar__menu-item--muted": game.status === "removed",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.menuItemButton}
|
className="sidebar__menu-item-button"
|
||||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||||
>
|
>
|
||||||
{game.iconUrl ? (
|
{game.iconUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.gameIcon}
|
className="sidebar__game-icon"
|
||||||
src={game.iconUrl}
|
src={game.iconUrl}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SteamLogo className={styles.gameIcon} />
|
<SteamLogo className="sidebar__game-icon" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className={styles.menuItemButtonLabel}>
|
<span className="sidebar__menu-item-button-label">
|
||||||
{getGameTitle(game)}
|
{getGameTitle(game)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -257,10 +259,10 @@ export function Sidebar() {
|
|||||||
{hasActiveSubscription && (
|
{hasActiveSubscription && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.helpButton}
|
className="sidebar__help-button"
|
||||||
data-open-support-chat
|
data-open-support-chat
|
||||||
>
|
>
|
||||||
<div className={styles.helpButtonIcon}>
|
<div className="sidebar__help-button-icon">
|
||||||
<CommentDiscussionIcon size={14} />
|
<CommentDiscussionIcon size={14} />
|
||||||
</div>
|
</div>
|
||||||
<span>{t("need_help")}</span>
|
<span>{t("need_help")}</span>
|
||||||
@@ -269,7 +271,7 @@ export function Sidebar() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.handle}
|
className="sidebar__handle"
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</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,
|
|
||||||
});
|
|
||||||
75
src/renderer/src/components/text-field/text-field.scss
Normal file
75
src/renderer/src/components/text-field/text-field.scss
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.text-field-container {
|
||||||
|
flex: 1;
|
||||||
|
gap: vars.$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 vars.$border-color;
|
||||||
|
height: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
|
||||||
|
&__primary {
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dark {
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__has-error {
|
||||||
|
border-color: vars.$danger-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--focused {
|
||||||
|
border-color: vars.$search-border-color-focused;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(&--focused):hover {
|
||||||
|
border-color: vars.$search-border-color-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
color: vars.$search-input-color;
|
||||||
|
cursor: default;
|
||||||
|
font-family: inherit;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: vars.$spacing-unit;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__read-only {
|
||||||
|
text-overflow: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle-password-button {
|
||||||
|
cursor: pointer;
|
||||||
|
color: vars.$muted-color;
|
||||||
|
padding: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-label {
|
||||||
|
color: vars.$danger-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import React, { useId, useMemo, useState } from "react";
|
import React, { useId, useMemo, useState } from "react";
|
||||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
|
||||||
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
import * as styles from "./text-field.css";
|
import "./text-field.scss";
|
||||||
|
|
||||||
export interface TextFieldProps
|
export interface TextFieldProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
|
theme?: "primary" | "dark";
|
||||||
label?: string | React.ReactNode;
|
label?: string | React.ReactNode;
|
||||||
hint?: string | React.ReactNode;
|
hint?: string | React.ReactNode;
|
||||||
textFieldProps?: React.DetailedHTMLProps<
|
textFieldProps?: React.DetailedHTMLProps<
|
||||||
@@ -41,9 +41,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
) => {
|
) => {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation("forms");
|
const { t } = useTranslation("forms");
|
||||||
|
|
||||||
const showPasswordToggleButton = props.type === "password";
|
const showPasswordToggleButton = props.type === "password";
|
||||||
@@ -54,7 +52,12 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
}, [props.type, isPasswordVisible]);
|
}, [props.type, isPasswordVisible]);
|
||||||
|
|
||||||
const hintContent = useMemo(() => {
|
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__error-label">
|
||||||
|
{error.message as string}
|
||||||
|
</small>
|
||||||
|
);
|
||||||
|
|
||||||
if (hint) return <small>{hint}</small>;
|
if (hint) return <small>{hint}</small>;
|
||||||
return null;
|
return null;
|
||||||
@@ -73,22 +76,23 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
const hasError = !!error;
|
const hasError = !!error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.textFieldContainer} {...containerProps}>
|
<div className="text-field-container" {...containerProps}>
|
||||||
{label && <label htmlFor={id}>{label}</label>}
|
{label && <label htmlFor={id}>{label}</label>}
|
||||||
|
|
||||||
<div className={styles.textFieldWrapper}>
|
<div className="text-field__wrapper">
|
||||||
<div
|
<div
|
||||||
className={styles.textField({
|
className={cn("text-field", `text-field__${theme}`, {
|
||||||
theme,
|
"text-field__has-error": hasError,
|
||||||
hasError,
|
"text-field--focused": isFocused,
|
||||||
focused: isFocused,
|
|
||||||
})}
|
})}
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={id}
|
id={id}
|
||||||
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
className={cn("text-field__input", {
|
||||||
|
"text-field__input__read-only": props.readOnly,
|
||||||
|
})}
|
||||||
{...props}
|
{...props}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
@@ -98,7 +102,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
{showPasswordToggleButton && (
|
{showPasswordToggleButton && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.togglePasswordButton}
|
className="text-field__toggle-password-button"
|
||||||
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||||
aria-label={t("toggle_password_visibility")}
|
aria-label={t("toggle_password_visibility")}
|
||||||
>
|
>
|
||||||
@@ -120,4 +124,4 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
TextField.displayName = "TextField";
|
TextField.displayName = "TextField";
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
92
src/renderer/src/components/toast/toast.scss
Normal file
92
src/renderer/src/components/toast/toast.scss
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
0% {
|
||||||
|
transform: translateY(96px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(96px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
max-height: 80px;
|
||||||
|
position: fixed;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: solid 1px vars.$border-color;
|
||||||
|
right: vars.$spacing-unit * 2;
|
||||||
|
bottom: 42px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
z-index: vars.$toast-z-index;
|
||||||
|
max-width: 500px;
|
||||||
|
|
||||||
|
&--closing {
|
||||||
|
animation-name: slideOut;
|
||||||
|
transform: translateY(96px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--opening {
|
||||||
|
animation-name: slideIn;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
gap: vars.$spacing-unit * 2;
|
||||||
|
padding: vars.$spacing-unit * 2;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 5px;
|
||||||
|
|
||||||
|
&::-webkit-progress-bar {
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: vars.$muted-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button {
|
||||||
|
color: vars.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon-container {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__success-icon {
|
||||||
|
color: vars.$success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-icon {
|
||||||
|
color: vars.$danger-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__warning-icon {
|
||||||
|
color: vars.$warning-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
} from "@primer/octicons-react";
|
} 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 {
|
export interface ToastProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -77,22 +77,28 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
|||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.toast({ closing: isClosing })}>
|
<div
|
||||||
<div className={styles.toastContent}>
|
className={cn("toast", {
|
||||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
toast__closing: isClosing,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="toast__content">
|
||||||
|
<div className="toast__icon-container">
|
||||||
{type === "success" && (
|
{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>
|
<span style={{ fontWeight: "bold" }}>{message}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.closeButton}
|
className="toast__close-button"
|
||||||
onClick={startAnimateClosing}
|
onClick={startAnimateClosing}
|
||||||
aria-label="Close toast"
|
aria-label="Close toast"
|
||||||
>
|
>
|
||||||
@@ -100,7 +106,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<progress className={styles.progress} value={progress} max={100} />
|
<progress className="toast__progress" value={progress} max={100} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/renderer/src/declaration.d.ts
vendored
3
src/renderer/src/declaration.d.ts
vendored
@@ -260,6 +260,9 @@ declare global {
|
|||||||
|
|
||||||
/* Notifications */
|
/* Notifications */
|
||||||
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
openEditorWindow: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const Downloads = React.lazy(() => import("./pages/downloads/downloads"));
|
|||||||
const Settings = React.lazy(() => import("./pages/settings/settings"));
|
const Settings = React.lazy(() => import("./pages/settings/settings"));
|
||||||
const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue"));
|
const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue"));
|
||||||
const Profile = React.lazy(() => import("./pages/profile/profile"));
|
const Profile = React.lazy(() => import("./pages/profile/profile"));
|
||||||
|
const Editor = React.lazy(() => import("./pages/editor/editor"));
|
||||||
const Achievements = React.lazy(
|
const Achievements = React.lazy(
|
||||||
() => import("./pages/achievements/achievements")
|
() => import("./pages/achievements/achievements")
|
||||||
);
|
);
|
||||||
@@ -104,6 +105,11 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
element={<SuspenseWrapper Component={Achievements} />}
|
element={<SuspenseWrapper Component={Achievements} />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/editor"
|
||||||
|
element={<SuspenseWrapper Component={Editor} />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useDate } from "@renderer/hooks";
|
import { useDate } from "@renderer/hooks";
|
||||||
import type { UserAchievement } from "@types";
|
import type { UserAchievement } from "@types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./achievements.css";
|
|
||||||
import { EyeClosedIcon } from "@primer/octicons-react";
|
import { EyeClosedIcon } from "@primer/octicons-react";
|
||||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import { vars } from "@renderer/theme.css";
|
import classNames from "classnames";
|
||||||
|
import "./achievements.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
interface AchievementListProps {
|
interface AchievementListProps {
|
||||||
achievements: UserAchievement[];
|
achievements: UserAchievement[];
|
||||||
@@ -17,16 +18,16 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
|||||||
const { formatDateTime } = useDate();
|
const { formatDateTime } = useDate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={styles.list}>
|
<ul className="achievements__list">
|
||||||
{achievements.map((achievement) => (
|
{achievements.map((achievement) => (
|
||||||
<li
|
<li
|
||||||
key={achievement.name}
|
key={achievement.name}
|
||||||
className={styles.listItem}
|
className="achievements__list-item"
|
||||||
style={{ display: "flex" }}
|
style={{ display: "flex" }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className={styles.listItemImage({
|
className={classNames("achievements__list-item-image", {
|
||||||
unlocked: achievement.unlocked,
|
"achievements__list-item-image--unlocked": achievement.unlocked,
|
||||||
})}
|
})}
|
||||||
src={achievement.icon}
|
src={achievement.icon}
|
||||||
alt={achievement.displayName}
|
alt={achievement.displayName}
|
||||||
@@ -66,7 +67,7 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "4px",
|
gap: "4px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: vars.color.warning,
|
color: "var(--warning-color)",
|
||||||
}}
|
}}
|
||||||
title={t("achievement_earn_points", {
|
title={t("achievement_earn_points", {
|
||||||
points: "???",
|
points: "???",
|
||||||
|
|||||||
@@ -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",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
66
src/renderer/src/pages/achievements/achievement-panel.scss
Normal file
66
src/renderer/src/pages/achievements/achievement-panel.scss
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.achievement-panel {
|
||||||
|
width: 100%;
|
||||||
|
padding: #{vars.$spacing-unit * 2} #{vars.$spacing-unit * 3};
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: solid 1px vars.$border-color;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__download-details-row {
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
color: vars.$body-color;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__downloads-link {
|
||||||
|
color: vars.$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: vars.$muted-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
opacity: vars.$disabled-opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__link {
|
||||||
|
text-align: start;
|
||||||
|
color: vars.$body-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
|||||||
import { UserAchievement } from "@types";
|
import { UserAchievement } from "@types";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
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 {
|
export interface AchievementPanelProps {
|
||||||
achievements: UserAchievement[];
|
achievements: UserAchievement[];
|
||||||
@@ -28,17 +28,17 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
|||||||
|
|
||||||
if (!hasActiveSubscription) {
|
if (!hasActiveSubscription) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className="achievement-panel">
|
||||||
<div className={styles.content}>
|
<div className="achievement-panel__content">
|
||||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||||
??? / ???
|
??? / ???
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showHydraCloudModal("achievements-points")}
|
onClick={() => showHydraCloudModal("achievements-points")}
|
||||||
className={styles.link}
|
className="achievement-panel__link"
|
||||||
>
|
>
|
||||||
<small style={{ color: vars.color.warning }}>
|
<small style={{ color: "#ffc107" }}>
|
||||||
{t("how_to_earn_achievements_points")}
|
{t("how_to_earn_achievements_points")}
|
||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
@@ -47,8 +47,8 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className="achievement-panel">
|
||||||
<div className={styles.content}>
|
<div className="achievement-panel__content">
|
||||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||||
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,18 +8,19 @@ import {
|
|||||||
formatDownloadProgress,
|
formatDownloadProgress,
|
||||||
} from "@renderer/helpers";
|
} from "@renderer/helpers";
|
||||||
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import type { ComparedAchievements } from "@types";
|
import type { ComparedAchievements } from "@types";
|
||||||
import { average } from "color.js";
|
import { average } from "color.js";
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
import { ComparedAchievementList } from "./compared-achievement-list";
|
import { ComparedAchievementList } from "./compared-achievement-list";
|
||||||
import * as styles from "./achievements.css";
|
|
||||||
import { AchievementList } from "./achievement-list";
|
import { AchievementList } from "./achievement-list";
|
||||||
import { AchievementPanel } from "./achievement-panel";
|
import { AchievementPanel } from "./achievement-panel";
|
||||||
import { ComparedAchievementPanel } from "./compared-achievement-panel";
|
import { ComparedAchievementPanel } from "./compared-achievement-panel";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import "./achievements.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -48,10 +49,10 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
|||||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.profileAvatar}>
|
<div className="achievements__profile-avatar">
|
||||||
{user.profileImageUrl ? (
|
{user.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.profileAvatar}
|
className="achievements__profile-avatar"
|
||||||
src={user.profileImageUrl}
|
src={user.profileImageUrl}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
/>
|
/>
|
||||||
@@ -64,97 +65,38 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
|||||||
|
|
||||||
if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) {
|
if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="achievements__summary achievements__summary--locked">
|
||||||
style={{
|
<div className="achievements__summary-overlay">
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
position: "relative",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: 2,
|
|
||||||
inset: 0,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
background: "rgba(0, 0, 0, 0.7)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
borderRadius: "4px",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LockIcon size={24} />
|
<LockIcon size={24} />
|
||||||
<h3>
|
<h3>
|
||||||
<button
|
<button
|
||||||
className={styles.subscriptionRequiredButton}
|
className="achievements__subscription-required-button"
|
||||||
onClick={() => showHydraCloudModal("achievements")}
|
onClick={() => showHydraCloudModal("achievements")}
|
||||||
>
|
>
|
||||||
{t("subscription_needed")}
|
{t("subscription_needed")}
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="achievements__summary-content achievements__summary-content--blurred">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
height: "62px",
|
|
||||||
position: "relative",
|
|
||||||
filter: "blur(4px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getProfileImage(user)}
|
{getProfileImage(user)}
|
||||||
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
|
<h1 className="achievements__summary-title">{user.displayName}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="achievements__summary">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getProfileImage(user)}
|
{getProfileImage(user)}
|
||||||
<div
|
<div className="achievements__summary-details">
|
||||||
style={{
|
<h1 className="achievements__summary-title">{user.displayName}</h1>
|
||||||
display: "flex",
|
<div className="achievements__summary-stats">
|
||||||
flexDirection: "column",
|
<div className="achievements__summary-count">
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: 8,
|
|
||||||
color: vars.color.muted,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrophyIcon size={13} />
|
<TrophyIcon size={13} />
|
||||||
<span>
|
<span>
|
||||||
{user.unlockedAchievementCount} / {user.totalAchievementCount}
|
{user.unlockedAchievementCount} / {user.totalAchievementCount}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{formatDownloadProgress(
|
{formatDownloadProgress(
|
||||||
user.unlockedAchievementCount / user.totalAchievementCount
|
user.unlockedAchievementCount / user.totalAchievementCount
|
||||||
@@ -164,7 +106,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
|||||||
<progress
|
<progress
|
||||||
max={1}
|
max={1}
|
||||||
value={user.unlockedAchievementCount / user.totalAchievementCount}
|
value={user.unlockedAchievementCount / user.totalAchievementCount}
|
||||||
className={styles.achievementsProgressBar}
|
className="achievements__progress-bar"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,9 +143,10 @@ export function AchievementsContent({
|
|||||||
|
|
||||||
setGameColor(backgroundColor);
|
setGameColor(backgroundColor);
|
||||||
};
|
};
|
||||||
|
const HERO_HEIGHT = 150;
|
||||||
|
|
||||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
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 scrollY = (event.target as HTMLDivElement).scrollTop;
|
||||||
if (scrollY >= heroHeight && !isHeaderStuck) {
|
if (scrollY >= heroHeight && !isHeaderStuck) {
|
||||||
@@ -219,10 +162,10 @@ export function AchievementsContent({
|
|||||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.profileAvatarSmall}>
|
<div className="achievements__profile-avatar-small">
|
||||||
{user.profileImageUrl ? (
|
{user.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.profileAvatarSmall}
|
className="achievements__profile-avatar-small"
|
||||||
src={user.profileImageUrl}
|
src={user.profileImageUrl}
|
||||||
alt={user.displayName}
|
alt={user.displayName}
|
||||||
/>
|
/>
|
||||||
@@ -236,10 +179,10 @@ export function AchievementsContent({
|
|||||||
if (!objectId || !shop || !gameTitle || !userDetails) return null;
|
if (!objectId || !shop || !gameTitle || !userDetails) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className="achievements__wrapper">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(objectId)}
|
src={steamUrlBuilder.libraryHero(objectId)}
|
||||||
style={{ display: "none" }}
|
className="achievements__hidden-image"
|
||||||
alt={gameTitle}
|
alt={gameTitle}
|
||||||
onLoad={handleHeroLoad}
|
onLoad={handleHeroLoad}
|
||||||
/>
|
/>
|
||||||
@@ -247,38 +190,29 @@ export function AchievementsContent({
|
|||||||
<section
|
<section
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
className={styles.container}
|
className="achievements__container"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
className="achievements__gradient-background"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
background: `linear-gradient(0deg, #1c1c1c 0%, ${gameColor} 100%)`,
|
||||||
flexDirection: "column",
|
|
||||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 0%, ${gameColor} 100%)`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={heroRef} className={styles.hero}>
|
<div ref={heroRef} className="achievements__hero">
|
||||||
<div className={styles.heroContent}>
|
<div className="achievements__hero-content">
|
||||||
<Link
|
<Link
|
||||||
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
|
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.logo(objectId)}
|
src={steamUrlBuilder.logo(objectId)}
|
||||||
className={styles.gameLogo}
|
className="achievements__game-logo"
|
||||||
alt={gameTitle}
|
alt={gameTitle}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="achievements__summary-container">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
width: "100%",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AchievementSummary
|
<AchievementSummary
|
||||||
user={{
|
user={{
|
||||||
...userDetails,
|
...userDetails,
|
||||||
@@ -298,24 +232,24 @@ export function AchievementsContent({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{otherUser && (
|
{otherUser && (
|
||||||
<div className={styles.tableHeader({ stuck: isHeaderStuck })}>
|
<div
|
||||||
|
className={classNames("achievements__table-header", {
|
||||||
|
"achievements__table-header--stuck": isHeaderStuck,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
className={classNames("achievements__grid-container", {
|
||||||
display: "grid",
|
"achievements__grid-container--no-subscription":
|
||||||
gridTemplateColumns: hasActiveSubscription
|
!hasActiveSubscription,
|
||||||
? "3fr 1fr 1fr"
|
})}
|
||||||
: "3fr 2fr",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 3}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div></div>
|
<div></div>
|
||||||
{hasActiveSubscription && (
|
{hasActiveSubscription && (
|
||||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
<div className="achievements__profile-center">
|
||||||
{getProfileImage({ ...userDetails })}
|
{getProfileImage({ ...userDetails })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
<div className="achievements__profile-center">
|
||||||
{getProfileImage(otherUser)}
|
{getProfileImage(otherUser)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import * as styles from "./achievements.css";
|
import "./achievements.scss";
|
||||||
|
|
||||||
export function AchievementsSkeleton() {
|
export function AchievementsSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className="achievements__container">
|
||||||
<div className={styles.hero}>
|
<div className="achievements__hero">
|
||||||
<Skeleton className={styles.heroImageSkeleton} />
|
<Skeleton className="achievements__hero-image-skeleton" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.heroPanelSkeleton}></div>
|
<div className="achievements__hero-panel-skeleton"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
|
||||||
import { style } from "@vanilla-extract/css";
|
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
export const HERO_HEIGHT = 150;
|
|
||||||
const LOGO_HEIGHT = 100;
|
|
||||||
const LOGO_MAX_WIDTH = 200;
|
|
||||||
|
|
||||||
export const wrapper = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
overflow: "hidden",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
transition: "all ease 0.3s",
|
|
||||||
});
|
|
||||||
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const heroContent = style({
|
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const gameLogo = style({
|
|
||||||
width: LOGO_MAX_WIDTH,
|
|
||||||
height: LOGO_HEIGHT,
|
|
||||||
objectFit: "contain",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
":hover": {
|
|
||||||
transform: "scale(1.05)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const container = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
overflow: "auto",
|
|
||||||
zIndex: "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tableHeader = recipe({
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
backgroundColor: vars.color.darkBackground,
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
|
||||||
position: "sticky",
|
|
||||||
top: "0",
|
|
||||||
zIndex: "1",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
stuck: {
|
|
||||||
true: {
|
|
||||||
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.8)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const list = style({
|
|
||||||
listStyle: "none",
|
|
||||||
margin: "0",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
|
||||||
width: "100%",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const listItem = style({
|
|
||||||
transition: "all ease 0.1s",
|
|
||||||
color: vars.color.muted,
|
|
||||||
width: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: "4px",
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
alignItems: "center",
|
|
||||||
textAlign: "left",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
||||||
textDecoration: "none",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const listItemImage = recipe({
|
|
||||||
base: {
|
|
||||||
width: "54px",
|
|
||||||
height: "54px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
objectFit: "cover",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
unlocked: {
|
|
||||||
false: {
|
|
||||||
filter: "grayscale(100%)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const achievementsProgressBar = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "8px",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
"::-webkit-progress-bar": {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
},
|
|
||||||
"::-webkit-progress-value": {
|
|
||||||
backgroundColor: vars.color.muted,
|
|
||||||
borderRadius: "4px",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const heroLogoBackdrop = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
position: "absolute",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const heroImageSkeleton = style({
|
|
||||||
height: "150px",
|
|
||||||
});
|
|
||||||
|
|
||||||
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}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const listItemSkeleton = style({
|
|
||||||
width: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
borderRadius: "4px",
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileAvatar = style({
|
|
||||||
height: "54px",
|
|
||||||
width: "54px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
position: "relative",
|
|
||||||
objectFit: "cover",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const profileAvatarSmall = style({
|
|
||||||
height: "32px",
|
|
||||||
width: "32px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
position: "relative",
|
|
||||||
objectFit: "cover",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const subscriptionRequiredButton = style({
|
|
||||||
textDecoration: "none",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: "100%",
|
|
||||||
gap: `${SPACING_UNIT / 2}px`,
|
|
||||||
color: vars.color.body,
|
|
||||||
cursor: "pointer",
|
|
||||||
":hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
188
src/renderer/src/pages/achievements/achievements.scss
Normal file
188
src/renderer/src/pages/achievements/achievements.scss
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.achievements {
|
||||||
|
&__wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero {
|
||||||
|
width: 100%;
|
||||||
|
height: vars.$hero-sub-height;
|
||||||
|
min-height: vars.$hero-sub-height;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-content {
|
||||||
|
padding: #{vars.$spacing-unit * 2};
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-logo {
|
||||||
|
width: vars.$logo-max-width;
|
||||||
|
height: vars.$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: vars.$dark-background-color;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
border-bottom: solid 1px vars.$border-color;
|
||||||
|
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: #{vars.$spacing-unit * 2};
|
||||||
|
padding: #{vars.$spacing-unit * 2};
|
||||||
|
width: 100%;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-item {
|
||||||
|
transition: all ease 0.1s;
|
||||||
|
color: vars.$muted-color;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: vars.$spacing-unit;
|
||||||
|
gap: #{vars.$spacing-unit * 2};
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-item-image {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
&--unlocked {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__achievements-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: vars.$muted-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-logo-backdrop {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-image-skeleton {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-panel-skeleton {
|
||||||
|
width: 100%;
|
||||||
|
padding: #{vars.$spacing-unit * 2};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
height: 72px;
|
||||||
|
border-bottom: solid 1px vars.$border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-item-skeleton {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: vars.$spacing-unit;
|
||||||
|
gap: #{vars.$spacing-unit * 2};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__profile-avatar {
|
||||||
|
height: 54px;
|
||||||
|
width: 54px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
position: relative;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__profile-avatar-small {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
position: relative;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subscription-required-button {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: calc(vars.$spacing-unit / 2);
|
||||||
|
color: vars.$body-color;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ import { useAppDispatch, useUserDetails } from "@renderer/hooks";
|
|||||||
import type { ComparedAchievements, GameShop } from "@types";
|
import type { ComparedAchievements, GameShop } from "@types";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
|
import "./achievements.scss";
|
||||||
import {
|
import {
|
||||||
GameDetailsContextConsumer,
|
GameDetailsContextConsumer,
|
||||||
GameDetailsContextProvider,
|
GameDetailsContextProvider,
|
||||||
@@ -75,10 +76,7 @@ export default function Achievements() {
|
|||||||
(otherUserId && comparedAchievements === null);
|
(otherUserId && comparedAchievements === null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme
|
<SkeletonTheme baseColor="var(--background-color)" highlightColor="#444">
|
||||||
baseColor={vars.color.background}
|
|
||||||
highlightColor="#444"
|
|
||||||
>
|
|
||||||
{showSkeleton ? (
|
{showSkeleton ? (
|
||||||
<AchievementsSkeleton />
|
<AchievementsSkeleton />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import type { ComparedAchievements } from "@types";
|
import type { ComparedAchievements } from "@types";
|
||||||
import * as styles from "./achievements.css";
|
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
EyeClosedIcon,
|
EyeClosedIcon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { useDate } from "@renderer/hooks";
|
import { useDate } from "@renderer/hooks";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import "./achievements.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
export interface ComparedAchievementListProps {
|
export interface ComparedAchievementListProps {
|
||||||
achievements: ComparedAchievements;
|
achievements: ComparedAchievements;
|
||||||
@@ -20,11 +21,11 @@ export function ComparedAchievementList({
|
|||||||
const { formatDateTime } = useDate();
|
const { formatDateTime } = useDate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={styles.list}>
|
<ul className="achievements__list">
|
||||||
{achievements.achievements.map((achievement, index) => (
|
{achievements.achievements.map((achievement, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className={styles.listItem}
|
className="achievements__list-item"
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: achievement.ownerStat
|
gridTemplateColumns: achievement.ownerStat
|
||||||
@@ -37,12 +38,14 @@ export function ComparedAchievementList({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `var(--spacing-unit)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className={styles.listItemImage({
|
className={classNames("achievements__list-item-image", {
|
||||||
unlocked: true,
|
"achievements__list-item-image--unlocked":
|
||||||
|
achievement.ownerStat?.unlocked ||
|
||||||
|
achievement.targetStat.unlocked,
|
||||||
})}
|
})}
|
||||||
src={achievement.icon}
|
src={achievement.icon}
|
||||||
alt={achievement.displayName}
|
alt={achievement.displayName}
|
||||||
@@ -71,7 +74,7 @@ export function ComparedAchievementList({
|
|||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `var(--spacing-unit)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
title={formatDateTime(achievement.ownerStat.unlockTime!)}
|
title={formatDateTime(achievement.ownerStat.unlockTime!)}
|
||||||
@@ -82,7 +85,7 @@ export function ComparedAchievementList({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
padding: `${SPACING_UNIT}px`,
|
padding: `var(--spacing-unit)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -97,7 +100,7 @@ export function ComparedAchievementList({
|
|||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `var(--spacing-unit)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
title={formatDateTime(achievement.targetStat.unlockTime!)}
|
title={formatDateTime(achievement.targetStat.unlockTime!)}
|
||||||
@@ -108,7 +111,7 @@ export function ComparedAchievementList({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
padding: `${SPACING_UNIT}px`,
|
padding: `var(--spacing-unit)`,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as styles from "./achievement-panel.css";
|
|
||||||
|
|
||||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||||
import { ComparedAchievements } from "@types";
|
import { ComparedAchievements } from "@types";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import "./achievement-panel.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
export interface ComparedAchievementPanelProps {
|
export interface ComparedAchievementPanelProps {
|
||||||
achievements: ComparedAchievements;
|
achievements: ComparedAchievements;
|
||||||
@@ -18,24 +20,24 @@ export function ComparedAchievementPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.panel}
|
className={classNames("achievement-panel", {
|
||||||
style={{
|
"achievement-panel--subscribed": hasActiveSubscription,
|
||||||
display: "grid",
|
})}
|
||||||
gridTemplateColumns: hasActiveSubscription ? "3fr 1fr 1fr" : "3fr 2fr",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
<div className="achievement-panel__points">
|
||||||
{t("available_points")} <HydraIcon width={20} height={20} />{" "}
|
{t("available_points")}
|
||||||
|
<HydraIcon width={20} height={20} />
|
||||||
{achievements.achievementsPointsTotal}
|
{achievements.achievementsPointsTotal}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasActiveSubscription && (
|
{hasActiveSubscription && (
|
||||||
<div className={styles.content}>
|
<div className="achievement-panel__content">
|
||||||
<HydraIcon width={20} height={20} />
|
<HydraIcon width={20} height={20} />
|
||||||
{achievements.owner.achievementsPointsEarnedSum ?? 0}
|
{achievements.owner.achievementsPointsEarnedSum ?? 0}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.content}>
|
|
||||||
|
<div className="achievement-panel__content">
|
||||||
<HydraIcon width={20} height={20} />
|
<HydraIcon width={20} height={20} />
|
||||||
{achievements.target.achievementsPointsEarnedSum}
|
{achievements.target.achievementsPointsEarnedSum}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
@use "../../scss/globals.scss";
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
.catalogue {
|
.catalogue {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(vars.$spacing-unit * 2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
@@ -13,10 +13,16 @@
|
|||||||
width: 270px;
|
width: 270px;
|
||||||
min-width: 270px;
|
min-width: 270px;
|
||||||
max-width: 270px;
|
max-width: 270px;
|
||||||
background-color: globals.$dark-background-color;
|
background-color: vars.$dark-background-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid globals.$border-color;
|
border: 1px solid vars.$border-color;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(var(--spacing-unit) * 2);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import "./catalogue.scss";
|
import "./catalogue.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
import { downloadSourcesTable } from "@renderer/dexie";
|
import { downloadSourcesTable } from "@renderer/dexie";
|
||||||
import { FilterSection } from "./filter-section";
|
import { FilterSection } from "./filter-section";
|
||||||
import { setFilters, setPage } from "@renderer/features";
|
import { setFilters, setPage } from "@renderer/features";
|
||||||
@@ -270,13 +270,7 @@ export default function Catalogue() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="catalogue__header">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: SPACING_UNIT * 2,
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -287,8 +281,8 @@ export default function Catalogue() {
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<SkeletonTheme
|
<SkeletonTheme
|
||||||
baseColor={vars.color.darkBackground}
|
baseColor="var(--dark-background-color)"
|
||||||
highlightColor={vars.color.background}
|
highlightColor="var(--background-color)"
|
||||||
>
|
>
|
||||||
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
|
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
@@ -296,7 +290,7 @@ export default function Catalogue() {
|
|||||||
style={{
|
style={{
|
||||||
height: 105,
|
height: 105,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px var(--border-color)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
import { XIcon } from "@primer/octicons-react";
|
import { XIcon } from "@primer/octicons-react";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
interface FilterItemProps {
|
interface FilterItemProps {
|
||||||
filter: string;
|
filter: string;
|
||||||
@@ -13,11 +13,11 @@ export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
color: vars.color.body,
|
color: "var(--body-color)",
|
||||||
backgroundColor: vars.color.darkBackground,
|
backgroundColor: "var(--dark-background-color",
|
||||||
padding: "6px 12px",
|
padding: "6px 12px",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px var(--border-color)`,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -35,7 +35,7 @@ export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
style={{
|
style={{
|
||||||
color: vars.color.body,
|
color: "var(--body-color)",
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useFormat } from "@renderer/hooks";
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
import List from "rc-virtual-list";
|
import List from "rc-virtual-list";
|
||||||
import { vars } from "@renderer/theme.css";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
export interface FilterSectionProps {
|
export interface FilterSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -80,7 +80,7 @@ export function FilterSection({
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
display: "block",
|
display: "block",
|
||||||
color: vars.color.body,
|
color: "var(--body-color)",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
textDecoration: "underline",
|
textDecoration: "underline",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@use "../../scss/globals.scss";
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
.game-item {
|
.game-item {
|
||||||
background-color: globals.$dark-background-color;
|
background-color: vars.$dark-background-color;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid globals.$border-color;
|
border: 1px solid vars.$border-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(vars.$spacing-unit * 2);
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
width: 200px;
|
width: 200px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-right: 1px solid globals.$border-color;
|
border-right: 1px solid vars.$border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__details {
|
&__details {
|
||||||
@@ -30,11 +30,11 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: calc(globals.$spacing-unit * 2) 0;
|
padding: calc(vars.$spacing-unit * 2) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__genres {
|
&__genres {
|
||||||
color: globals.$body-color;
|
color: vars.$body-color;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
&__repackers {
|
&__repackers {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: globals.$spacing-unit;
|
gap: vars.$spacing-unit;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/variables" as vars;
|
||||||
|
|
||||||
|
.delete-game-modal {
|
||||||
|
&__actions-buttons-ctn {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { Button, Modal } from "@renderer/components";
|
import { Button, Modal } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./delete-game-modal.css";
|
import "./delete-game-modal.scss";
|
||||||
|
|
||||||
interface DeleteGameModalProps {
|
interface DeleteGameModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -29,7 +29,7 @@ export function DeleteGameModal({
|
|||||||
description={t("delete_modal_description")}
|
description={t("delete_modal_description")}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className={styles.deleteActionsButtonsCtn}>
|
<div className="delete-game-modal__actions-buttons-ctn">
|
||||||
<Button onClick={handleDeleteGame} theme="outline">
|
<Button onClick={handleDeleteGame} theme="outline">
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</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`,
|
|
||||||
});
|
|
||||||
117
src/renderer/src/pages/downloads/download-group.scss
Normal file
117
src/renderer/src/pages/downloads/download-group.scss
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.download-group {
|
||||||
|
&__title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: vars.$spacing-unit;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: vars.$body-color;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 16px;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__downloads-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: vars.$spacing-unit * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__downloads {
|
||||||
|
width: 100%;
|
||||||
|
gap: #{vars.$spacing-unit * 2};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cover {
|
||||||
|
width: 280px;
|
||||||
|
min-width: 280px;
|
||||||
|
height: auto;
|
||||||
|
border-right: solid 1px vars.$border-color;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cover-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: vars.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cover-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cover-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__download {
|
||||||
|
width: 100%;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: solid 1px vars.$border-color;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0px 0px 5px 0px #000000;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
height: 140px;
|
||||||
|
min-height: 140px;
|
||||||
|
max-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
gap: calc(vars.$spacing-unit / 2);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right-content {
|
||||||
|
display: flex;
|
||||||
|
padding: #{vars.$spacing-unit * 2};
|
||||||
|
flex: 1;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: #{vars.$spacing-unit * 2};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,10 @@ import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
|||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
import { useAppSelector, useDownload } from "@renderer/hooks";
|
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import * as styles from "./download-group.css";
|
import "./download-group.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -238,54 +239,47 @@ export function DownloadGroup({
|
|||||||
if (!library.length) return null;
|
if (!library.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.downloadGroup}>
|
<div className="download-group">
|
||||||
<div
|
<div className="download-group__downloads-group">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: vars.color.border,
|
backgroundColor: "var(--border-color)",
|
||||||
height: "1px",
|
height: "1px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<h3 style={{ fontWeight: "400" }}>{library.length}</h3>
|
<h3 style={{ fontWeight: "400" }}>{library.length}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className={styles.downloads}>
|
<ul className="download-group__downloads">
|
||||||
{library.map((game) => {
|
{library.map((game) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={game.id}
|
key={game.id}
|
||||||
className={styles.download}
|
className="download-group__download"
|
||||||
style={{ position: "relative" }}
|
style={{ position: "relative" }}
|
||||||
>
|
>
|
||||||
<div className={styles.downloadCover}>
|
<div className="download-group__cover">
|
||||||
<div className={styles.downloadCoverBackdrop}>
|
<div className="download-group__cover-backdrop">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectID)}
|
src={steamUrlBuilder.library(game.objectID)}
|
||||||
className={styles.downloadCoverImage}
|
className="download-group__cover-image"
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.downloadCoverContent}>
|
<div className="download-group__cover-content">
|
||||||
<Badge>{DOWNLOADER_NAME[game.downloader]}</Badge>
|
<Badge>{DOWNLOADER_NAME[game.downloader]}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.downloadRightContent}>
|
<div className="download-group__right-content">
|
||||||
<div className={styles.downloadDetails}>
|
<div className="download-group__details">
|
||||||
<div className={styles.downloadTitleWrapper}>
|
<div className="download-group__title-wrapper">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.downloadTitle}
|
className="download-group__title"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(
|
navigate(
|
||||||
buildGameDetailsPath({
|
buildGameDetailsPath({
|
||||||
|
|||||||
@@ -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/variables" as vars;
|
||||||
|
|
||||||
|
.downloads {
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
padding: #{vars.$spacing-unit * 3};
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__groups {
|
||||||
|
display: flex;
|
||||||
|
gap: #{vars.$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: #{vars.$spacing-unit * 2};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-downloads {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,15 @@ import { useDownload, useLibrary } from "@renderer/hooks";
|
|||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||||
import * as styles from "./downloads.css";
|
|
||||||
import { DeleteGameModal } from "./delete-game-modal";
|
import { DeleteGameModal } from "./delete-game-modal";
|
||||||
import { DownloadGroup } from "./download-group";
|
import { DownloadGroup } from "./download-group";
|
||||||
import type { LibraryGame, SeedingStatus } from "@types";
|
import type { LibraryGame, SeedingStatus } from "@types";
|
||||||
import { orderBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import "./downloads.scss";
|
||||||
|
|
||||||
export default function Downloads() {
|
export default function Downloads() {
|
||||||
const { library, updateLibrary } = useLibrary();
|
const { library, updateLibrary } = useLibrary();
|
||||||
|
|
||||||
@@ -121,8 +123,8 @@ export default function Downloads() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{hasItemsInLibrary ? (
|
{hasItemsInLibrary ? (
|
||||||
<section className={styles.downloadsContainer}>
|
<section className="downloads__container">
|
||||||
<div className={styles.downloadGroups}>
|
<div className="downloads__groups">
|
||||||
{downloadGroups.map((group) => (
|
{downloadGroups.map((group) => (
|
||||||
<DownloadGroup
|
<DownloadGroup
|
||||||
key={group.title}
|
key={group.title}
|
||||||
@@ -136,8 +138,8 @@ export default function Downloads() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.noDownloads}>
|
<div className="downloads__no-downloads">
|
||||||
<div className={styles.arrowIcon}>
|
<div className="downloads__arrow-icon">
|
||||||
<ArrowDownIcon size={24} />
|
<ArrowDownIcon size={24} />
|
||||||
</div>
|
</div>
|
||||||
<h2>{t("no_downloads_title")}</h2>
|
<h2>{t("no_downloads_title")}</h2>
|
||||||
|
|||||||
18
src/renderer/src/pages/editor/editor.scss
Normal file
18
src/renderer/src/pages/editor/editor.scss
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
padding: 0 calc(vars.$spacing-unit * 2);
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header-title {
|
||||||
|
font-size: 7px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: vars.$body-color;
|
||||||
|
}
|
||||||
34
src/renderer/src/pages/editor/editor.tsx
Normal file
34
src/renderer/src/pages/editor/editor.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
import "./editor.scss";
|
||||||
|
import "../../scss/_variables.scss";
|
||||||
|
|
||||||
|
export default function Editor() {
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("spectre");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SkeletonTheme baseColor="var(--background-color)" highlightColor="#444">
|
||||||
|
<div className="editor-header">
|
||||||
|
<div className="editor-header-title">
|
||||||
|
<h1>CSS Editor</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="editor"
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>spectre</p>
|
||||||
|
</div>
|
||||||
|
</SkeletonTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,27 @@
|
|||||||
|
@use "../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.cloud-sync-files-modal {
|
||||||
|
&__mapping-methods {
|
||||||
|
display: grid;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
margin-top: #{vars.$spacing-unit * 2};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file-item {
|
||||||
|
flex: 1;
|
||||||
|
color: vars.$muted-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CheckCircleFillIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
import { CheckCircleFillIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import * as styles from "./cloud-sync-files-modal.css";
|
|
||||||
import { formatBytes } from "@shared";
|
import { formatBytes } from "@shared";
|
||||||
import { useToast } from "@renderer/hooks";
|
import { useToast } from "@renderer/hooks";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -99,7 +98,7 @@ export function CloudSyncFilesModal({
|
|||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
<span style={{ marginBottom: 8 }}>{t("mapping_method_label")}</span>
|
<span style={{ marginBottom: 8 }}>{t("mapping_method_label")}</span>
|
||||||
|
|
||||||
<div className={styles.mappingMethods}>
|
<div className="clound-sync-files-modal__mapping-methods">
|
||||||
{Object.values(FileMappingMethod).map((mappingMethod) => (
|
{Object.values(FileMappingMethod).map((mappingMethod) => (
|
||||||
<Button
|
<Button
|
||||||
key={mappingMethod}
|
key={mappingMethod}
|
||||||
@@ -142,11 +141,11 @@ export function CloudSyncFilesModal({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ul className={styles.fileList}>
|
<ul className="cloud-sync-files-modal__files-list">
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<li key={file.path} style={{ display: "flex" }}>
|
<li key={file.path} style={{ display: "flex" }}>
|
||||||
<button
|
<button
|
||||||
className={styles.fileItem}
|
className="cloud-sync-files-modal__file-item"
|
||||||
onClick={() => window.electron.showItemInFolder(file.path)}
|
onClick={() => window.electron.showItemInFolder(file.path)}
|
||||||
>
|
>
|
||||||
{file.path.split("/").at(-1)}
|
{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,70 @@
|
|||||||
|
@use "../../../scss/variables" as vars;
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud-sync-modal {
|
||||||
|
&__artifacts {
|
||||||
|
display: flex;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
flex-direction: column;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__artifact-button {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
color: vars.$body-color;
|
||||||
|
padding: #{vars.$spacing-unit * 2};
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
border: 1px solid vars.$border-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sync-icon {
|
||||||
|
animation-name: rotate;
|
||||||
|
animation-duration: 1s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 5px;
|
||||||
|
|
||||||
|
&::-webkit-progress-bar {
|
||||||
|
background-color: vars.$dark-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-progress-value {
|
||||||
|
background-color: vars.$muted-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__manage-files-button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: vars.$body-color;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: vars.$disabled-opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import { Button, Modal, ModalProps } from "@renderer/components";
|
|||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
import * as styles from "./cloud-sync-modal.css";
|
|
||||||
import { formatBytes } from "@shared";
|
import { formatBytes } from "@shared";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +17,9 @@ import { useAppSelector, useToast } from "@renderer/hooks";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AxiosProgressEvent } from "axios";
|
import { AxiosProgressEvent } from "axios";
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
|
import "./cloud-sync-modal.scss";
|
||||||
|
import "../../../scss/_variables.scss";
|
||||||
|
|
||||||
export interface CloudSyncModalProps
|
export interface CloudSyncModalProps
|
||||||
extends Omit<ModalProps, "children" | "title"> {}
|
extends Omit<ModalProps, "children" | "title"> {}
|
||||||
@@ -95,7 +96,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
if (uploadingBackup) {
|
if (uploadingBackup) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SyncIcon className={styles.syncIcon} />
|
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||||
{t("uploading_backup")}
|
{t("uploading_backup")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -104,7 +105,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
if (restoringBackup) {
|
if (restoringBackup) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SyncIcon className={styles.syncIcon} />
|
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||||
{t("restoring_backup", {
|
{t("restoring_backup", {
|
||||||
progress: formatDownloadProgress(
|
progress: formatDownloadProgress(
|
||||||
backupDownloadProgress?.progress ?? 0
|
backupDownloadProgress?.progress ?? 0
|
||||||
@@ -117,7 +118,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
if (loadingPreview) {
|
if (loadingPreview) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SyncIcon className={styles.syncIcon} />
|
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||||
{t("loading_save_preview")}
|
{t("loading_save_preview")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -171,7 +172,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.manageFilesButton}
|
className="clound-sync-modal__manage-files-button"
|
||||||
onClick={() => setShowCloudSyncFilesModal(true)}
|
onClick={() => setShowCloudSyncFilesModal(true)}
|
||||||
disabled={disableActions}
|
disabled={disableActions}
|
||||||
>
|
>
|
||||||
@@ -199,7 +200,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: SPACING_UNIT,
|
gap: "var(--spacing-unit)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2>{t("backups")}</h2>
|
<h2>{t("backups")}</h2>
|
||||||
@@ -210,9 +211,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{artifacts.length > 0 ? (
|
{artifacts.length > 0 ? (
|
||||||
<ul className={styles.artifacts}>
|
<ul className="clound-sync-modal__artifacts">
|
||||||
{artifacts.map((artifact) => (
|
{artifacts.map((artifact) => (
|
||||||
<li key={artifact.id} className={styles.artifactButton}>
|
<li key={artifact.id} className="cloud-sync-modal__artifact-button">
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -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/variables" as vars;
|
||||||
|
|
||||||
|
.description-header {
|
||||||
|
width: 100%;
|
||||||
|
padding: #{vars.$spacing-unit * 2};
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: vars.$background-color;
|
||||||
|
height: 72px;
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
gap: vars.$spacing-unit;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./description-header.css";
|
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
@@ -12,8 +11,8 @@ export function DescriptionHeader() {
|
|||||||
if (!shopDetails) return null;
|
if (!shopDetails) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.descriptionHeader}>
|
<div className="description-header">
|
||||||
<section className={styles.descriptionHeaderInfo}>
|
<section className="description-header__info">
|
||||||
<p>
|
<p>
|
||||||
{t("release_date", {
|
{t("release_date", {
|
||||||
date: shopDetails?.release_date.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,123 @@
|
|||||||
|
@use "../../../scss/variables" as vars;
|
||||||
|
|
||||||
|
.gallery-slider {
|
||||||
|
&__container {
|
||||||
|
padding: #{vars.$spacing-unit * 3} #{vars.$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: vars.$spacing-unit 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
gap: calc(vars.$spacing-unit / 2);
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__media-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 vars.$border-color;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__media-preview {
|
||||||
|
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: vars.$muted-color;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--left {
|
||||||
|
left: 0;
|
||||||
|
margin-left: vars.$spacing-unit;
|
||||||
|
transform: translateX(-#{48 + vars.$spacing-unit}px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--right {
|
||||||
|
right: 0;
|
||||||
|
margin-right: vars.$spacing-unit;
|
||||||
|
transform: translateX(#{48 + vars.$spacing-unit}px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--visible {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import * as styles from "./gallery-slider.css";
|
import "./gallery-slider.scss";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
|
||||||
|
const getButtonClasses = (visible: boolean, direction: "left" | "right") => {
|
||||||
|
return classNames("gallery-slider__button", {
|
||||||
|
"gallery-slider__button--visible": visible,
|
||||||
|
[`gallery-slider__button--${direction}`]: direction,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviewClasses = (isActive: boolean) => {
|
||||||
|
return classNames("gallery-slider__media-preview-button", {
|
||||||
|
"gallery-slider__media-preview-button--active": isActive,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export function GallerySlider() {
|
export function GallerySlider() {
|
||||||
const { shopDetails } = useContext(gameDetailsContext);
|
const { shopDetails } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
@@ -97,11 +111,11 @@ export function GallerySlider() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasScreenshots && (
|
{hasScreenshots && (
|
||||||
<div className={styles.gallerySliderContainer}>
|
<div className="gallery-slider__container">
|
||||||
<div
|
<div
|
||||||
onMouseEnter={() => setShowArrows(true)}
|
onMouseEnter={() => setShowArrows(true)}
|
||||||
onMouseLeave={() => setShowArrows(false)}
|
onMouseLeave={() => setShowArrows(false)}
|
||||||
className={styles.gallerySliderAnimationContainer}
|
className="gallery-slider__animation-container"
|
||||||
ref={mediaContainerRef}
|
ref={mediaContainerRef}
|
||||||
>
|
>
|
||||||
{shopDetails.movies &&
|
{shopDetails.movies &&
|
||||||
@@ -109,7 +123,7 @@ export function GallerySlider() {
|
|||||||
<video
|
<video
|
||||||
key={video.id}
|
key={video.id}
|
||||||
controls
|
controls
|
||||||
className={styles.gallerySliderMedia}
|
className="gallery-slider__media"
|
||||||
poster={video.thumbnail}
|
poster={video.thumbnail}
|
||||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||||
loop
|
loop
|
||||||
@@ -124,7 +138,7 @@ export function GallerySlider() {
|
|||||||
shopDetails.screenshots?.map((image, i) => (
|
shopDetails.screenshots?.map((image, i) => (
|
||||||
<img
|
<img
|
||||||
key={image.id}
|
key={image.id}
|
||||||
className={styles.gallerySliderMedia}
|
className="gallery-slider__media"
|
||||||
src={image.path_full}
|
src={image.path_full}
|
||||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||||
alt={t("screenshot", { number: i + 1 })}
|
alt={t("screenshot", { number: i + 1 })}
|
||||||
@@ -135,10 +149,7 @@ export function GallerySlider() {
|
|||||||
<button
|
<button
|
||||||
onClick={showPrevImage}
|
onClick={showPrevImage}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.gallerySliderButton({
|
className={getButtonClasses(showArrows, "left")}
|
||||||
visible: showArrows,
|
|
||||||
direction: "left",
|
|
||||||
})}
|
|
||||||
aria-label={t("previous_screenshot")}
|
aria-label={t("previous_screenshot")}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@@ -148,10 +159,7 @@ export function GallerySlider() {
|
|||||||
<button
|
<button
|
||||||
onClick={showNextImage}
|
onClick={showNextImage}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.gallerySliderButton({
|
className={getButtonClasses(showArrows, "right")}
|
||||||
visible: showArrows,
|
|
||||||
direction: "right",
|
|
||||||
})}
|
|
||||||
aria-label={t("next_screenshot")}
|
aria-label={t("next_screenshot")}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@@ -159,20 +167,18 @@ export function GallerySlider() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gallerySliderPreview} ref={scrollContainerRef}>
|
<div className="gallery-slider__preview" ref={scrollContainerRef}>
|
||||||
{previews.map((media, i) => (
|
{previews.map((media, i) => (
|
||||||
<button
|
<button
|
||||||
key={media.id}
|
key={media.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.mediaPreviewButton({
|
className={getPreviewClasses(mediaIndex === i)}
|
||||||
active: mediaIndex === i,
|
|
||||||
})}
|
|
||||||
onClick={() => setMediaIndex(i)}
|
onClick={() => setMediaIndex(i)}
|
||||||
aria-label={t("open_screenshot", { number: i + 1 })}
|
aria-label={t("open_screenshot", { number: i + 1 })}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={media.thumbnail}
|
src={media.thumbnail}
|
||||||
className={styles.mediaPreview}
|
className="gallery-slider__media-preview"
|
||||||
alt={t("screenshot", { number: i + 1 })}
|
alt={t("screenshot", { number: i + 1 })}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { DescriptionHeader } from "./description-header/description-header";
|
|||||||
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||||
import { Sidebar } from "./sidebar/sidebar";
|
import { Sidebar } from "./sidebar/sidebar";
|
||||||
|
|
||||||
import * as styles from "./game-details.css";
|
import "./game-details.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
import { AuthPage, steamUrlBuilder } from "@shared";
|
import { AuthPage, steamUrlBuilder } from "@shared";
|
||||||
@@ -25,14 +25,8 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const {
|
const { objectId, shopDetails, game, gameColor, setGameColor } =
|
||||||
objectId,
|
useContext(gameDetailsContext);
|
||||||
shopDetails,
|
|
||||||
game,
|
|
||||||
gameColor,
|
|
||||||
setGameColor,
|
|
||||||
hasNSFWContentBlocked,
|
|
||||||
} = useContext(gameDetailsContext);
|
|
||||||
|
|
||||||
const { showHydraCloudModal } = useSubscription();
|
const { showHydraCloudModal } = useSubscription();
|
||||||
|
|
||||||
@@ -78,9 +72,10 @@ export function GameDetailsContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBackdropOpacity(1);
|
setBackdropOpacity(1);
|
||||||
}, [objectId]);
|
}, [objectId]);
|
||||||
|
const HERO_HEIGHT = 150;
|
||||||
|
|
||||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
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 scrollY = (event.target as HTMLDivElement).scrollTop;
|
||||||
const opacity = Math.max(
|
const opacity = Math.max(
|
||||||
@@ -118,10 +113,10 @@ export function GameDetailsContent() {
|
|||||||
}, [getGameArtifacts]);
|
}, [getGameArtifacts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
|
<div className="game-details__blurred-content">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(objectId!)}
|
src={steamUrlBuilder.libraryHero(objectId!)}
|
||||||
className={styles.heroImage}
|
className="game-details__hero-image"
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
onLoad={handleHeroLoad}
|
onLoad={handleHeroLoad}
|
||||||
/>
|
/>
|
||||||
@@ -129,9 +124,9 @@ export function GameDetailsContent() {
|
|||||||
<section
|
<section
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
className={styles.container}
|
className="game-details__container"
|
||||||
>
|
>
|
||||||
<div ref={heroRef} className={styles.hero}>
|
<div ref={heroRef} className="game-details__hero">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: gameColor,
|
backgroundColor: gameColor,
|
||||||
@@ -141,19 +136,19 @@ export function GameDetailsContent() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles.heroLogoBackdrop}
|
className="game-details__hero-logo-backdrop"
|
||||||
style={{ opacity: backdropOpactiy }}
|
style={{ opacity: backdropOpactiy }}
|
||||||
>
|
>
|
||||||
<div className={styles.heroContent}>
|
<div className="game-details__hero-content">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.logo(objectId!)}
|
src={steamUrlBuilder.logo(objectId!)}
|
||||||
className={styles.gameLogo}
|
className="game-details__game-logo"
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.cloudSyncButton}
|
className="game-details__cloud-sync-button"
|
||||||
onClick={handleCloudSaveButtonClick}
|
onClick={handleCloudSaveButtonClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -180,8 +175,8 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
<HeroPanel isHeaderStuck={isHeaderStuck} />
|
<HeroPanel isHeaderStuck={isHeaderStuck} />
|
||||||
|
|
||||||
<div className={styles.descriptionContainer}>
|
<div className="game-details__description-container">
|
||||||
<div className={styles.descriptionContent}>
|
<div className="game-details__description-content">
|
||||||
<DescriptionHeader />
|
<DescriptionHeader />
|
||||||
<GallerySlider />
|
<GallerySlider />
|
||||||
|
|
||||||
@@ -189,7 +184,7 @@ export function GameDetailsContent() {
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: aboutTheGame,
|
__html: aboutTheGame,
|
||||||
}}
|
}}
|
||||||
className={styles.description}
|
className="game-details__description"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import Skeleton from "react-loading-skeleton";
|
|||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./game-details.css";
|
import "./game-details.scss";
|
||||||
import * as sidebarStyles from "./sidebar/sidebar.css";
|
|
||||||
import * as descriptionHeaderStyles from "./description-header/description-header.css";
|
import "./sidebar/sidebar.scss";
|
||||||
|
|
||||||
|
import "./description-header/description-header.scss";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -12,54 +14,54 @@ export function GameDetailsSkeleton() {
|
|||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className="game-details_container">
|
||||||
<div className={styles.hero}>
|
<div className="game-details_hero">
|
||||||
<Skeleton className={styles.heroImageSkeleton} />
|
<Skeleton className="game-details__hero-image-skeleton" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.heroPanelSkeleton}>
|
<div className="game-details__hero-panel-skeleton">
|
||||||
<section className={descriptionHeaderStyles.descriptionHeaderInfo}>
|
<section className="description-header">
|
||||||
<Skeleton width={155} />
|
<Skeleton width={155} />
|
||||||
<Skeleton width={135} />
|
<Skeleton width={135} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.descriptionContainer}>
|
<div className="game-details__description-container">
|
||||||
<div className={styles.descriptionContent}>
|
<div className="game-details__description-content">
|
||||||
<div className={descriptionHeaderStyles.descriptionHeader}>
|
<div className="description-header">
|
||||||
<section className={descriptionHeaderStyles.descriptionHeaderInfo}>
|
<section className="description-header__info">
|
||||||
<Skeleton width={145} />
|
<Skeleton width={145} />
|
||||||
<Skeleton width={150} />
|
<Skeleton width={150} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.descriptionSkeleton}>
|
<div className="game-details__description-skeleton">
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<Skeleton key={index} />
|
<Skeleton key={index} />
|
||||||
))}
|
))}
|
||||||
<Skeleton className={styles.heroImageSkeleton} />
|
<Skeleton className="game-details__hero-image-skeleton" />
|
||||||
{Array.from({ length: 2 }).map((_, index) => (
|
{Array.from({ length: 2 }).map((_, index) => (
|
||||||
<Skeleton key={index} />
|
<Skeleton key={index} />
|
||||||
))}
|
))}
|
||||||
<Skeleton className={styles.heroImageSkeleton} />
|
<Skeleton className="game-details__hero-image-skeleton" />
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={sidebarStyles.contentSidebar}>
|
<div className="sidebar__content-sidebar">
|
||||||
<div className={sidebarStyles.requirementButtonContainer}>
|
<div className="sidebar__requirements-button-container">
|
||||||
<Button
|
<Button
|
||||||
className={sidebarStyles.requirementButton}
|
className="sidebar__requirement-button"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
{t("minimum")}
|
{t("minimum")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={sidebarStyles.requirementButton}
|
className="sidebar__requirement-button"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
{t("recommended")}
|
{t("recommended")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={sidebarStyles.requirementsDetailsSkeleton}>
|
<div className="sidebar__requirements-details-skeleton">
|
||||||
{Array.from({ length: 6 }).map((_, index) => (
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
<Skeleton key={index} height={20} />
|
<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)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user