mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-15 04:32:13 +00:00
feat: starting showing local achievements
This commit is contained in:
parent
5b0cf1e82b
commit
7e3cf0a00e
25 changed files with 102 additions and 12 deletions
|
@ -5,14 +5,14 @@ import {
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { Game } from "./game.entity";
|
import type { Game } from "./game.entity";
|
||||||
|
|
||||||
@Entity("game_achievement")
|
@Entity("game_achievement")
|
||||||
export class GameAchievement {
|
export class GameAchievement {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@OneToOne(() => Game)
|
@OneToOne("Game", "achievements")
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
game: Game;
|
game: Game;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Repack } from "./repack.entity";
|
||||||
import type { GameShop, GameStatus } from "@types";
|
import type { GameShop, GameStatus } from "@types";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import type { DownloadQueue } from "./download-queue.entity";
|
import type { DownloadQueue } from "./download-queue.entity";
|
||||||
|
import { GameAchievement } from "./game-achievements.entity";
|
||||||
|
|
||||||
@Entity("game")
|
@Entity("game")
|
||||||
export class Game {
|
export class Game {
|
||||||
|
@ -76,6 +77,9 @@ export class Game {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
repack: Repack;
|
repack: Repack;
|
||||||
|
|
||||||
|
@OneToOne("GameAchievement", "game")
|
||||||
|
achievements: GameAchievement;
|
||||||
|
|
||||||
@OneToOne("DownloadQueue", "game")
|
@OneToOne("DownloadQueue", "game")
|
||||||
downloadQueue: DownloadQueue;
|
downloadQueue: DownloadQueue;
|
||||||
|
|
||||||
|
|
46
src/main/events/catalogue/get-game-achievements.ts
Normal file
46
src/main/events/catalogue/get-game-achievements.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { GameAchievement } from "@main/entity";
|
||||||
|
|
||||||
|
const getGameAchievements = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop
|
||||||
|
): Promise<GameAchievement[]> => {
|
||||||
|
const game = await gameRepository.findOne({
|
||||||
|
where: { objectID: objectId, shop },
|
||||||
|
relations: {
|
||||||
|
achievements: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const gameAchievements = await HydraApi.get(
|
||||||
|
"/games/achievements",
|
||||||
|
{ objectId, shop },
|
||||||
|
{ needsAuth: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const unlockedAchievements = JSON.parse(
|
||||||
|
game?.achievements?.unlockedAchievements || "[]"
|
||||||
|
) as { name: string; unlockTime: number }[];
|
||||||
|
|
||||||
|
return gameAchievements.map((achievement) => {
|
||||||
|
const unlockedAchiement = unlockedAchievements.find((localAchievement) => {
|
||||||
|
return localAchievement.name == achievement.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unlockedAchiement) {
|
||||||
|
return {
|
||||||
|
...achievement,
|
||||||
|
unlocked: true,
|
||||||
|
unlockTime: unlockedAchiement.unlockTime * 1000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...achievement, unlocked: false, unlockTime: null };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getGameAchievements", getGameAchievements);
|
|
@ -9,13 +9,10 @@ const getGameStats = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => {
|
) => {
|
||||||
const params = new URLSearchParams({
|
|
||||||
objectId,
|
|
||||||
shop,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await HydraApi.get<GameStats>(
|
const response = await HydraApi.get<GameStats>(
|
||||||
`/games/stats?${params.toString()}`
|
`/games/stats`,
|
||||||
|
{ objectId, shop },
|
||||||
|
{ needsAuth: false }
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ import "./catalogue/search-games";
|
||||||
import "./catalogue/search-game-repacks";
|
import "./catalogue/search-game-repacks";
|
||||||
import "./catalogue/get-game-stats";
|
import "./catalogue/get-game-stats";
|
||||||
import "./catalogue/get-trending-games";
|
import "./catalogue/get-trending-games";
|
||||||
|
import "./catalogue/get-game-achievements";
|
||||||
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";
|
||||||
|
|
|
@ -67,6 +67,7 @@ const runMigrations = async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await knexClient.migrate.down(migrationConfig);
|
||||||
await knexClient.migrate.latest(migrationConfig);
|
await knexClient.migrate.latest(migrationConfig);
|
||||||
await knexClient.destroy();
|
await knexClient.destroy();
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { publishNewRepacksNotifications } from "./services/notifications";
|
||||||
import { MoreThan } from "typeorm";
|
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";
|
||||||
import { saveAllLocalSteamAchivements } from "./events/achievements/services/save-all-local-steam-achivements";
|
import { saveAllLocalSteamAchivements } from "./services/achievements/services/save-all-local-steam-achivements";
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
RepacksManager.updateRepacks();
|
RepacksManager.updateRepacks();
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { Game } from "@main/entity";
|
||||||
import {
|
import {
|
||||||
startGameAchievementObserver,
|
startGameAchievementObserver,
|
||||||
stopGameAchievementObserver,
|
stopGameAchievementObserver,
|
||||||
} from "@main/events/achievements/game-achievements-observer";
|
} from "@main/services/achievements/game-achievements-observer";
|
||||||
|
|
||||||
export const gamesPlaytime = new Map<
|
export const gamesPlaytime = new Map<
|
||||||
number,
|
number,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { CatalogueCategory } from "@shared";
|
import type { CatalogueCategory } from "@shared";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
/* Torrenting */
|
/* Torrenting */
|
||||||
|
@ -49,6 +50,8 @@ 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"),
|
||||||
|
getGameAchievements: (objectId: string, shop: GameShop) =>
|
||||||
|
ipcRenderer.invoke("getGameAchievements", objectId, shop),
|
||||||
|
|
||||||
/* User preferences */
|
/* User preferences */
|
||||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Game,
|
Game,
|
||||||
|
GameAchievement,
|
||||||
GameRepack,
|
GameRepack,
|
||||||
GameShop,
|
GameShop,
|
||||||
GameStats,
|
GameStats,
|
||||||
|
@ -53,6 +54,7 @@ export function GameDetailsContextProvider({
|
||||||
|
|
||||||
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
||||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||||
|
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const [game, setGame] = useState<Game | null>(null);
|
||||||
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
|
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
|
||||||
|
|
||||||
|
@ -99,8 +101,9 @@ export function GameDetailsContextProvider({
|
||||||
),
|
),
|
||||||
window.electron.searchGameRepacks(gameTitle),
|
window.electron.searchGameRepacks(gameTitle),
|
||||||
window.electron.getGameStats(objectID!, shop as GameShop),
|
window.electron.getGameStats(objectID!, shop as GameShop),
|
||||||
|
window.electron.getGameAchievements(objectID!, shop as GameShop),
|
||||||
])
|
])
|
||||||
.then(([appDetailsResult, repacksResult, statsResult]) => {
|
.then(([appDetailsResult, repacksResult, statsResult, achievements]) => {
|
||||||
if (appDetailsResult.status === "fulfilled") {
|
if (appDetailsResult.status === "fulfilled") {
|
||||||
setShopDetails(appDetailsResult.value);
|
setShopDetails(appDetailsResult.value);
|
||||||
|
|
||||||
|
@ -117,6 +120,11 @@ export function GameDetailsContextProvider({
|
||||||
setRepacks(repacksResult.value);
|
setRepacks(repacksResult.value);
|
||||||
|
|
||||||
if (statsResult.status === "fulfilled") setStats(statsResult.value);
|
if (statsResult.status === "fulfilled") setStats(statsResult.value);
|
||||||
|
|
||||||
|
if (achievements.status === "fulfilled") {
|
||||||
|
console.log(achievements.value);
|
||||||
|
setAchievements(achievements.value);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -193,6 +201,7 @@ export function GameDetailsContextProvider({
|
||||||
showGameOptionsModal,
|
showGameOptionsModal,
|
||||||
showRepacksModal,
|
showRepacksModal,
|
||||||
stats,
|
stats,
|
||||||
|
achievements,
|
||||||
hasNSFWContentBlocked,
|
hasNSFWContentBlocked,
|
||||||
setHasNSFWContentBlocked,
|
setHasNSFWContentBlocked,
|
||||||
setGameColor,
|
setGameColor,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
Game,
|
Game,
|
||||||
|
GameAchievement,
|
||||||
GameRepack,
|
GameRepack,
|
||||||
GameShop,
|
GameShop,
|
||||||
GameStats,
|
GameStats,
|
||||||
|
@ -19,6 +20,7 @@ export interface GameDetailsContext {
|
||||||
showRepacksModal: boolean;
|
showRepacksModal: boolean;
|
||||||
showGameOptionsModal: boolean;
|
showGameOptionsModal: boolean;
|
||||||
stats: GameStats | null;
|
stats: GameStats | null;
|
||||||
|
achievements: GameAchievement[];
|
||||||
hasNSFWContentBlocked: boolean;
|
hasNSFWContentBlocked: boolean;
|
||||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||||
selectGameExecutable: () => Promise<string | null>;
|
selectGameExecutable: () => Promise<string | null>;
|
||||||
|
|
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
|
@ -25,6 +25,7 @@ import type {
|
||||||
UserStats,
|
UserStats,
|
||||||
UserDetails,
|
UserDetails,
|
||||||
FriendRequestSync,
|
FriendRequestSync,
|
||||||
|
GameAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
|
@ -65,6 +66,10 @@ 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[]>;
|
||||||
|
getGameAchievements: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop
|
||||||
|
) => Promise<GameAchievement[]>;
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
|
|
|
@ -17,7 +17,8 @@ export function Sidebar() {
|
||||||
const [activeRequirement, setActiveRequirement] =
|
const [activeRequirement, setActiveRequirement] =
|
||||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||||
|
|
||||||
const { gameTitle, shopDetails, stats } = useContext(gameDetailsContext);
|
const { gameTitle, shopDetails, stats, achievements } =
|
||||||
|
useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
@ -45,6 +46,17 @@ export function Sidebar() {
|
||||||
isLoading={howLongToBeat.isLoading}
|
isLoading={howLongToBeat.isLoading}
|
||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
|
{achievements.map((achievement, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<img
|
||||||
|
src={achievement.unlocked ? achievement.icon : achievement.icongray}
|
||||||
|
/>
|
||||||
|
<p>{achievement.displayName}</p>
|
||||||
|
{achievement.unlockTime &&
|
||||||
|
new Date(achievement.unlockTime).toDateString()}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -28,6 +28,16 @@ export interface GameRepack {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GameAchievement {
|
||||||
|
name: string;
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue