feat: adding artifact limit

This commit is contained in:
Chubby Granny Chaser 2024-10-21 19:19:18 +01:00
parent 2599b332fd
commit 34a33ccef3
No known key found for this signature in database
24 changed files with 290 additions and 255 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "hydralauncher", "name": "hydralauncher",
"version": "2.1.7-preview", "version": "3.0.0",
"description": "Hydra", "description": "Hydra",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Los Broxas", "author": "Los Broxas",

View file

@ -148,7 +148,19 @@
"backup_deleted": "Backup deleted", "backup_deleted": "Backup deleted",
"backup_restored": "Backup restored", "backup_restored": "Backup restored",
"see_all_achievements": "See all achievements", "see_all_achievements": "See all achievements",
"sign_in_to_see_achievements": "Sign in to see achievements" "sign_in_to_see_achievements": "Sign in to see achievements",
"mapping_method_automatic": "Automatic",
"mapping_method_manual": "Manual",
"mapping_method_label": "Mapping method",
"files_automatically_mapped": "Files automatically mapped",
"no_backups_created": "No backups created for this game",
"manage_files": "Manage files",
"loading_save_preview": "Searching for save games…",
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "The Wine prefix used to run this game",
"no_download_option_info": "No information available",
"backup_deletion_failed": "Failed to delete backup",
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",
@ -333,7 +345,9 @@
"report_reason_spam": "Spam", "report_reason_spam": "Spam",
"report_reason_other": "Other", "report_reason_other": "Other",
"profile_reported": "Profile reported", "profile_reported": "Profile reported",
"your_friend_code": "Your friend code:" "your_friend_code": "Your friend code:",
"upload_banner": "Upload banner",
"uploading_banner": "Uploading banner…"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Achievement unlocked", "achievement_unlocked": "Achievement unlocked",

View file

@ -5,9 +5,9 @@
}, },
"home": { "home": {
"featured": "Destaques", "featured": "Destaques",
"trending": "Populares", "hot": "Populares",
"hot": "Populares agora",
"weekly": "📅 Mais baixados da semana", "weekly": "📅 Mais baixados da semana",
"achievements": "🏆 Pra platinar",
"surprise_me": "Surpreenda-me", "surprise_me": "Surpreenda-me",
"no_results": "Nenhum resultado encontrado", "no_results": "Nenhum resultado encontrado",
"start_typing": "Comece a digitar para pesquisar…" "start_typing": "Comece a digitar para pesquisar…"
@ -144,7 +144,19 @@
"backup_deleted": "Backup apagado", "backup_deleted": "Backup apagado",
"backup_restored": "Backup restaurado", "backup_restored": "Backup restaurado",
"see_all_achievements": "Ver todas as conquistas", "see_all_achievements": "Ver todas as conquistas",
"sign_in_to_see_achievements": "Faça login para ver as conquistas" "sign_in_to_see_achievements": "Faça login para ver as conquistas",
"mapping_method_automatic": "Automático",
"mapping_method_manual": "Manual",
"mapping_method_label": "Método de mapeamento",
"files_automatically_mapped": "Arquivos automaticamente mapeados",
"no_backups_created": "Nenhum backup criado para este jogo",
"manage_files": "Gerenciar arquivos",
"loading_save_preview": "Buscando por arquivos de salvamento…",
"wine_prefix": "Prefixo Wine",
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
"no_download_option_info": "Sem informações disponíveis",
"backup_deletion_failed": "Falha ao apagar backup",
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",
@ -335,7 +347,9 @@
"report_reason_spam": "Spam", "report_reason_spam": "Spam",
"report_reason_other": "Outro", "report_reason_other": "Outro",
"profile_reported": "Perfil reportado", "profile_reported": "Perfil reportado",
"your_friend_code": "Seu código de amigo:" "your_friend_code": "Seu código de amigo:",
"upload_banner": "Carregar banner",
"uploading_banner": "Carregando banner…"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "Conquista desbloqueada", "achievement_unlocked": "Conquista desbloqueada",

View file

@ -15,7 +15,7 @@ const getCatalogue = async (
}); });
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>( const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
`/games/${category}?${params.toString()}`, `/catalogue/${category}?${params.toString()}`,
{}, {},
{ needsAuth: false } { needsAuth: false }
); );

