mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-02 16:23:48 +03:00
feat: migrating achievements to level
This commit is contained in:
parent
2c881a6100
commit
a23106b0b1
@ -1,23 +1,11 @@
|
||||
import { DataSource } from "typeorm";
|
||||
import {
|
||||
DownloadQueue,
|
||||
Game,
|
||||
GameShopCache,
|
||||
UserPreferences,
|
||||
GameAchievement,
|
||||
} from "@main/entity";
|
||||
import { DownloadQueue, Game, UserPreferences } from "@main/entity";
|
||||
|
||||
import { databasePath } from "./constants";
|
||||
|
||||
export const dataSource = new DataSource({
|
||||
type: "better-sqlite3",
|
||||
entities: [
|
||||
Game,
|
||||
UserPreferences,
|
||||
GameShopCache,
|
||||
DownloadQueue,
|
||||
GameAchievement,
|
||||
],
|
||||
entities: [Game, UserPreferences, DownloadQueue],
|
||||
synchronize: false,
|
||||
database: databasePath,
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
export * from "./game.entity";
|
||||
export * from "./user-preferences.entity";
|
||||
export * from "./game-shop-cache.entity";
|
||||
export * from "./game-achievements.entity";
|
||||
export * from "./download-queue.entity";
|
||||
|
@ -3,7 +3,7 @@ import jwt from "jsonwebtoken";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { db } from "@main/level";
|
||||
import type { Auth } from "@types";
|
||||
import { levelKeys } from "@main/level/sublevels/keys";
|
||||
import { levelKeys } from "@main/level";
|
||||
import { Crypto } from "@main/services";
|
||||
|
||||
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
|
@ -4,7 +4,7 @@ import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { PythonRPC } from "@main/services/python-rpc";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level/sublevels/keys";
|
||||
import { levelKeys } from "@main/level";
|
||||
|
||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const databaseOperations = dataSource
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { gameShopCacheRepository } from "@main/repository";
|
||||
import { getSteamAppDetails } from "@main/services";
|
||||
import { getSteamAppDetails, logger } from "@main/services";
|
||||
|
||||
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
||||
import type { ShopDetails, GameShop } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const getLocalizedSteamAppDetails = async (
|
||||
objectId: string,
|
||||
@ -39,35 +39,27 @@ const getGameShopDetails = async (
|
||||
language: string
|
||||
): Promise<ShopDetails | null> => {
|
||||
if (shop === "steam") {
|
||||
const cachedData = await gameShopCacheRepository.findOne({
|
||||
where: { objectID: objectId, language },
|
||||
});
|
||||
const cachedData = await gamesShopCacheSublevel.get(
|
||||
levelKeys.gameShopCacheItem(shop, objectId, language)
|
||||
);
|
||||
|
||||
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
||||
(result) => {
|
||||
if (result) {
|
||||
gameShopCacheRepository.upsert(
|
||||
{
|
||||
objectID: objectId,
|
||||
shop: "steam",
|
||||
language,
|
||||
serializedData: JSON.stringify(result),
|
||||
},
|
||||
["objectID"]
|
||||
);
|
||||
gamesShopCacheSublevel
|
||||
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
|
||||
.catch((err) => {
|
||||
logger.error("Could not cache game details", err);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
const cachedGame = cachedData?.serializedData
|
||||
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
|
||||
: null;
|
||||
|
||||
if (cachedGame) {
|
||||
if (cachedData) {
|
||||
return {
|
||||
...cachedGame,
|
||||
...cachedData,
|
||||
objectId,
|
||||
} as ShopDetails;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
|
||||
import fs from "fs";
|
||||
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
|
||||
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
|
||||
import { gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const resetGameAchievements = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -23,12 +24,21 @@ const resetGameAchievements = async (
|
||||
}
|
||||
}
|
||||
|
||||
await gameAchievementRepository.update(
|
||||
{ objectId: game.objectID },
|
||||
{
|
||||
unlockedAchievements: null,
|
||||
}
|
||||
);
|
||||
const levelKey = levelKeys.game(game.shop, game.objectID);
|
||||
|
||||
await gameAchievementsSublevel
|
||||
.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(
|
||||
() =>
|
||||
|
@ -3,7 +3,7 @@ import { registerEvent } from "../register-event";
|
||||
import { Crypto, HydraApi } from "@main/services";
|
||||
import { db } from "@main/level";
|
||||
import type { Auth } from "@types";
|
||||
import { levelKeys } from "@main/level/sublevels/keys";
|
||||
import { levelKeys } from "@main/level";
|
||||
|
||||
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||
|
@ -1,19 +1,17 @@
|
||||
import type { GameShop, UnlockedAchievement, UserAchievement } from "@types";
|
||||
import type { GameShop, UserAchievement } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||
import { gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const getUnlockedAchievements = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
useCachedData: boolean
|
||||
): Promise<UserAchievement[]> => {
|
||||
const cachedAchievements = await gameAchievementRepository.findOne({
|
||||
where: { objectId, shop },
|
||||
});
|
||||
const cachedAchievements = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
@ -25,12 +23,10 @@ export const getUnlockedAchievements = async (
|
||||
const achievementsData = await getGameAchievementData(
|
||||
objectId,
|
||||
shop,
|
||||
useCachedData ? cachedAchievements : null
|
||||
useCachedData
|
||||
);
|
||||
|
||||
const unlockedAchievements = JSON.parse(
|
||||
cachedAchievements?.unlockedAchievements || "[]"
|
||||
) as UnlockedAchievement[];
|
||||
const unlockedAchievements = cachedAchievements?.unlockedAchievements ?? [];
|
||||
|
||||
return achievementsData
|
||||
.map((achievementData) => {
|
||||
|
@ -2,7 +2,7 @@ import { db } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { User, UserFriends } from "@types";
|
||||
import { levelKeys } from "@main/level/sublevels/keys";
|
||||
import { levelKeys } from "@main/level/sublevels";
|
||||
|
||||
export const getUserFriends = async (
|
||||
userId: string,
|
||||
|
11
src/main/level/sublevels/game-achievements.ts
Normal file
11
src/main/level/sublevels/game-achievements.ts
Normal 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",
|
||||
}
|
||||
);
|
11
src/main/level/sublevels/game-shop-cache.ts
Normal file
11
src/main/level/sublevels/game-shop-cache.ts
Normal 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",
|
||||
}
|
||||
);
|
@ -1,4 +1,5 @@
|
||||
import { Game } from "@types";
|
||||
import type { Game } from "@types";
|
||||
|
||||
import { db } from "../level";
|
||||
import { levelKeys } from "./keys";
|
||||
|
||||
|
@ -1 +1,5 @@
|
||||
export * from "./games";
|
||||
export * from "./game-shop-cache";
|
||||
export * from "./game-achievements";
|
||||
|
||||
export * from "./keys";
|
||||
|
@ -5,4 +5,8 @@ export const levelKeys = {
|
||||
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
|
||||
user: "user",
|
||||
auth: "auth",
|
||||
gameShopCache: "gameShopCache",
|
||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||
`${shop}:${objectId}:${language}`,
|
||||
gameAchievements: "gameAchievements",
|
||||
};
|
||||
|
@ -1,20 +1,9 @@
|
||||
import { dataSource } from "./data-source";
|
||||
import {
|
||||
DownloadQueue,
|
||||
Game,
|
||||
GameShopCache,
|
||||
UserPreferences,
|
||||
GameAchievement,
|
||||
} from "@main/entity";
|
||||
import { DownloadQueue, Game, UserPreferences } from "@main/entity";
|
||||
|
||||
export const gameRepository = dataSource.getRepository(Game);
|
||||
|
||||
export const userPreferencesRepository =
|
||||
dataSource.getRepository(UserPreferences);
|
||||
|
||||
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
|
||||
|
||||
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||
|
||||
export const gameAchievementRepository =
|
||||
dataSource.getRepository(GameAchievement);
|
||||
|
@ -1,40 +1,36 @@
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import type { AchievementData, GameShop } from "@types";
|
||||
import type { GameShop, SteamAchievement } from "@types";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { logger } from "../logger";
|
||||
import { GameAchievement } from "@main/entity";
|
||||
import { gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const getGameAchievementData = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
cachedAchievements: GameAchievement | null
|
||||
useCachedData: boolean
|
||||
) => {
|
||||
if (cachedAchievements && cachedAchievements.achievements) {
|
||||
return JSON.parse(cachedAchievements.achievements) as AchievementData[];
|
||||
}
|
||||
const cachedAchievements = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
|
||||
if (cachedAchievements && useCachedData)
|
||||
return cachedAchievements.achievements;
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
return HydraApi.get<AchievementData[]>("/games/achievements", {
|
||||
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
|
||||
shop,
|
||||
objectId,
|
||||
language: userPreferences?.language || "en",
|
||||
})
|
||||
.then((achievements) => {
|
||||
gameAchievementRepository.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop,
|
||||
achievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
);
|
||||
.then(async (achievements) => {
|
||||
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
||||
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
||||
achievements,
|
||||
});
|
||||
|
||||
return achievements;
|
||||
})
|
||||
@ -42,15 +38,9 @@ export const getGameAchievementData = async (
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
logger.error("Failed to get game achievements", err);
|
||||
return gameAchievementRepository
|
||||
.findOne({
|
||||
where: { objectId, shop },
|
||||
})
|
||||
.then((gameAchievements) => {
|
||||
return JSON.parse(
|
||||
gameAchievements?.achievements || "[]"
|
||||
) as AchievementData[];
|
||||
});
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
@ -1,8 +1,5 @@
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import type { AchievementData, GameShop, UnlockedAchievement } from "@types";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import type { GameShop, UnlockedAchievement } from "@types";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
||||
@ -10,33 +7,36 @@ import { Game } from "@main/entity";
|
||||
import { publishNewAchievementNotification } from "../notifications";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const saveAchievementsOnLocal = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
achievements: UnlockedAchievement[],
|
||||
unlockedAchievements: UnlockedAchievement[],
|
||||
sendUpdateEvent: boolean
|
||||
) => {
|
||||
return gameAchievementRepository
|
||||
.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop,
|
||||
unlockedAchievements: JSON.stringify(achievements),
|
||||
},
|
||||
["objectId", "shop"]
|
||||
)
|
||||
.then(() => {
|
||||
if (!sendUpdateEvent) return;
|
||||
const levelKey = levelKeys.game(shop, objectId);
|
||||
|
||||
return getUnlockedAchievements(objectId, shop, true)
|
||||
.then((achievements) => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${objectId}-${shop}`,
|
||||
achievements
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
return gameAchievementsSublevel
|
||||
.get(levelKey)
|
||||
.then(async (gameAchievement) => {
|
||||
if (gameAchievement) {
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
...gameAchievement,
|
||||
unlockedAchievements: unlockedAchievements,
|
||||
});
|
||||
|
||||
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
|
||||
) => {
|
||||
const [localGameAchievement, userPreferences] = await Promise.all([
|
||||
gameAchievementRepository.findOne({
|
||||
where: {
|
||||
objectId: game.objectID,
|
||||
shop: game.shop,
|
||||
},
|
||||
}),
|
||||
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectID)),
|
||||
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
||||
]);
|
||||
|
||||
const achievementsData = JSON.parse(
|
||||
localGameAchievement?.achievements || "[]"
|
||||
) as AchievementData[];
|
||||
|
||||
const unlockedAchievements = JSON.parse(
|
||||
localGameAchievement?.unlockedAchievements || "[]"
|
||||
).filter((achievement) => achievement.name) as UnlockedAchievement[];
|
||||
const achievementsData = localGameAchievement?.achievements ?? [];
|
||||
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
||||
|
||||
const newAchievementsMap = new Map(
|
||||
achievements.reverse().map((achievement) => {
|
||||
|
@ -10,7 +10,7 @@ import { appVersion } from "@main/constants";
|
||||
import { getUserData } from "./user/get-user-data";
|
||||
import { isFuture, isToday } from "date-fns";
|
||||
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 { Crypto } from "./crypto";
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { HydraApi } from "../hydra-api";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { logger } from "../logger";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level/sublevels/keys";
|
||||
import { levelKeys } from "@main/level/sublevels";
|
||||
|
||||
export const getUserData = async () => {
|
||||
return HydraApi.get<UserDetails>(`/profile/me`)
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import type { LibraryGame } from "@types";
|
||||
import type { Game } from "@types";
|
||||
|
||||
import { TextField } from "@renderer/components";
|
||||
import {
|
||||
@ -35,7 +35,7 @@ export function Sidebar() {
|
||||
const { library, updateLibrary } = useLibrary();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
||||
const [filteredLibrary, setFilteredLibrary] = useState<Game[]>([]);
|
||||
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(
|
||||
@ -117,7 +117,7 @@ export function Sidebar() {
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
const getGameTitle = (game: LibraryGame) => {
|
||||
const getGameTitle = (game: Game) => {
|
||||
if (lastPacket?.game.id === game.id) {
|
||||
return t("downloading", {
|
||||
title: game.title,
|
||||
@ -140,10 +140,7 @@ export function Sidebar() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSidebarGameClick = (
|
||||
event: React.MouseEvent,
|
||||
game: LibraryGame
|
||||
) => {
|
||||
const handleSidebarGameClick = (event: React.MouseEvent, game: Game) => {
|
||||
const path = buildGameDetailsPath({
|
||||
...game,
|
||||
objectId: game.objectID,
|
||||
|
6
src/renderer/src/declaration.d.ts
vendored
6
src/renderer/src/declaration.d.ts
vendored
@ -2,7 +2,6 @@ import type { CatalogueCategory } from "@shared";
|
||||
import type {
|
||||
AppUpdaterEvent,
|
||||
Game,
|
||||
LibraryGame,
|
||||
GameShop,
|
||||
HowLongToBeatCategory,
|
||||
ShopDetails,
|
||||
@ -23,7 +22,6 @@ import type {
|
||||
UserStats,
|
||||
UserDetails,
|
||||
FriendRequestSync,
|
||||
GameAchievement,
|
||||
GameArtifact,
|
||||
LudusaviBackup,
|
||||
UserAchievement,
|
||||
@ -77,7 +75,7 @@ declare global {
|
||||
onUpdateAchievements: (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
cb: (achievements: GameAchievement[]) => void
|
||||
cb: (achievements: UserAchievement[]) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
getPublishers: () => Promise<string[]>;
|
||||
getDevelopers: () => Promise<string[]>;
|
||||
@ -102,7 +100,7 @@ declare global {
|
||||
winePrefixPath: string | null
|
||||
) => Promise<void>;
|
||||
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
||||
getLibrary: () => Promise<LibraryGame[]>;
|
||||
getLibrary: () => Promise<Game[]>;
|
||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||
openGameInstallerPath: (gameId: number) => Promise<boolean>;
|
||||
openGameExecutablePath: (gameId: number) => Promise<void>;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
import type { LibraryGame } from "@types";
|
||||
import type { Game } from "@types";
|
||||
|
||||
export interface LibraryState {
|
||||
value: LibraryGame[];
|
||||
value: Game[];
|
||||
}
|
||||
|
||||
const initialState: LibraryState = {
|
||||
|
@ -47,7 +47,14 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
||||
</h4>
|
||||
<p>{achievement.description}</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
{achievement.points != undefined ? (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: "4px" }}
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 {
|
||||
@ -32,7 +32,7 @@ import {
|
||||
} from "@primer/octicons-react";
|
||||
|
||||
export interface DownloadGroupProps {
|
||||
library: LibraryGame[];
|
||||
library: Game[];
|
||||
title: string;
|
||||
openDeleteGameModal: (gameId: number) => void;
|
||||
openGameInstaller: (gameId: number) => void;
|
||||
@ -65,7 +65,7 @@ export function DownloadGroup({
|
||||
resumeSeeding,
|
||||
} = useDownload();
|
||||
|
||||
const getFinalDownloadSize = (game: LibraryGame) => {
|
||||
const getFinalDownloadSize = (game: Game) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
if (game.fileSize) return formatBytes(game.fileSize);
|
||||
@ -86,7 +86,7 @@ export function DownloadGroup({
|
||||
return map;
|
||||
}, [seedingStatus]);
|
||||
|
||||
const getGameInfo = (game: LibraryGame) => {
|
||||
const getGameInfo = (game: Game) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
const finalDownloadSize = getFinalDownloadSize(game);
|
||||
const seedingStatus = seedingMap.get(game.id);
|
||||
@ -165,7 +165,7 @@ export function DownloadGroup({
|
||||
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 deleting = isGameDeleting(game.id);
|
||||
|
@ -7,7 +7,7 @@ import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./downloads.css";
|
||||
import { DeleteGameModal } from "./delete-game-modal";
|
||||
import { DownloadGroup } from "./download-group";
|
||||
import type { LibraryGame, SeedingStatus } from "@types";
|
||||
import type { Game, SeedingStatus } from "@types";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||
|
||||
@ -49,8 +49,8 @@ export default function Downloads() {
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const libraryGroup: Record<string, LibraryGame[]> = useMemo(() => {
|
||||
const initialValue: Record<string, LibraryGame[]> = {
|
||||
const libraryGroup: Record<string, Game[]> = useMemo(() => {
|
||||
const initialValue: Record<string, Game[]> = {
|
||||
downloading: [],
|
||||
queued: [],
|
||||
complete: [],
|
||||
|
@ -23,7 +23,7 @@ import { buildGameAchievementPath } from "@renderer/helpers";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
|
||||
const fakeAchievements: UserAchievement[] = [
|
||||
const achievementsPlaceholder: UserAchievement[] = [
|
||||
{
|
||||
displayName: "Timber!!",
|
||||
name: "",
|
||||
@ -140,7 +140,7 @@ export function Sidebar() {
|
||||
<h3>{t("sign_in_to_see_achievements")}</h3>
|
||||
</div>
|
||||
<ul className={styles.list} style={{ filter: "blur(4px)" }}>
|
||||
{fakeAchievements.map((achievement, index) => (
|
||||
{achievementsPlaceholder.map((achievement, index) => (
|
||||
<li key={index}>
|
||||
<div className={styles.listItem}>
|
||||
<img
|
||||
|
167
src/types/download.types.ts
Normal file
167
src/types/download.types.ts
Normal 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
59
src/types/game.types.ts
Normal 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;
|
||||
}
|
@ -1,17 +1,7 @@
|
||||
import type { Cracker, DownloadSourceStatus, Downloader } from "@shared";
|
||||
import type { SteamAppDetails } from "./steam.types";
|
||||
import type { Subscription } from "./level.types";
|
||||
|
||||
export type GameStatus =
|
||||
| "active"
|
||||
| "waiting"
|
||||
| "paused"
|
||||
| "error"
|
||||
| "complete"
|
||||
| "seeding"
|
||||
| "removed";
|
||||
|
||||
export type GameShop = "steam" | "epic";
|
||||
import type { GameShop } from "./game.types";
|
||||
|
||||
export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL";
|
||||
|
||||
@ -32,48 +22,6 @@ export interface GameRepack {
|
||||
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 & {
|
||||
objectId: string;
|
||||
};
|
||||
@ -96,40 +44,6 @@ export interface UserGame {
|
||||
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 {
|
||||
id?: number;
|
||||
title: string;
|
||||
@ -139,24 +53,6 @@ export interface GameRunning {
|
||||
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 {
|
||||
downloadsPath: string | null;
|
||||
language: string;
|
||||
@ -344,11 +240,6 @@ export interface UserStats {
|
||||
unlockedAchievementSum?: number;
|
||||
}
|
||||
|
||||
export interface UnlockedAchievement {
|
||||
name: string;
|
||||
unlockTime: number;
|
||||
}
|
||||
|
||||
export interface AchievementFile {
|
||||
type: Cracker;
|
||||
filePath: string;
|
||||
@ -407,9 +298,9 @@ export interface CatalogueSearchPayload {
|
||||
developers: string[];
|
||||
}
|
||||
|
||||
export * from "./game.types";
|
||||
export * from "./steam.types";
|
||||
export * from "./real-debrid.types";
|
||||
export * from "./download.types";
|
||||
export * from "./ludusavi.types";
|
||||
export * from "./how-long-to-beat.types";
|
||||
export * from "./torbox.types";
|
||||
export * from "./level.types";
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { SteamAchievement, UnlockedAchievement } from "./game.types";
|
||||
|
||||
export type SubscriptionStatus = "active" | "pending" | "cancelled";
|
||||
|
||||
export interface Subscription {
|
||||
@ -21,3 +23,8 @@ export interface User {
|
||||
backgroundImageUrl: string | null;
|
||||
subscription: Subscription | null;
|
||||
}
|
||||
|
||||
export interface GameAchievement {
|
||||
achievements: SteamAchievement[];
|
||||
unlockedAchievements: UnlockedAchievement[];
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user