feat: migrating achievements to level

This commit is contained in:
Chubby Granny Chaser 2025-01-16 02:30:09 +00:00
parent 2c881a6100
commit a23106b0b1
No known key found for this signature in database
34 changed files with 388 additions and 475 deletions

View File

@ -1,23 +1,11 @@
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { import { DownloadQueue, Game, UserPreferences } from "@main/entity";
DownloadQueue,
Game,
GameShopCache,
UserPreferences,
GameAchievement,
} from "@main/entity";
import { databasePath } from "./constants"; import { databasePath } from "./constants";
export const dataSource = new DataSource({ export const dataSource = new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
entities: [ entities: [Game, UserPreferences, DownloadQueue],
Game,
UserPreferences,
GameShopCache,
DownloadQueue,
GameAchievement,
],
synchronize: false, synchronize: false,
database: databasePath, database: databasePath,
}); });

View File

@ -1,19 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity("game_achievement")
export class GameAchievement {
@PrimaryGeneratedColumn()
id: number;
@Column("text")
objectId: string;
@Column("text")
shop: string;
@Column("text", { nullable: true })
unlockedAchievements: string | null;
@Column("text", { nullable: true })
achievements: string | null;
}

View File

@ -1,35 +0,0 @@
import {
Entity,
PrimaryColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
import type { GameShop } from "@types";
@Entity("game_shop_cache")
export class GameShopCache {
@PrimaryColumn("text", { unique: true })
objectID: string;
@Column("text")
shop: GameShop;
@Column("text", { nullable: true })
serializedData: string;
/**
* @deprecated Use IndexedDB's `howLongToBeatEntries` instead
*/
@Column("text", { nullable: true })
howLongToBeatSerializedData: string;
@Column("text", { nullable: true })
language: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,5 +1,3 @@
export * from "./game.entity"; export * from "./game.entity";
export * from "./user-preferences.entity"; export * from "./user-preferences.entity";
export * from "./game-shop-cache.entity";
export * from "./game-achievements.entity";
export * from "./download-queue.entity"; export * from "./download-queue.entity";

View File

@ -3,7 +3,7 @@ import jwt from "jsonwebtoken";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { db } from "@main/level"; import { db } from "@main/level";
import type { Auth } from "@types"; import type { Auth } from "@types";
import { levelKeys } from "@main/level/sublevels/keys"; import { levelKeys } from "@main/level";
import { Crypto } from "@main/services"; import { Crypto } from "@main/services";
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => { const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {

View File

@ -4,7 +4,7 @@ import { dataSource } from "@main/data-source";
import { DownloadQueue, Game } from "@main/entity"; import { DownloadQueue, Game } from "@main/entity";
import { PythonRPC } from "@main/services/python-rpc"; import { PythonRPC } from "@main/services/python-rpc";
import { db } from "@main/level"; import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels/keys"; import { levelKeys } from "@main/level";
const signOut = async (_event: Electron.IpcMainInvokeEvent) => { const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
const databaseOperations = dataSource const databaseOperations = dataSource

View File

@ -1,10 +1,10 @@
import { gameShopCacheRepository } from "@main/repository"; import { getSteamAppDetails, logger } from "@main/services";
import { getSteamAppDetails } from "@main/services";
import type { ShopDetails, GameShop, SteamAppDetails } from "@types"; import type { ShopDetails, GameShop } from "@types";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { steamGamesWorker } from "@main/workers"; import { steamGamesWorker } from "@main/workers";
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
const getLocalizedSteamAppDetails = async ( const getLocalizedSteamAppDetails = async (
objectId: string, objectId: string,
@ -39,35 +39,27 @@ const getGameShopDetails = async (
language: string language: string
): Promise<ShopDetails | null> => { ): Promise<ShopDetails | null> => {
if (shop === "steam") { if (shop === "steam") {
const cachedData = await gameShopCacheRepository.findOne({ const cachedData = await gamesShopCacheSublevel.get(
where: { objectID: objectId, language }, levelKeys.gameShopCacheItem(shop, objectId, language)
}); );
const appDetails = getLocalizedSteamAppDetails(objectId, language).then( const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
(result) => { (result) => {
if (result) { if (result) {
gameShopCacheRepository.upsert( gamesShopCacheSublevel
{ .put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
objectID: objectId, .catch((err) => {
shop: "steam", logger.error("Could not cache game details", err);
language, });
serializedData: JSON.stringify(result),
},
["objectID"]
);
} }
return result; return result;
} }
); );
const cachedGame = cachedData?.serializedData if (cachedData) {
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
: null;
if (cachedGame) {
return { return {
...cachedGame, ...cachedData,
objectId, objectId,
} as ShopDetails; } as ShopDetails;
} }

View File

@ -1,9 +1,10 @@
import { gameAchievementRepository, gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files"; import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
import fs from "fs"; import fs from "fs";
import { achievementsLogger, HydraApi, WindowManager } from "@main/services"; import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
import { getUnlockedAchievements } from "../user/get-unlocked-achievements"; import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
import { gameAchievementsSublevel, levelKeys } from "@main/level";
const resetGameAchievements = async ( const resetGameAchievements = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -23,12 +24,21 @@ const resetGameAchievements = async (
} }
} }
await gameAchievementRepository.update( const levelKey = levelKeys.game(game.shop, game.objectID);
{ objectId: game.objectID },
{ await gameAchievementsSublevel
unlockedAchievements: null, .get(levelKey)
} .then(async (gameAchievements) => {
); if (gameAchievements) {
await gameAchievementsSublevel.put(
levelKeys.game(game.shop, game.objectID),
{
...gameAchievements,
unlockedAchievements: [],
}
);
}
});
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then( await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
() => () =>

View File

@ -3,7 +3,7 @@ import { registerEvent } from "../register-event";
import { Crypto, HydraApi } from "@main/services"; import { Crypto, HydraApi } from "@main/services";
import { db } from "@main/level"; import { db } from "@main/level";
import type { Auth } from "@types"; import type { Auth } from "@types";
import { levelKeys } from "@main/level/sublevels/keys"; import { levelKeys } from "@main/level";
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const auth = await db.get<string, Auth>(levelKeys.auth, { const auth = await db.get<string, Auth>(levelKeys.auth, {

View File

@ -1,19 +1,17 @@
import type { GameShop, UnlockedAchievement, UserAchievement } from "@types"; import type { GameShop, UserAchievement } from "@types";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { import { userPreferencesRepository } from "@main/repository";
gameAchievementRepository,
userPreferencesRepository,
} from "@main/repository";
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
import { gameAchievementsSublevel, levelKeys } from "@main/level";
export const getUnlockedAchievements = async ( export const getUnlockedAchievements = async (
objectId: string, objectId: string,
shop: GameShop, shop: GameShop,
useCachedData: boolean useCachedData: boolean
): Promise<UserAchievement[]> => { ): Promise<UserAchievement[]> => {
const cachedAchievements = await gameAchievementRepository.findOne({ const cachedAchievements = await gameAchievementsSublevel.get(
where: { objectId, shop }, levelKeys.game(shop, objectId)
}); );
const userPreferences = await userPreferencesRepository.findOne({ const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 }, where: { id: 1 },
@ -25,12 +23,10 @@ export const getUnlockedAchievements = async (
const achievementsData = await getGameAchievementData( const achievementsData = await getGameAchievementData(
objectId, objectId,
shop, shop,
useCachedData ? cachedAchievements : null useCachedData
); );
const unlockedAchievements = JSON.parse( const unlockedAchievements = cachedAchievements?.unlockedAchievements ?? [];
cachedAchievements?.unlockedAchievements || "[]"
) as UnlockedAchievement[];
return achievementsData return achievementsData
.map((achievementData) => { .map((achievementData) => {

View File

@ -2,7 +2,7 @@ import { db } from "@main/level";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services"; import { HydraApi } from "@main/services";
import type { User, UserFriends } from "@types"; import type { User, UserFriends } from "@types";
import { levelKeys } from "@main/level/sublevels/keys"; import { levelKeys } from "@main/level/sublevels";
export const getUserFriends = async ( export const getUserFriends = async (
userId: string, userId: string,

View File

@ -0,0 +1,11 @@
import type { GameAchievement } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";
export const gameAchievementsSublevel = db.sublevel<string, GameAchievement>(
levelKeys.gameAchievements,
{
valueEncoding: "json",
}
);

View File

@ -0,0 +1,11 @@
import type { ShopDetails } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";
export const gamesShopCacheSublevel = db.sublevel<string, ShopDetails>(
levelKeys.gameShopCache,
{
valueEncoding: "json",
}
);

View File

@ -1,4 +1,5 @@
import { Game } from "@types"; import type { Game } from "@types";
import { db } from "../level"; import { db } from "../level";
import { levelKeys } from "./keys"; import { levelKeys } from "./keys";

View File

@ -1 +1,5 @@
export * from "./games"; export * from "./games";
export * from "./game-shop-cache";
export * from "./game-achievements";
export * from "./keys";

View File

@ -5,4 +5,8 @@ export const levelKeys = {
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`, game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
user: "user", user: "user",
auth: "auth", auth: "auth",
gameShopCache: "gameShopCache",
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
`${shop}:${objectId}:${language}`,
gameAchievements: "gameAchievements",
}; };

View File

@ -1,20 +1,9 @@
import { dataSource } from "./data-source"; import { dataSource } from "./data-source";
import { import { DownloadQueue, Game, UserPreferences } from "@main/entity";
DownloadQueue,
Game,
GameShopCache,
UserPreferences,
GameAchievement,
} from "@main/entity";
export const gameRepository = dataSource.getRepository(Game); export const gameRepository = dataSource.getRepository(Game);
export const userPreferencesRepository = export const userPreferencesRepository =
dataSource.getRepository(UserPreferences); dataSource.getRepository(UserPreferences);
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
export const gameAchievementRepository =
dataSource.getRepository(GameAchievement);

View File

@ -1,40 +1,36 @@
import { import { userPreferencesRepository } from "@main/repository";
gameAchievementRepository,
userPreferencesRepository,
} from "@main/repository";
import { HydraApi } from "../hydra-api"; import { HydraApi } from "../hydra-api";
import type { AchievementData, GameShop } from "@types"; import type { GameShop, SteamAchievement } from "@types";
import { UserNotLoggedInError } from "@shared"; import { UserNotLoggedInError } from "@shared";
import { logger } from "../logger"; import { logger } from "../logger";
import { GameAchievement } from "@main/entity"; import { gameAchievementsSublevel, levelKeys } from "@main/level";
export const getGameAchievementData = async ( export const getGameAchievementData = async (
objectId: string, objectId: string,
shop: GameShop, shop: GameShop,
cachedAchievements: GameAchievement | null useCachedData: boolean
) => { ) => {
if (cachedAchievements && cachedAchievements.achievements) { const cachedAchievements = await gameAchievementsSublevel.get(
return JSON.parse(cachedAchievements.achievements) as AchievementData[]; levelKeys.game(shop, objectId)
} );
if (cachedAchievements && useCachedData)
return cachedAchievements.achievements;
const userPreferences = await userPreferencesRepository.findOne({ const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 }, where: { id: 1 },
}); });
return HydraApi.get<AchievementData[]>("/games/achievements", { return HydraApi.get<SteamAchievement[]>("/games/achievements", {
shop, shop,
objectId, objectId,
language: userPreferences?.language || "en", language: userPreferences?.language || "en",
}) })
.then((achievements) => { .then(async (achievements) => {
gameAchievementRepository.upsert( await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
{ unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
objectId, achievements,
shop, });
achievements: JSON.stringify(achievements),
},
["objectId", "shop"]
);
return achievements; return achievements;
}) })
@ -42,15 +38,9 @@ export const getGameAchievementData = async (
if (err instanceof UserNotLoggedInError) { if (err instanceof UserNotLoggedInError) {
throw err; throw err;
} }
logger.error("Failed to get game achievements", err); logger.error("Failed to get game achievements", err);
return gameAchievementRepository
.findOne({ return [];
where: { objectId, shop },
})
.then((gameAchievements) => {
return JSON.parse(
gameAchievements?.achievements || "[]"
) as AchievementData[];
});
}); });
}; };

View File

@ -1,8 +1,5 @@
import { import { userPreferencesRepository } from "@main/repository";
gameAchievementRepository, import type { GameShop, UnlockedAchievement } from "@types";
userPreferencesRepository,
} from "@main/repository";
import type { AchievementData, GameShop, UnlockedAchievement } from "@types";
import { WindowManager } from "../window-manager"; import { WindowManager } from "../window-manager";
import { HydraApi } from "../hydra-api"; import { HydraApi } from "../hydra-api";
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
@ -10,33 +7,36 @@ import { Game } from "@main/entity";
import { publishNewAchievementNotification } from "../notifications"; import { publishNewAchievementNotification } from "../notifications";
import { SubscriptionRequiredError } from "@shared"; import { SubscriptionRequiredError } from "@shared";
import { achievementsLogger } from "../logger"; import { achievementsLogger } from "../logger";
import { gameAchievementsSublevel, levelKeys } from "@main/level";
const saveAchievementsOnLocal = async ( const saveAchievementsOnLocal = async (
objectId: string, objectId: string,
shop: GameShop, shop: GameShop,
achievements: UnlockedAchievement[], unlockedAchievements: UnlockedAchievement[],
sendUpdateEvent: boolean sendUpdateEvent: boolean
) => { ) => {
return gameAchievementRepository const levelKey = levelKeys.game(shop, objectId);
.upsert(
{
objectId,
shop,
unlockedAchievements: JSON.stringify(achievements),
},
["objectId", "shop"]
)
.then(() => {
if (!sendUpdateEvent) return;
return getUnlockedAchievements(objectId, shop, true) return gameAchievementsSublevel
.then((achievements) => { .get(levelKey)
WindowManager.mainWindow?.webContents.send( .then(async (gameAchievement) => {
`on-update-achievements-${objectId}-${shop}`, if (gameAchievement) {
achievements await gameAchievementsSublevel.put(levelKey, {
); ...gameAchievement,
}) unlockedAchievements: unlockedAchievements,
.catch(() => {}); });
if (!sendUpdateEvent) return;
return getUnlockedAchievements(objectId, shop, true)
.then((achievements) => {
WindowManager.mainWindow?.webContents.send(
`on-update-achievements-${objectId}-${shop}`,
achievements
);
})
.catch(() => {});
}
}); });
}; };
@ -46,22 +46,12 @@ export const mergeAchievements = async (
publishNotification: boolean publishNotification: boolean
) => { ) => {
const [localGameAchievement, userPreferences] = await Promise.all([ const [localGameAchievement, userPreferences] = await Promise.all([
gameAchievementRepository.findOne({ gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectID)),
where: {
objectId: game.objectID,
shop: game.shop,
},
}),
userPreferencesRepository.findOne({ where: { id: 1 } }), userPreferencesRepository.findOne({ where: { id: 1 } }),
]); ]);
const achievementsData = JSON.parse( const achievementsData = localGameAchievement?.achievements ?? [];
localGameAchievement?.achievements || "[]" const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
) as AchievementData[];
const unlockedAchievements = JSON.parse(
localGameAchievement?.unlockedAchievements || "[]"
).filter((achievement) => achievement.name) as UnlockedAchievement[];
const newAchievementsMap = new Map( const newAchievementsMap = new Map(
achievements.reverse().map((achievement) => { achievements.reverse().map((achievement) => {

View File

@ -10,7 +10,7 @@ import { appVersion } from "@main/constants";
import { getUserData } from "./user/get-user-data"; import { getUserData } from "./user/get-user-data";
import { isFuture, isToday } from "date-fns"; import { isFuture, isToday } from "date-fns";
import { db } from "@main/level"; import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels/keys"; import { levelKeys } from "@main/level/sublevels";
import type { Auth, User } from "@types"; import type { Auth, User } from "@types";
import { Crypto } from "./crypto"; import { Crypto } from "./crypto";

View File

@ -3,7 +3,7 @@ import { HydraApi } from "../hydra-api";
import { UserNotLoggedInError } from "@shared"; import { UserNotLoggedInError } from "@shared";
import { logger } from "../logger"; import { logger } from "../logger";
import { db } from "@main/level"; import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels/keys"; import { levelKeys } from "@main/level/sublevels";
export const getUserData = async () => { export const getUserData = async () => {
return HydraApi.get<UserDetails>(`/profile/me`) return HydraApi.get<UserDetails>(`/profile/me`)

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import type { LibraryGame } from "@types"; import type { Game } from "@types";
import { TextField } from "@renderer/components"; import { TextField } from "@renderer/components";
import { import {
@ -35,7 +35,7 @@ export function Sidebar() {
const { library, updateLibrary } = useLibrary(); const { library, updateLibrary } = useLibrary();
const navigate = useNavigate(); const navigate = useNavigate();
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]); const [filteredLibrary, setFilteredLibrary] = useState<Game[]>([]);
const [isResizing, setIsResizing] = useState(false); const [isResizing, setIsResizing] = useState(false);
const [sidebarWidth, setSidebarWidth] = useState( const [sidebarWidth, setSidebarWidth] = useState(
@ -117,7 +117,7 @@ export function Sidebar() {
}; };
}, [isResizing]); }, [isResizing]);
const getGameTitle = (game: LibraryGame) => { const getGameTitle = (game: Game) => {
if (lastPacket?.game.id === game.id) { if (lastPacket?.game.id === game.id) {
return t("downloading", { return t("downloading", {
title: game.title, title: game.title,
@ -140,10 +140,7 @@ export function Sidebar() {
} }
}; };
const handleSidebarGameClick = ( const handleSidebarGameClick = (event: React.MouseEvent, game: Game) => {
event: React.MouseEvent,
game: LibraryGame
) => {
const path = buildGameDetailsPath({ const path = buildGameDetailsPath({
...game, ...game,
objectId: game.objectID, objectId: game.objectID,

View File

@ -2,7 +2,6 @@ import type { CatalogueCategory } from "@shared";
import type { import type {
AppUpdaterEvent, AppUpdaterEvent,
Game, Game,
LibraryGame,
GameShop, GameShop,
HowLongToBeatCategory, HowLongToBeatCategory,
ShopDetails, ShopDetails,
@ -23,7 +22,6 @@ import type {
UserStats, UserStats,
UserDetails, UserDetails,
FriendRequestSync, FriendRequestSync,
GameAchievement,
GameArtifact, GameArtifact,
LudusaviBackup, LudusaviBackup,
UserAchievement, UserAchievement,
@ -77,7 +75,7 @@ declare global {
onUpdateAchievements: ( onUpdateAchievements: (
objectId: string, objectId: string,
shop: GameShop, shop: GameShop,
cb: (achievements: GameAchievement[]) => void cb: (achievements: UserAchievement[]) => void
) => () => Electron.IpcRenderer; ) => () => Electron.IpcRenderer;
getPublishers: () => Promise<string[]>; getPublishers: () => Promise<string[]>;
getDevelopers: () => Promise<string[]>; getDevelopers: () => Promise<string[]>;
@ -102,7 +100,7 @@ declare global {
winePrefixPath: string | null winePrefixPath: string | null
) => Promise<void>; ) => Promise<void>;
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>; verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
getLibrary: () => Promise<LibraryGame[]>; getLibrary: () => Promise<Game[]>;
openGameInstaller: (gameId: number) => Promise<boolean>; openGameInstaller: (gameId: number) => Promise<boolean>;
openGameInstallerPath: (gameId: number) => Promise<boolean>; openGameInstallerPath: (gameId: number) => Promise<boolean>;
openGameExecutablePath: (gameId: number) => Promise<void>; openGameExecutablePath: (gameId: number) => Promise<void>;

View File

@ -1,10 +1,10 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit";
import type { LibraryGame } from "@types"; import type { Game } from "@types";
export interface LibraryState { export interface LibraryState {
value: LibraryGame[]; value: Game[];
} }
const initialState: LibraryState = { const initialState: LibraryState = {

View File

@ -47,7 +47,14 @@ export function AchievementList({ achievements }: AchievementListProps) {
</h4> </h4>
<p>{achievement.description}</p> <p>{achievement.description}</p>
</div> </div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}> <div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
alignItems: "flex-end",
}}
>
{achievement.points != undefined ? ( {achievement.points != undefined ? (
<div <div
style={{ display: "flex", alignItems: "center", gap: "4px" }} style={{ display: "flex", alignItems: "center", gap: "4px" }}

View File

@ -1,6 +1,6 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import type { LibraryGame, SeedingStatus } from "@types"; import type { Game, SeedingStatus } from "@types";
import { Badge, Button } from "@renderer/components"; import { Badge, Button } from "@renderer/components";
import { import {
@ -32,7 +32,7 @@ import {
} from "@primer/octicons-react"; } from "@primer/octicons-react";
export interface DownloadGroupProps { export interface DownloadGroupProps {
library: LibraryGame[]; library: Game[];
title: string; title: string;
openDeleteGameModal: (gameId: number) => void; openDeleteGameModal: (gameId: number) => void;
openGameInstaller: (gameId: number) => void; openGameInstaller: (gameId: number) => void;
@ -65,7 +65,7 @@ export function DownloadGroup({
resumeSeeding, resumeSeeding,
} = useDownload(); } = useDownload();
const getFinalDownloadSize = (game: LibraryGame) => { const getFinalDownloadSize = (game: Game) => {
const isGameDownloading = lastPacket?.game.id === game.id; const isGameDownloading = lastPacket?.game.id === game.id;
if (game.fileSize) return formatBytes(game.fileSize); if (game.fileSize) return formatBytes(game.fileSize);
@ -86,7 +86,7 @@ export function DownloadGroup({
return map; return map;
}, [seedingStatus]); }, [seedingStatus]);
const getGameInfo = (game: LibraryGame) => { const getGameInfo = (game: Game) => {
const isGameDownloading = lastPacket?.game.id === game.id; const isGameDownloading = lastPacket?.game.id === game.id;
const finalDownloadSize = getFinalDownloadSize(game); const finalDownloadSize = getFinalDownloadSize(game);
const seedingStatus = seedingMap.get(game.id); const seedingStatus = seedingMap.get(game.id);
@ -165,7 +165,7 @@ export function DownloadGroup({
return <p>{t(game.status as string)}</p>; return <p>{t(game.status as string)}</p>;
}; };
const getGameActions = (game: LibraryGame): DropdownMenuItem[] => { const getGameActions = (game: Game): DropdownMenuItem[] => {
const isGameDownloading = lastPacket?.game.id === game.id; const isGameDownloading = lastPacket?.game.id === game.id;
const deleting = isGameDeleting(game.id); const deleting = isGameDeleting(game.id);

View File

@ -7,7 +7,7 @@ import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
import * as styles from "./downloads.css"; import * as styles from "./downloads.css";
import { DeleteGameModal } from "./delete-game-modal"; import { DeleteGameModal } from "./delete-game-modal";
import { DownloadGroup } from "./download-group"; import { DownloadGroup } from "./download-group";
import type { LibraryGame, SeedingStatus } from "@types"; import type { Game, SeedingStatus } from "@types";
import { orderBy } from "lodash-es"; import { orderBy } from "lodash-es";
import { ArrowDownIcon } from "@primer/octicons-react"; import { ArrowDownIcon } from "@primer/octicons-react";
@ -49,8 +49,8 @@ export default function Downloads() {
setShowDeleteModal(true); setShowDeleteModal(true);
}; };
const libraryGroup: Record<string, LibraryGame[]> = useMemo(() => { const libraryGroup: Record<string, Game[]> = useMemo(() => {
const initialValue: Record<string, LibraryGame[]> = { const initialValue: Record<string, Game[]> = {
downloading: [], downloading: [],
queued: [], queued: [],
complete: [], complete: [],

View File

@ -23,7 +23,7 @@ import { buildGameAchievementPath } from "@renderer/helpers";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { useSubscription } from "@renderer/hooks/use-subscription"; import { useSubscription } from "@renderer/hooks/use-subscription";
const fakeAchievements: UserAchievement[] = [ const achievementsPlaceholder: UserAchievement[] = [
{ {
displayName: "Timber!!", displayName: "Timber!!",
name: "", name: "",
@ -140,7 +140,7 @@ export function Sidebar() {
<h3>{t("sign_in_to_see_achievements")}</h3> <h3>{t("sign_in_to_see_achievements")}</h3>
</div> </div>
<ul className={styles.list} style={{ filter: "blur(4px)" }}> <ul className={styles.list} style={{ filter: "blur(4px)" }}>
{fakeAchievements.map((achievement, index) => ( {achievementsPlaceholder.map((achievement, index) => (
<li key={index}> <li key={index}>
<div className={styles.listItem}> <div className={styles.listItem}>
<img <img

167
src/types/download.types.ts Normal file
View File

@ -0,0 +1,167 @@
import type { Game, GameStatus } from "./game.types";
export interface DownloadProgress {
downloadSpeed: number;
timeRemaining: number;
numPeers: number;
numSeeds: number;
isDownloadingMetadata: boolean;
isCheckingFiles: boolean;
progress: number;
gameId: number;
game: Game;
}
/* Torbox */
export interface TorBoxUser {
id: number;
email: string;
plan: string;
expiration: string;
}
export interface TorBoxUserRequest {
success: boolean;
detail: string;
error: string;
data: TorBoxUser;
}
export interface TorBoxFile {
id: number;
md5: string;
s3_path: string;
name: string;
size: number;
mimetype: string;
short_name: string;
}
export interface TorBoxTorrentInfo {
id: number;
hash: string;
created_at: string;
updated_at: string;
magnet: string;
size: number;
active: boolean;
cached: boolean;
auth_id: string;
download_state:
| "downloading"
| "uploading"
| "stalled (no seeds)"
| "paused"
| "completed"
| "cached"
| "metaDL"
| "checkingResumeData";
seeds: number;
ratio: number;
progress: number;
download_speed: number;
upload_speed: number;
name: string;
eta: number;
files: TorBoxFile[];
}
export interface TorBoxTorrentInfoRequest {
success: boolean;
detail: string;
error: string;
data: TorBoxTorrentInfo[];
}
export interface TorBoxAddTorrentRequest {
success: boolean;
detail: string;
error: string;
data: {
torrent_id: number;
name: string;
hash: string;
};
}
export interface TorBoxRequestLinkRequest {
success: boolean;
detail: string;
error: string;
data: string;
}
/* Real-Debrid */
export interface RealDebridUnrestrictLink {
id: string;
filename: string;
mimeType: string;
filesize: number;
link: string;
host: string;
host_icon: string;
chunks: number;
crc: number;
download: string;
streamable: number;
}
export interface RealDebridAddMagnet {
id: string;
// URL of the created resource
uri: string;
}
export interface RealDebridTorrentInfo {
id: string;
filename: string;
original_filename: string;
hash: string;
bytes: number;
original_bytes: number;
host: string;
split: number;
progress: number;
status:
| "magnet_error"
| "magnet_conversion"
| "waiting_files_selection"
| "queued"
| "downloading"
| "downloaded"
| "error"
| "virus"
| "compressing"
| "uploading"
| "dead";
added: string;
files: {
id: number;
path: string;
bytes: number;
selected: number;
}[];
links: string[];
ended: string;
speed: number;
seeders: number;
}
export interface RealDebridUser {
id: number;
username: string;
email: string;
points: number;
locale: string;
avatar: string;
type: string;
premium: number;
expiration: string;
}
/* Torrent */
export interface SeedingStatus {
gameId: number;
status: GameStatus;
uploadSpeed: number;
}

59
src/types/game.types.ts Normal file
View File

@ -0,0 +1,59 @@
import type { Downloader } from "@shared";
export type GameStatus =
| "active"
| "waiting"
| "paused"
| "error"
| "complete"
| "seeding"
| "removed";
export type GameShop = "steam" | "epic";
export interface Game {
// TODO: To be depreacted
id: number;
title: string;
iconUrl: string;
status: GameStatus | null;
folderName: string;
downloadPath: string | null;
progress: number;
bytesDownloaded: number;
playTimeInMilliseconds: number;
downloader: Downloader;
winePrefixPath: string | null;
executablePath: string | null;
launchOptions: string | null;
lastTimePlayed: Date | null;
uri: string | null;
fileSize: number;
objectID: string;
shop: GameShop;
// downloadQueue: DownloadQueue | null;
downloadQueue: any | null;
shouldSeed: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface UnlockedAchievement {
name: string;
unlockTime: number;
}
export interface SteamAchievement {
name: string;
displayName: string;
description?: string;
icon: string;
icongray: string;
hidden: boolean;
points?: number;
}
export interface UserAchievement extends SteamAchievement {
unlocked: boolean;
unlockTime: number | null;
}

View File

@ -1,17 +1,7 @@
import type { Cracker, DownloadSourceStatus, Downloader } from "@shared"; import type { Cracker, DownloadSourceStatus, Downloader } from "@shared";
import type { SteamAppDetails } from "./steam.types"; import type { SteamAppDetails } from "./steam.types";
import type { Subscription } from "./level.types"; import type { Subscription } from "./level.types";
import type { GameShop } from "./game.types";
export type GameStatus =
| "active"
| "waiting"
| "paused"
| "error"
| "complete"
| "seeding"
| "removed";
export type GameShop = "steam" | "epic";
export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL"; export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL";
@ -32,48 +22,6 @@ export interface GameRepack {
updatedAt: Date; updatedAt: Date;
} }
export interface AchievementData {
name: string;
displayName: string;
description?: string;
icon: string;
icongray: string;
hidden: boolean;
points?: number;
}
export interface UserAchievement {
name: string;
hidden: boolean;
displayName: string;
points?: number;
description?: string;
unlocked: boolean;
unlockTime: number | null;
icon: string;
icongray: string;
}
export interface RemoteUnlockedAchievement {
name: string;
hidden: boolean;
icon: string;
displayName: string;
description?: string;
unlockTime: number;
}
export interface GameAchievement {
name: string;
hidden: boolean;
displayName: string;
description?: string;
unlocked: boolean;
unlockTime: number | null;
icon: string;
icongray: string;
}
export type ShopDetails = SteamAppDetails & { export type ShopDetails = SteamAppDetails & {
objectId: string; objectId: string;
}; };
@ -96,40 +44,6 @@ export interface UserGame {
achievementsPointsEarnedSum: number; achievementsPointsEarnedSum: number;
} }
export interface DownloadQueue {
id: number;
createdAt: Date;
updatedAt: Date;
}
/* Used by the library */
export interface Game {
id: number;
title: string;
iconUrl: string;
status: GameStatus | null;
folderName: string;
downloadPath: string | null;
progress: number;
bytesDownloaded: number;
playTimeInMilliseconds: number;
downloader: Downloader;
winePrefixPath: string | null;
executablePath: string | null;
launchOptions: string | null;
lastTimePlayed: Date | null;
uri: string | null;
fileSize: number;
objectID: string;
shop: GameShop;
downloadQueue: DownloadQueue | null;
shouldSeed: boolean;
createdAt: Date;
updatedAt: Date;
}
export type LibraryGame = Omit<Game, "repacks">;
export interface GameRunning { export interface GameRunning {
id?: number; id?: number;
title: string; title: string;
@ -139,24 +53,6 @@ export interface GameRunning {
sessionDurationInMillis: number; sessionDurationInMillis: number;
} }
export interface DownloadProgress {
downloadSpeed: number;
timeRemaining: number;
numPeers: number;
numSeeds: number;
isDownloadingMetadata: boolean;
isCheckingFiles: boolean;
progress: number;
gameId: number;
game: LibraryGame;
}
export interface SeedingStatus {
gameId: number;
status: GameStatus;
uploadSpeed: number;
}
export interface UserPreferences { export interface UserPreferences {
downloadsPath: string | null; downloadsPath: string | null;
language: string; language: string;
@ -344,11 +240,6 @@ export interface UserStats {
unlockedAchievementSum?: number; unlockedAchievementSum?: number;
} }
export interface UnlockedAchievement {
name: string;
unlockTime: number;
}
export interface AchievementFile { export interface AchievementFile {
type: Cracker; type: Cracker;
filePath: string; filePath: string;
@ -407,9 +298,9 @@ export interface CatalogueSearchPayload {
developers: string[]; developers: string[];
} }
export * from "./game.types";
export * from "./steam.types"; export * from "./steam.types";
export * from "./real-debrid.types"; export * from "./download.types";
export * from "./ludusavi.types"; export * from "./ludusavi.types";
export * from "./how-long-to-beat.types"; export * from "./how-long-to-beat.types";
export * from "./torbox.types";
export * from "./level.types"; export * from "./level.types";

View File

@ -1,3 +1,5 @@
import type { SteamAchievement, UnlockedAchievement } from "./game.types";
export type SubscriptionStatus = "active" | "pending" | "cancelled"; export type SubscriptionStatus = "active" | "pending" | "cancelled";
export interface Subscription { export interface Subscription {
@ -21,3 +23,8 @@ export interface User {
backgroundImageUrl: string | null; backgroundImageUrl: string | null;
subscription: Subscription | null; subscription: Subscription | null;
} }
export interface GameAchievement {
achievements: SteamAchievement[];
unlockedAchievements: UnlockedAchievement[];
}

View File

@ -1,66 +0,0 @@
export interface RealDebridUnrestrictLink {
id: string;
filename: string;
mimeType: string;
filesize: number;
link: string;
host: string;
host_icon: string;
chunks: number;
crc: number;
download: string;
streamable: number;
}
export interface RealDebridAddMagnet {
id: string;
// URL of the created resource
uri: string;
}
export interface RealDebridTorrentInfo {
id: string;
filename: string;
original_filename: string;
hash: string;
bytes: number;
original_bytes: number;
host: string;
split: number;
progress: number;
status:
| "magnet_error"
| "magnet_conversion"
| "waiting_files_selection"
| "queued"
| "downloading"
| "downloaded"
| "error"
| "virus"
| "compressing"
| "uploading"
| "dead";
added: string;
files: {
id: number;
path: string;
bytes: number;
selected: number;
}[];
links: string[];
ended: string;
speed: number;
seeders: number;
}
export interface RealDebridUser {
id: number;
username: string;
email: string;
points: number;
locale: string;
avatar: string;
type: string;
premium: number;
expiration: string;
}

View File

@ -1,77 +0,0 @@
export interface TorBoxUser {
id: number;
email: string;
plan: string;
expiration: string;
}
export interface TorBoxUserRequest {
success: boolean;
detail: string;
error: string;
data: TorBoxUser;
}
export interface TorBoxFile {
id: number;
md5: string;
s3_path: string;
name: string;
size: number;
mimetype: string;
short_name: string;
}
export interface TorBoxTorrentInfo {
id: number;
hash: string;
created_at: string;
updated_at: string;
magnet: string;
size: number;
active: boolean;
cached: boolean;
auth_id: string;
download_state:
| "downloading"
| "uploading"
| "stalled (no seeds)"
| "paused"
| "completed"
| "cached"
| "metaDL"
| "checkingResumeData";
seeds: number;
ratio: number;
progress: number;
download_speed: number;
upload_speed: number;
name: string;
eta: number;
files: TorBoxFile[];
}
export interface TorBoxTorrentInfoRequest {
success: boolean;
detail: string;
error: string;
data: TorBoxTorrentInfo[];
}
export interface TorBoxAddTorrentRequest {
success: boolean;
detail: string;
error: string;
data: {
torrent_id: number;
name: string;
hash: string;
};
}
export interface TorBoxRequestLinkRequest {
success: boolean;
detail: string;
error: string;
data: string;
}