mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-02 16:23:48 +03:00
docs: moving readme
This commit is contained in:
commit
55a92fd68a
26
README.md
26
README.md
@ -13,20 +13,20 @@
|
|||||||
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
|
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
|
||||||
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
|
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
|
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](./README.pt-BR.md)
|
||||||
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
|
[![en](https://img.shields.io/badge/lang-en-red.svg)](./README.md)
|
||||||
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
|
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](./README.ru.md)
|
||||||
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
|
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](./README.uk-UA.md)
|
||||||
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
|
[![be](https://img.shields.io/badge/lang-be-orange)](./README.be.md)
|
||||||
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
|
[![es](https://img.shields.io/badge/lang-es-red)](./README.es.md)
|
||||||
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
|
[![fr](https://img.shields.io/badge/lang-fr-blue)](./README.fr.md)
|
||||||
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
|
[![de](https://img.shields.io/badge/lang-de-black)](./README.de.md)
|
||||||
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
|
[![ita](https://img.shields.io/badge/lang-it-red)](./README.it.md)
|
||||||
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
|
[![cs](https://img.shields.io/badge/lang-cs-purple)](./README.cs.md)
|
||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](./README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](./README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Katalog](./docs/screenshot.png)
|
![Hydra Katalog](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -25,7 +25,7 @@
|
|||||||
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
|
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
|
||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Katalog](./docs/screenshot.png)
|
![Hydra Katalog](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Catalogue Hydra](./docs/screenshot.png)
|
![Catalogue Hydra](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -25,7 +25,7 @@
|
|||||||
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
|
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,7 +26,7 @@
|
|||||||
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
|
||||||
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
|
||||||
|
|
||||||
![Hydra Catalogue](./docs/screenshot.png)
|
![Hydra Catalogue](./screenshot.png)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
appId: site.hydralauncher.hydra
|
appId: gg.hydralauncher.hydra
|
||||||
productName: Hydra
|
productName: Hydra
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
libtorrent
|
libtorrent
|
||||||
cx_Freeze
|
cx_Freeze
|
||||||
cx_Logging; sys_platform == 'win32'
|
cx_Logging; sys_platform == 'win32'
|
||||||
lief; sys_platform == 'win32'
|
|
||||||
pywin32; sys_platform == 'win32'
|
pywin32; sys_platform == 'win32'
|
||||||
psutil
|
psutil
|
||||||
Pillow
|
Pillow
|
||||||
|
requests
|
||||||
|
@ -136,14 +136,15 @@
|
|||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
"install_backup": "Install",
|
"install_backup": "Install",
|
||||||
"delete_backup": "Delete",
|
"delete_backup": "Delete",
|
||||||
"create_backup": "Create backup",
|
"create_backup": "New backup",
|
||||||
"last_backup_date": "Last backup on {{date}}",
|
"last_backup_date": "Last backup on {{date}}",
|
||||||
"no_backup_preview": "Hydra could not locate any save games for this game",
|
"no_backup_preview": "No save games were found for this title",
|
||||||
"restoring_backup": "Restoring backup…",
|
"restoring_backup": "Restoring backup ({{progress}} complete)…",
|
||||||
"uploading_backup": "Uploading backup…",
|
"uploading_backup": "Uploading backup…",
|
||||||
"no_backups": "You haven't created any backups for this game yet",
|
"no_backups": "You haven't created any backups for this game yet",
|
||||||
"backup_uploaded": "Backup uploaded",
|
"backup_uploaded": "Backup uploaded",
|
||||||
"backup_deleted": "Backup deleted"
|
"backup_deleted": "Backup deleted",
|
||||||
|
"backup_restored": "Backup restored"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
@ -132,14 +132,15 @@
|
|||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
"install_backup": "Restaurar",
|
"install_backup": "Restaurar",
|
||||||
"delete_backup": "Apagar",
|
"delete_backup": "Apagar",
|
||||||
"create_backup": "Criar backup",
|
"create_backup": "Novo backup",
|
||||||
"last_backup_date": "Último backup em {{date}}",
|
"last_backup_date": "Último backup em {{date}}",
|
||||||
"no_backup_preview": "Hydra não encontrou nenhum save para este jogo",
|
"no_backup_preview": "Não foi possível encontrar nenhum salvamento para este jogo",
|
||||||
"restoring_backup": "Restaurando backup…",
|
"restoring_backup": "Restaurando backup ({{progress}} concluído)…",
|
||||||
"uploading_backup": "Criando backup…",
|
"uploading_backup": "Criando backup…",
|
||||||
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
||||||
"backup_uploaded": "Backup criado",
|
"backup_uploaded": "Backup criado",
|
||||||
"backup_deleted": "Backup apagado"
|
"backup_deleted": "Backup apagado",
|
||||||
|
"backup_restored": "Backup restaurado"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@ -181,7 +182,7 @@
|
|||||||
"enable_download_notifications": "Quando um download for concluído",
|
"enable_download_notifications": "Quando um download for concluído",
|
||||||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||||
"real_debrid_api_token_label": "Token de API do Real-Debrid",
|
"real_debrid_api_token_label": "Token de API do Real-Debrid",
|
||||||
"quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar.",
|
"quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar",
|
||||||
"launch_with_system": "Iniciar o Hydra junto com o sistema",
|
"launch_with_system": "Iniciar o Hydra junto com o sistema",
|
||||||
"general": "Geral",
|
"general": "Geral",
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
|
@ -157,7 +157,7 @@
|
|||||||
"enable_download_notifications": "Quando uma transferência for concluída",
|
"enable_download_notifications": "Quando uma transferência for concluída",
|
||||||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||||
"real_debrid_api_token_label": "Token de API do Real-Debrid",
|
"real_debrid_api_token_label": "Token de API do Real-Debrid",
|
||||||
"quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar.",
|
"quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar",
|
||||||
"launch_with_system": "Iniciar o Hydra com o sistema",
|
"launch_with_system": "Iniciar o Hydra com o sistema",
|
||||||
"general": "Geral",
|
"general": "Geral",
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
|
@ -3,32 +3,15 @@ import { shuffle } from "lodash-es";
|
|||||||
import { getSteam250List } from "@main/services";
|
import { getSteam250List } from "@main/services";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
// import { getSteamGameById } from "../helpers/search-games";
|
|
||||||
import type { Steam250Game } from "@types";
|
import type { Steam250Game } from "@types";
|
||||||
|
|
||||||
const state = { games: Array<Steam250Game>(), index: 0 };
|
const state = { games: Array<Steam250Game>(), index: 0 };
|
||||||
|
|
||||||
const filterGames = async (_games: Steam250Game[]) => {
|
|
||||||
const results: Steam250Game[] = [];
|
|
||||||
|
|
||||||
// for (const game of games) {
|
|
||||||
// const steamGame = await getSteamGameById(game.objectID);
|
|
||||||
|
|
||||||
// if (steamGame?.repacks.length) {
|
|
||||||
// results.push(game);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
if (state.games.length == 0) {
|
if (state.games.length == 0) {
|
||||||
const steam250List = await getSteam250List();
|
const steam250List = await getSteam250List();
|
||||||
|
|
||||||
const filteredSteam250List = await filterGames(steam250List);
|
state.games = shuffle(steam250List);
|
||||||
|
|
||||||
state.games = shuffle(filteredSteam250List);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.games.length == 0) {
|
if (state.games.length == 0) {
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { knexClient } from "@main/knex-client";
|
|
||||||
|
|
||||||
const getRepacks = (_event: Electron.IpcMainInvokeEvent) =>
|
|
||||||
knexClient.select("*").from("repack");
|
|
||||||
|
|
||||||
registerEvent("getRepacks", getRepacks);
|
|
@ -1,7 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||||
import { CatalogueEntry } from "@types";
|
import { CatalogueEntry } from "@types";
|
||||||
import { HydraApi, RepacksManager } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
const searchGamesEvent = async (
|
const searchGamesEvent = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -11,15 +11,13 @@ const searchGamesEvent = async (
|
|||||||
{ objectId: string; title: string; shop: string }[]
|
{ objectId: string; title: string; shop: string }[]
|
||||||
>("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false });
|
>("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false });
|
||||||
|
|
||||||
const steamGames = games.map((game) => {
|
return games.map((game) => {
|
||||||
return convertSteamGameToCatalogueEntry({
|
return convertSteamGameToCatalogueEntry({
|
||||||
id: Number(game.objectId),
|
id: Number(game.objectId),
|
||||||
name: game.title,
|
name: game.title,
|
||||||
clientIcon: null,
|
clientIcon: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return RepacksManager.findRepacksForCatalogueEntries(steamGames);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("searchGames", searchGamesEvent);
|
registerEvent("searchGames", searchGamesEvent);
|
||||||
|
@ -19,13 +19,19 @@ const downloadGameArtifact = async (
|
|||||||
objectKey: string;
|
objectKey: string;
|
||||||
}>(`/games/artifacts/${gameArtifactId}/download`);
|
}>(`/games/artifacts/${gameArtifactId}/download`);
|
||||||
|
|
||||||
const response = await axios.get(downloadUrl, {
|
|
||||||
responseType: "stream",
|
|
||||||
});
|
|
||||||
|
|
||||||
const zipLocation = path.join(app.getPath("userData"), objectKey);
|
const zipLocation = path.join(app.getPath("userData"), objectKey);
|
||||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||||
|
|
||||||
|
const response = await axios.get(downloadUrl, {
|
||||||
|
responseType: "stream",
|
||||||
|
onDownloadProgress: (progressEvent) => {
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-backup-download-progress-${objectId}-${shop}`,
|
||||||
|
progressEvent
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const writer = fs.createWriteStream(zipLocation);
|
const writer = fs.createWriteStream(zipLocation);
|
||||||
|
|
||||||
response.data.pipe(writer);
|
response.data.pipe(writer);
|
||||||
@ -45,7 +51,7 @@ const downloadGameArtifact = async (
|
|||||||
|
|
||||||
Ludusavi.restoreBackup(backupPath).then(() => {
|
Ludusavi.restoreBackup(backupPath).then(() => {
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
`on-download-complete-${objectId}-${shop}`,
|
`on-backup-download-complete-${objectId}-${shop}`,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -73,26 +73,31 @@ const uploadSaveGame = async (
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
axios.put(uploadUrl, fileBuffer, {
|
await axios.put(uploadUrl, fileBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/zip",
|
"Content-Type": "application/zip",
|
||||||
},
|
},
|
||||||
onUploadProgress: (progressEvent) => {
|
onUploadProgress: (progressEvent) => {
|
||||||
if (progressEvent.progress === 1) {
|
console.log(progressEvent);
|
||||||
fs.rm(zipLocation, (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error("Failed to remove zip file", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
|
||||||
`on-upload-complete-${objectId}-${shop}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-upload-complete-${objectId}-${shop}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// fs.rm(zipLocation, (err) => {
|
||||||
|
// if (err) {
|
||||||
|
// logger.error("Failed to remove zip file", err);
|
||||||
|
// throw err;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// WindowManager.mainWindow?.webContents.send(
|
||||||
|
// `on-upload-complete-${objectId}-${shop}`,
|
||||||
|
// true
|
||||||
|
// );
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { knexClient } from "@main/knex-client";
|
||||||
|
|
||||||
|
const deleteDownloadSource = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
id: number
|
||||||
|
) => knexClient("download_source").where({ id }).delete();
|
||||||
|
|
||||||
|
registerEvent("deleteDownloadSource", deleteDownloadSource);
|
@ -1,13 +0,0 @@
|
|||||||
import { downloadSourcesWorker } from "@main/workers";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import type { DownloadSource } from "@types";
|
|
||||||
|
|
||||||
const syncDownloadSources = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
downloadSources: DownloadSource[]
|
|
||||||
) =>
|
|
||||||
downloadSourcesWorker.run(downloadSources, {
|
|
||||||
name: "getUpdatedRepacks",
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEvent("syncDownloadSources", syncDownloadSources);
|
|
@ -1,12 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { downloadSourcesWorker } from "@main/workers";
|
|
||||||
|
|
||||||
const validateDownloadSource = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
url: string
|
|
||||||
) =>
|
|
||||||
downloadSourcesWorker.run(url, {
|
|
||||||
name: "validateDownloadSource",
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEvent("validateDownloadSource", validateDownloadSource);
|
|
@ -1,7 +1,6 @@
|
|||||||
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { RepacksManager } from "@main/services";
|
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
export interface SearchGamesArgs {
|
export interface SearchGamesArgs {
|
||||||
@ -28,9 +27,5 @@ export const getSteamGameById = async (
|
|||||||
|
|
||||||
if (!steamGame) return null;
|
if (!steamGame) return null;
|
||||||
|
|
||||||
const catalogueEntry = convertSteamGameToCatalogueEntry(steamGame);
|
return convertSteamGameToCatalogueEntry(steamGame);
|
||||||
|
|
||||||
const result = RepacksManager.findRepacksForCatalogueEntry(catalogueEntry);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const downloadSourceSchema = z.object({
|
|
||||||
name: z.string().max(255),
|
|
||||||
downloads: z.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string().max(255),
|
|
||||||
uris: z.array(z.string()),
|
|
||||||
uploadDate: z.string().max(255),
|
|
||||||
fileSize: z.string().max(255),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
|
@ -9,7 +9,6 @@ import "./catalogue/get-random-game";
|
|||||||
import "./catalogue/search-games";
|
import "./catalogue/search-games";
|
||||||
import "./catalogue/get-game-stats";
|
import "./catalogue/get-game-stats";
|
||||||
import "./catalogue/get-trending-games";
|
import "./catalogue/get-trending-games";
|
||||||
import "./catalogue/get-repacks";
|
|
||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
import "./library/create-game-shortcut";
|
import "./library/create-game-shortcut";
|
||||||
@ -37,9 +36,8 @@ import "./user-preferences/auto-launch";
|
|||||||
import "./autoupdater/check-for-updates";
|
import "./autoupdater/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
import "./user-preferences/authenticate-real-debrid";
|
import "./user-preferences/authenticate-real-debrid";
|
||||||
|
import "./download-sources/delete-download-source";
|
||||||
import "./download-sources/get-download-sources";
|
import "./download-sources/get-download-sources";
|
||||||
import "./download-sources/validate-download-source";
|
|
||||||
import "./download-sources/sync-download-sources";
|
|
||||||
import "./auth/sign-out";
|
import "./auth/sign-out";
|
||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
@ -64,6 +62,7 @@ import "./cloud-save/get-game-backup-preview";
|
|||||||
import "./cloud-save/upload-save-game";
|
import "./cloud-save/upload-save-game";
|
||||||
import "./cloud-save/check-game-cloud-sync-support";
|
import "./cloud-save/check-game-cloud-sync-support";
|
||||||
import "./cloud-save/delete-game-artifact";
|
import "./cloud-save/delete-game-artifact";
|
||||||
|
import "./notifications/publish-new-repacks-notification";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
|
@ -3,7 +3,6 @@ import { gameRepository } from "@main/repository";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { getFileBase64 } from "@main/helpers";
|
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
@ -36,20 +35,12 @@ const addGameToLibrary = async (
|
|||||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await gameRepository
|
await gameRepository.insert({
|
||||||
.insert({
|
title,
|
||||||
title,
|
iconUrl,
|
||||||
iconUrl,
|
objectID,
|
||||||
objectID,
|
shop,
|
||||||
shop,
|
});
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (iconUrl) {
|
|
||||||
getFileBase64(iconUrl).then((base64) =>
|
|
||||||
gameRepository.update({ objectID }, { iconUrl: base64 })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const game = await gameRepository.findOne({ where: { objectID } });
|
const game = await gameRepository.findOne({ where: { objectID } });
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Notification } from "electron";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
|
const publishNewRepacksNotification = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
newRepacksCount: number
|
||||||
|
) => {
|
||||||
|
if (newRepacksCount < 1) return;
|
||||||
|
|
||||||
|
const userPreferences = await userPreferencesRepository.findOne({
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
||||||
|
new Notification({
|
||||||
|
title: t("repack_list_updated", {
|
||||||
|
ns: "notifications",
|
||||||
|
}),
|
||||||
|
body: t("repack_count", {
|
||||||
|
ns: "notifications",
|
||||||
|
count: newRepacksCount,
|
||||||
|
}),
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("publishNewRepacksNotification", publishNewRepacksNotification);
|
@ -1,7 +1,6 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
import { getFileBase64 } from "@main/helpers";
|
|
||||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
@ -60,26 +59,16 @@ const startGameDownload = async (
|
|||||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await gameRepository
|
await gameRepository.insert({
|
||||||
.insert({
|
title,
|
||||||
title,
|
iconUrl,
|
||||||
iconUrl,
|
objectID,
|
||||||
objectID,
|
downloader,
|
||||||
downloader,
|
shop,
|
||||||
shop,
|
status: "active",
|
||||||
status: "active",
|
downloadPath,
|
||||||
downloadPath,
|
uri,
|
||||||
uri,
|
});
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
if (iconUrl) {
|
|
||||||
getFileBase64(iconUrl).then((base64) =>
|
|
||||||
gameRepository.update({ objectID }, { iconUrl: base64 })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedGame = await gameRepository.findOne({
|
const updatedGame = await gameRepository.findOne({
|
||||||
|
@ -7,16 +7,6 @@ export const getFileBuffer = async (url: string) =>
|
|||||||
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getFileBase64 = async (url: string) =>
|
|
||||||
fetch(url, { method: "GET" }).then((response) =>
|
|
||||||
response.arrayBuffer().then((buffer) => {
|
|
||||||
const base64 = Buffer.from(buffer).toString("base64");
|
|
||||||
const contentType = response.headers.get("content-type");
|
|
||||||
|
|
||||||
return `data:${contentType};base64,${base64}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sleep = (ms: number) =>
|
export const sleep = (ms: number) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow, net, protocol } from "electron";
|
import { app, BrowserWindow, net, protocol, session } from "electron";
|
||||||
import { init } from "@sentry/electron/main";
|
import { init } from "@sentry/electron/main";
|
||||||
import updater from "electron-updater";
|
import updater from "electron-updater";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
@ -74,7 +74,7 @@ const runMigrations = async () => {
|
|||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
electronApp.setAppUserModelId("site.hydralauncher.hydra");
|
electronApp.setAppUserModelId("gg.hydralauncher.hydra");
|
||||||
|
|
||||||
protocol.handle("local", (request) => {
|
protocol.handle("local", (request) => {
|
||||||
const filePath = request.url.slice("local:".length);
|
const filePath = request.url.slice("local:".length);
|
||||||
@ -103,6 +103,46 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
WindowManager.createMainWindow();
|
WindowManager.createMainWindow();
|
||||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||||
|
|
||||||
|
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||||
|
callback({
|
||||||
|
requestHeaders: {
|
||||||
|
...details.requestHeaders,
|
||||||
|
"user-agent":
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||||
|
const headers = {
|
||||||
|
"access-control-allow-origin": ["*"],
|
||||||
|
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
|
||||||
|
"access-control-expose-headers": ["ETag"],
|
||||||
|
"access-control-allow-headers": [
|
||||||
|
"Content-Type, Authorization, X-Requested-With, If-None-Match",
|
||||||
|
],
|
||||||
|
"access-control-allow-credentials": ["true"],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (details.method === "OPTIONS") {
|
||||||
|
callback({
|
||||||
|
cancel: false,
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
statusLine: "HTTP/1.1 200 OK",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import { DownloadManager, PythonInstance, startMainLoop } from "./services";
|
import { DownloadManager, PythonInstance, startMainLoop } from "./services";
|
||||||
import {
|
import {
|
||||||
downloadQueueRepository,
|
downloadQueueRepository,
|
||||||
// repackRepository,
|
|
||||||
userPreferencesRepository,
|
userPreferencesRepository,
|
||||||
} from "./repository";
|
} from "./repository";
|
||||||
import { UserPreferences } from "./entity";
|
import { UserPreferences } from "./entity";
|
||||||
import { RealDebridClient } from "./services/real-debrid";
|
import { RealDebridClient } from "./services/real-debrid";
|
||||||
// import { fetchDownloadSourcesAndUpdate } from "./helpers";
|
|
||||||
// import { publishNewRepacksNotifications } from "./services/notifications";
|
|
||||||
// import { MoreThan } from "typeorm";
|
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
|
|
||||||
@ -39,18 +35,6 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startMainLoop();
|
startMainLoop();
|
||||||
|
|
||||||
// const now = new Date();
|
|
||||||
|
|
||||||
// fetchDownloadSourcesAndUpdate().then(async () => {
|
|
||||||
// const newRepacksCount = await repackRepository.count({
|
|
||||||
// where: {
|
|
||||||
// createdAt: MoreThan(now),
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount);
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
userPreferencesRepository
|
userPreferencesRepository
|
||||||
|
@ -7,6 +7,5 @@ export * from "./download";
|
|||||||
export * from "./how-long-to-beat";
|
export * from "./how-long-to-beat";
|
||||||
export * from "./process-watcher";
|
export * from "./process-watcher";
|
||||||
export * from "./main-loop";
|
export * from "./main-loop";
|
||||||
export * from "./repacks-manager";
|
|
||||||
export * from "./hydra-api";
|
export * from "./hydra-api";
|
||||||
export * from "./ludusavi";
|
export * from "./ludusavi";
|
||||||
|
@ -49,24 +49,6 @@ export const publishDownloadCompleteNotification = async (game: Game) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const publishNewRepacksNotifications = async (count: number) => {
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
|
||||||
new Notification({
|
|
||||||
title: t("repack_list_updated", {
|
|
||||||
ns: "notifications",
|
|
||||||
}),
|
|
||||||
body: t("repack_count", {
|
|
||||||
ns: "notifications",
|
|
||||||
count: count,
|
|
||||||
}),
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const publishNotificationUpdateReadyToInstall = async (
|
export const publishNotificationUpdateReadyToInstall = async (
|
||||||
version: string
|
version: string
|
||||||
) => {
|
) => {
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import { repackRepository } from "@main/repository";
|
|
||||||
import { formatName } from "@shared";
|
|
||||||
import { CatalogueEntry, GameRepack } from "@types";
|
|
||||||
import flexSearch from "flexsearch";
|
|
||||||
|
|
||||||
export class RepacksManager {
|
|
||||||
public static repacks: GameRepack[] = [];
|
|
||||||
private static repacksIndex = new flexSearch.Index();
|
|
||||||
|
|
||||||
public static async updateRepacks() {
|
|
||||||
this.repacks = await repackRepository
|
|
||||||
.find({
|
|
||||||
order: {
|
|
||||||
createdAt: "DESC",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((repacks) =>
|
|
||||||
repacks.map((repack) => {
|
|
||||||
const uris: string[] = [];
|
|
||||||
const magnet = repack?.magnet;
|
|
||||||
|
|
||||||
if (magnet) uris.push(magnet);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...repack,
|
|
||||||
uris: [...uris, ...JSON.parse(repack.uris)],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < this.repacks.length; i++) {
|
|
||||||
this.repacksIndex.remove(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.repacksIndex = new flexSearch.Index();
|
|
||||||
|
|
||||||
for (let i = 0; i < this.repacks.length; i++) {
|
|
||||||
const repack = this.repacks[i];
|
|
||||||
|
|
||||||
const formattedTitle = formatName(repack.title);
|
|
||||||
|
|
||||||
this.repacksIndex.add(i, formattedTitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static search(options: flexSearch.SearchOptions) {
|
|
||||||
return this.repacksIndex
|
|
||||||
.search({ ...options, query: formatName(options.query ?? "") })
|
|
||||||
.map((index) => this.repacks[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static findRepacksForCatalogueEntry(entry: CatalogueEntry) {
|
|
||||||
const repacks = this.search({ query: formatName(entry.title) });
|
|
||||||
return { ...entry, repacks };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static findRepacksForCatalogueEntries(entries: CatalogueEntry[]) {
|
|
||||||
return entries.map((entry) => {
|
|
||||||
const repacks = this.search({ query: formatName(entry.title) });
|
|
||||||
return { ...entry, repacks };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -101,6 +101,8 @@ export class WindowManager {
|
|||||||
|
|
||||||
authWindow.removeMenu();
|
authWindow.removeMenu();
|
||||||
|
|
||||||
|
if (!app.isPackaged) authWindow.webContents.openDevTools();
|
||||||
|
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
lng: i18next.language,
|
lng: i18next.language,
|
||||||
});
|
});
|
||||||
@ -132,7 +134,7 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static createSystemTray(language: string) {
|
public static createSystemTray(language: string) {
|
||||||
let tray;
|
let tray: Tray;
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
const macIcon = nativeImage
|
const macIcon = nativeImage
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import { downloadSourceSchema } from "@main/events/helpers/validators";
|
|
||||||
import { DownloadSourceStatus } from "@shared";
|
|
||||||
import type { DownloadSource } from "@types";
|
|
||||||
import axios, { AxiosError, AxiosHeaders } from "axios";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export type DownloadSourceResponse = z.infer<typeof downloadSourceSchema> & {
|
|
||||||
etag: string | null;
|
|
||||||
status: DownloadSourceStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => {
|
|
||||||
const results: DownloadSourceResponse[] = [];
|
|
||||||
|
|
||||||
for (const downloadSource of downloadSources) {
|
|
||||||
const headers = new AxiosHeaders();
|
|
||||||
|
|
||||||
if (downloadSource.etag) {
|
|
||||||
headers.set("If-None-Match", downloadSource.etag);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(downloadSource.url, {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const source = downloadSourceSchema.parse(response.data);
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
...downloadSource,
|
|
||||||
downloads: source.downloads,
|
|
||||||
etag: response.headers["etag"],
|
|
||||||
status: DownloadSourceStatus.UpToDate,
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const isNotModified = (err as AxiosError).response?.status === 304;
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
...downloadSource,
|
|
||||||
downloads: [],
|
|
||||||
etag: null,
|
|
||||||
status: isNotModified
|
|
||||||
? DownloadSourceStatus.UpToDate
|
|
||||||
: DownloadSourceStatus.Errored,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateDownloadSource = async (url: string) => {
|
|
||||||
const response = await axios.get(url);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...downloadSourceSchema.parse(response.data),
|
|
||||||
etag: response.headers["etag"],
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,6 +1,5 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
||||||
import downloadSourcesWorkerPath from "./download-sources.worker?modulePath";
|
|
||||||
|
|
||||||
import Piscina from "piscina";
|
import Piscina from "piscina";
|
||||||
|
|
||||||
@ -13,7 +12,3 @@ export const steamGamesWorker = new Piscina({
|
|||||||
},
|
},
|
||||||
maxThreads: 1,
|
maxThreads: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloadSourcesWorker = new Piscina({
|
|
||||||
filename: downloadSourcesWorkerPath,
|
|
||||||
});
|
|
||||||
|
@ -11,9 +11,9 @@ import type {
|
|||||||
GameRunning,
|
GameRunning,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
DownloadSource,
|
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { CatalogueCategory } from "@shared";
|
import type { CatalogueCategory } from "@shared";
|
||||||
|
import type { AxiosProgressEvent } from "axios";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
/* Torrenting */
|
/* Torrenting */
|
||||||
@ -50,8 +50,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getGameStats: (objectId: string, shop: GameShop) =>
|
getGameStats: (objectId: string, shop: GameShop) =>
|
||||||
ipcRenderer.invoke("getGameStats", objectId, shop),
|
ipcRenderer.invoke("getGameStats", objectId, shop),
|
||||||
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
||||||
/* Meant for Dexie migration */
|
|
||||||
getRepacks: () => ipcRenderer.invoke("getRepacks"),
|
|
||||||
|
|
||||||
/* User preferences */
|
/* User preferences */
|
||||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||||
@ -63,10 +61,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
||||||
validateDownloadSource: (url: string) =>
|
deleteDownloadSource: (id: number) =>
|
||||||
ipcRenderer.invoke("validateDownloadSource", url),
|
ipcRenderer.invoke("deleteDownloadSource", id),
|
||||||
syncDownloadSources: (downloadSources: DownloadSource[]) =>
|
|
||||||
ipcRenderer.invoke("syncDownloadSources", downloadSources),
|
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
|
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
|
||||||
@ -141,12 +137,32 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
listener
|
listener
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onDownloadComplete: (objectId: string, shop: GameShop, cb: () => void) => {
|
onBackupDownloadProgress: (
|
||||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
objectId: string,
|
||||||
ipcRenderer.on(`on-download-complete-${objectId}-${shop}`, listener);
|
shop: GameShop,
|
||||||
|
cb: (progress: AxiosProgressEvent) => void
|
||||||
|
) => {
|
||||||
|
const listener = (
|
||||||
|
_event: Electron.IpcRendererEvent,
|
||||||
|
progress: AxiosProgressEvent
|
||||||
|
) => cb(progress);
|
||||||
|
ipcRenderer.on(`on-backup-download-progress-${objectId}-${shop}`, listener);
|
||||||
return () =>
|
return () =>
|
||||||
ipcRenderer.removeListener(
|
ipcRenderer.removeListener(
|
||||||
`on-download-complete-${objectId}-${shop}`,
|
`on-backup-download-complete-${objectId}-${shop}`,
|
||||||
|
listener
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onBackupDownloadComplete: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
cb: () => void
|
||||||
|
) => {
|
||||||
|
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||||
|
ipcRenderer.on(`on-backup-download-complete-${objectId}-${shop}`, listener);
|
||||||
|
return () =>
|
||||||
|
ipcRenderer.removeListener(
|
||||||
|
`on-backup-download-complete-${objectId}-${shop}`,
|
||||||
listener
|
listener
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -218,4 +234,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.on("on-signout", listener);
|
ipcRenderer.on("on-signout", listener);
|
||||||
return () => ipcRenderer.removeListener("on-signout", listener);
|
return () => ipcRenderer.removeListener("on-signout", listener);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* Notifications */
|
||||||
|
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||||
|
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||||
});
|
});
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||||
// import { migrationWorker } from "./workers";
|
import { downloadSourcesWorker } from "./workers";
|
||||||
import { repacksContext } from "./context";
|
import { repacksContext } from "./context";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
@ -39,6 +39,8 @@ export function App() {
|
|||||||
|
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
|
|
||||||
|
const downloadSourceMigrationLock = useRef(false);
|
||||||
|
|
||||||
const { clearDownload, setLastPacket } = useDownload();
|
const { clearDownload, setLastPacket } = useDownload();
|
||||||
|
|
||||||
const { indexRepacks } = useContext(repacksContext);
|
const { indexRepacks } = useContext(repacksContext);
|
||||||
@ -211,22 +213,46 @@ export function App() {
|
|||||||
}, [dispatch, draggingDisabled]);
|
}, [dispatch, draggingDisabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// window.electron.getRepacks().then((repacks) => {
|
if (downloadSourceMigrationLock.current) return;
|
||||||
// migrationWorker.postMessage(["MIGRATE_REPACKS", repacks]);
|
|
||||||
// });
|
downloadSourceMigrationLock.current = true;
|
||||||
// window.electron.getDownloadSources().then((downloadSources) => {
|
|
||||||
// migrationWorker.postMessage([
|
window.electron.getDownloadSources().then(async (downloadSources) => {
|
||||||
// "MIGRATE_DOWNLOAD_SOURCES",
|
if (!downloadSources.length) {
|
||||||
// downloadSources,
|
const id = crypto.randomUUID();
|
||||||
// ]);
|
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||||
// });
|
|
||||||
// migrationWorker.onmessage = (
|
channel.onmessage = (event: MessageEvent<number>) => {
|
||||||
// event: MessageEvent<"MIGRATE_REPACKS_COMPLETE">
|
const newRepacksCount = event.data;
|
||||||
// ) => {
|
window.electron.publishNewRepacksNotification(newRepacksCount);
|
||||||
// if (event.data === "MIGRATE_REPACKS_COMPLETE") {
|
};
|
||||||
// indexRepacks();
|
|
||||||
// }
|
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||||
// };
|
}
|
||||||
|
|
||||||
|
for (const downloadSource of downloadSources) {
|
||||||
|
const channel = new BroadcastChannel(
|
||||||
|
`download_sources:import:${downloadSource.url}`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
downloadSourcesWorker.postMessage([
|
||||||
|
"IMPORT_DOWNLOAD_SOURCE",
|
||||||
|
downloadSource.url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
channel.onmessage = () => {
|
||||||
|
window.electron.deleteDownloadSource(downloadSource.id).then(() => {
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
indexRepacks();
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
|
}).catch(() => channel.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSourceMigrationLock.current = false;
|
||||||
|
});
|
||||||
}, [indexRepacks]);
|
}, [indexRepacks]);
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
const handleToastClose = useCallback(() => {
|
||||||
|
@ -8,6 +8,7 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export enum CloudSyncState {
|
export enum CloudSyncState {
|
||||||
New,
|
New,
|
||||||
@ -20,12 +21,14 @@ export interface CloudSyncContext {
|
|||||||
backupPreview: LudusaviBackup | null;
|
backupPreview: LudusaviBackup | null;
|
||||||
artifacts: GameArtifact[];
|
artifacts: GameArtifact[];
|
||||||
showCloudSyncModal: boolean;
|
showCloudSyncModal: boolean;
|
||||||
|
showCloudSyncFilesModal: boolean;
|
||||||
supportsCloudSync: boolean | null;
|
supportsCloudSync: boolean | null;
|
||||||
backupState: CloudSyncState;
|
backupState: CloudSyncState;
|
||||||
setShowCloudSyncModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowCloudSyncModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
downloadGameArtifact: (gameArtifactId: string) => Promise<void>;
|
downloadGameArtifact: (gameArtifactId: string) => Promise<void>;
|
||||||
uploadSaveGame: () => Promise<void>;
|
uploadSaveGame: () => Promise<void>;
|
||||||
deleteGameArtifact: (gameArtifactId: string) => Promise<void>;
|
deleteGameArtifact: (gameArtifactId: string) => Promise<void>;
|
||||||
|
setShowCloudSyncFilesModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
restoringBackup: boolean;
|
restoringBackup: boolean;
|
||||||
uploadingBackup: boolean;
|
uploadingBackup: boolean;
|
||||||
}
|
}
|
||||||
@ -40,6 +43,8 @@ export const cloudSyncContext = createContext<CloudSyncContext>({
|
|||||||
uploadSaveGame: async () => {},
|
uploadSaveGame: async () => {},
|
||||||
artifacts: [],
|
artifacts: [],
|
||||||
deleteGameArtifact: async () => {},
|
deleteGameArtifact: async () => {},
|
||||||
|
showCloudSyncFilesModal: false,
|
||||||
|
setShowCloudSyncFilesModal: () => {},
|
||||||
restoringBackup: false,
|
restoringBackup: false,
|
||||||
uploadingBackup: false,
|
uploadingBackup: false,
|
||||||
});
|
});
|
||||||
@ -58,6 +63,8 @@ export function CloudSyncContextProvider({
|
|||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
}: CloudSyncContextProviderProps) {
|
}: CloudSyncContextProviderProps) {
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const [supportsCloudSync, setSupportsCloudSync] = useState<boolean | null>(
|
const [supportsCloudSync, setSupportsCloudSync] = useState<boolean | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@ -68,6 +75,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 { showSuccessToast } = useToast();
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
@ -101,7 +109,7 @@ export function CloudSyncContextProvider({
|
|||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
() => {
|
() => {
|
||||||
showSuccessToast("backup_uploaded");
|
showSuccessToast(t("backup_uploaded"));
|
||||||
|
|
||||||
setUploadingBackup(false);
|
setUploadingBackup(false);
|
||||||
gameBackupsTable.add({
|
gameBackupsTable.add({
|
||||||
@ -114,22 +122,19 @@ export function CloudSyncContextProvider({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeDownloadCompleteListener = window.electron.onDownloadComplete(
|
const removeDownloadCompleteListener =
|
||||||
objectId,
|
window.electron.onBackupDownloadComplete(objectId, shop, () => {
|
||||||
shop,
|
showSuccessToast(t("backup_restored"));
|
||||||
() => {
|
|
||||||
showSuccessToast("backup_restored");
|
|
||||||
|
|
||||||
setRestoringBackup(false);
|
setRestoringBackup(false);
|
||||||
getGameBackupPreview();
|
getGameBackupPreview();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeUploadCompleteListener();
|
removeUploadCompleteListener();
|
||||||
removeDownloadCompleteListener();
|
removeDownloadCompleteListener();
|
||||||
};
|
};
|
||||||
}, [objectId, shop, showSuccessToast, getGameBackupPreview]);
|
}, [objectId, shop, showSuccessToast, t, getGameBackupPreview]);
|
||||||
|
|
||||||
const deleteGameArtifact = useCallback(
|
const deleteGameArtifact = useCallback(
|
||||||
async (gameArtifactId: string) => {
|
async (gameArtifactId: string) => {
|
||||||
@ -181,10 +186,12 @@ export function CloudSyncContextProvider({
|
|||||||
backupState,
|
backupState,
|
||||||
restoringBackup,
|
restoringBackup,
|
||||||
uploadingBackup,
|
uploadingBackup,
|
||||||
|
showCloudSyncFilesModal,
|
||||||
setShowCloudSyncModal,
|
setShowCloudSyncModal,
|
||||||
uploadSaveGame,
|
uploadSaveGame,
|
||||||
downloadGameArtifact,
|
downloadGameArtifact,
|
||||||
deleteGameArtifact,
|
deleteGameArtifact,
|
||||||
|
setShowCloudSyncFilesModal,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -33,6 +33,7 @@ export function RepacksContextProvider({ children }: RepacksContextProps) {
|
|||||||
const channel = new BroadcastChannel(`repacks:search:${channelId}`);
|
const channel = new BroadcastChannel(`repacks:search:${channelId}`);
|
||||||
channel.onmessage = (event: MessageEvent<GameRepack[]>) => {
|
channel.onmessage = (event: MessageEvent<GameRepack[]>) => {
|
||||||
resolve(event.data);
|
resolve(event.data);
|
||||||
|
channel.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
19
src/renderer/src/declaration.d.ts
vendored
19
src/renderer/src/declaration.d.ts
vendored
@ -25,10 +25,10 @@ import type {
|
|||||||
UserStats,
|
UserStats,
|
||||||
UserDetails,
|
UserDetails,
|
||||||
FriendRequestSync,
|
FriendRequestSync,
|
||||||
DownloadSourceValidationResult,
|
|
||||||
GameArtifact,
|
GameArtifact,
|
||||||
LudusaviBackup,
|
LudusaviBackup,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@ -68,8 +68,6 @@ declare global {
|
|||||||
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
||||||
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
||||||
getTrendingGames: () => Promise<TrendingGame[]>;
|
getTrendingGames: () => Promise<TrendingGame[]>;
|
||||||
/* Meant for Dexie migration */
|
|
||||||
getRepacks: () => Promise<GameRepack[]>;
|
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
@ -107,10 +105,7 @@ declare global {
|
|||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
getDownloadSources: () => Promise<DownloadSource[]>;
|
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||||
validateDownloadSource: (
|
deleteDownloadSource: (id: number) => Promise<void>;
|
||||||
url: string
|
|
||||||
) => Promise<DownloadSourceValidationResult>;
|
|
||||||
syncDownloadSources: (downloadSources: DownloadSource[]) => Promise<void>;
|
|
||||||
|
|
||||||
/* Hardware */
|
/* Hardware */
|
||||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||||
@ -135,7 +130,7 @@ declare global {
|
|||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>;
|
deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>;
|
||||||
onDownloadComplete: (
|
onBackupDownloadComplete: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
cb: () => void
|
cb: () => void
|
||||||
@ -145,6 +140,11 @@ declare global {
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
cb: () => void
|
cb: () => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
|
onBackupDownloadProgress: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
cb: (progress: AxiosProgressEvent) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
openExternal: (src: string) => Promise<void>;
|
openExternal: (src: string) => Promise<void>;
|
||||||
@ -205,6 +205,9 @@ declare global {
|
|||||||
action: FriendRequestAction
|
action: FriendRequestAction
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
sendFriendRequest: (userId: string) => Promise<void>;
|
sendFriendRequest: (userId: string) => Promise<void>;
|
||||||
|
|
||||||
|
/* Notifications */
|
||||||
|
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -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,77 @@
|
|||||||
|
import { Button, Modal, ModalProps, TextField } from "@renderer/components";
|
||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
import { cloudSyncContext } from "@renderer/context";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CheckCircleFillIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
export interface CloudSyncFilesModalProps
|
||||||
|
extends Omit<ModalProps, "children" | "title"> {}
|
||||||
|
|
||||||
|
export function CloudSyncFilesModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
}: CloudSyncFilesModalProps) {
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const { backupPreview } = useContext(cloudSyncContext);
|
||||||
|
|
||||||
|
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> */}
|
||||||
|
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
listStyle: "none",
|
||||||
|
gap: 16,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{files.map((file) => (
|
||||||
|
<li key={file.path}>
|
||||||
|
<TextField value={file.path} readOnly />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,14 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { keyframes, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
|
|
||||||
|
export const rotate = keyframes({
|
||||||
|
"0%": { transform: "rotate(0deg)" },
|
||||||
|
"100%": {
|
||||||
|
transform: "rotate(360deg)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const artifacts = style({
|
export const artifacts = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
@ -24,3 +31,10 @@ export const artifactButton = style({
|
|||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const syncIcon = style({
|
||||||
|
animationName: rotate,
|
||||||
|
animationDuration: "1s",
|
||||||
|
animationIterationCount: "infinite",
|
||||||
|
animationTimingFunction: "linear",
|
||||||
|
});
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
CheckCircleFillIcon,
|
CheckCircleFillIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
DeviceDesktopIcon,
|
DeviceDesktopIcon,
|
||||||
DownloadIcon,
|
HistoryIcon,
|
||||||
SyncIcon,
|
SyncIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
@ -17,6 +17,9 @@ import {
|
|||||||
import { useToast } from "@renderer/hooks";
|
import { useToast } from "@renderer/hooks";
|
||||||
import { GameBackup, gameBackupsTable } from "@renderer/dexie";
|
import { GameBackup, gameBackupsTable } from "@renderer/dexie";
|
||||||
import { useTranslation } from "react-i18next";
|
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
|
export interface CloudSyncModalProps
|
||||||
extends Omit<ModalProps, "children" | "title"> {}
|
extends Omit<ModalProps, "children" | "title"> {}
|
||||||
@ -24,6 +27,8 @@ export interface CloudSyncModalProps
|
|||||||
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 [lastBackup, setLastBackup] = useState<GameBackup | null>(null);
|
||||||
|
const [backupDownloadProgress, setBackupDownloadProgress] =
|
||||||
|
useState<AxiosProgressEvent | null>(null);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
@ -35,6 +40,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
uploadSaveGame,
|
uploadSaveGame,
|
||||||
downloadGameArtifact,
|
downloadGameArtifact,
|
||||||
deleteGameArtifact,
|
deleteGameArtifact,
|
||||||
|
setShowCloudSyncFilesModal,
|
||||||
} = useContext(cloudSyncContext);
|
} = useContext(cloudSyncContext);
|
||||||
|
|
||||||
const { objectID, shop, gameTitle } = useContext(gameDetailsContext);
|
const { objectID, shop, gameTitle } = useContext(gameDetailsContext);
|
||||||
@ -60,13 +66,31 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
.where({ shop: shop, objectId: objectID })
|
.where({ shop: shop, objectId: objectID })
|
||||||
.last()
|
.last()
|
||||||
.then((lastBackup) => setLastBackup(lastBackup || null));
|
.then((lastBackup) => setLastBackup(lastBackup || null));
|
||||||
|
|
||||||
|
const removeBackupDownloadProgressListener =
|
||||||
|
window.electron.onBackupDownloadProgress(
|
||||||
|
objectID!,
|
||||||
|
shop,
|
||||||
|
(progressEvent) => {
|
||||||
|
setBackupDownloadProgress(progressEvent);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeBackupDownloadProgressListener();
|
||||||
|
};
|
||||||
}, [backupPreview, objectID, shop]);
|
}, [backupPreview, objectID, shop]);
|
||||||
|
|
||||||
|
const handleBackupInstallClick = async (artifactId: string) => {
|
||||||
|
setBackupDownloadProgress(null);
|
||||||
|
downloadGameArtifact(artifactId);
|
||||||
|
};
|
||||||
|
|
||||||
const backupStateLabel = useMemo(() => {
|
const backupStateLabel = useMemo(() => {
|
||||||
if (uploadingBackup) {
|
if (uploadingBackup) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SyncIcon />
|
<SyncIcon className={styles.syncIcon} />
|
||||||
{t("uploading_backup")}
|
{t("uploading_backup")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -75,8 +99,12 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
if (restoringBackup) {
|
if (restoringBackup) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SyncIcon />
|
<SyncIcon className={styles.syncIcon} />
|
||||||
{t("restoring_backup")}
|
{t("restoring_backup", {
|
||||||
|
progress: formatDownloadProgress(
|
||||||
|
backupDownloadProgress?.progress ?? 0
|
||||||
|
),
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -84,7 +112,10 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
if (lastBackup) {
|
if (lastBackup) {
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<CheckCircleFillIcon />
|
<i style={{ color: vars.color.success }}>
|
||||||
|
<CheckCircleFillIcon />
|
||||||
|
</i>
|
||||||
|
|
||||||
{t("last_backup_date", {
|
{t("last_backup_date", {
|
||||||
date: format(lastBackup.createdAt, "dd/MM/yyyy HH:mm"),
|
date: format(lastBackup.createdAt, "dd/MM/yyyy HH:mm"),
|
||||||
})}
|
})}
|
||||||
@ -97,7 +128,14 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return t("no_backups");
|
return t("no_backups");
|
||||||
}, [uploadingBackup, lastBackup, backupPreview, restoringBackup, t]);
|
}, [
|
||||||
|
uploadingBackup,
|
||||||
|
backupDownloadProgress?.progress,
|
||||||
|
lastBackup,
|
||||||
|
backupPreview,
|
||||||
|
restoringBackup,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
const disableActions = uploadingBackup || restoringBackup || deletingArtifact;
|
||||||
|
|
||||||
@ -120,6 +158,22 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
||||||
<h2>{gameTitle}</h2>
|
<h2>{gameTitle}</h2>
|
||||||
<p>{backupStateLabel}</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -132,7 +186,17 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 style={{ marginBottom: 16 }}>{t("backups")}</h2>
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: SPACING_UNIT,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>{t("backups")}</h2>
|
||||||
|
<small>2 / 2</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul className={styles.artifacts}>
|
<ul className={styles.artifacts}>
|
||||||
{artifacts.map((artifact) => (
|
{artifacts.map((artifact) => (
|
||||||
@ -163,10 +227,10 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => downloadGameArtifact(artifact.id)}
|
onClick={() => handleBackupInstallClick(artifact.id)}
|
||||||
disabled={disableActions}
|
disabled={disableActions}
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<HistoryIcon />
|
||||||
{t("install_backup")}
|
{t("install_backup")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
@ -14,6 +14,7 @@ import { steamUrlBuilder } from "@shared";
|
|||||||
import Lottie from "lottie-react";
|
import Lottie from "lottie-react";
|
||||||
|
|
||||||
import downloadingAnimation from "@renderer/assets/lottie/cloud.json";
|
import downloadingAnimation from "@renderer/assets/lottie/cloud.json";
|
||||||
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
|
|
||||||
const HERO_ANIMATION_THRESHOLD = 25;
|
const HERO_ANIMATION_THRESHOLD = 25;
|
||||||
|
|
||||||
@ -33,6 +34,8 @@ export function GameDetailsContent() {
|
|||||||
hasNSFWContentBlocked,
|
hasNSFWContentBlocked,
|
||||||
} = useContext(gameDetailsContext);
|
} = useContext(gameDetailsContext);
|
||||||
|
|
||||||
|
const { userDetails } = useUserDetails();
|
||||||
|
|
||||||
const { supportsCloudSync, setShowCloudSyncModal } =
|
const { supportsCloudSync, setShowCloudSyncModal } =
|
||||||
useContext(cloudSyncContext);
|
useContext(cloudSyncContext);
|
||||||
|
|
||||||
@ -75,6 +78,15 @@ export function GameDetailsContent() {
|
|||||||
setBackdropOpacity(opacity);
|
setBackdropOpacity(opacity);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloudSaveButtonClick = () => {
|
||||||
|
if (!userDetails) {
|
||||||
|
window.electron.openAuthWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowCloudSyncModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
|
<div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
|
||||||
<img
|
<img
|
||||||
@ -113,7 +125,7 @@ export function GameDetailsContent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.cloudSyncButton}
|
className={styles.cloudSyncButton}
|
||||||
onClick={() => setShowCloudSyncModal(true)}
|
onClick={handleCloudSaveButtonClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -27,6 +27,7 @@ import { useDownload } from "@renderer/hooks";
|
|||||||
import { GameOptionsModal, RepacksModal } from "./modals";
|
import { GameOptionsModal, RepacksModal } from "./modals";
|
||||||
import { Downloader, getDownloadersForUri } from "@shared";
|
import { Downloader, getDownloadersForUri } from "@shared";
|
||||||
import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal";
|
import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal";
|
||||||
|
import { CloudSyncFilesModal } from "./cloud-sync-files-modal/cloud-sync-files-modal";
|
||||||
|
|
||||||
export function GameDetails() {
|
export function GameDetails() {
|
||||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
@ -128,11 +129,23 @@ export function GameDetails() {
|
|||||||
shop={shop! as GameShop}
|
shop={shop! as GameShop}
|
||||||
>
|
>
|
||||||
<CloudSyncContextConsumer>
|
<CloudSyncContextConsumer>
|
||||||
{({ showCloudSyncModal, setShowCloudSyncModal }) => (
|
{({
|
||||||
<CloudSyncModal
|
showCloudSyncModal,
|
||||||
onClose={() => setShowCloudSyncModal(false)}
|
setShowCloudSyncModal,
|
||||||
visible={showCloudSyncModal}
|
showCloudSyncFilesModal,
|
||||||
/>
|
setShowCloudSyncFilesModal,
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<CloudSyncModal
|
||||||
|
onClose={() => setShowCloudSyncModal(false)}
|
||||||
|
visible={showCloudSyncModal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CloudSyncFilesModal
|
||||||
|
onClose={() => setShowCloudSyncFilesModal(false)}
|
||||||
|
visible={showCloudSyncFilesModal}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</CloudSyncContextConsumer>
|
</CloudSyncContextConsumer>
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import parseTorrent from "parse-torrent";
|
|
||||||
|
|
||||||
import { Badge, Button, Modal, TextField } from "@renderer/components";
|
import { Badge, Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
@ -33,8 +32,6 @@ export function RepacksModal({
|
|||||||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||||
|
|
||||||
const [infoHash, setInfoHash] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { repacks, game } = useContext(gameDetailsContext);
|
const { repacks, game } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
@ -43,18 +40,9 @@ export function RepacksModal({
|
|||||||
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
|
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
|
||||||
}, [repacks]);
|
}, [repacks]);
|
||||||
|
|
||||||
const getInfoHash = useCallback(async () => {
|
|
||||||
if (game?.uri?.startsWith("magnet:")) {
|
|
||||||
const torrent = await parseTorrent(game?.uri ?? "");
|
|
||||||
if (torrent.infoHash) setInfoHash(torrent.infoHash);
|
|
||||||
}
|
|
||||||
}, [game]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredRepacks(sortedRepacks);
|
setFilteredRepacks(sortedRepacks);
|
||||||
|
}, [sortedRepacks, visible, game]);
|
||||||
if (game?.uri) getInfoHash();
|
|
||||||
}, [sortedRepacks, visible, game, getInfoHash]);
|
|
||||||
|
|
||||||
const handleRepackClick = (repack: GameRepack) => {
|
const handleRepackClick = (repack: GameRepack) => {
|
||||||
setRepack(repack);
|
setRepack(repack);
|
||||||
@ -77,10 +65,8 @@ export function RepacksModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
||||||
if (infoHash) return repack.uris.some((uri) => uri.includes(infoHash));
|
if (!game) return false;
|
||||||
if (!game?.uri) return false;
|
return repack.uris.some((uri) => uri.includes(game.uri!));
|
||||||
|
|
||||||
return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -6,6 +6,7 @@ export const gameCover = style({
|
|||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
boxShadow: "0 8px 10px -2px rgba(0, 0, 0, 0.5)",
|
boxShadow: "0 8px 10px -2px rgba(0, 0, 0, 0.5)",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
position: "relative",
|
||||||
":before": {
|
":before": {
|
||||||
content: "",
|
content: "",
|
||||||
top: "0",
|
top: "0",
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { userProfileContext } from "@renderer/context";
|
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 { ProfileHero } from "../profile-hero/profile-hero";
|
||||||
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { steamUrlBuilder } from "@shared";
|
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 * as styles from "./profile-content.css";
|
||||||
import { TelescopeIcon } from "@primer/octicons-react";
|
import { ClockIcon, TelescopeIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { LockedProfile } from "./locked-profile";
|
import { LockedProfile } from "./locked-profile";
|
||||||
@ -16,6 +16,7 @@ import { FriendsBox } from "./friends-box";
|
|||||||
import { RecentGamesBox } from "./recent-games-box";
|
import { RecentGamesBox } from "./recent-games-box";
|
||||||
import { UserGame } from "@types";
|
import { UserGame } from "@types";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
|
|
||||||
export function ProfileContent() {
|
export function ProfileContent() {
|
||||||
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
||||||
@ -46,6 +47,22 @@ export function ProfileContent() {
|
|||||||
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(() => {
|
const content = useMemo(() => {
|
||||||
if (!userProfile) return null;
|
if (!userProfile) return null;
|
||||||
|
|
||||||
@ -109,13 +126,31 @@ export function ProfileContent() {
|
|||||||
className={styles.gameCover}
|
className={styles.gameCover}
|
||||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||||
>
|
>
|
||||||
|
<div style={{ position: "absolute", padding: 4 }}>
|
||||||
|
<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 4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClockIcon size={11} />
|
||||||
|
{formatPlayTime(game.playTimeInSeconds)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.cover(game.objectId)}
|
src={steamUrlBuilder.cover(game.objectId)}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -4,6 +4,7 @@ import { style } from "@vanilla-extract/css";
|
|||||||
export const profileContentBox = style({
|
export const profileContentBox = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
position: "relative",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileAvatarButton = style({
|
export const profileAvatarButton = style({
|
||||||
|
@ -272,64 +272,86 @@ export function ProfileHero() {
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
className={styles.profileContentBox}
|
className={styles.profileContentBox}
|
||||||
style={{ background: heroBackground }}
|
// style={{ background: heroBackground }}
|
||||||
>
|
>
|
||||||
<div className={styles.userInformation}>
|
<img
|
||||||
<button
|
src="https://wallpapers.com/images/featured/cyberpunk-anime-dfyw8eb7bqkw278u.jpg"
|
||||||
type="button"
|
alt=""
|
||||||
className={styles.profileAvatarButton}
|
style={{
|
||||||
onClick={handleAvatarClick}
|
position: "absolute",
|
||||||
>
|
width: "100%",
|
||||||
{userProfile?.profileImageUrl ? (
|
height: "100%",
|
||||||
<img
|
objectFit: "cover",
|
||||||
className={styles.profileAvatar}
|
}}
|
||||||
alt={userProfile?.displayName}
|
/>
|
||||||
src={userProfile?.profileImageUrl}
|
<div
|
||||||
/>
|
style={{
|
||||||
) : (
|
background: heroBackground,
|
||||||
<PersonIcon size={72} />
|
width: "100%",
|
||||||
)}
|
height: "100%",
|
||||||
</button>
|
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}>
|
<div className={styles.profileInformation}>
|
||||||
{userProfile ? (
|
{userProfile ? (
|
||||||
<h2 className={styles.profileDisplayName}>
|
<h2 className={styles.profileDisplayName}>
|
||||||
{userProfile?.displayName}
|
{userProfile?.displayName}
|
||||||
</h2>
|
</h2>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton width={150} height={28} />
|
<Skeleton width={150} height={28} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentGame && (
|
{currentGame && (
|
||||||
<div className={styles.currentGameWrapper}>
|
<div className={styles.currentGameWrapper}>
|
||||||
<div className={styles.currentGameDetails}>
|
<div className={styles.currentGameDetails}>
|
||||||
<Link
|
<Link
|
||||||
to={buildGameDetailsPath({
|
to={buildGameDetailsPath({
|
||||||
...currentGame,
|
...currentGame,
|
||||||
objectID: currentGame.objectId,
|
objectID: currentGame.objectId,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{currentGame.title}
|
{currentGame.title}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small>
|
<small>
|
||||||
{t("playing_for", {
|
{t("playing_for", {
|
||||||
amount: formatDistance(
|
amount: formatDistance(
|
||||||
addSeconds(
|
addSeconds(
|
||||||
new Date(),
|
new Date(),
|
||||||
-currentGame.sessionDurationInSeconds
|
-currentGame.sessionDurationInSeconds
|
||||||
|
),
|
||||||
|
new Date()
|
||||||
),
|
),
|
||||||
new Date()
|
})}
|
||||||
),
|
</small>
|
||||||
})}
|
</div>
|
||||||
</small>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.heroPanel}>
|
<div
|
||||||
|
className={styles.heroPanel}
|
||||||
|
style={{ background: heroBackground }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -67,8 +67,21 @@ export function AddDownloadSourceModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await window.electron.validateDownloadSource(values.url);
|
downloadSourcesWorker.postMessage([
|
||||||
setValidationResult(result);
|
"VALIDATE_DOWNLOAD_SOURCE",
|
||||||
|
values.url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const channel = new BroadcastChannel(
|
||||||
|
`download_sources:validate:${values.url}`
|
||||||
|
);
|
||||||
|
|
||||||
|
channel.onmessage = (
|
||||||
|
event: MessageEvent<DownloadSourceValidationResult>
|
||||||
|
) => {
|
||||||
|
setValidationResult(event.data);
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
|
|
||||||
setUrl(values.url);
|
setUrl(values.url);
|
||||||
},
|
},
|
||||||
@ -93,16 +106,14 @@ export function AddDownloadSourceModal({
|
|||||||
if (validationResult) {
|
if (validationResult) {
|
||||||
const channel = new BroadcastChannel(`download_sources:import:${url}`);
|
const channel = new BroadcastChannel(`download_sources:import:${url}`);
|
||||||
|
|
||||||
downloadSourcesWorker.postMessage([
|
downloadSourcesWorker.postMessage(["IMPORT_DOWNLOAD_SOURCE", url]);
|
||||||
"IMPORT_DOWNLOAD_SOURCE",
|
|
||||||
{ ...validationResult, url },
|
|
||||||
]);
|
|
||||||
|
|
||||||
channel.onmessage = () => {
|
channel.onmessage = () => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
onAddDownloadSource();
|
onAddDownloadSource();
|
||||||
|
channel.close();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -159,9 +170,9 @@ export function AddDownloadSourceModal({
|
|||||||
<h4>{validationResult?.name}</h4>
|
<h4>{validationResult?.name}</h4>
|
||||||
<small>
|
<small>
|
||||||
{t("found_download_option", {
|
{t("found_download_option", {
|
||||||
count: validationResult?.downloads.length,
|
count: validationResult?.downloadCount,
|
||||||
countFormatted:
|
countFormatted:
|
||||||
validationResult?.downloads.length.toLocaleString(),
|
validationResult?.downloadCount.toLocaleString(),
|
||||||
})}
|
})}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,6 +59,7 @@ export function SettingsDownloadSources() {
|
|||||||
getDownloadSources();
|
getDownloadSources();
|
||||||
indexRepacks();
|
indexRepacks();
|
||||||
setIsRemovingDownloadSource(false);
|
setIsRemovingDownloadSource(false);
|
||||||
|
channel.close();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,15 +72,17 @@ export function SettingsDownloadSources() {
|
|||||||
const syncDownloadSources = async () => {
|
const syncDownloadSources = async () => {
|
||||||
setIsSyncingDownloadSources(true);
|
setIsSyncingDownloadSources(true);
|
||||||
|
|
||||||
window.electron
|
const id = crypto.randomUUID();
|
||||||
.syncDownloadSources(downloadSources)
|
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||||
.then(() => {
|
|
||||||
showSuccessToast(t("download_sources_synced"));
|
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||||
getDownloadSources();
|
|
||||||
})
|
channel.onmessage = () => {
|
||||||
.finally(() => {
|
showSuccessToast(t("download_sources_synced"));
|
||||||
setIsSyncingDownloadSources(false);
|
getDownloadSources();
|
||||||
});
|
setIsSyncingDownloadSources(false);
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusTitle = {
|
const statusTitle = {
|
||||||
|
@ -1,16 +1,45 @@
|
|||||||
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import axios, { AxiosError, AxiosHeaders } from "axios";
|
||||||
import { DownloadSourceStatus } from "@shared";
|
import { DownloadSourceStatus } from "@shared";
|
||||||
import type { DownloadSourceValidationResult } from "@types";
|
|
||||||
|
export const downloadSourceSchema = z.object({
|
||||||
|
name: z.string().max(255),
|
||||||
|
downloads: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string().max(255),
|
||||||
|
uris: z.array(z.string()),
|
||||||
|
uploadDate: z.string().max(255),
|
||||||
|
fileSize: z.string().max(255),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
type Payload =
|
type Payload =
|
||||||
| ["IMPORT_DOWNLOAD_SOURCE", DownloadSourceValidationResult & { url: string }]
|
| ["IMPORT_DOWNLOAD_SOURCE", string]
|
||||||
| ["DELETE_DOWNLOAD_SOURCE", number];
|
| ["DELETE_DOWNLOAD_SOURCE", number]
|
||||||
|
| ["VALIDATE_DOWNLOAD_SOURCE", string]
|
||||||
db.open();
|
| ["SYNC_DOWNLOAD_SOURCES", string];
|
||||||
|
|
||||||
self.onmessage = async (event: MessageEvent<Payload>) => {
|
self.onmessage = async (event: MessageEvent<Payload>) => {
|
||||||
const [type, data] = event.data;
|
const [type, data] = event.data;
|
||||||
|
|
||||||
|
if (type === "VALIDATE_DOWNLOAD_SOURCE") {
|
||||||
|
const response =
|
||||||
|
await axios.get<z.infer<typeof downloadSourceSchema>>(data);
|
||||||
|
|
||||||
|
const { name } = downloadSourceSchema.parse(response.data);
|
||||||
|
|
||||||
|
const channel = new BroadcastChannel(`download_sources:validate:${data}`);
|
||||||
|
|
||||||
|
channel.postMessage({
|
||||||
|
name,
|
||||||
|
etag: response.headers["etag"],
|
||||||
|
downloadCount: response.data.downloads.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "DELETE_DOWNLOAD_SOURCE") {
|
if (type === "DELETE_DOWNLOAD_SOURCE") {
|
||||||
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||||
await repacksTable.where({ downloadSourceId: data }).delete();
|
await repacksTable.where({ downloadSourceId: data }).delete();
|
||||||
@ -23,28 +52,29 @@ self.onmessage = async (event: MessageEvent<Payload>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === "IMPORT_DOWNLOAD_SOURCE") {
|
if (type === "IMPORT_DOWNLOAD_SOURCE") {
|
||||||
const result = data;
|
const response =
|
||||||
|
await axios.get<z.infer<typeof downloadSourceSchema>>(data);
|
||||||
|
|
||||||
await db.transaction("rw", downloadSourcesTable, repacksTable, async () => {
|
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const id = await downloadSourcesTable.add({
|
const id = await downloadSourcesTable.add({
|
||||||
url: result.url,
|
url: data,
|
||||||
name: result.name,
|
name: response.data.name,
|
||||||
etag: result.etag,
|
etag: response.headers["etag"],
|
||||||
status: DownloadSourceStatus.UpToDate,
|
status: DownloadSourceStatus.UpToDate,
|
||||||
downloadCount: result.downloads.length,
|
downloadCount: response.data.downloads.length,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadSource = await downloadSourcesTable.get(id);
|
const downloadSource = await downloadSourcesTable.get(id);
|
||||||
|
|
||||||
const repacks = result.downloads.map((download) => ({
|
const repacks = response.data.downloads.map((download) => ({
|
||||||
title: download.title,
|
title: download.title,
|
||||||
uris: download.uris,
|
uris: download.uris,
|
||||||
fileSize: download.fileSize,
|
fileSize: download.fileSize,
|
||||||
repacker: result.name,
|
repacker: response.data.name,
|
||||||
uploadDate: download.uploadDate,
|
uploadDate: download.uploadDate,
|
||||||
downloadSourceId: downloadSource!.id,
|
downloadSourceId: downloadSource!.id,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
@ -54,10 +84,82 @@ self.onmessage = async (event: MessageEvent<Payload>) => {
|
|||||||
await repacksTable.bulkAdd(repacks);
|
await repacksTable.bulkAdd(repacks);
|
||||||
});
|
});
|
||||||
|
|
||||||
const channel = new BroadcastChannel(
|
const channel = new BroadcastChannel(`download_sources:import:${data}`);
|
||||||
`download_sources:import:${result.url}`
|
|
||||||
);
|
|
||||||
|
|
||||||
channel.postMessage(true);
|
channel.postMessage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "SYNC_DOWNLOAD_SOURCES") {
|
||||||
|
const channel = new BroadcastChannel(`download_sources:sync:${data}`);
|
||||||
|
let newRepacksCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const downloadSources = await downloadSourcesTable.toArray();
|
||||||
|
const existingRepacks = await repacksTable.toArray();
|
||||||
|
|
||||||
|
for (const downloadSource of downloadSources) {
|
||||||
|
const headers = new AxiosHeaders();
|
||||||
|
|
||||||
|
if (downloadSource.etag) {
|
||||||
|
headers.set("If-None-Match", downloadSource.etag);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(downloadSource.url, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const source = downloadSourceSchema.parse(response.data);
|
||||||
|
|
||||||
|
await db.transaction(
|
||||||
|
"rw",
|
||||||
|
repacksTable,
|
||||||
|
downloadSourcesTable,
|
||||||
|
async () => {
|
||||||
|
await downloadSourcesTable.update(downloadSource.id, {
|
||||||
|
etag: response.headers["etag"],
|
||||||
|
downloadCount: source.downloads.length,
|
||||||
|
status: DownloadSourceStatus.UpToDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const repacks = source.downloads
|
||||||
|
.filter(
|
||||||
|
(download) =>
|
||||||
|
!existingRepacks.some(
|
||||||
|
(repack) => repack.title === download.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((download) => ({
|
||||||
|
title: download.title,
|
||||||
|
uris: download.uris,
|
||||||
|
fileSize: download.fileSize,
|
||||||
|
repacker: source.name,
|
||||||
|
uploadDate: download.uploadDate,
|
||||||
|
downloadSourceId: downloadSource.id,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}));
|
||||||
|
|
||||||
|
newRepacksCount += repacks.length;
|
||||||
|
|
||||||
|
await repacksTable.bulkAdd(repacks);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const isNotModified = (err as AxiosError).response?.status === 304;
|
||||||
|
|
||||||
|
await downloadSourcesTable.update(downloadSource.id, {
|
||||||
|
status: isNotModified
|
||||||
|
? DownloadSourceStatus.UpToDate
|
||||||
|
: DownloadSourceStatus.Errored,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.postMessage(newRepacksCount);
|
||||||
|
} catch (err) {
|
||||||
|
channel.postMessage(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import MigrationWorker from "./migration.worker?worker";
|
|
||||||
import RepacksWorker from "./repacks.worker?worker";
|
import RepacksWorker from "./repacks.worker?worker";
|
||||||
import DownloadSourcesWorker from "./download-sources.worker?worker";
|
import DownloadSourcesWorker from "./download-sources.worker?worker";
|
||||||
|
|
||||||
export const migrationWorker = new MigrationWorker();
|
|
||||||
export const repacksWorker = new RepacksWorker();
|
export const repacksWorker = new RepacksWorker();
|
||||||
export const downloadSourcesWorker = new DownloadSourcesWorker();
|
export const downloadSourcesWorker = new DownloadSourcesWorker();
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
// import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
|
||||||
import { DownloadSource, GameRepack } from "@types";
|
|
||||||
|
|
||||||
export type Payload = [DownloadSource[], GameRepack[]];
|
|
||||||
|
|
||||||
self.onmessage = async (_event: MessageEvent<Payload>) => {
|
|
||||||
// const [downloadSources, gameRepacks] = event.data;
|
|
||||||
// const downloadSourcesCount = await downloadSourcesTable.count();
|
|
||||||
// if (downloadSources.length > downloadSourcesCount) {
|
|
||||||
// await db.transaction(
|
|
||||||
// "rw",
|
|
||||||
// downloadSourcesTable,
|
|
||||||
// repacksTable,
|
|
||||||
// async () => {}
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// if (type === "MIGRATE_DOWNLOAD_SOURCES") {
|
|
||||||
// const dexieDownloadSources = await downloadSourcesTable.count();
|
|
||||||
// if (data.length > dexieDownloadSources) {
|
|
||||||
// await downloadSourcesTable.clear();
|
|
||||||
// await downloadSourcesTable.bulkAdd(data);
|
|
||||||
// }
|
|
||||||
// self.postMessage("MIGRATE_DOWNLOAD_SOURCES_COMPLETE");
|
|
||||||
// }
|
|
||||||
// if (type === "MIGRATE_REPACKS") {
|
|
||||||
// const dexieRepacks = await repacksTable.count();
|
|
||||||
// if (data.length > dexieRepacks) {
|
|
||||||
// await repacksTable.clear();
|
|
||||||
// await repacksTable.bulkAdd(
|
|
||||||
// data.map((repack) => ({ ...repack, uris: JSON.stringify(repack.uris) }))
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// self.postMessage("MIGRATE_REPACKS_COMPLETE");
|
|
||||||
// }
|
|
||||||
};
|
|
@ -233,8 +233,8 @@ export interface DownloadSourceDownload {
|
|||||||
|
|
||||||
export interface DownloadSourceValidationResult {
|
export interface DownloadSourceValidationResult {
|
||||||
name: string;
|
name: string;
|
||||||
downloads: DownloadSourceDownload[];
|
|
||||||
etag: string;
|
etag: string;
|
||||||
|
downloadCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadSource {
|
export interface DownloadSource {
|
||||||
|
@ -3,6 +3,10 @@ export interface LudusaviScanChange {
|
|||||||
decision: "Processed" | "Cancelled" | "Ignore";
|
decision: "Processed" | "Cancelled" | "Ignore";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LudusaviGame extends LudusaviScanChange {
|
||||||
|
files: Record<string, LudusaviScanChange>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LudusaviBackup {
|
export interface LudusaviBackup {
|
||||||
overall: {
|
overall: {
|
||||||
totalGames: number;
|
totalGames: number;
|
||||||
@ -15,7 +19,7 @@ export interface LudusaviBackup {
|
|||||||
same: number;
|
same: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
games: Record<string, LudusaviScanChange>;
|
games: Record<string, LudusaviGame>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LudusaviFindResult {
|
export interface LudusaviFindResult {
|
||||||
|
Loading…
Reference in New Issue
Block a user