mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge branch 'feature/cloud-sync' into feature/game-achievements
# Conflicts: # src/locales/en/translation.json # src/locales/pt-BR/translation.json # src/main/events/library/add-game-to-library.ts # src/renderer/src/pages/game-details/sidebar/sidebar.css.ts # src/renderer/src/pages/game-details/sidebar/sidebar.tsx
This commit is contained in:
commit
e93088e8b9
103 changed files with 3548 additions and 584 deletions
|
@ -26,6 +26,10 @@ globalStyle("::-webkit-scrollbar-thumb", {
|
|||
borderRadius: "24px",
|
||||
});
|
||||
|
||||
globalStyle("::-webkit-scrollbar-thumb:hover", {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.16)",
|
||||
});
|
||||
|
||||
globalStyle("html, body, #root, main", {
|
||||
height: "100%",
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||
import { downloadSourcesWorker } from "./workers";
|
||||
import { repacksContext } from "./context";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -231,6 +232,8 @@ export function App() {
|
|||
}
|
||||
|
||||
for (const downloadSource of downloadSources) {
|
||||
logger.info("Migrating download source", downloadSource.url);
|
||||
|
||||
const channel = new BroadcastChannel(
|
||||
`download_sources:import:${downloadSource.url}`
|
||||
);
|
||||
|
@ -243,6 +246,10 @@ export function App() {
|
|||
channel.onmessage = () => {
|
||||
window.electron.deleteDownloadSource(downloadSource.id).then(() => {
|
||||
resolve(true);
|
||||
logger.info(
|
||||
"Deleted download source from SQLite",
|
||||
downloadSource.url
|
||||
);
|
||||
});
|
||||
|
||||
indexRepacks();
|
||||
|
|
725
src/renderer/src/assets/lottie/cloud.json
Normal file
725
src/renderer/src/assets/lottie/cloud.json
Normal file
|
@ -0,0 +1,725 @@
|
|||
{
|
||||
"v": "5.12.1",
|
||||
"fr": 30,
|
||||
"ip": 0,
|
||||
"op": 60,
|
||||
"w": 400,
|
||||
"h": 400,
|
||||
"nm": "Cloud",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "Layer 6",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [322.789, 202.565, 0],
|
||||
"to": [-1.5, -0.167, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 30,
|
||||
"s": [313.789, 201.565, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [-1.5, -0.167, 0]
|
||||
},
|
||||
{ "t": 60, "s": [322.789, 202.565, 0] }
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, -38.564],
|
||||
[38.564, 0],
|
||||
[0, 38.564],
|
||||
[-38.564, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 38.564],
|
||||
[-38.564, 0],
|
||||
[0, -38.564],
|
||||
[38.564, 0]
|
||||
],
|
||||
"v": [
|
||||
[69.827, 0],
|
||||
[0, 69.827],
|
||||
[-69.827, 0],
|
||||
[0, -69.827]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||
"ix": 4
|
||||
},
|
||||
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 270,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 3,
|
||||
"ty": 4,
|
||||
"nm": "Layer 5",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [243.704, 202.565, 0],
|
||||
"to": [-1.667, 0, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 30,
|
||||
"s": [233.704, 202.565, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [-1.667, 0, 0]
|
||||
},
|
||||
{ "t": 60, "s": [243.704, 202.565, 0] }
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, -38.564],
|
||||
[38.564, 0],
|
||||
[0, 38.564],
|
||||
[-38.564, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 38.564],
|
||||
[-38.564, 0],
|
||||
[0, -38.564],
|
||||
[38.564, 0]
|
||||
],
|
||||
"v": [
|
||||
[69.827, 0],
|
||||
[0, 69.827],
|
||||
[-69.827, 0],
|
||||
[0, -69.827]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||
"ix": 4
|
||||
},
|
||||
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 270,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 4,
|
||||
"ty": 4,
|
||||
"nm": "Layer 4",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [260.681, 151.053, 0],
|
||||
"to": [1.333, -1.333, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 30,
|
||||
"s": [268.681, 143.053, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [1.333, -1.333, 0]
|
||||
},
|
||||
{ "t": 60, "s": [260.681, 151.053, 0] }
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, -38.564],
|
||||
[38.564, 0],
|
||||
[0, 38.564],
|
||||
[-38.564, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 38.564],
|
||||
[-38.564, 0],
|
||||
[0, -38.564],
|
||||
[38.564, 0]
|
||||
],
|
||||
"v": [
|
||||
[69.827, 0],
|
||||
[0, 69.827],
|
||||
[-69.827, 0],
|
||||
[0, -69.827]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||
"ix": 4
|
||||
},
|
||||
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 270,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 5,
|
||||
"ty": 4,
|
||||
"nm": "Layer 3",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [162.135, 206.563, 0],
|
||||
"to": [-0.833, -0.167, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 30,
|
||||
"s": [157.135, 205.563, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [-0.833, -0.167, 0]
|
||||
},
|
||||
{ "t": 60, "s": [162.135, 206.563, 0] }
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, -36.66],
|
||||
[36.66, 0],
|
||||
[0, 36.66],
|
||||
[-36.66, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 36.66],
|
||||
[-36.66, 0],
|
||||
[0, -36.66],
|
||||
[36.66, 0]
|
||||
],
|
||||
"v": [
|
||||
[66.378, 0],
|
||||
[0, 66.378],
|
||||
[-66.378, 0],
|
||||
[0, -66.378]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||
"ix": 4
|
||||
},
|
||||
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 270,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 6,
|
||||
"ty": 4,
|
||||
"nm": "Layer 2",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [180.178, 132.225, 0],
|
||||
"to": [-0.5, -2.333, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 30,
|
||||
"s": [177.178, 118.225, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [-0.5, -2.333, 0]
|
||||
},
|
||||
{ "t": 60, "s": [180.178, 132.225, 0] }
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, -50.068],
|
||||
[50.068, 0],
|
||||
[0, 50.068],
|
||||
[-50.068, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 50.068],
|
||||
[-50.068, 0],
|
||||
[0, -50.068],
|
||||
[50.068, 0]
|
||||
],
|
||||
"v": [
|
||||
[90.655, 0],
|
||||
[0, 90.655],
|
||||
[-90.655, 0],
|
||||
[0, -90.655]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||
"ix": 4
|
||||
},
|
||||
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 270,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 7,
|
||||
"ty": 4,
|
||||
"nm": "Layer 1",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 0,
|
||||
"s": [95.756, 208.288, 0],
|
||||
"to": [-1.167, 0, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": { "x": 0.667, "y": 1 },
|
||||
"o": { "x": 0.333, "y": 0 },
|
||||
"t": 30,
|
||||
"s": [88.756, 208.288, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [-1.167, 0, 0]
|
||||
},
|
||||
{ "t": 60, "s": [95.756, 208.288, 0] }
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, -35.403],
|
||||
[35.403, 0],
|
||||
[0, 35.403],
|
||||
[-35.403, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 35.403],
|
||||
[-35.403, 0],
|
||||
[0, -35.403],
|
||||
[35.403, 0]
|
||||
],
|
||||
"v": [
|
||||
[64.103, 0],
|
||||
[0, 64.103],
|
||||
[-64.103, 0],
|
||||
[0, -64.103]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "Path 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "fl",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||
"ix": 4
|
||||
},
|
||||
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||
"r": 1,
|
||||
"bm": 0,
|
||||
"nm": "Fill 1",
|
||||
"mn": "ADBE Vector Graphic - Fill",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||
"nm": "Transform"
|
||||
}
|
||||
],
|
||||
"nm": "Group 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 270,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 8,
|
||||
"ty": 3,
|
||||
"nm": "Null 1",
|
||||
"parent": 6,
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": { "a": 0, "k": 0, "ix": 11 },
|
||||
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||
"p": { "a": 0, "k": [19.822, 67.775, 0], "ix": 2, "l": 2 },
|
||||
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||
},
|
||||
"ao": 0,
|
||||
"ip": 0,
|
||||
"op": 270,
|
||||
"st": 0,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": [],
|
||||
"props": {}
|
||||
}
|
|
@ -44,7 +44,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
|||
|
||||
const handleHover = useCallback(() => {
|
||||
if (!stats) {
|
||||
window.electron.getGameStats(game.objectID, game.shop).then((stats) => {
|
||||
window.electron.getGameStats(game.objectId, game.shop).then((stats) => {
|
||||
setStats(stats);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -140,7 +140,10 @@ export function Sidebar() {
|
|||
event: React.MouseEvent,
|
||||
game: LibraryGame
|
||||
) => {
|
||||
const path = buildGameDetailsPath(game);
|
||||
const path = buildGameDetailsPath({
|
||||
...game,
|
||||
objectId: game.objectID,
|
||||
});
|
||||
if (path !== location.pathname) {
|
||||
navigate(path);
|
||||
}
|
||||
|
|
213
src/renderer/src/context/cloud-sync/cloud-sync.context.tsx
Normal file
213
src/renderer/src/context/cloud-sync/cloud-sync.context.tsx
Normal file
|
@ -0,0 +1,213 @@
|
|||
import { gameBackupsTable } from "@renderer/dexie";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { logger } from "@renderer/logger";
|
||||
import type { LudusaviBackup, GameArtifact, GameShop } from "@types";
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export enum CloudSyncState {
|
||||
New,
|
||||
Different,
|
||||
Same,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
export interface CloudSyncContext {
|
||||
backupPreview: LudusaviBackup | null;
|
||||
artifacts: GameArtifact[];
|
||||
showCloudSyncModal: boolean;
|
||||
showCloudSyncFilesModal: boolean;
|
||||
supportsCloudSync: boolean | null;
|
||||
backupState: CloudSyncState;
|
||||
setShowCloudSyncModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
downloadGameArtifact: (gameArtifactId: string) => Promise<void>;
|
||||
uploadSaveGame: () => Promise<void>;
|
||||
deleteGameArtifact: (gameArtifactId: string) => Promise<void>;
|
||||
setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
restoringBackup: boolean;
|
||||
uploadingBackup: boolean;
|
||||
}
|
||||
|
||||
export const cloudSyncContext = createContext<CloudSyncContext>({
|
||||
backupPreview: null,
|
||||
showCloudSyncModal: false,
|
||||
supportsCloudSync: null,
|
||||
backupState: CloudSyncState.Unknown,
|
||||
setShowCloudSyncModal: () => {},
|
||||
downloadGameArtifact: async () => {},
|
||||
uploadSaveGame: async () => {},
|
||||
artifacts: [],
|
||||
deleteGameArtifact: async () => {},
|
||||
showCloudSyncFilesModal: false,
|
||||
setShowCloudSyncFilesModal: () => {},
|
||||
restoringBackup: false,
|
||||
uploadingBackup: false,
|
||||
});
|
||||
|
||||
const { Provider } = cloudSyncContext;
|
||||
export const { Consumer: CloudSyncContextConsumer } = cloudSyncContext;
|
||||
|
||||
export interface CloudSyncContextProviderProps {
|
||||
children: React.ReactNode;
|
||||
objectId: string;
|
||||
shop: GameShop;
|
||||
}
|
||||
|
||||
export function CloudSyncContextProvider({
|
||||
children,
|
||||
objectId,
|
||||
shop,
|
||||
}: CloudSyncContextProviderProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [supportsCloudSync, setSupportsCloudSync] = useState<boolean | null>(
|
||||
null
|
||||
);
|
||||
const [artifacts, setArtifacts] = useState<GameArtifact[]>([]);
|
||||
const [showCloudSyncModal, setShowCloudSyncModal] = useState(false);
|
||||
const [backupPreview, setBackupPreview] = useState<LudusaviBackup | null>(
|
||||
null
|
||||
);
|
||||
const [restoringBackup, setRestoringBackup] = useState(false);
|
||||
const [uploadingBackup, setUploadingBackup] = useState(false);
|
||||
const [showCloudSyncFilesModal, setShowCloudSyncFilesModal] = useState(false);
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const downloadGameArtifact = useCallback(
|
||||
async (gameArtifactId: string) => {
|
||||
setRestoringBackup(true);
|
||||
window.electron.downloadGameArtifact(objectId, shop, gameArtifactId);
|
||||
},
|
||||
[objectId, shop]
|
||||
);
|
||||
|
||||
const getGameBackupPreview = useCallback(async () => {
|
||||
window.electron.getGameArtifacts(objectId, shop).then((results) => {
|
||||
setArtifacts(results);
|
||||
});
|
||||
|
||||
window.electron
|
||||
.getGameBackupPreview(objectId, shop)
|
||||
.then((preview) => {
|
||||
logger.info("Game backup preview", objectId, shop, preview);
|
||||
if (preview && Object.keys(preview.games).length) {
|
||||
setBackupPreview(preview);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to get game backup preview", objectId, shop, err);
|
||||
});
|
||||
}, [objectId, shop]);
|
||||
|
||||
const uploadSaveGame = useCallback(async () => {
|
||||
setUploadingBackup(true);
|
||||
window.electron.uploadSaveGame(objectId, shop);
|
||||
}, [objectId, shop]);
|
||||
|
||||
useEffect(() => {
|
||||
const removeUploadCompleteListener = window.electron.onUploadComplete(
|
||||
objectId,
|
||||
shop,
|
||||
() => {
|
||||
showSuccessToast(t("backup_uploaded"));
|
||||
|
||||
setUploadingBackup(false);
|
||||
gameBackupsTable.add({
|
||||
objectId,
|
||||
shop,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
getGameBackupPreview();
|
||||
}
|
||||
);
|
||||
|
||||
const removeDownloadCompleteListener =
|
||||
window.electron.onBackupDownloadComplete(objectId, shop, () => {
|
||||
showSuccessToast(t("backup_restored"));
|
||||
|
||||
setRestoringBackup(false);
|
||||
getGameBackupPreview();
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeUploadCompleteListener();
|
||||
removeDownloadCompleteListener();
|
||||
};
|
||||
}, [objectId, shop, showSuccessToast, t, getGameBackupPreview]);
|
||||
|
||||
const deleteGameArtifact = useCallback(
|
||||
async (gameArtifactId: string) => {
|
||||
return window.electron.deleteGameArtifact(gameArtifactId).then(() => {
|
||||
getGameBackupPreview();
|
||||
});
|
||||
},
|
||||
[getGameBackupPreview]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron
|
||||
.checkGameCloudSyncSupport(objectId, shop)
|
||||
.then((result) => {
|
||||
logger.info("Cloud sync support", objectId, shop, result);
|
||||
setSupportsCloudSync(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to check cloud sync support", err);
|
||||
});
|
||||
}, [objectId, shop, getGameBackupPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
setBackupPreview(null);
|
||||
setArtifacts([]);
|
||||
setSupportsCloudSync(null);
|
||||
setShowCloudSyncModal(false);
|
||||
setRestoringBackup(false);
|
||||
setUploadingBackup(false);
|
||||
}, [objectId, shop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCloudSyncModal) {
|
||||
getGameBackupPreview();
|
||||
}
|
||||
}, [getGameBackupPreview, showCloudSyncModal]);
|
||||
|
||||
const backupState = useMemo(() => {
|
||||
if (!backupPreview) return CloudSyncState.Unknown;
|
||||
if (backupPreview.overall.changedGames.new) return CloudSyncState.New;
|
||||
if (backupPreview.overall.changedGames.different)
|
||||
return CloudSyncState.Different;
|
||||
if (backupPreview.overall.changedGames.same) return CloudSyncState.Same;
|
||||
|
||||
return CloudSyncState.Unknown;
|
||||
}, [backupPreview]);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
supportsCloudSync,
|
||||
backupPreview,
|
||||
showCloudSyncModal,
|
||||
artifacts,
|
||||
backupState,
|
||||
restoringBackup,
|
||||
uploadingBackup,
|
||||
showCloudSyncFilesModal,
|
||||
setShowCloudSyncModal,
|
||||
uploadSaveGame,
|
||||
downloadGameArtifact,
|
||||
deleteGameArtifact,
|
||||
setShowCloudSyncFilesModal,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,6 @@ import {
|
|||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { getSteamLanguage } from "@renderer/helpers";
|
||||
|
@ -33,7 +32,7 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
|||
gameTitle: "",
|
||||
isGameRunning: false,
|
||||
isLoading: false,
|
||||
objectID: undefined,
|
||||
objectId: undefined,
|
||||
gameColor: "",
|
||||
showRepacksModal: false,
|
||||
showGameOptionsModal: false,
|
||||
|
@ -53,13 +52,17 @@ export const { Consumer: GameDetailsContextConsumer } = gameDetailsContext;
|
|||
|
||||
export interface GameDetailsContextProps {
|
||||
children: React.ReactNode;
|
||||
objectId: string;
|
||||
gameTitle: string;
|
||||
shop: GameShop;
|
||||
}
|
||||
|
||||
export function GameDetailsContextProvider({
|
||||
children,
|
||||
objectId,
|
||||
gameTitle,
|
||||
shop,
|
||||
}: GameDetailsContextProps) {
|
||||
const { objectID, shop } = useParams();
|
||||
|
||||
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
||||
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
|
@ -75,10 +78,6 @@ export function GameDetailsContextProvider({
|
|||
|
||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const gameTitle = searchParams.get("title")!;
|
||||
|
||||
const { searchRepacks, isIndexingRepacks } = useContext(repacksContext);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -101,9 +100,9 @@ export function GameDetailsContextProvider({
|
|||
|
||||
const updateGame = useCallback(async () => {
|
||||
return window.electron
|
||||
.getGameByObjectID(objectID!)
|
||||
.getGameByObjectId(objectId!)
|
||||
.then((result) => setGame(result));
|
||||
}, [setGame, objectID]);
|
||||
}, [setGame, objectId]);
|
||||
|
||||
const isGameDownloading = lastPacket?.game.id === game?.id;
|
||||
|
||||
|
@ -114,7 +113,7 @@ export function GameDetailsContextProvider({
|
|||
useEffect(() => {
|
||||
window.electron
|
||||
.getGameShopDetails(
|
||||
objectID!,
|
||||
objectId!,
|
||||
shop as GameShop,
|
||||
getSteamLanguage(i18n.language)
|
||||
)
|
||||
|
@ -133,15 +132,12 @@ export function GameDetailsContextProvider({
|
|||
setIsLoading(false);
|
||||
});
|
||||
|
||||
window.electron
|
||||
.getGameStats(objectID!, shop as GameShop)
|
||||
.then((result) => {
|
||||
setStats(result);
|
||||
})
|
||||
.catch(() => {});
|
||||
window.electron.getGameStats(objectId!, shop as GameShop).then((result) => {
|
||||
setStats(result);
|
||||
});
|
||||
|
||||
window.electron
|
||||
.getGameAchievements(objectID!, shop as GameShop)
|
||||
.getGameAchievements(objectId!, shop as GameShop)
|
||||
.then((achievements) => {
|
||||
setAchievements(achievements);
|
||||
})
|
||||
|
@ -150,7 +146,7 @@ export function GameDetailsContextProvider({
|
|||
});
|
||||
|
||||
updateGame();
|
||||
}, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]);
|
||||
}, [updateGame, dispatch, gameTitle, objectId, shop, i18n.language]);
|
||||
|
||||
useEffect(() => {
|
||||
setShopDetails(null);
|
||||
|
@ -159,7 +155,7 @@ export function GameDetailsContextProvider({
|
|||
setisGameRunning(false);
|
||||
setAchievements([]);
|
||||
dispatch(setHeaderTitle(gameTitle));
|
||||
}, [objectID, gameTitle, dispatch]);
|
||||
}, [objectId, gameTitle, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onGamesRunning((gamesIds) => {
|
||||
|
@ -181,10 +177,10 @@ export function GameDetailsContextProvider({
|
|||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onAchievementUnlocked(
|
||||
(objectId, shop) => {
|
||||
if (objectID !== objectId || shop !== shop) return;
|
||||
if (objectId !== objectId || shop !== shop) return;
|
||||
|
||||
window.electron
|
||||
.getGameAchievements(objectID!, shop as GameShop)
|
||||
.getGameAchievements(objectId!, shop as GameShop)
|
||||
.then(setAchievements)
|
||||
.catch(() => {});
|
||||
}
|
||||
|
@ -193,7 +189,7 @@ export function GameDetailsContextProvider({
|
|||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [objectID, shop]);
|
||||
}, [objectId, shop]);
|
||||
|
||||
const getDownloadsPath = async () => {
|
||||
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||
|
@ -233,7 +229,7 @@ export function GameDetailsContextProvider({
|
|||
gameTitle,
|
||||
isGameRunning,
|
||||
isLoading,
|
||||
objectID,
|
||||
objectId,
|
||||
gameColor,
|
||||
showGameOptionsModal,
|
||||
showRepacksModal,
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface GameDetailsContext {
|
|||
gameTitle: string;
|
||||
isGameRunning: boolean;
|
||||
isLoading: boolean;
|
||||
objectID: string | undefined;
|
||||
objectId: string | undefined;
|
||||
gameColor: string;
|
||||
showRepacksModal: boolean;
|
||||
showGameOptionsModal: boolean;
|
||||
|
|
|
@ -2,3 +2,4 @@ export * from "./game-details/game-details.context";
|
|||
export * from "./settings/settings.context";
|
||||
export * from "./user-profile/user-profile.context";
|
||||
export * from "./repacks/repacks.context";
|
||||
export * from "./cloud-sync/cloud-sync.context";
|
||||
|
|
|
@ -41,15 +41,18 @@ export function RepacksContextProvider({ children }: RepacksContextProps) {
|
|||
}, []);
|
||||
|
||||
const indexRepacks = useCallback(() => {
|
||||
console.log("INDEXING");
|
||||
setIsIndexingRepacks(true);
|
||||
repacksWorker.postMessage("INDEX_REPACKS");
|
||||
|
||||
repacksWorker.onmessage = () => {
|
||||
console.log("INDEXING COMPLETE");
|
||||
setIsIndexingRepacks(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("CALLED");
|
||||
indexRepacks();
|
||||
}, [indexRepacks]);
|
||||
|
||||
|
|
52
src/renderer/src/declaration.d.ts
vendored
52
src/renderer/src/declaration.d.ts
vendored
|
@ -26,7 +26,10 @@ import type {
|
|||
UserDetails,
|
||||
FriendRequestSync,
|
||||
GameAchievement,
|
||||
GameArtifact,
|
||||
LudusaviBackup,
|
||||
} from "@types";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
declare global {
|
||||
|
@ -49,20 +52,15 @@ declare global {
|
|||
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
||||
getCatalogue: (category: CatalogueCategory) => Promise<CatalogueEntry[]>;
|
||||
getGameShopDetails: (
|
||||
objectID: string,
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
language: string
|
||||
) => Promise<ShopDetails | null>;
|
||||
getRandomGame: () => Promise<Steam250Game>;
|
||||
getHowLongToBeat: (
|
||||
objectID: string,
|
||||
shop: GameShop,
|
||||
title: string
|
||||
) => Promise<HowLongToBeatCategory[] | null>;
|
||||
getGames: (
|
||||
take?: number,
|
||||
prevCursor?: number
|
||||
) => Promise<{ results: CatalogueEntry[]; cursor: number }>;
|
||||
getGames: (take?: number, skip?: number) => Promise<CatalogueEntry[]>;
|
||||
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
||||
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
||||
getTrendingGames: () => Promise<TrendingGame[]>;
|
||||
|
@ -81,7 +79,7 @@ declare global {
|
|||
|
||||
/* Library */
|
||||
addGameToLibrary: (
|
||||
objectID: string,
|
||||
objectId: string,
|
||||
title: string,
|
||||
shop: GameShop
|
||||
) => Promise<void>;
|
||||
|
@ -97,7 +95,7 @@ declare global {
|
|||
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
||||
removeGame: (gameId: number) => Promise<void>;
|
||||
deleteGameFolder: (gameId: number) => Promise<unknown>;
|
||||
getGameByObjectID: (objectID: string) => Promise<Game | null>;
|
||||
getGameByObjectId: (objectId: string) => Promise<Game | null>;
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
|
@ -120,6 +118,42 @@ declare global {
|
|||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||
|
||||
/* Cloud save */
|
||||
uploadSaveGame: (objectId: string, shop: GameShop) => Promise<void>;
|
||||
downloadGameArtifact: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
gameArtifactId: string
|
||||
) => Promise<void>;
|
||||
getGameArtifacts: (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => Promise<GameArtifact[]>;
|
||||
getGameBackupPreview: (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => Promise<LudusaviBackup | null>;
|
||||
checkGameCloudSyncSupport: (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => Promise<boolean>;
|
||||
deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>;
|
||||
onBackupDownloadComplete: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
cb: () => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
onUploadComplete: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
cb: () => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
onBackupDownloadProgress: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
cb: (progress: AxiosProgressEvent) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
|
||||
/* Misc */
|
||||
openExternal: (src: string) => Promise<void>;
|
||||
getVersion: () => Promise<string>;
|
||||
|
|
|
@ -1,13 +1,36 @@
|
|||
import type { GameShop, HowLongToBeatCategory } from "@types";
|
||||
import { Dexie } from "dexie";
|
||||
|
||||
export interface GameBackup {
|
||||
id?: number;
|
||||
shop: GameShop;
|
||||
objectId: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface HowLongToBeatEntry {
|
||||
id?: number;
|
||||
objectId: string;
|
||||
categories: HowLongToBeatCategory[];
|
||||
shop: GameShop;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export const db = new Dexie("Hydra");
|
||||
|
||||
db.version(1).stores({
|
||||
db.version(4).stores({
|
||||
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
||||
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
|
||||
gameBackups: `++id, [shop+objectId], createdAt`,
|
||||
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
|
||||
});
|
||||
|
||||
export const downloadSourcesTable = db.table("downloadSources");
|
||||
export const repacksTable = db.table("repacks");
|
||||
export const gameBackupsTable = db.table<GameBackup>("gameBackups");
|
||||
export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>(
|
||||
"howLongToBeatEntries"
|
||||
);
|
||||
|
||||
db.open();
|
||||
|
|
|
@ -27,11 +27,11 @@ export const getSteamLanguage = (language: string) => {
|
|||
};
|
||||
|
||||
export const buildGameDetailsPath = (
|
||||
game: { shop: GameShop; objectID: string; title: string },
|
||||
game: { shop: GameShop; objectId: string; title: string },
|
||||
params: Record<string, string> = {}
|
||||
) => {
|
||||
const searchParams = new URLSearchParams({ title: game.title, ...params });
|
||||
return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
|
||||
return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`;
|
||||
};
|
||||
|
||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
UserDetails,
|
||||
} from "@types";
|
||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||
import { gameBackupsTable } from "@renderer/dexie";
|
||||
|
||||
export function useUserDetails() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -32,6 +33,7 @@ export function useUserDetails() {
|
|||
dispatch(setUserDetails(null));
|
||||
dispatch(setProfileBackground(null));
|
||||
|
||||
await gameBackupsTable.clear();
|
||||
window.localStorage.removeItem("userDetails");
|
||||
}, [dispatch]);
|
||||
|
||||
|
@ -44,32 +46,9 @@ export function useUserDetails() {
|
|||
const updateUserDetails = useCallback(
|
||||
async (userDetails: UserDetails) => {
|
||||
dispatch(setUserDetails(userDetails));
|
||||
|
||||
if (userDetails.profileImageUrl) {
|
||||
// TODO: Decide if we want to use this
|
||||
// const profileBackground = await profileBackgroundFromProfileImage(
|
||||
// userDetails.profileImageUrl
|
||||
// ).catch((err) => {
|
||||
// logger.error("profileBackgroundFromProfileImage", err);
|
||||
// return `#151515B3`;
|
||||
// });
|
||||
// dispatch(setProfileBackground(profileBackground));
|
||||
|
||||
window.localStorage.setItem(
|
||||
"userDetails",
|
||||
JSON.stringify({ ...userDetails, profileBackground })
|
||||
);
|
||||
} else {
|
||||
const profileBackground = `#151515B3`;
|
||||
dispatch(setProfileBackground(profileBackground));
|
||||
|
||||
window.localStorage.setItem(
|
||||
"userDetails",
|
||||
JSON.stringify({ ...userDetails, profileBackground })
|
||||
);
|
||||
}
|
||||
window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
|
||||
},
|
||||
[dispatch, profileBackground]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const fetchUserDetails = useCallback(async () => {
|
||||
|
|
3
src/renderer/src/logger.ts
Normal file
3
src/renderer/src/logger.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import log from "electron-log/renderer";
|
||||
|
||||
export const logger = log.scope("renderer");
|
|
@ -66,7 +66,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||
<Route path="/" Component={Home} />
|
||||
<Route path="/catalogue" Component={Catalogue} />
|
||||
<Route path="/downloads" Component={Downloads} />
|
||||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||
<Route path="/game/:shop/:objectId" Component={GameDetails} />
|
||||
<Route path="/search" Component={SearchResults} />
|
||||
<Route path="/settings" Component={Settings} />
|
||||
<Route path="/profile/:userId" Component={Profile} />
|
||||
|
|
|
@ -24,12 +24,10 @@ export function Catalogue() {
|
|||
|
||||
const contentRef = useRef<HTMLElement>(null);
|
||||
|
||||
const cursorRef = useRef<number>(0);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const cursor = Number(searchParams.get("cursor") ?? 0);
|
||||
const skip = Number(searchParams.get("skip") ?? 0);
|
||||
|
||||
const handleGameClick = (game: CatalogueEntry) => {
|
||||
dispatch(clearSearch());
|
||||
|
@ -42,11 +40,10 @@ export function Catalogue() {
|
|||
setSearchResults([]);
|
||||
|
||||
window.electron
|
||||
.getGames(24, cursor)
|
||||
.then(({ results, cursor }) => {
|
||||
.getGames(24, skip)
|
||||
.then((results) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
cursorRef.current = cursor;
|
||||
setSearchResults(results);
|
||||
resolve(null);
|
||||
}, 500);
|
||||
|
@ -55,11 +52,11 @@ export function Catalogue() {
|
|||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [dispatch, cursor, searchParams]);
|
||||
}, [dispatch, skip, searchParams]);
|
||||
|
||||
const handleNextPage = () => {
|
||||
const params = new URLSearchParams({
|
||||
cursor: cursorRef.current.toString(),
|
||||
skip: String(skip + 24),
|
||||
});
|
||||
|
||||
navigate(`/catalogue?${params.toString()}`);
|
||||
|
@ -80,7 +77,7 @@ export function Catalogue() {
|
|||
<Button
|
||||
onClick={() => navigate(-1)}
|
||||
theme="outline"
|
||||
disabled={cursor === 0 || isLoading}
|
||||
disabled={skip === 0 || isLoading}
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
{t("previous_page")}
|
||||
|
@ -103,7 +100,7 @@ export function Catalogue() {
|
|||
<>
|
||||
{searchResults.map((game) => (
|
||||
<GameCard
|
||||
key={game.objectID}
|
||||
key={game.objectId}
|
||||
game={game}
|
||||
onClick={() => handleGameClick(game)}
|
||||
/>
|
||||
|
|
|
@ -93,6 +93,7 @@ export const downloadRightContent = style({
|
|||
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({
|
||||
|
|
|
@ -227,7 +227,14 @@ export function DownloadGroup({
|
|||
<button
|
||||
type="button"
|
||||
className={styles.downloadTitle}
|
||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
buildGameDetailsPath({
|
||||
...game,
|
||||
objectId: game.objectID,
|
||||
})
|
||||
)
|
||||
}
|
||||
>
|
||||
{game.title}
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
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",
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
import { Modal, ModalProps } from "@renderer/components";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { cloudSyncContext } from "@renderer/context";
|
||||
|
||||
export interface CloudSyncFilesModalProps
|
||||
extends Omit<ModalProps, "children" | "title"> {}
|
||||
|
||||
export function CloudSyncFilesModal({
|
||||
visible,
|
||||
onClose,
|
||||
}: CloudSyncFilesModalProps) {
|
||||
const { backupPreview } = useContext(cloudSyncContext);
|
||||
|
||||
console.log(backupPreview);
|
||||
|
||||
const files = useMemo(() => {
|
||||
if (!backupPreview) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [game] = Object.values(backupPreview.games);
|
||||
const entries = Object.entries(game.files);
|
||||
|
||||
return entries.map(([key, value]) => {
|
||||
return { path: key, ...value };
|
||||
});
|
||||
}, [backupPreview]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title="Gerenciar arquivos"
|
||||
description="Escolha quais diretórios serão sincronizados"
|
||||
onClose={onClose}
|
||||
>
|
||||
{/* <div className={styles.downloaders}>
|
||||
{["AUTOMATIC", "CUSTOM"].map((downloader) => (
|
||||
<Button
|
||||
key={downloader}
|
||||
className={styles.downloaderOption}
|
||||
theme={selectedDownloader === downloader ? "primary" : "outline"}
|
||||
disabled={
|
||||
downloader === Downloader.RealDebrid &&
|
||||
!userPreferences?.realDebridApiToken
|
||||
}
|
||||
onClick={() => setSelectedDownloader(downloader)}
|
||||
>
|
||||
{selectedDownloader === downloader && (
|
||||
<CheckCircleFillIcon className={styles.downloaderIcon} />
|
||||
)}
|
||||
{DOWNLOADER_NAME[downloader]}
|
||||
</Button>
|
||||
))}
|
||||
</div> */}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: "left" }}>Arquivo</th>
|
||||
<th style={{ textAlign: "left" }}>Hash</th>
|
||||
<th style={{ textAlign: "left" }}>Tamanho</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file) => (
|
||||
<tr key={file.path}>
|
||||
<td style={{ textAlign: "left" }}>{file.path}</td>
|
||||
<td style={{ textAlign: "left" }}>{file.change}</td>
|
||||
<td style={{ textAlign: "left" }}>{file.path}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
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",
|
||||
});
|
|
@ -0,0 +1,251 @@
|
|||
import { Button, Modal, ModalProps } from "@renderer/components";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
|
||||
import * as styles from "./cloud-sync-modal.css";
|
||||
import { formatBytes } from "@shared";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
CheckCircleFillIcon,
|
||||
ClockIcon,
|
||||
DeviceDesktopIcon,
|
||||
HistoryIcon,
|
||||
SyncIcon,
|
||||
TrashIcon,
|
||||
UploadIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { GameBackup, gameBackupsTable } from "@renderer/dexie";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AxiosProgressEvent } from "axios";
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
|
||||
export interface CloudSyncModalProps
|
||||
extends Omit<ModalProps, "children" | "title"> {}
|
||||
|
||||
export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
const [deletingArtifact, setDeletingArtifact] = useState(false);
|
||||
const [lastBackup, setLastBackup] = useState<GameBackup | null>(null);
|
||||
const [backupDownloadProgress, setBackupDownloadProgress] =
|
||||
useState<AxiosProgressEvent | null>(null);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const {
|
||||
artifacts,
|
||||
backupPreview,
|
||||
uploadingBackup,
|
||||
restoringBackup,
|
||||
uploadSaveGame,
|
||||
downloadGameArtifact,
|
||||
deleteGameArtifact,
|
||||
setShowCloudSyncFilesModal,
|
||||
} = useContext(cloudSyncContext);
|
||||
|
||||
const { objectId, shop, gameTitle } = useContext(gameDetailsContext);
|
||||
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const handleDeleteArtifactClick = async (gameArtifactId: string) => {
|
||||
setDeletingArtifact(true);
|
||||
|
||||
try {
|
||||
await deleteGameArtifact(gameArtifactId);
|
||||
|
||||
showSuccessToast(t("backup_deleted"));
|
||||
} catch (err) {
|
||||
showErrorToast("backup_deletion_failed");
|
||||
} finally {
|
||||
setDeletingArtifact(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
gameBackupsTable
|
||||
.where({ shop: shop, objectId })
|
||||
.last()
|
||||
.then((lastBackup) => setLastBackup(lastBackup || null));
|
||||
|
||||
const removeBackupDownloadProgressListener =
|
||||
window.electron.onBackupDownloadProgress(
|
||||
objectId!,
|
||||
shop,
|
||||
(progressEvent) => {
|
||||
setBackupDownloadProgress(progressEvent);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
removeBackupDownloadProgressListener();
|
||||
};
|
||||
}, [backupPreview, objectId, shop]);
|
||||
|
||||
const handleBackupInstallClick = async (artifactId: string) => {
|
||||
setBackupDownloadProgress(null);
|
||||
downloadGameArtifact(artifactId);
|
||||
};
|
||||
|
||||
const backupStateLabel = useMemo(() => {
|
||||
if (uploadingBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
{t("uploading_backup")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (restoringBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
{t("restoring_backup", {
|
||||
progress: formatDownloadProgress(
|
||||
backupDownloadProgress?.progress ?? 0
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (lastBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<i style={{ color: vars.color.success }}>
|
||||
<CheckCircleFillIcon />
|
||||
</i>
|
||||
|
||||
{t("last_backup_date", {
|
||||
date: format(lastBackup.createdAt, "dd/MM/yyyy HH:mm"),
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!backupPreview) {
|
||||
return t("no_backup_preview");
|
||||
}
|
||||
|
||||
return t("no_backups");
|
||||
}, [
|
||||
uploadingBackup,
|
||||
backupDownloadProgress?.progress,
|
||||
lastBackup,
|
||||
backupPreview,
|
||||
restoringBackup,
|
||||
t,
|
||||
]);
|
||||
|
||||
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("cloud_save")}
|
||||
description={t("cloud_save_description")}
|
||||
onClose={onClose}
|
||||
large
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
||||
<h2>{gameTitle}</h2>
|
||||
<p>{backupStateLabel}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
alignSelf: "flex-start",
|
||||
fontSize: 14,
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline",
|
||||
color: vars.color.body,
|
||||
}}
|
||||
onClick={() => setShowCloudSyncFilesModal(true)}
|
||||
>
|
||||
Gerenciar arquivos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={uploadSaveGame}
|
||||
disabled={disableActions || !backupPreview}
|
||||
>
|
||||
<UploadIcon />
|
||||
{t("create_backup")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: SPACING_UNIT,
|
||||
}}
|
||||
>
|
||||
<h2>{t("backups")}</h2>
|
||||
<small>{artifacts.length} / 2</small>
|
||||
</div>
|
||||
|
||||
<ul className={styles.artifacts}>
|
||||
{artifacts.map((artifact) => (
|
||||
<li key={artifact.id} className={styles.artifactButton}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<h3>Backup do dia {format(artifact.createdAt, "dd/MM")}</h3>
|
||||
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
|
||||
</div>
|
||||
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<DeviceDesktopIcon size={14} />
|
||||
{artifact.hostname}
|
||||
</span>
|
||||
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<ClockIcon size={14} />
|
||||
{format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleBackupInstallClick(artifact.id)}
|
||||
disabled={disableActions}
|
||||
>
|
||||
<HistoryIcon />
|
||||
{t("install_backup")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteArtifactClick(artifact.id)}
|
||||
theme="danger"
|
||||
disabled={disableActions}
|
||||
>
|
||||
<TrashIcon />
|
||||
{t("delete_backup")}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -9,8 +9,12 @@ import { Sidebar } from "./sidebar/sidebar";
|
|||
|
||||
import * as styles from "./game-details.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import Lottie from "lottie-react";
|
||||
|
||||
import downloadingAnimation from "@renderer/assets/lottie/cloud.json";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
|
||||
const HERO_ANIMATION_THRESHOLD = 25;
|
||||
|
||||
|
@ -22,7 +26,7 @@ export function GameDetailsContent() {
|
|||
const { t } = useTranslation("game_details");
|
||||
|
||||
const {
|
||||
objectID,
|
||||
objectId,
|
||||
shopDetails,
|
||||
game,
|
||||
gameColor,
|
||||
|
@ -30,10 +34,15 @@ export function GameDetailsContent() {
|
|||
hasNSFWContentBlocked,
|
||||
} = useContext(gameDetailsContext);
|
||||
|
||||
const { userDetails } = useUserDetails();
|
||||
|
||||
const { supportsCloudSync, setShowCloudSyncModal } =
|
||||
useContext(cloudSyncContext);
|
||||
|
||||
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
||||
|
||||
const handleHeroLoad = async () => {
|
||||
const output = await average(steamUrlBuilder.libraryHero(objectID!), {
|
||||
const output = await average(steamUrlBuilder.libraryHero(objectId!), {
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
});
|
||||
|
@ -47,7 +56,7 @@ export function GameDetailsContent() {
|
|||
|
||||
useEffect(() => {
|
||||
setBackdropOpacity(1);
|
||||
}, [objectID]);
|
||||
}, [objectId]);
|
||||
|
||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
||||
const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT;
|
||||
|
@ -69,10 +78,19 @@ export function GameDetailsContent() {
|
|||
setBackdropOpacity(opacity);
|
||||
};
|
||||
|
||||
const handleCloudSaveButtonClick = () => {
|
||||
if (!userDetails) {
|
||||
window.electron.openAuthWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
setShowCloudSyncModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectID!)}
|
||||
src={steamUrlBuilder.libraryHero(objectId!)}
|
||||
className={styles.heroImage}
|
||||
alt={game?.title}
|
||||
onLoad={handleHeroLoad}
|
||||
|
@ -98,10 +116,37 @@ export function GameDetailsContent() {
|
|||
>
|
||||
<div className={styles.heroContent}>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectID!)}
|
||||
src={steamUrlBuilder.logo(objectId!)}
|
||||
className={styles.gameLogo}
|
||||
alt={game?.title}
|
||||
/>
|
||||
|
||||
{supportsCloudSync && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.cloudSyncButton}
|
||||
onClick={handleCloudSaveButtonClick}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 16 + 4,
|
||||
height: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Lottie
|
||||
animationData={downloadingAnimation}
|
||||
loop
|
||||
autoplay
|
||||
style={{ width: 26, position: "absolute", top: -3 }}
|
||||
/>
|
||||
</div>
|
||||
{t("cloud_save")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -43,23 +43,6 @@ export function GameDetailsSkeleton() {
|
|||
</div>
|
||||
</div>
|
||||
<div className={sidebarStyles.contentSidebar}>
|
||||
{/* <div className={sidebarStyles.contentSidebarTitle}>
|
||||
<h3>HowLongToBeat</h3>
|
||||
</div>
|
||||
<ul className={sidebarStyles.howLongToBeatCategoriesList}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className={sidebarStyles.howLongToBeatCategorySkeleton}
|
||||
/>
|
||||
))}
|
||||
</ul> */}
|
||||
<div
|
||||
className={sidebarStyles.contentSidebarTitle}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
<h3>{t("requirements")}</h3>
|
||||
</div>
|
||||
<div className={sidebarStyles.requirementButtonContainer}>
|
||||
<Button
|
||||
className={sidebarStyles.requirementButton}
|
||||
|
|
|
@ -6,8 +6,8 @@ import { recipe } from "@vanilla-extract/recipes";
|
|||
export const HERO_HEIGHT = 300;
|
||||
|
||||
export const slideIn = keyframes({
|
||||
"0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)` },
|
||||
"100%": { transform: "translateY(0)" },
|
||||
"0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)`, opacity: "0px" },
|
||||
"100%": { transform: "translateY(0)", opacity: "1" },
|
||||
});
|
||||
|
||||
export const wrapper = recipe({
|
||||
|
@ -49,6 +49,8 @@ export const heroContent = style({
|
|||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
});
|
||||
|
||||
export const heroLogoBackdrop = style({
|
||||
|
@ -200,3 +202,33 @@ globalStyle(`${description} img`, {
|
|||
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)",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -18,21 +18,26 @@ import { vars } from "@renderer/theme.css";
|
|||
|
||||
import { GameDetailsContent } from "./game-details-content";
|
||||
import {
|
||||
CloudSyncContextConsumer,
|
||||
CloudSyncContextProvider,
|
||||
GameDetailsContextConsumer,
|
||||
GameDetailsContextProvider,
|
||||
} from "@renderer/context";
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import { GameOptionsModal, RepacksModal } from "./modals";
|
||||
import { Downloader, getDownloadersForUri } from "@shared";
|
||||
import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal";
|
||||
import { CloudSyncFilesModal } from "./cloud-sync-files-modal/cloud-sync-files-modal";
|
||||
|
||||
export function GameDetails() {
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
const [randomizerLocked, setRandomizerLocked] = useState(false);
|
||||
|
||||
const { objectID } = useParams();
|
||||
const { objectId, shop } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const fromRandomizer = searchParams.get("fromRandomizer");
|
||||
const gameTitle = searchParams.get("title");
|
||||
|
||||
const { startDownload } = useDownload();
|
||||
|
||||
|
@ -45,7 +50,7 @@ export function GameDetails() {
|
|||
window.electron.getRandomGame().then((randomGame) => {
|
||||
setRandomGame(randomGame);
|
||||
});
|
||||
}, [objectID]);
|
||||
}, [objectId]);
|
||||
|
||||
const handleRandomizerClick = () => {
|
||||
if (randomGame) {
|
||||
|
@ -74,7 +79,11 @@ export function GameDetails() {
|
|||
repack.uris.find((uri) => getDownloadersForUri(uri).includes(downloader))!;
|
||||
|
||||
return (
|
||||
<GameDetailsContextProvider>
|
||||
<GameDetailsContextProvider
|
||||
gameTitle={gameTitle!}
|
||||
shop={shop! as GameShop}
|
||||
objectId={objectId!}
|
||||
>
|
||||
<GameDetailsContextConsumer>
|
||||
{({
|
||||
isLoading,
|
||||
|
@ -96,7 +105,7 @@ export function GameDetails() {
|
|||
) => {
|
||||
await startDownload({
|
||||
repackId: repack.id,
|
||||
objectID: objectID!,
|
||||
objectId: objectId!,
|
||||
title: gameTitle,
|
||||
downloader,
|
||||
shop: shop as GameShop,
|
||||
|
@ -115,64 +124,92 @@ export function GameDetails() {
|
|||
};
|
||||
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={vars.color.background}
|
||||
highlightColor="#444"
|
||||
<CloudSyncContextProvider
|
||||
objectId={objectId!}
|
||||
shop={shop! as GameShop}
|
||||
>
|
||||
{isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />}
|
||||
|
||||
<RepacksModal
|
||||
visible={showRepacksModal}
|
||||
startDownload={handleStartDownload}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={hasNSFWContentBlocked}
|
||||
onClose={handleNSFWContentRefuse}
|
||||
title={t("nsfw_content_title")}
|
||||
descriptionText={t("nsfw_content_description", {
|
||||
title: gameTitle,
|
||||
})}
|
||||
confirmButtonLabel={t("allow_nsfw_content")}
|
||||
cancelButtonLabel={t("refuse_nsfw_content")}
|
||||
onConfirm={() => setHasNSFWContentBlocked(false)}
|
||||
clickOutsideToClose={false}
|
||||
/>
|
||||
|
||||
{game && (
|
||||
<GameOptionsModal
|
||||
visible={showGameOptionsModal}
|
||||
game={game}
|
||||
onClose={() => {
|
||||
setShowGameOptionsModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{fromRandomizer && (
|
||||
<Button
|
||||
className={styles.randomizerButton}
|
||||
onClick={handleRandomizerClick}
|
||||
theme="outline"
|
||||
disabled={!randomGame || randomizerLocked}
|
||||
>
|
||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||
<Lottie
|
||||
animationData={starsAnimation}
|
||||
style={{
|
||||
width: 70,
|
||||
position: "absolute",
|
||||
top: -28,
|
||||
left: -27,
|
||||
}}
|
||||
loop
|
||||
<CloudSyncContextConsumer>
|
||||
{({
|
||||
showCloudSyncModal,
|
||||
setShowCloudSyncModal,
|
||||
showCloudSyncFilesModal,
|
||||
setShowCloudSyncFilesModal,
|
||||
}) => (
|
||||
<>
|
||||
<CloudSyncModal
|
||||
onClose={() => setShowCloudSyncModal(false)}
|
||||
visible={showCloudSyncModal}
|
||||
/>
|
||||
</div>
|
||||
{t("next_suggestion")}
|
||||
</Button>
|
||||
)}
|
||||
</SkeletonTheme>
|
||||
|
||||
<CloudSyncFilesModal
|
||||
onClose={() => setShowCloudSyncFilesModal(false)}
|
||||
visible={showCloudSyncFilesModal}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CloudSyncContextConsumer>
|
||||
|
||||
<SkeletonTheme
|
||||
baseColor={vars.color.background}
|
||||
highlightColor="#444"
|
||||
>
|
||||
{isLoading ? <GameDetailsSkeleton /> : <GameDetailsContent />}
|
||||
|
||||
<RepacksModal
|
||||
visible={showRepacksModal}
|
||||
startDownload={handleStartDownload}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={hasNSFWContentBlocked}
|
||||
onClose={handleNSFWContentRefuse}
|
||||
title={t("nsfw_content_title")}
|
||||
descriptionText={t("nsfw_content_description", {
|
||||
title: gameTitle,
|
||||
})}
|
||||
confirmButtonLabel={t("allow_nsfw_content")}
|
||||
cancelButtonLabel={t("refuse_nsfw_content")}
|
||||
onConfirm={() => setHasNSFWContentBlocked(false)}
|
||||
clickOutsideToClose={false}
|
||||
/>
|
||||
|
||||
{game && (
|
||||
<GameOptionsModal
|
||||
visible={showGameOptionsModal}
|
||||
game={game}
|
||||
onClose={() => {
|
||||
setShowGameOptionsModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{fromRandomizer && (
|
||||
<Button
|
||||
className={styles.randomizerButton}
|
||||
onClick={handleRandomizerClick}
|
||||
theme="outline"
|
||||
disabled={!randomGame || randomizerLocked}
|
||||
>
|
||||
<div
|
||||
style={{ width: 16, height: 16, position: "relative" }}
|
||||
>
|
||||
<Lottie
|
||||
animationData={starsAnimation}
|
||||
style={{
|
||||
width: 70,
|
||||
position: "absolute",
|
||||
top: -28,
|
||||
left: -27,
|
||||
}}
|
||||
loop
|
||||
/>
|
||||
</div>
|
||||
{t("next_suggestion")}
|
||||
</Button>
|
||||
)}
|
||||
</SkeletonTheme>
|
||||
</CloudSyncContextProvider>
|
||||
);
|
||||
}}
|
||||
</GameDetailsContextConsumer>
|
||||
|
|
|
@ -18,7 +18,7 @@ export function HeroPanelActions() {
|
|||
game,
|
||||
repacks,
|
||||
isGameRunning,
|
||||
objectID,
|
||||
objectId,
|
||||
gameTitle,
|
||||
setShowGameOptionsModal,
|
||||
setShowRepacksModal,
|
||||
|
@ -39,7 +39,7 @@ export function HeroPanelActions() {
|
|||
setToggleLibraryGameDisabled(true);
|
||||
|
||||
try {
|
||||
await window.electron.addGameToLibrary(objectID!, gameTitle, "steam");
|
||||
await window.electron.addGameToLibrary(objectId!, gameTitle, "steam");
|
||||
|
||||
updateLibrary();
|
||||
updateGame();
|
||||
|
|
|
@ -29,6 +29,7 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
|||
const [latestRepack] = repacks;
|
||||
|
||||
if (latestRepack) {
|
||||
console.log(latestRepack);
|
||||
const lastUpdate = format(latestRepack.uploadDate!, "dd/MM/yyyy");
|
||||
const repacksCount = repacks.length;
|
||||
|
||||
|
|
|
@ -65,7 +65,8 @@ export function RepacksModal({
|
|||
};
|
||||
|
||||
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
||||
return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
|
||||
if (!game) return false;
|
||||
return repack.uris.some((uri) => uri.includes(game.uri!));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { recipe } from "@vanilla-extract/recipes";
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const sidebarSectionButton = style({
|
||||
height: "72px",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
color: vars.color.muted,
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.2s",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||
},
|
||||
":active": {
|
||||
opacity: vars.opacity.active,
|
||||
},
|
||||
});
|
||||
|
||||
export const chevron = recipe({
|
||||
base: {
|
||||
transition: "transform ease 0.2s",
|
||||
},
|
||||
variants: {
|
||||
open: {
|
||||
true: {
|
||||
transform: "rotate(180deg)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { ChevronDownIcon } from "@primer/octicons-react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import * as styles from "./sidebar-section.css";
|
||||
|
||||
export interface SidebarSectionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SidebarSection({ title, children }: SidebarSectionProps) {
|
||||
const content = useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={styles.sidebarSectionButton}
|
||||
>
|
||||
<ChevronDownIcon className={styles.chevron({ open: isOpen })} />
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
ref={content}
|
||||
style={{
|
||||
maxHeight: isOpen ? `${content.current?.scrollHeight}px` : "0",
|
||||
overflow: "hidden",
|
||||
transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,6 +4,7 @@ import type { HowLongToBeatCategory } from "@types";
|
|||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
|
||||
const durationTranslation: Record<string, string> = {
|
||||
Hours: "hours",
|
||||
|
@ -30,41 +31,42 @@ export function HowLongToBeatSection({
|
|||
|
||||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<div className={styles.contentSidebarTitle}>
|
||||
<h3>HowLongToBeat</h3>
|
||||
</div>
|
||||
|
||||
<ul className={styles.howLongToBeatCategoriesList}>
|
||||
{howLongToBeatData
|
||||
? howLongToBeatData.map((category) => (
|
||||
<li key={category.title} className={styles.howLongToBeatCategory}>
|
||||
<p
|
||||
className={styles.howLongToBeatCategoryLabel}
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
<SidebarSection title="HowLongToBeat">
|
||||
<ul className={styles.howLongToBeatCategoriesList}>
|
||||
{howLongToBeatData
|
||||
? howLongToBeatData.map((category) => (
|
||||
<li
|
||||
key={category.title}
|
||||
className={styles.howLongToBeatCategory}
|
||||
>
|
||||
{category.title}
|
||||
</p>
|
||||
<p
|
||||
className={styles.howLongToBeatCategoryLabel}
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{category.title}
|
||||
</p>
|
||||
|
||||
<p className={styles.howLongToBeatCategoryLabel}>
|
||||
{getDuration(category.duration)}
|
||||
</p>
|
||||
<p className={styles.howLongToBeatCategoryLabel}>
|
||||
{getDuration(category.duration)}
|
||||
</p>
|
||||
|
||||
{category.accuracy !== "00" && (
|
||||
<small>
|
||||
{t("accuracy", { accuracy: category.accuracy })}
|
||||
</small>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: Array.from({ length: 4 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className={styles.howLongToBeatCategorySkeleton}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{category.accuracy !== "00" && (
|
||||
<small>
|
||||
{t("accuracy", { accuracy: category.accuracy })}
|
||||
</small>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: Array.from({ length: 4 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className={styles.howLongToBeatCategorySkeleton}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</SidebarSection>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ import { globalStyle, style } from "@vanilla-extract/css";
|
|||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const contentSidebar = style({
|
||||
borderLeft: `solid 1px ${vars.color.border};`,
|
||||
borderLeft: `solid 1px ${vars.color.border}`,
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"@media": {
|
||||
|
@ -18,15 +19,6 @@ export const contentSidebar = style({
|
|||
},
|
||||
});
|
||||
|
||||
export const contentSidebarTitle = style({
|
||||
height: "72px",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
justifyContent: "space-between",
|
||||
});
|
||||
|
||||
export const requirementButtonContainer = style({
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
|
@ -56,7 +48,7 @@ export const requirementsDetailsSkeleton = style({
|
|||
|
||||
export const howLongToBeatCategoriesList = style({
|
||||
margin: "0",
|
||||
padding: "16px",
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "16px",
|
||||
|
@ -66,7 +58,8 @@ export const howLongToBeatCategory = style({
|
|||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
backgroundColor: vars.color.background,
|
||||
background:
|
||||
"linear-gradient(90deg, transparent 20%, rgb(255 255 255 / 2%) 100%)",
|
||||
borderRadius: "4px",
|
||||
padding: `8px 16px`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
|
@ -87,6 +80,8 @@ export const statsSection = style({
|
|||
gap: `${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
justifyContent: "space-between",
|
||||
transition: "max-height ease 0.5s",
|
||||
overflow: "hidden",
|
||||
"@media": {
|
||||
"(min-width: 1024px)": {
|
||||
flexDirection: "column",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Link } from "@renderer/components";
|
||||
|
@ -8,9 +8,12 @@ import { gameDetailsContext } from "@renderer/context";
|
|||
import { useDate, useFormat } from "@renderer/hooks";
|
||||
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
||||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
|
||||
export function Sidebar() {
|
||||
const [_howLongToBeat, _setHowLongToBeat] = useState<{
|
||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||
isLoading: boolean;
|
||||
data: HowLongToBeatCategory[] | null;
|
||||
}>({ isLoading: true, data: null });
|
||||
|
@ -18,7 +21,7 @@ export function Sidebar() {
|
|||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
const { gameTitle, shopDetails, stats, achievements, shop, objectID } =
|
||||
const { gameTitle, shopDetails, objectId, shop, stats, achievements } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
@ -27,50 +30,60 @@ export function Sidebar() {
|
|||
const { numberFormatter } = useFormat();
|
||||
|
||||
const buildGameAchievementPath = () => {
|
||||
const urlParams = new URLSearchParams({ objectId: objectID!, shop });
|
||||
const urlParams = new URLSearchParams({ objectId: objectId!, shop });
|
||||
return `/achievements?${urlParams.toString()}`;
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// if (objectID) {
|
||||
// setHowLongToBeat({ isLoading: true, data: null });
|
||||
useEffect(() => {
|
||||
if (objectId) {
|
||||
setHowLongToBeat({ isLoading: true, data: null });
|
||||
|
||||
// window.electron
|
||||
// .getHowLongToBeat(objectID, "steam", gameTitle)
|
||||
// .then((howLongToBeat) => {
|
||||
// setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||
// })
|
||||
// .catch(() => {
|
||||
// setHowLongToBeat({ isLoading: false, data: null });
|
||||
// });
|
||||
// }
|
||||
// }, [objectID, gameTitle]);
|
||||
howLongToBeatEntriesTable
|
||||
.where({ shop, objectId })
|
||||
.first()
|
||||
.then(async (cachedHowLongToBeat) => {
|
||||
if (cachedHowLongToBeat) {
|
||||
setHowLongToBeat({
|
||||
isLoading: false,
|
||||
data: cachedHowLongToBeat.categories,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const howLongToBeat =
|
||||
await window.electron.getHowLongToBeat(gameTitle);
|
||||
|
||||
if (howLongToBeat) {
|
||||
howLongToBeatEntriesTable.add({
|
||||
objectId,
|
||||
shop: "steam",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
categories: howLongToBeat,
|
||||
});
|
||||
}
|
||||
|
||||
setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||
} catch (err) {
|
||||
setHowLongToBeat({ isLoading: false, data: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [objectId, shop, gameTitle]);
|
||||
|
||||
return (
|
||||
<aside className={styles.contentSidebar}>
|
||||
{/* <HowLongToBeatSection
|
||||
howLongToBeatData={howLongToBeat.data}
|
||||
isLoading={howLongToBeat.isLoading}
|
||||
/> */}
|
||||
|
||||
{achievements.length > 0 && (
|
||||
<>
|
||||
<div
|
||||
className={styles.contentSidebarTitle}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
<h3>
|
||||
{t("achievements")}{" "}
|
||||
<span style={{ fontSize: "12px" }}>
|
||||
({achievements.filter((a) => a.unlocked).length}/
|
||||
{achievements.length})
|
||||
</span>
|
||||
</h3>
|
||||
<span>
|
||||
<Link to={buildGameAchievementPath()}>Ver todas</Link>
|
||||
<a></a>
|
||||
</span>
|
||||
</div>
|
||||
<SidebarSection
|
||||
title={t("achievements", {
|
||||
unlockedCount: achievements.filter((a) => a.unlocked).length,
|
||||
achievementsCount: achievements.length,
|
||||
})}
|
||||
>
|
||||
<span>
|
||||
<Link to={buildGameAchievementPath()}>Ver todas</Link>
|
||||
<a></a>
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
@ -111,18 +124,11 @@ export function Sidebar() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
{stats && (
|
||||
<>
|
||||
<div
|
||||
className={styles.contentSidebarTitle}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
<h3>{t("stats")}</h3>
|
||||
</div>
|
||||
|
||||
<SidebarSection title={t("stats")}>
|
||||
<div className={styles.statsSection}>
|
||||
<div className={styles.statsCategory}>
|
||||
<p className={styles.statsCategoryTitle}>
|
||||
|
@ -140,40 +146,44 @@ export function Sidebar() {
|
|||
<p>{numberFormatter.format(stats?.playerCount)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
<div className={styles.contentSidebarTitle} style={{ border: "none" }}>
|
||||
<h3>{t("requirements")}</h3>
|
||||
</div>
|
||||
<div className={styles.requirementButtonContainer}>
|
||||
<Button
|
||||
className={styles.requirementButton}
|
||||
onClick={() => setActiveRequirement("minimum")}
|
||||
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
||||
>
|
||||
{t("minimum")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className={styles.requirementButton}
|
||||
onClick={() => setActiveRequirement("recommended")}
|
||||
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
||||
>
|
||||
{t("recommended")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.requirementsDetails}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
shopDetails?.pc_requirements?.[activeRequirement] ??
|
||||
t(`no_${activeRequirement}_requirements`, {
|
||||
gameTitle,
|
||||
}),
|
||||
}}
|
||||
<HowLongToBeatSection
|
||||
howLongToBeatData={howLongToBeat.data}
|
||||
isLoading={howLongToBeat.isLoading}
|
||||
/>
|
||||
|
||||
<SidebarSection title={t("requirements")}>
|
||||
<div className={styles.requirementButtonContainer}>
|
||||
<Button
|
||||
className={styles.requirementButton}
|
||||
onClick={() => setActiveRequirement("minimum")}
|
||||
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
||||
>
|
||||
{t("minimum")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className={styles.requirementButton}
|
||||
onClick={() => setActiveRequirement("recommended")}
|
||||
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
||||
>
|
||||
{t("recommended")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.requirementsDetails}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
shopDetails?.pc_requirements?.[activeRequirement] ??
|
||||
t(`no_${activeRequirement}_requirements`, {
|
||||
gameTitle,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</SidebarSection>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -186,7 +186,7 @@ export function Home() {
|
|||
))
|
||||
: catalogue[currentCatalogueCategory].map((result) => (
|
||||
<GameCard
|
||||
key={result.objectID}
|
||||
key={result.objectId}
|
||||
game={result}
|
||||
onClick={() => navigate(buildGameDetailsPath(result))}
|
||||
/>
|
||||
|
|
|
@ -115,7 +115,7 @@ export function SearchResults() {
|
|||
<>
|
||||
{searchResults.map((game) => (
|
||||
<GameCard
|
||||
key={game.objectID}
|
||||
key={game.objectId}
|
||||
game={game}
|
||||
onClick={() => handleGameClick(game)}
|
||||
/>
|
||||
|
|
|
@ -64,6 +64,8 @@ export function EditProfileModal(
|
|||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
console.log(values);
|
||||
|
||||
return patchUser(values)
|
||||
.then(async () => {
|
||||
await Promise.allSettled([fetchUserDetails(), getUserProfile()]);
|
||||
|
@ -118,6 +120,8 @@ export function EditProfileModal(
|
|||
return { imagePath: null };
|
||||
});
|
||||
|
||||
console.log(imagePath);
|
||||
|
||||
onChange(imagePath);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ export const gameCover = style({
|
|||
transition: "all ease 0.2s",
|
||||
boxShadow: "0 8px 10px -2px rgba(0, 0, 0, 0.5)",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
":before": {
|
||||
content: "",
|
||||
top: "0",
|
||||
|
@ -14,7 +15,7 @@ export const gameCover = style({
|
|||
height: "172%",
|
||||
position: "absolute",
|
||||
background:
|
||||
"linear-gradient(35deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 51.5%, rgba(255, 255, 255, 0.15) 54%, rgba(255, 255, 255, 0.15) 100%);",
|
||||
"linear-gradient(35deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 51.5%, rgba(255, 255, 255, 0.15) 54%, rgba(255, 255, 255, 0.15) 100%)",
|
||||
transition: "all ease 0.3s",
|
||||
transform: "translateY(-36%)",
|
||||
opacity: "0.5",
|
||||
|
@ -188,3 +189,15 @@ export const defaultAvatarWrapper = style({
|
|||
border: `solid 1px ${vars.color.border}`,
|
||||
borderRadius: "4px",
|
||||
});
|
||||
|
||||
export const achievementsProgressBar = style({
|
||||
width: "100%",
|
||||
height: "4px",
|
||||
transition: "all ease 0.2s",
|
||||
"::-webkit-progress-bar": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
"::-webkit-progress-value": {
|
||||
backgroundColor: vars.color.muted,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { userProfileContext } from "@renderer/context";
|
||||
import { useContext, useEffect, useMemo } from "react";
|
||||
import { useCallback, useContext, useEffect, useMemo } from "react";
|
||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile-content.css";
|
||||
import { TelescopeIcon } from "@primer/octicons-react";
|
||||
import { ClockIcon, TelescopeIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LockedProfile } from "./locked-profile";
|
||||
|
@ -15,7 +15,11 @@ import { ReportProfile } from "../report-profile/report-profile";
|
|||
import { FriendsBox } from "./friends-box";
|
||||
import { RecentGamesBox } from "./recent-games-box";
|
||||
import { UserGame } from "@types";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
formatDownloadProgress,
|
||||
} from "@renderer/helpers";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
|
||||
export function ProfileContent() {
|
||||
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
||||
|
@ -43,9 +47,25 @@ export function ProfileContent() {
|
|||
const buildUserGameDetailsPath = (game: UserGame) =>
|
||||
buildGameDetailsPath({
|
||||
...game,
|
||||
objectID: game.objectId,
|
||||
objectId: game.objectId,
|
||||
});
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
(playTimeInSeconds = 0) => {
|
||||
const minutes = playTimeInSeconds / 60;
|
||||
|
||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||
return t("amount_minutes", {
|
||||
amount: minutes.toFixed(0),
|
||||
});
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
},
|
||||
[numberFormatter, t]
|
||||
);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (!userProfile) return null;
|
||||
|
||||
|
@ -98,6 +118,7 @@ export function ProfileContent() {
|
|||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
}}
|
||||
className={styles.game}
|
||||
>
|
||||
|
@ -109,13 +130,93 @@ export function ProfileContent() {
|
|||
className={styles.gameCover}
|
||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
background:
|
||||
"linear-gradient(0deg, rgba(0, 0, 0, 0.7) 20%, transparent 100%)",
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<small
|
||||
style={{
|
||||
backgroundColor: vars.color.background,
|
||||
color: vars.color.muted,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
borderRadius: 4,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
padding: "4px",
|
||||
}}
|
||||
>
|
||||
<ClockIcon size={11} />
|
||||
{formatPlayTime(game.playTimeInSeconds)}
|
||||
</small>
|
||||
|
||||
<div
|
||||
style={{
|
||||
color: "white",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 8,
|
||||
color: vars.color.muted,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{game.unlockedAchievementCount} /{" "}
|
||||
{game.achievementCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
game.unlockedAchievementCount /
|
||||
game.achievementCount
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<progress
|
||||
max={1}
|
||||
value={
|
||||
game.unlockedAchievementCount /
|
||||
game.achievementCount
|
||||
}
|
||||
className={styles.achievementsProgressBar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={steamUrlBuilder.cover(game.objectId)}
|
||||
alt={game.title}
|
||||
style={{
|
||||
width: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: 4,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
@ -143,6 +244,7 @@ export function ProfileContent() {
|
|||
userStats,
|
||||
numberFormatter,
|
||||
t,
|
||||
formatPlayTime,
|
||||
navigate,
|
||||
]);
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export function RecentGamesBox() {
|
|||
const buildUserGameDetailsPath = (game: UserGame) =>
|
||||
buildGameDetailsPath({
|
||||
...game,
|
||||
objectID: game.objectId,
|
||||
objectId: game.objectId,
|
||||
});
|
||||
|
||||
if (!userProfile?.recentGames.length) return null;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { style } from "@vanilla-extract/css";
|
|||
export const profileContentBox = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const profileAvatarButton = style({
|
||||
|
@ -69,7 +70,7 @@ export const heroPanel = style({
|
|||
|
||||
export const userInformation = style({
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
|
||||
padding: `${SPACING_UNIT * 6}px ${SPACING_UNIT * 3}px`,
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
|
||||
import * as styles from "./profile-hero.css";
|
||||
import { useCallback, useContext, useMemo, useState } from "react";
|
||||
|
@ -10,6 +10,7 @@ import {
|
|||
PersonAddIcon,
|
||||
PersonIcon,
|
||||
SignOutIcon,
|
||||
UploadIcon,
|
||||
XCircleFillIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
@ -36,8 +37,7 @@ export function ProfileHero() {
|
|||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||
|
||||
const { isMe, heroBackground, getUserProfile, userProfile } =
|
||||
useContext(userProfileContext);
|
||||
const { isMe, getUserProfile, userProfile } = useContext(userProfileContext);
|
||||
const {
|
||||
signOut,
|
||||
updateFriendRequestState,
|
||||
|
@ -48,6 +48,8 @@ export function ProfileHero() {
|
|||
|
||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||
|
||||
const [hero, setHero] = useState("");
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { formatDistance } = useDate();
|
||||
|
||||
|
@ -124,6 +126,7 @@ export function ProfileHero() {
|
|||
theme="outline"
|
||||
onClick={() => setShowEditProfileModal(true)}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
>
|
||||
<PencilIcon />
|
||||
{t("edit_profile")}
|
||||
|
@ -148,6 +151,7 @@ export function ProfileHero() {
|
|||
theme="outline"
|
||||
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
>
|
||||
<PersonAddIcon />
|
||||
{t("add_friend")}
|
||||
|
@ -198,6 +202,7 @@ export function ProfileHero() {
|
|||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||
}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
>
|
||||
<XCircleFillIcon /> {t("cancel_request")}
|
||||
</Button>
|
||||
|
@ -212,11 +217,12 @@ export function ProfileHero() {
|
|||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||
}
|
||||
disabled={isPerformingAction}
|
||||
style={{ borderColor: vars.color.body }}
|
||||
>
|
||||
<CheckCircleFillIcon /> {t("accept_request")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="outline"
|
||||
theme="danger"
|
||||
onClick={() =>
|
||||
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||
}
|
||||
|
@ -246,7 +252,6 @@ export function ProfileHero() {
|
|||
if (gameRunning)
|
||||
return {
|
||||
...gameRunning,
|
||||
objectId: gameRunning.objectID,
|
||||
sessionDurationInSeconds: gameRunning.sessionDurationInMillis / 1000,
|
||||
};
|
||||
|
||||
|
@ -255,6 +260,35 @@ export function ProfileHero() {
|
|||
return userProfile?.currentGame;
|
||||
}, [isMe, userProfile, gameRunning]);
|
||||
|
||||
const handleChangeCoverClick = async () => {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
|
||||
setHero(path);
|
||||
|
||||
// onChange(imagePath);
|
||||
}
|
||||
};
|
||||
|
||||
const getImageUrl = () => {
|
||||
if (hero) return `local:${hero}`;
|
||||
// if (userDetails?.profileImageUrl) return userDetails.profileImageUrl;
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
// const imageUrl = getImageUrl();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <ConfirmationModal
|
||||
|
@ -270,66 +304,104 @@ export function ProfileHero() {
|
|||
onClose={() => setShowEditProfileModal(false)}
|
||||
/>
|
||||
|
||||
<section
|
||||
className={styles.profileContentBox}
|
||||
style={{ background: heroBackground }}
|
||||
>
|
||||
<div className={styles.userInformation}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileAvatarButton}
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
{userProfile?.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile?.displayName}
|
||||
src={userProfile?.profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={72} />
|
||||
)}
|
||||
</button>
|
||||
<section className={styles.profileContentBox}>
|
||||
<img
|
||||
src={getImageUrl()}
|
||||
alt=""
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, rgb(0 0 0 / 70%), rgb(0 0 0 / 60%))",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<div className={styles.userInformation}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.profileAvatarButton}
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
{userProfile?.profileImageUrl ? (
|
||||
<img
|
||||
className={styles.profileAvatar}
|
||||
alt={userProfile?.displayName}
|
||||
src={userProfile?.profileImageUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonIcon size={72} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className={styles.profileInformation}>
|
||||
{userProfile ? (
|
||||
<h2 className={styles.profileDisplayName}>
|
||||
{userProfile?.displayName}
|
||||
</h2>
|
||||
) : (
|
||||
<Skeleton width={150} height={28} />
|
||||
)}
|
||||
<div className={styles.profileInformation}>
|
||||
{userProfile ? (
|
||||
<h2 className={styles.profileDisplayName}>
|
||||
{userProfile?.displayName}
|
||||
</h2>
|
||||
) : (
|
||||
<Skeleton width={150} height={28} />
|
||||
)}
|
||||
|
||||
{currentGame && (
|
||||
<div className={styles.currentGameWrapper}>
|
||||
<div className={styles.currentGameDetails}>
|
||||
<Link
|
||||
to={buildGameDetailsPath({
|
||||
...currentGame,
|
||||
objectID: currentGame.objectId,
|
||||
})}
|
||||
>
|
||||
{currentGame.title}
|
||||
</Link>
|
||||
</div>
|
||||
{currentGame && (
|
||||
<div className={styles.currentGameWrapper}>
|
||||
<div className={styles.currentGameDetails}>
|
||||
<Link
|
||||
to={buildGameDetailsPath({
|
||||
...currentGame,
|
||||
objectId: currentGame.objectID,
|
||||
})}
|
||||
>
|
||||
{currentGame.title}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<small>
|
||||
{t("playing_for", {
|
||||
amount: formatDistance(
|
||||
addSeconds(
|
||||
new Date(),
|
||||
-currentGame.sessionDurationInSeconds
|
||||
<small>
|
||||
{t("playing_for", {
|
||||
amount: formatDistance(
|
||||
addSeconds(
|
||||
new Date(),
|
||||
-currentGame.sessionDurationInSeconds
|
||||
),
|
||||
new Date()
|
||||
),
|
||||
new Date()
|
||||
),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
theme="outline"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
borderColor: vars.color.body,
|
||||
}}
|
||||
onClick={handleChangeCoverClick}
|
||||
>
|
||||
<UploadIcon />
|
||||
Upload cover
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.heroPanel}>
|
||||
<div
|
||||
className={styles.heroPanel}
|
||||
// style={{ background: heroBackground }}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(135deg, rgb(0 0 0 / 70%), rgb(0 0 0 / 60%))",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
|
|
@ -3,20 +3,21 @@ import { formatName } from "@shared";
|
|||
import { GameRepack } from "@types";
|
||||
import flexSearch from "flexsearch";
|
||||
|
||||
const index = new flexSearch.Index();
|
||||
|
||||
const state = {
|
||||
repacks: [] as any[],
|
||||
};
|
||||
|
||||
interface SerializedGameRepack extends Omit<GameRepack, "uris"> {
|
||||
uris: string;
|
||||
}
|
||||
|
||||
const state = {
|
||||
repacks: [] as SerializedGameRepack[],
|
||||
index: null as flexSearch.Index | null,
|
||||
};
|
||||
|
||||
self.onmessage = async (
|
||||
event: MessageEvent<[string, string] | "INDEX_REPACKS">
|
||||
) => {
|
||||
if (event.data === "INDEX_REPACKS") {
|
||||
state.index = new flexSearch.Index();
|
||||
|
||||
repacksTable
|
||||
.toCollection()
|
||||
.sortBy("uploadDate")
|
||||
|
@ -26,7 +27,7 @@ self.onmessage = async (
|
|||
for (let i = 0; i < state.repacks.length; i++) {
|
||||
const repack = state.repacks[i];
|
||||
const formattedTitle = formatName(repack.title);
|
||||
index.add(i, formattedTitle);
|
||||
state.index!.add(i, formattedTitle);
|
||||
}
|
||||
|
||||
self.postMessage("INDEXING_COMPLETE");
|
||||
|
@ -34,7 +35,7 @@ self.onmessage = async (
|
|||
} else {
|
||||
const [requestId, query] = event.data;
|
||||
|
||||
const results = index.search(formatName(query)).map((index) => {
|
||||
const results = state.index!.search(formatName(query)).map((index) => {
|
||||
const repack = state.repacks.at(index as number) as SerializedGameRepack;
|
||||
|
||||
return {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue