mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding artifact limit
This commit is contained in:
parent
2599b332fd
commit
34a33ccef3
24 changed files with 290 additions and 255 deletions
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 }
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -81,10 +81,10 @@ export function BottomPanel() {
|
||||||
<small>{status}</small>
|
<small>{status}</small>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* <small>
|
<small>
|
||||||
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
||||||
{VERSION_CODENAME}"
|
{VERSION_CODENAME}"
|
||||||
</small> */}
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}
|
||||||
|
|
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
|
@ -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 */
|
||||||
|
|
|
@ -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"
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })}>
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Reference in a new issue