View file

@ -66,6 +66,7 @@ import "./cloud-save/upload-save-game";
import "./cloud-save/delete-game-artifact"; import "./cloud-save/delete-game-artifact";
import "./notifications/publish-new-repacks-notification"; import "./notifications/publish-new-repacks-notification";
import { isPortableVersion } from "@main/helpers"; import { isPortableVersion } from "@main/helpers";
import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion); ipcMain.handle("getVersion", () => appVersion);

View file

@ -7,9 +7,6 @@ import path from "node:path";
import YAML from "yaml"; import YAML from "yaml";
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath"; import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
import axios from "axios";
let a: Record<string, string> | null = null;
export class Ludusavi { export class Ludusavi {
private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi"); private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi");
@ -65,26 +62,14 @@ export class Ludusavi {
} }
static async getBackupPreview( static async getBackupPreview(
_shop: GameShop, shop: GameShop,
objectId: string, objectId: string,
backupPath: string backupPath: string
): Promise<LudusaviBackup | null> { ): Promise<LudusaviBackup | null> {
if (!a) { const games = await this.findGames(shop, objectId);
await axios
.get(
"https://gist.githubusercontent.com/thegrannychaseroperation/b23d53e654e3ea060066a5c01b0cacc8/raw/57bf254a1c99dab9315136f660ff7b3d547de215/keys.json"
)
.then((response) => {
a = response.data;
return response.data;
});
}
const game = a?.[objectId]; if (!games.length) return null;
const [game] = games;
// if (!games.length) return null;
// const [game] = games;
const backupData = await this.worker.run( const backupData = await this.worker.run(
{ title: game, backupPath, preview: true }, { title: game, backupPath, preview: true },

View file

@ -215,6 +215,8 @@ contextBridge.exposeInMainWorld("electron", {
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
showOpenDialog: (options: Electron.OpenDialogOptions) => showOpenDialog: (options: Electron.OpenDialogOptions) =>
ipcRenderer.invoke("showOpenDialog", options), ipcRenderer.invoke("showOpenDialog", options),
showItemInFolder: (path: string) =>
ipcRenderer.invoke("showItemInFolder", path),
platform: process.platform, platform: process.platform,
/* Auto update */ /* Auto update */

View file

@ -81,10 +81,10 @@ export function BottomPanel() {
<small>{status}</small> <small>{status}</small>
</button> </button>
{/* <small> <small>
{sessionHash ? `${sessionHash} -` : ""} v{version} &quot; {sessionHash ? `${sessionHash} -` : ""} v{version} &quot;
{VERSION_CODENAME}&quot; {VERSION_CODENAME}&quot;
</small> */} </small>
</footer> </footer>
); );
} }

View file

@ -1,6 +1,6 @@
import { Downloader } from "@shared"; import { Downloader } from "@shared";
export const VERSION_CODENAME = "Leviticus"; export const VERSION_CODENAME = "Skyscraper";
export const DOWNLOADER_NAME = { export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid", [Downloader.RealDebrid]: "Real-Debrid",

View file

@ -1,4 +1,3 @@
import { gameBackupsTable } from "@renderer/dexie";
import { useToast } from "@renderer/hooks"; import { useToast } from "@renderer/hooks";
import { logger } from "@renderer/logger"; import { logger } from "@renderer/logger";
import type { LudusaviBackup, GameArtifact, GameShop } from "@types"; import type { LudusaviBackup, GameArtifact, GameShop } from "@types";
@ -7,7 +6,6 @@ import React, {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from "react"; } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -31,8 +29,10 @@ export interface CloudSyncContext {
deleteGameArtifact: (gameArtifactId: string) => Promise<void>; deleteGameArtifact: (gameArtifactId: string) => Promise<void>;
setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>; setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>;
getGameBackupPreview: () => Promise<void>; getGameBackupPreview: () => Promise<void>;
getGameArtifacts: () => Promise<void>;
restoringBackup: boolean; restoringBackup: boolean;
uploadingBackup: boolean; uploadingBackup: boolean;
loadingPreview: boolean;
} }
export const cloudSyncContext = createContext<CloudSyncContext>({ export const cloudSyncContext = createContext<CloudSyncContext>({
@ -47,8 +47,10 @@ export const cloudSyncContext = createContext<CloudSyncContext>({
showCloudSyncFilesModal: false, showCloudSyncFilesModal: false,
setShowCloudSyncFilesModal: () => {}, setShowCloudSyncFilesModal: () => {},
getGameBackupPreview: async () => {}, getGameBackupPreview: async () => {},
getGameArtifacts: async () => {},
restoringBackup: false, restoringBackup: false,
uploadingBackup: false, uploadingBackup: false,
loadingPreview: false,
}); });
const { Provider } = cloudSyncContext; const { Provider } = cloudSyncContext;
@ -67,8 +69,6 @@ export function CloudSyncContextProvider({
}: CloudSyncContextProviderProps) { }: CloudSyncContextProviderProps) {
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
const backupPreviewLock = useRef("");
const [artifacts, setArtifacts] = useState<GameArtifact[]>([]); const [artifacts, setArtifacts] = useState<GameArtifact[]>([]);
const [showCloudSyncModal, setShowCloudSyncModal] = useState(false); const [showCloudSyncModal, setShowCloudSyncModal] = useState(false);
const [backupPreview, setBackupPreview] = useState<LudusaviBackup | null>( const [backupPreview, setBackupPreview] = useState<LudusaviBackup | null>(
@ -77,6 +77,7 @@ export function CloudSyncContextProvider({
const [restoringBackup, setRestoringBackup] = useState(false); const [restoringBackup, setRestoringBackup] = useState(false);
const [uploadingBackup, setUploadingBackup] = useState(false); const [uploadingBackup, setUploadingBackup] = useState(false);
const [showCloudSyncFilesModal, setShowCloudSyncFilesModal] = useState(false); const [showCloudSyncFilesModal, setShowCloudSyncFilesModal] = useState(false);
const [loadingPreview, setLoadingPreview] = useState(false);
const { showSuccessToast } = useToast(); const { showSuccessToast } = useToast();
@ -88,32 +89,25 @@ export function CloudSyncContextProvider({
[objectId, shop] [objectId, shop]
); );
const getGameBackupPreview = useCallback(async () => { const getGameArtifacts = useCallback(async () => {
const backupPreviewLockKey = `${objectId}-${shop}`; const results = await window.electron.getGameArtifacts(objectId, shop);
setArtifacts(results);
}, [objectId, shop]);
if (backupPreviewLock.current !== backupPreviewLockKey) { const getGameBackupPreview = useCallback(async () => {
backupPreviewLock.current = backupPreviewLockKey; setLoadingPreview(true);
await Promise.allSettled([
window.electron.getGameArtifacts(objectId, shop).then((results) => { try {
setArtifacts(results); const preview = await window.electron.getGameBackupPreview(
}), objectId,
window.electron shop
.getGameBackupPreview(objectId, shop) );
.then((preview) => {
backupPreviewLock.current = ""; setBackupPreview(preview);
if (preview && Object.keys(preview.games).length) { } catch (err) {
setBackupPreview(preview); logger.error("Failed to get game backup preview", objectId, shop, err);
} } finally {
}) setLoadingPreview(false);
.catch((err) => {
logger.error(
"Failed to get game backup preview",
objectId,
shop,
err
);
}),
]);
} }
}, [objectId, shop]); }, [objectId, shop]);
@ -131,14 +125,8 @@ export function CloudSyncContextProvider({
shop, shop,
() => { () => {
showSuccessToast(t("backup_uploaded")); showSuccessToast(t("backup_uploaded"));
setUploadingBackup(false); setUploadingBackup(false);
gameBackupsTable.add({ getGameArtifacts();
objectId,
shop,
createdAt: new Date(),
});
getGameBackupPreview(); getGameBackupPreview();
} }
); );
@ -148,6 +136,7 @@ export function CloudSyncContextProvider({
showSuccessToast(t("backup_restored")); showSuccessToast(t("backup_restored"));
setRestoringBackup(false); setRestoringBackup(false);
getGameArtifacts();
getGameBackupPreview(); getGameBackupPreview();
}); });
@ -155,15 +144,23 @@ export function CloudSyncContextProvider({
removeUploadCompleteListener(); removeUploadCompleteListener();
removeDownloadCompleteListener(); removeDownloadCompleteListener();
}; };
}, [objectId, shop, showSuccessToast, t, getGameBackupPreview]); }, [
objectId,
shop,
showSuccessToast,
t,
getGameBackupPreview,
getGameArtifacts,
]);
const deleteGameArtifact = useCallback( const deleteGameArtifact = useCallback(
async (gameArtifactId: string) => { async (gameArtifactId: string) => {
return window.electron.deleteGameArtifact(gameArtifactId).then(() => { return window.electron.deleteGameArtifact(gameArtifactId).then(() => {
getGameBackupPreview(); getGameBackupPreview();
getGameArtifacts();
}); });
}, },
[getGameBackupPreview] [getGameBackupPreview, getGameArtifacts]
); );
useEffect(() => { useEffect(() => {
@ -194,12 +191,14 @@ export function CloudSyncContextProvider({
restoringBackup, restoringBackup,
uploadingBackup, uploadingBackup,
showCloudSyncFilesModal, showCloudSyncFilesModal,
loadingPreview,
setShowCloudSyncModal, setShowCloudSyncModal,
uploadSaveGame, uploadSaveGame,
downloadGameArtifact, downloadGameArtifact,
deleteGameArtifact, deleteGameArtifact,
setShowCloudSyncFilesModal, setShowCloudSyncFilesModal,
getGameBackupPreview, getGameBackupPreview,
getGameArtifacts,
}} }}
> >
{children} {children}

View file

@ -171,6 +171,7 @@ declare global {
showOpenDialog: ( showOpenDialog: (
options: Electron.OpenDialogOptions options: Electron.OpenDialogOptions
) => Promise<Electron.OpenDialogReturnValue>; ) => Promise<Electron.OpenDialogReturnValue>;
showItemInFolder: (path: string) => Promise<void>;
platform: NodeJS.Platform; platform: NodeJS.Platform;
/* Auto update */ /* Auto update */

View file

@ -1,13 +1,6 @@
import type { GameShop, HowLongToBeatCategory } from "@types"; import type { GameShop, HowLongToBeatCategory } from "@types";
import { Dexie } from "dexie"; import { Dexie } from "dexie";
export interface GameBackup {
id?: number;
shop: GameShop;
objectId: string;
createdAt: Date;
}
export interface HowLongToBeatEntry { export interface HowLongToBeatEntry {
id?: number; id?: number;
objectId: string; objectId: string;
@ -22,13 +15,11 @@ export const db = new Dexie("Hydra");
db.version(4).stores({ db.version(4).stores({
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`, repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`, downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
gameBackups: `++id, [shop+objectId], createdAt`,
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`, howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
}); });
export const downloadSourcesTable = db.table("downloadSources"); export const downloadSourcesTable = db.table("downloadSources");
export const repacksTable = db.table("repacks"); export const repacksTable = db.table("repacks");
export const gameBackupsTable = db.table<GameBackup>("gameBackups");
export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>( export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>(
"howLongToBeatEntries" "howLongToBeatEntries"
); );

View file

@ -14,7 +14,6 @@ import type {
UserDetails, UserDetails,
} from "@types"; } from "@types";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import { gameBackupsTable } from "@renderer/dexie";
export function useUserDetails() { export function useUserDetails() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -33,7 +32,6 @@ export function useUserDetails() {
dispatch(setUserDetails(null)); dispatch(setUserDetails(null));
dispatch(setProfileBackground(null)); dispatch(setProfileBackground(null));
await gameBackupsTable.clear();
window.localStorage.removeItem("userDetails"); window.localStorage.removeItem("userDetails");
}, [dispatch]); }, [dispatch]);
@ -130,7 +128,7 @@ export function useUserDetails() {
const unblockUser = (userId: string) => window.electron.unblockUser(userId); const unblockUser = (userId: string) => window.electron.unblockUser(userId);
const hasActiveSubscription = useMemo(() => { const hasActiveSubscription = useMemo(() => {
if (!userDetails?.subscription) { if (!userDetails?.subscription?.plan) {
return false; return false;
} }

View file

@ -1,26 +1,9 @@
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../../theme.css"; import { SPACING_UNIT } from "../../../theme.css";
export const artifacts = style({ export const mappingMethods = style({
display: "flex", display: "grid",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
flexDirection: "column", gridTemplateColumns: "repeat(2, 1fr)",
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",
}); });

View file

@ -1,18 +1,34 @@
import { Modal, ModalProps } from "@renderer/components"; import { Button, Modal, ModalProps } from "@renderer/components";
import { useContext, useMemo } from "react"; import { useContext, useMemo, useState } from "react";
import { cloudSyncContext } from "@renderer/context"; import { cloudSyncContext } from "@renderer/context";
// import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CheckCircleFillIcon } from "@primer/octicons-react";
import * as styles from "./cloud-sync-files-modal.css";
import { formatBytes } from "@shared";
import { vars } from "@renderer/theme.css";
// import { useToast } from "@renderer/hooks";
export interface CloudSyncFilesModalProps export interface CloudSyncFilesModalProps
extends Omit<ModalProps, "children" | "title"> {} extends Omit<ModalProps, "children" | "title"> {}
export enum FileMappingMethod {
Automatic = "AUTOMATIC",
Manual = "MANUAL",
}
export function CloudSyncFilesModal({ export function CloudSyncFilesModal({
visible, visible,
onClose, onClose,
}: CloudSyncFilesModalProps) { }: CloudSyncFilesModalProps) {
const [selectedFileMappingMethod, setSelectedFileMappingMethod] =
useState<FileMappingMethod>(FileMappingMethod.Automatic);
const { backupPreview } = useContext(cloudSyncContext); const { backupPreview } = useContext(cloudSyncContext);
// const { gameTitle } = useContext(gameDetailsContext);
// const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
// const { showSuccessToast } = useToast();
const files = useMemo(() => { const files = useMemo(() => {
if (!backupPreview) { if (!backupPreview) {
@ -20,6 +36,7 @@ export function CloudSyncFilesModal({
} }
const [game] = Object.values(backupPreview.games); const [game] = Object.values(backupPreview.games);
if (!game) return [];
const entries = Object.entries(game.files); const entries = Object.entries(game.files);
return entries.map(([key, value]) => { return entries.map(([key, value]) => {
@ -27,6 +44,19 @@ export function CloudSyncFilesModal({
}); });
}, [backupPreview]); }, [backupPreview]);
// const handleAddCustomPathClick = useCallback(async () => {
// const { filePaths } = await window.electron.showOpenDialog({
// properties: ["openDirectory"],
// });
// if (filePaths && filePaths.length > 0) {
// const path = filePaths[0];
// await window.electron.selectGameBackupDirectory(gameTitle, path);
// showSuccessToast("custom_backup_location_set");
// getGameBackupPreview();
// }
// }, [gameTitle, showSuccessToast, getGameBackupPreview]);
return ( return (
<Modal <Modal
visible={visible} visible={visible}
@ -34,61 +64,80 @@ export function CloudSyncFilesModal({
description="Escolha quais diretórios serão sincronizados" description="Escolha quais diretórios serão sincronizados"
onClose={onClose} onClose={onClose}
> >
{/* <div> <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{["AUTOMATIC", "CUSTOM"].map((downloader) => ( <span>{t("mapping_method_label")}</span>
<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> */}
{/* <TextField <div className={styles.mappingMethods}>
// value={game.executablePath || ""} {Object.values(FileMappingMethod).map((mappingMethod) => (
readOnly <Button
theme="dark" key={mappingMethod}
disabled theme={
placeholder={t("no_directory_selected")} selectedFileMappingMethod === mappingMethod
rightContent={ ? "primary"
<Button : "outline"
type="button" }
theme="outline" onClick={() => setSelectedFileMappingMethod(mappingMethod)}
onClick={handleChangeExecutableLocation} disabled={mappingMethod === FileMappingMethod.Manual}
> >
{t("select_directory")} {selectedFileMappingMethod === mappingMethod && (
</Button> <CheckCircleFillIcon />
} )}
/> */} {t(`mapping_method_${mappingMethod.toLowerCase()}`)}
</Button>
<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> </div>
</table> </div>
<div style={{ marginTop: 16 }}>
{/* <TextField
readOnly
theme="dark"
disabled
placeholder={t("select_folder")}
rightContent={
<Button
type="button"
theme="outline"
onClick={handleAddCustomPathClick}
>
<FileDirectoryIcon />
{t("select_executable")}
</Button>
}
/> */}
<p>{t("files_automatically_mapped")}</p>
<ul
style={{
listStyle: "none",
margin: 0,
padding: 0,
display: "flex",
flexDirection: "column",
gap: 8,
marginTop: 16,
}}
>
{files.map((file) => (
<li key={file.path} style={{ display: "flex" }}>
<button
style={{
flex: 1,
color: vars.color.muted,
textDecoration: "underline",
display: "flex",
cursor: "pointer",
}}
onClick={() => window.electron.showItemInFolder(file.path)}
>
{file.path.split("/").at(-1)}
</button>
<p>{formatBytes(file.bytes)}</p>
</li>
))}
</ul>
</div>
</Modal> </Modal>
); );
} }

View file

@ -49,3 +49,17 @@ export const progress = style({
backgroundColor: vars.color.muted, backgroundColor: vars.color.muted,
}, },
}); });
export const manageFilesButton = style({
margin: "0",
padding: "0",
alignSelf: "flex-start",
fontSize: 14,
cursor: "pointer",
textDecoration: "underline",
color: vars.color.body,
":disabled": {
cursor: "not-allowed",
opacity: vars.opacity.disabled,
},
});

View file

@ -6,7 +6,6 @@ import * as styles from "./cloud-sync-modal.css";
import { formatBytes } from "@shared"; import { formatBytes } from "@shared";
import { format } from "date-fns"; import { format } from "date-fns";
import { import {
CheckCircleFillIcon,
ClockIcon, ClockIcon,
DeviceDesktopIcon, DeviceDesktopIcon,
HistoryIcon, HistoryIcon,
@ -16,18 +15,16 @@ import {
UploadIcon, UploadIcon,
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import { useToast } from "@renderer/hooks"; import { useToast } from "@renderer/hooks";
import { GameBackup, gameBackupsTable } from "@renderer/dexie";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AxiosProgressEvent } from "axios"; import { AxiosProgressEvent } from "axios";
import { formatDownloadProgress } from "@renderer/helpers"; import { formatDownloadProgress } from "@renderer/helpers";
import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
export interface CloudSyncModalProps export interface CloudSyncModalProps
extends Omit<ModalProps, "children" | "title"> {} extends Omit<ModalProps, "children" | "title"> {}
export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
const [deletingArtifact, setDeletingArtifact] = useState(false); const [deletingArtifact, setDeletingArtifact] = useState(false);
const [lastBackup, setLastBackup] = useState<GameBackup | null>(null);
const [backupDownloadProgress, setBackupDownloadProgress] = const [backupDownloadProgress, setBackupDownloadProgress] =
useState<AxiosProgressEvent | null>(null); useState<AxiosProgressEvent | null>(null);
@ -38,6 +35,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
backupPreview, backupPreview,
uploadingBackup, uploadingBackup,
restoringBackup, restoringBackup,
loadingPreview,
uploadSaveGame, uploadSaveGame,
downloadGameArtifact, downloadGameArtifact,
deleteGameArtifact, deleteGameArtifact,
@ -64,11 +62,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
}; };
useEffect(() => { useEffect(() => {
gameBackupsTable
.where({ shop: shop, objectId })
.last()
.then((lastBackup) => setLastBackup(lastBackup || null));
const removeBackupDownloadProgressListener = const removeBackupDownloadProgressListener =
window.electron.onBackupDownloadProgress( window.electron.onBackupDownloadProgress(
objectId!, objectId!,
@ -111,20 +104,19 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
); );
} }
if (lastBackup) { if (loadingPreview) {
return ( return (
<span style={{ display: "flex", alignItems: "center", gap: 8 }}> <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
<i style={{ color: vars.color.success }}> <SyncIcon className={styles.syncIcon} />
<CheckCircleFillIcon /> {t("loading_save_preview")}
</i>
{t("last_backup_date", {
date: format(lastBackup.createdAt, "dd/MM/yyyy HH:mm"),
})}
</span> </span>
); );
} }
if (artifacts.length >= 2) {
return t("max_number_of_artifacts_reached");
}
if (!backupPreview) { if (!backupPreview) {
return t("no_backup_preview"); return t("no_backup_preview");
} }
@ -133,93 +125,76 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
}, [ }, [
uploadingBackup, uploadingBackup,
backupDownloadProgress?.progress, backupDownloadProgress?.progress,
lastBackup,
backupPreview, backupPreview,
restoringBackup, restoringBackup,
loadingPreview,
artifacts,
t, t,
]); ]);
const disableActions = uploadingBackup || restoringBackup || deletingArtifact; const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
return ( return (
<> <Modal
{/* <ConfirmationModal visible={visible}
confirmButtonLabel="confirm" title={t("cloud_save")}
cancelButtonLabel="cancel" description={t("cloud_save_description")}
descriptionText="description" onClose={onClose}
title="title" large
onConfirm={() => {}} >
onClose={() => {}} <div
visible style={{
/> */} marginBottom: 24,
display: "flex",
<Modal justifyContent: "space-between",
visible={visible} alignItems: "center",
title={t("cloud_save")} }}
description={t("cloud_save_description")}
onClose={onClose}
large
> >
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
<h2>{gameTitle}</h2>
<p>{backupStateLabel}</p>
<button
type="button"
className={styles.manageFilesButton}
onClick={() => setShowCloudSyncFilesModal(true)}
disabled={disableActions || !backupPreview?.overall.totalGames}
>
{t("manage_files")}
</button>
</div>
<Button
type="button"
onClick={() => uploadSaveGame(lastDownloadedOption?.title ?? null)}
disabled={
disableActions ||
!backupPreview?.overall.totalGames ||
artifacts.length >= 2
}
>
<UploadIcon />
{t("create_backup")}
</Button>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div <div
style={{ style={{
marginBottom: 24, marginBottom: 16,
display: "flex", display: "flex",
justifyContent: "space-between",
alignItems: "center", alignItems: "center",
gap: SPACING_UNIT,
}} }}
> >
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}> <h2>{t("backups")}</h2>
<h2>{gameTitle}</h2> <small>{artifacts.length} / 2</small>
<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(lastDownloadedOption?.title ?? null)}
disabled={disableActions || !backupPreview}
>
<UploadIcon />
{t("create_backup")}
</Button>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div
style={{
marginBottom: 16,
display: "flex",
alignItems: "center",
gap: SPACING_UNIT,
}}
>
<h2>{t("backups")}</h2>
<small>{artifacts.length} / 2</small>
</div>
<div style={{ display: "flex", flexDirection: "column" }}>
<small>Espaço usado</small>
<progress className={styles.progress} />
</div>
</div> </div>
</div>
{artifacts.length > 0 ? (
<ul className={styles.artifacts}> <ul className={styles.artifacts}>
{artifacts.map((artifact) => ( {artifacts.map((artifact, index) => (
<li key={artifact.id} className={styles.artifactButton}> <li key={artifact.id} className={styles.artifactButton}>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}> <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<div <div
@ -230,7 +205,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
marginBottom: 4, marginBottom: 4,
}} }}
> >
<h3>Backup do dia {format(artifact.createdAt, "dd/MM")}</h3> <h3>{t("backup_title", { number: index + 1 })}</h3>
<small>{formatBytes(artifact.artifactLengthInBytes)}</small> <small>{formatBytes(artifact.artifactLengthInBytes)}</small>
</div> </div>
@ -272,7 +247,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
</li> </li>
))} ))}
</ul> </ul>
</Modal> ) : (
</> <p>{t("no_backups_created")}</p>
)}
</Modal>
); );
} }

View file

@ -36,7 +36,7 @@ export function GameDetailsContent() {
const { userDetails } = useUserDetails(); const { userDetails } = useUserDetails();
const { setShowCloudSyncModal, getGameBackupPreview } = const { setShowCloudSyncModal, getGameBackupPreview, getGameArtifacts } =
useContext(cloudSyncContext); useContext(cloudSyncContext);
const aboutTheGame = useMemo(() => { const aboutTheGame = useMemo(() => {
@ -108,7 +108,8 @@ export function GameDetailsContent() {
useEffect(() => { useEffect(() => {
getGameBackupPreview(); getGameBackupPreview();
}, [getGameBackupPreview]); getGameArtifacts();
}, [getGameBackupPreview, getGameArtifacts]);
return ( return (
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}> <div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>

View file

@ -34,6 +34,7 @@ export default function Home() {
>({ >({
[CatalogueCategory.Hot]: [], [CatalogueCategory.Hot]: [],
[CatalogueCategory.Weekly]: [], [CatalogueCategory.Weekly]: [],
[CatalogueCategory.Achievements]: [],
}); });
const getCatalogue = useCallback((category: CatalogueCategory) => { const getCatalogue = useCallback((category: CatalogueCategory) => {

View file

@ -52,8 +52,7 @@ export const profileDisplayName = style({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
position: "relative", position: "relative",
textShadow: textShadow: "0 0 5px rgb(0 0 0 / 40%)",
"0 0 40px rgb(0 0 0), 0 0 20px rgb(0 0 0 / 50%), 0 0 10px rgb(0 0 0 / 20%)",
}); });
export const heroPanel = style({ export const heroPanel = style({

View file

@ -5,11 +5,14 @@ import { userProfileContext } from "@renderer/context";
import * as styles from "./upload-background-image-button.css"; import * as styles from "./upload-background-image-button.css";
import { useToast, useUserDetails } from "@renderer/hooks"; import { useToast, useUserDetails } from "@renderer/hooks";
import { useTranslation } from "react-i18next";
export function UploadBackgroundImageButton() { export function UploadBackgroundImageButton() {
const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] = const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] =
useState(false); useState(false);
const { hasActiveSubscription } = useUserDetails(); const { userDetails } = useUserDetails();
const { t } = useTranslation("user_profile");
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext); const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
const { patchUser, fetchUserDetails } = useUserDetails(); const { patchUser, fetchUserDetails } = useUserDetails();
@ -44,7 +47,8 @@ export function UploadBackgroundImageButton() {
} }
}; };
if (!isMe || !hasActiveSubscription) return null; if (!isMe || !userDetails?.subscription) return null;
if (userDetails.subscription.plan.name !== "plus") return null;
return ( return (
<Button <Button
@ -54,7 +58,7 @@ export function UploadBackgroundImageButton() {
disabled={isUploadingBackgroundImage} disabled={isUploadingBackgroundImage}
> >
<UploadIcon /> <UploadIcon />
{isUploadingBackgroundImage ? "Uploading..." : "Atualizar banner"} {isUploadingBackgroundImage ? t("uploading_banner") : t("upload_banner")}
</Button> </Button>
); );
} }

View file

@ -14,6 +14,7 @@ export enum DownloadSourceStatus {
export enum CatalogueCategory { export enum CatalogueCategory {
Hot = "hot", Hot = "hot",
Weekly = "weekly", Weekly = "weekly",
Achievements = "achievements",
} }
export enum SteamContentDescriptor { export enum SteamContentDescriptor {

View file

@ -237,7 +237,7 @@ export type SubscriptionStatus = "active" | "pending" | "cancelled";
export interface Subscription { export interface Subscription {
id: string; id: string;
status: SubscriptionStatus; status: SubscriptionStatus;
plan: { id: string; name: string }; plan: { id: string; name: "basic" | "plus" };
expiresAt: Date | null; expiresAt: Date | null;
} }

View file

@ -1,6 +1,7 @@
export interface LudusaviScanChange { export interface LudusaviScanChange {
change: "New" | "Different" | "Removed" | "Same" | "Unknown"; change: "New" | "Different" | "Removed" | "Same" | "Unknown";
decision: "Processed" | "Cancelled" | "Ignore"; decision: "Processed" | "Cancelled" | "Ignore";
bytes: number;
} }
export interface LudusaviGame extends LudusaviScanChange { export interface LudusaviGame extends LudusaviScanChange {