mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-30 06:11:03 +00:00
Compare commits
17 Commits
release/v3
...
feat/downl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a447f683a | ||
|
|
039df43123 | ||
|
|
10ac6c9d9c | ||
|
|
9ca6a114b1 | ||
|
|
2108a523bc | ||
|
|
fbbb2520e0 | ||
|
|
049a989e85 | ||
|
|
88b2581797 | ||
|
|
c9801644ac | ||
|
|
98cfe7be98 | ||
|
|
7293afb618 | ||
|
|
194e7918ca | ||
|
|
979958aca6 | ||
|
|
6e92e0f79f | ||
|
|
aef069d4c7 | ||
|
|
5d2dc3616c | ||
|
|
96140e614c |
@@ -92,7 +92,7 @@
|
|||||||
"user-agents": "^1.1.387",
|
"user-agents": "^1.1.387",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"winreg": "^1.2.5",
|
"winreg": "^1.2.5",
|
||||||
"workwonders-sdk": "0.1.3",
|
"workwonders-sdk": "0.0.10",
|
||||||
"ws": "^8.18.1",
|
"ws": "^8.18.1",
|
||||||
"yaml": "^2.6.1",
|
"yaml": "^2.6.1",
|
||||||
"yup": "^1.5.0"
|
"yup": "^1.5.0"
|
||||||
|
|||||||
@@ -108,7 +108,17 @@
|
|||||||
"search_results": "Search results",
|
"search_results": "Search results",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"version_available_install": "Version {{version}} available. Click here to restart and install.",
|
"version_available_install": "Version {{version}} available. Click here to restart and install.",
|
||||||
"version_available_download": "Version {{version}} available. Click here to download."
|
"version_available_download": "Version {{version}} available. Click here to download.",
|
||||||
|
"scan_games_tooltip": "Scan PC for installed games",
|
||||||
|
"scan_games_title": "Scan PC for installed games",
|
||||||
|
"scan_games_description": "This will scan your disks for known game executables. This may take several minutes.",
|
||||||
|
"scan_games_start": "Start Scan",
|
||||||
|
"scan_games_cancel": "Cancel",
|
||||||
|
"scan_games_result": "Found {{found}} of {{total}} games without executable path",
|
||||||
|
"scan_games_no_results": "We couldn't find any installed games.",
|
||||||
|
"scan_games_in_progress": "Scanning your disks for installed games...",
|
||||||
|
"scan_games_close": "Close",
|
||||||
|
"scan_games_scan_again": "Scan Again"
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "No downloads in progress",
|
"no_downloads_in_progress": "No downloads in progress",
|
||||||
@@ -372,6 +382,9 @@
|
|||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"filter_by_source": "Filter by source",
|
"filter_by_source": "Filter by source",
|
||||||
"no_repacks_found": "No sources found for this game",
|
"no_repacks_found": "No sources found for this game",
|
||||||
|
"source_online": "Source is online",
|
||||||
|
"source_partial": "Some links are offline",
|
||||||
|
"source_offline": "Source is offline",
|
||||||
"delete_review": "Delete review",
|
"delete_review": "Delete review",
|
||||||
"remove_review": "Remove Review",
|
"remove_review": "Remove Review",
|
||||||
"delete_review_modal_title": "Are you sure you want to delete your review?",
|
"delete_review_modal_title": "Are you sure you want to delete your review?",
|
||||||
@@ -619,7 +632,11 @@
|
|||||||
"game_extracted": "{{title}} extracted successfully",
|
"game_extracted": "{{title}} extracted successfully",
|
||||||
"friend_started_playing_game": "{{displayName}} started playing a game",
|
"friend_started_playing_game": "{{displayName}} started playing a game",
|
||||||
"test_achievement_notification_title": "This is a test notification",
|
"test_achievement_notification_title": "This is a test notification",
|
||||||
"test_achievement_notification_description": "Pretty cool, huh?"
|
"test_achievement_notification_description": "Pretty cool, huh?",
|
||||||
|
"scan_games_complete_title": "Scanning for games finished successfully",
|
||||||
|
"scan_games_complete_description": "Found {{count}} games without executable path set",
|
||||||
|
"scan_games_no_results_title": "Scanning for games finished",
|
||||||
|
"scan_games_no_results_description": "No installed games were found"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Open Hydra",
|
"open": "Open Hydra",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
|
|||||||
const achievements = await gameAchievementsSublevel.get(key);
|
const achievements = await gameAchievementsSublevel.get(key);
|
||||||
|
|
||||||
unlockedAchievementCount =
|
unlockedAchievementCount =
|
||||||
achievements?.unlockedAchievements.length ?? 0;
|
achievements?.unlockedAchievements?.length ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import "./remove-game-from-favorites";
|
|||||||
import "./remove-game-from-library";
|
import "./remove-game-from-library";
|
||||||
import "./remove-game";
|
import "./remove-game";
|
||||||
import "./reset-game-achievements";
|
import "./reset-game-achievements";
|
||||||
|
import "./scan-installed-games";
|
||||||
import "./select-game-wine-prefix";
|
import "./select-game-wine-prefix";
|
||||||
import "./toggle-automatic-cloud-sync";
|
import "./toggle-automatic-cloud-sync";
|
||||||
import "./toggle-game-pin";
|
import "./toggle-game-pin";
|
||||||
|
|||||||
143
src/main/events/library/scan-installed-games.ts
Normal file
143
src/main/events/library/scan-installed-games.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel } from "@main/level";
|
||||||
|
import {
|
||||||
|
GameExecutables,
|
||||||
|
LocalNotificationManager,
|
||||||
|
logger,
|
||||||
|
WindowManager,
|
||||||
|
} from "@main/services";
|
||||||
|
|
||||||
|
const SCAN_DIRECTORIES = [
|
||||||
|
String.raw`C:\Games`,
|
||||||
|
String.raw`D:\Games`,
|
||||||
|
String.raw`C:\Program Files (x86)\Steam\steamapps\common`,
|
||||||
|
String.raw`C:\Program Files\Steam\steamapps\common`,
|
||||||
|
String.raw`C:\Program Files (x86)\DODI-Repacks`,
|
||||||
|
];
|
||||||
|
|
||||||
|
interface FoundGame {
|
||||||
|
title: string;
|
||||||
|
executablePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScanResult {
|
||||||
|
foundGames: FoundGame[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchInDirectories(
|
||||||
|
executableNames: Set<string>
|
||||||
|
): Promise<string | null> {
|
||||||
|
for (const scanDir of SCAN_DIRECTORIES) {
|
||||||
|
if (!fs.existsSync(scanDir)) continue;
|
||||||
|
|
||||||
|
const foundPath = await findExecutableInFolder(scanDir, executableNames);
|
||||||
|
if (foundPath) return foundPath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishScanNotification(foundCount: number): Promise<void> {
|
||||||
|
const hasFoundGames = foundCount > 0;
|
||||||
|
|
||||||
|
await LocalNotificationManager.createNotification(
|
||||||
|
"SCAN_GAMES_COMPLETE",
|
||||||
|
t(
|
||||||
|
hasFoundGames
|
||||||
|
? "scan_games_complete_title"
|
||||||
|
: "scan_games_no_results_title",
|
||||||
|
{ ns: "notifications" }
|
||||||
|
),
|
||||||
|
t(
|
||||||
|
hasFoundGames
|
||||||
|
? "scan_games_complete_description"
|
||||||
|
: "scan_games_no_results_description",
|
||||||
|
{ ns: "notifications", count: foundCount }
|
||||||
|
),
|
||||||
|
{ url: "/library?openScanModal=true" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scanInstalledGames = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent
|
||||||
|
): Promise<ScanResult> => {
|
||||||
|
const games = await gamesSublevel
|
||||||
|
.iterator()
|
||||||
|
.all()
|
||||||
|
.then((results) =>
|
||||||
|
results
|
||||||
|
.filter(
|
||||||
|
([_key, game]) => game.isDeleted === false && game.shop !== "custom"
|
||||||
|
)
|
||||||
|
.map(([key, game]) => ({ key, game }))
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundGames: FoundGame[] = [];
|
||||||
|
const gamesToScan = games.filter((g) => !g.game.executablePath);
|
||||||
|
|
||||||
|
for (const { key, game } of gamesToScan) {
|
||||||
|
const executableNames = GameExecutables.getExecutablesForGame(
|
||||||
|
game.objectId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!executableNames || executableNames.length === 0) continue;
|
||||||
|
|
||||||
|
const normalizedNames = new Set(
|
||||||
|
executableNames.map((name) => name.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundPath = await searchInDirectories(normalizedNames);
|
||||||
|
|
||||||
|
if (foundPath) {
|
||||||
|
await gamesSublevel.put(key, { ...game, executablePath: foundPath });
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[ScanInstalledGames] Found executable for ${game.objectId}: ${foundPath}`
|
||||||
|
);
|
||||||
|
|
||||||
|
foundGames.push({ title: game.title, executablePath: foundPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowManager.mainWindow?.webContents.send("on-library-batch-complete");
|
||||||
|
await publishScanNotification(foundGames.length);
|
||||||
|
|
||||||
|
return { foundGames, total: gamesToScan.length };
|
||||||
|
};
|
||||||
|
|
||||||
|
async function findExecutableInFolder(
|
||||||
|
folderPath: string,
|
||||||
|
executableNames: Set<string>
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const entries = await fs.promises.readdir(folderPath, {
|
||||||
|
withFileTypes: true,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
|
||||||
|
const fileName = entry.name.toLowerCase();
|
||||||
|
|
||||||
|
if (executableNames.has(fileName)) {
|
||||||
|
const parentPath =
|
||||||
|
"parentPath" in entry ? entry.parentPath : folderPath;
|
||||||
|
|
||||||
|
return path.join(parentPath, entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`[ScanInstalledGames] Error reading folder ${folderPath}:`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEvent("scanInstalledGames", scanInstalledGames);
|
||||||
@@ -362,6 +362,11 @@ export class DownloadManager {
|
|||||||
|
|
||||||
if (download.automaticallyExtract) {
|
if (download.automaticallyExtract) {
|
||||||
this.handleExtraction(download, game);
|
this.handleExtraction(download, game);
|
||||||
|
} else {
|
||||||
|
// For downloads without extraction (e.g., torrents with ready-to-play files),
|
||||||
|
// search for executable in the download folder
|
||||||
|
const gameFilesManager = new GameFilesManager(game.shop, game.objectId);
|
||||||
|
gameFilesManager.searchAndBindExecutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.processNextQueuedDownload();
|
await this.processNextQueuedDownload();
|
||||||
|
|||||||
13
src/main/services/game-executables.ts
Normal file
13
src/main/services/game-executables.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { gameExecutables } from "./process-watcher";
|
||||||
|
|
||||||
|
export class GameExecutables {
|
||||||
|
static getExecutablesForGame(objectId: string): string[] | null {
|
||||||
|
const executables = gameExecutables[objectId];
|
||||||
|
|
||||||
|
if (!executables || executables.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return executables.map((exe) => exe.exe);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { SevenZip, ExtractionProgress } from "./7zip";
|
|||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { publishExtractionCompleteNotification } from "./notifications";
|
import { publishExtractionCompleteNotification } from "./notifications";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
|
import { GameExecutables } from "./game-executables";
|
||||||
|
|
||||||
const PROGRESS_THROTTLE_MS = 1000;
|
const PROGRESS_THROTTLE_MS = 1000;
|
||||||
|
|
||||||
@@ -151,6 +152,100 @@ export class GameFilesManager {
|
|||||||
if (publishNotification && game) {
|
if (publishNotification && game) {
|
||||||
publishExtractionCompleteNotification(game);
|
publishExtractionCompleteNotification(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.searchAndBindExecutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchAndBindExecutable(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const [download, game] = await Promise.all([
|
||||||
|
downloadsSublevel.get(this.gameKey),
|
||||||
|
gamesSublevel.get(this.gameKey),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!download || !game || game.executablePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const executableNames = GameExecutables.getExecutablesForGame(
|
||||||
|
this.objectId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!executableNames || executableNames.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!download.folderName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameFolderPath = path.join(
|
||||||
|
download.downloadPath,
|
||||||
|
download.folderName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(gameFolderPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundExePath = await this.findExecutableInFolder(
|
||||||
|
gameFolderPath,
|
||||||
|
executableNames
|
||||||
|
);
|
||||||
|
|
||||||
|
if (foundExePath) {
|
||||||
|
logger.info(
|
||||||
|
`[GameFilesManager] Auto-detected executable for ${this.objectId}: ${foundExePath}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await gamesSublevel.put(this.gameKey, {
|
||||||
|
...game,
|
||||||
|
executablePath: foundExePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
WindowManager.mainWindow?.webContents.send("on-library-batch-complete");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`[GameFilesManager] Error searching for executable: ${this.objectId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findExecutableInFolder(
|
||||||
|
folderPath: string,
|
||||||
|
executableNames: string[]
|
||||||
|
): Promise<string | null> {
|
||||||
|
const normalizedNames = new Set(
|
||||||
|
executableNames.map((name) => name.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.promises.readdir(folderPath, {
|
||||||
|
withFileTypes: true,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
|
||||||
|
const fileName = entry.name.toLowerCase();
|
||||||
|
|
||||||
|
if (normalizedNames.has(fileName)) {
|
||||||
|
const parentPath =
|
||||||
|
"parentPath" in entry
|
||||||
|
? entry.parentPath
|
||||||
|
: (entry as unknown as { path?: string }).path || folderPath;
|
||||||
|
|
||||||
|
return path.join(parentPath, entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently fail if folder cannot be read
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async extractDownloadedFile() {
|
async extractDownloadedFile() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export * from "./ludusavi";
|
|||||||
export * from "./cloud-sync";
|
export * from "./cloud-sync";
|
||||||
export * from "./7zip";
|
export * from "./7zip";
|
||||||
export * from "./game-files-manager";
|
export * from "./game-files-manager";
|
||||||
|
export * from "./game-executables";
|
||||||
export * from "./common-redist-manager";
|
export * from "./common-redist-manager";
|
||||||
export * from "./aria2";
|
export * from "./aria2";
|
||||||
export * from "./ws";
|
export * from "./ws";
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const getGameExecutables = async () => {
|
|||||||
return gameExecutables;
|
return gameExecutables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const gameExecutables = await getGameExecutables();
|
export const gameExecutables = await getGameExecutables();
|
||||||
|
|
||||||
const findGamePathByProcess = async (
|
const findGamePathByProcess = async (
|
||||||
processMap: Map<string, Set<string>>,
|
processMap: Map<string, Set<string>>,
|
||||||
|
|||||||
@@ -138,12 +138,21 @@ export class WindowManager {
|
|||||||
(details, callback) => {
|
(details, callback) => {
|
||||||
if (
|
if (
|
||||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||||
details.url.includes("chatwoot") ||
|
details.url.includes("chatwoot")
|
||||||
details.url.includes("workwonders")
|
|
||||||
) {
|
) {
|
||||||
return callback(details);
|
return callback(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (details.url.includes("workwonders")) {
|
||||||
|
return callback({
|
||||||
|
...details,
|
||||||
|
requestHeaders: {
|
||||||
|
Origin: "https://workwonders.app",
|
||||||
|
...details.requestHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const userAgent = new UserAgent();
|
const userAgent = new UserAgent();
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("changeGamePlayTime", shop, objectId, playtime),
|
ipcRenderer.invoke("changeGamePlayTime", shop, objectId, playtime),
|
||||||
extractGameDownload: (shop: GameShop, objectId: string) =>
|
extractGameDownload: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
||||||
|
scanInstalledGames: () => ipcRenderer.invoke("scanInstalledGames"),
|
||||||
getDefaultWinePrefixSelectionPath: () =>
|
getDefaultWinePrefixSelectionPath: () =>
|
||||||
ipcRenderer.invoke("getDefaultWinePrefixSelectionPath"),
|
ipcRenderer.invoke("getDefaultWinePrefixSelectionPath"),
|
||||||
createSteamShortcut: (shop: GameShop, objectId: string) =>
|
createSteamShortcut: (shop: GameShop, objectId: string) =>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||||
import { WorkWonders } from "workwonders-sdk";
|
import { WorkWondersSdk } from "workwonders-sdk";
|
||||||
import {
|
import {
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
@@ -52,7 +52,7 @@ export function App() {
|
|||||||
|
|
||||||
const { clearDownload, setLastPacket } = useDownload();
|
const { clearDownload, setLastPacket } = useDownload();
|
||||||
|
|
||||||
const workwondersRef = useRef<WorkWonders | null>(null);
|
const workwondersRef = useRef<WorkWondersSdk | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasActiveSubscription,
|
hasActiveSubscription,
|
||||||
@@ -125,18 +125,18 @@ export function App() {
|
|||||||
const parsedLocale =
|
const parsedLocale =
|
||||||
possibleLocales.find((l) => l === locale?.slice(0, 2)) ?? "en";
|
possibleLocales.find((l) => l === locale?.slice(0, 2)) ?? "en";
|
||||||
|
|
||||||
workwondersRef.current = new WorkWonders();
|
workwondersRef.current = new WorkWondersSdk();
|
||||||
await workwondersRef.current.init({
|
await workwondersRef.current.init({
|
||||||
organization: "hydra",
|
organization: "hydra",
|
||||||
token,
|
token,
|
||||||
locale: parsedLocale,
|
locale: parsedLocale,
|
||||||
});
|
});
|
||||||
|
|
||||||
await workwondersRef.current.changelog.initChangelogWidget();
|
await workwondersRef.current.initChangelogWidget();
|
||||||
workwondersRef.current.changelog.initChangelogWidgetMini();
|
workwondersRef.current.initChangelogWidgetMini();
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
workwondersRef.current.feedback.initFeedbackWidget();
|
workwondersRef.current.initFeedbackWidget();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[workwondersRef]
|
[workwondersRef]
|
||||||
|
|||||||
@@ -61,10 +61,26 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
padding: globals.$spacing-unit;
|
padding: globals.$spacing-unit;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #dadbe1;
|
color: #dadbe1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--scanning svg {
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__section {
|
&__section {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
SearchIcon,
|
||||||
|
SyncIcon,
|
||||||
|
XIcon,
|
||||||
|
} from "@primer/octicons-react";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
@@ -12,6 +18,7 @@ import {
|
|||||||
|
|
||||||
import "./header.scss";
|
import "./header.scss";
|
||||||
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
||||||
|
import { ScanGamesModal } from "./scan-games-modal";
|
||||||
import { setFilters, setLibrarySearchQuery } from "@renderer/features";
|
import { setFilters, setLibrarySearchQuery } from "@renderer/features";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { SearchDropdown } from "@renderer/components";
|
import { SearchDropdown } from "@renderer/components";
|
||||||
@@ -29,9 +36,11 @@ const pathTitle: Record<string, string> = {
|
|||||||
export function Header() {
|
export function Header() {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const searchContainerRef = useRef<HTMLDivElement>(null);
|
const searchContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scanButtonTooltipId = useId();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const { headerTitle, draggingDisabled } = useAppSelector(
|
const { headerTitle, draggingDisabled } = useAppSelector(
|
||||||
(state) => state.window
|
(state) => state.window
|
||||||
@@ -61,6 +70,12 @@ export function Header() {
|
|||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
});
|
});
|
||||||
|
const [showScanModal, setShowScanModal] = useState(false);
|
||||||
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
|
const [scanResult, setScanResult] = useState<{
|
||||||
|
foundGames: { title: string; executablePath: string }[];
|
||||||
|
total: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const { t } = useTranslation("header");
|
const { t } = useTranslation("header");
|
||||||
|
|
||||||
@@ -224,6 +239,25 @@ export function Header() {
|
|||||||
setActiveIndex(-1);
|
setActiveIndex(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStartScan = async () => {
|
||||||
|
if (isScanning) return;
|
||||||
|
|
||||||
|
setIsScanning(true);
|
||||||
|
setScanResult(null);
|
||||||
|
setShowScanModal(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.scanInstalledGames();
|
||||||
|
setScanResult(result);
|
||||||
|
} finally {
|
||||||
|
setIsScanning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearScanResult = () => {
|
||||||
|
setScanResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDropdownVisible) return;
|
if (!isDropdownVisible) return;
|
||||||
|
|
||||||
@@ -235,6 +269,14 @@ export function Header() {
|
|||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
}, [isDropdownVisible]);
|
}, [isDropdownVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParams.get("openScanModal") === "true") {
|
||||||
|
setShowScanModal(true);
|
||||||
|
searchParams.delete("openScanModal");
|
||||||
|
setSearchParams(searchParams, { replace: true });
|
||||||
|
}
|
||||||
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
@@ -265,6 +307,21 @@ export function Header() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="header__section">
|
<section className="header__section">
|
||||||
|
{isOnLibraryPage && window.electron.platform === "win32" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn("header__action-button", {
|
||||||
|
"header__action-button--scanning": isScanning,
|
||||||
|
})}
|
||||||
|
onClick={() => setShowScanModal(true)}
|
||||||
|
data-tooltip-id={scanButtonTooltipId}
|
||||||
|
data-tooltip-content={t("scan_games_tooltip")}
|
||||||
|
data-tooltip-place="bottom"
|
||||||
|
>
|
||||||
|
<SyncIcon size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={searchContainerRef}
|
ref={searchContainerRef}
|
||||||
className={cn("header__search", {
|
className={cn("header__search", {
|
||||||
@@ -304,6 +361,11 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{isOnLibraryPage && window.electron.platform === "win32" && (
|
||||||
|
<Tooltip id={scanButtonTooltipId} style={{ zIndex: 1 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
<AutoUpdateSubHeader />
|
<AutoUpdateSubHeader />
|
||||||
|
|
||||||
<SearchDropdown
|
<SearchDropdown
|
||||||
@@ -327,6 +389,15 @@ export function Header() {
|
|||||||
currentQuery={searchValue}
|
currentQuery={searchValue}
|
||||||
searchContainerRef={searchContainerRef}
|
searchContainerRef={searchContainerRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ScanGamesModal
|
||||||
|
visible={showScanModal}
|
||||||
|
onClose={() => setShowScanModal(false)}
|
||||||
|
isScanning={isScanning}
|
||||||
|
scanResult={scanResult}
|
||||||
|
onStartScan={handleStartScan}
|
||||||
|
onClearResult={handleClearScanResult}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
107
src/renderer/src/components/header/scan-games-modal.scss
Normal file
107
src/renderer/src/components/header/scan-games-modal.scss
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.scan-games-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 3);
|
||||||
|
min-width: 400px;
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__result {
|
||||||
|
color: globals.$body-color;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-results {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
padding: calc(globals.$spacing-unit * 2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__scanning {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
padding: calc(globals.$spacing-unit * 3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spinner {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__scanning-text {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__games-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding-bottom: globals.$spacing-unit;
|
||||||
|
border-bottom: 1px solid globals.$border-color;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-title {
|
||||||
|
color: globals.$body-color;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-path {
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/renderer/src/components/header/scan-games-modal.tsx
Normal file
126
src/renderer/src/components/header/scan-games-modal.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { SyncIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import { Button, Modal } from "@renderer/components";
|
||||||
|
|
||||||
|
import "./scan-games-modal.scss";
|
||||||
|
|
||||||
|
interface FoundGame {
|
||||||
|
title: string;
|
||||||
|
executablePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScanResult {
|
||||||
|
foundGames: FoundGame[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScanGamesModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
isScanning: boolean;
|
||||||
|
scanResult: ScanResult | null;
|
||||||
|
onStartScan: () => void;
|
||||||
|
onClearResult: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScanGamesModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
isScanning,
|
||||||
|
scanResult,
|
||||||
|
onStartScan,
|
||||||
|
onClearResult,
|
||||||
|
}: Readonly<ScanGamesModalProps>) {
|
||||||
|
const { t } = useTranslation("header");
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartScan = () => {
|
||||||
|
onStartScan();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScanAgain = () => {
|
||||||
|
onClearResult();
|
||||||
|
onStartScan();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("scan_games_title")}
|
||||||
|
onClose={handleClose}
|
||||||
|
clickOutsideToClose={!isScanning}
|
||||||
|
>
|
||||||
|
<div className="scan-games-modal">
|
||||||
|
{!scanResult && !isScanning && (
|
||||||
|
<p className="scan-games-modal__description">
|
||||||
|
{t("scan_games_description")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isScanning && !scanResult && (
|
||||||
|
<div className="scan-games-modal__scanning">
|
||||||
|
<SyncIcon size={24} className="scan-games-modal__spinner" />
|
||||||
|
<p className="scan-games-modal__scanning-text">
|
||||||
|
{t("scan_games_in_progress")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{scanResult && (
|
||||||
|
<div className="scan-games-modal__results">
|
||||||
|
{scanResult.foundGames.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<p className="scan-games-modal__result">
|
||||||
|
{t("scan_games_result", {
|
||||||
|
found: scanResult.foundGames.length,
|
||||||
|
total: scanResult.total,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="scan-games-modal__games-list">
|
||||||
|
{scanResult.foundGames.map((game) => (
|
||||||
|
<li
|
||||||
|
key={game.executablePath}
|
||||||
|
className="scan-games-modal__game-item"
|
||||||
|
>
|
||||||
|
<span className="scan-games-modal__game-title">
|
||||||
|
{game.title}
|
||||||
|
</span>
|
||||||
|
<span className="scan-games-modal__game-path">
|
||||||
|
{game.executablePath}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="scan-games-modal__no-results">
|
||||||
|
{t("scan_games_no_results")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="scan-games-modal__actions">
|
||||||
|
<Button theme="outline" onClick={handleClose}>
|
||||||
|
{scanResult ? t("scan_games_close") : t("scan_games_cancel")}
|
||||||
|
</Button>
|
||||||
|
{!scanResult && (
|
||||||
|
<Button onClick={handleStartScan} disabled={isScanning}>
|
||||||
|
{t("scan_games_start")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{scanResult && (
|
||||||
|
<Button onClick={handleScanAgain}>
|
||||||
|
{t("scan_games_scan_again")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -225,6 +225,16 @@ export function GameDetailsContextProvider({
|
|||||||
};
|
};
|
||||||
}, [game?.id, isGameRunning, updateGame]);
|
}, [game?.id, isGameRunning, updateGame]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onLibraryBatchComplete(() => {
|
||||||
|
updateGame();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [updateGame]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (ev: Event) => {
|
const handler = (ev: Event) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
@@ -211,6 +211,10 @@ declare global {
|
|||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
extractGameDownload: (shop: GameShop, objectId: string) => Promise<boolean>;
|
extractGameDownload: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||||
|
scanInstalledGames: () => Promise<{
|
||||||
|
foundGames: { title: string; executablePath: string }[];
|
||||||
|
total: number;
|
||||||
|
}>;
|
||||||
onExtractionComplete: (
|
onExtractionComplete: (
|
||||||
cb: (shop: GameShop, objectId: string) => void
|
cb: (shop: GameShop, objectId: string) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
|
|||||||
@@ -40,6 +40,34 @@
|
|||||||
gap: calc(globals.$spacing-unit * 1);
|
gap: calc(globals.$spacing-unit * 1);
|
||||||
color: globals.$body-color;
|
color: globals.$body-color;
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
padding-right: calc(globals.$spacing-unit * 4);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__availability-orb {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(globals.$spacing-unit * 1.5);
|
||||||
|
right: calc(globals.$spacing-unit * 1.5);
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&--online {
|
||||||
|
background-color: #22c55e;
|
||||||
|
box-shadow: 0 0 6px rgba(34, 197, 94, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--partial {
|
||||||
|
background-color: #eab308;
|
||||||
|
box-shadow: 0 0 6px rgba(234, 179, 8, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--offline {
|
||||||
|
background-color: #ef4444;
|
||||||
|
opacity: 0.7;
|
||||||
|
box-shadow: 0 0 6px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__repack-title {
|
&__repack-title {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronUpIcon,
|
ChevronUpIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -185,6 +186,20 @@ export function RepacksModal({
|
|||||||
);
|
);
|
||||||
}, [repacks, hashesInDebrid]);
|
}, [repacks, hashesInDebrid]);
|
||||||
|
|
||||||
|
const getRepackAvailabilityStatus = (
|
||||||
|
repack: GameRepack
|
||||||
|
): "online" | "partial" | "offline" => {
|
||||||
|
const unavailableSet = new Set(repack.unavailableUris ?? []);
|
||||||
|
const availableCount = repack.uris.filter(
|
||||||
|
(uri) => !unavailableSet.has(uri)
|
||||||
|
).length;
|
||||||
|
const unavailableCount = repack.uris.length - availableCount;
|
||||||
|
|
||||||
|
if (unavailableCount === 0) return "online";
|
||||||
|
if (availableCount === 0) return "offline";
|
||||||
|
return "partial";
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const term = filterTerm.trim().toLowerCase();
|
const term = filterTerm.trim().toLowerCase();
|
||||||
|
|
||||||
@@ -363,6 +378,8 @@ export function RepacksModal({
|
|||||||
filteredRepacks.map((repack) => {
|
filteredRepacks.map((repack) => {
|
||||||
const isLastDownloadedOption =
|
const isLastDownloadedOption =
|
||||||
checkIfLastDownloadedOption(repack);
|
checkIfLastDownloadedOption(repack);
|
||||||
|
const availabilityStatus = getRepackAvailabilityStatus(repack);
|
||||||
|
const tooltipId = `availability-orb-${repack.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -371,6 +388,13 @@ export function RepacksModal({
|
|||||||
onClick={() => handleRepackClick(repack)}
|
onClick={() => handleRepackClick(repack)}
|
||||||
className="repacks-modal__repack-button"
|
className="repacks-modal__repack-button"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
className={`repacks-modal__availability-orb repacks-modal__availability-orb--${availabilityStatus}`}
|
||||||
|
data-tooltip-id={tooltipId}
|
||||||
|
data-tooltip-content={t(`source_${availabilityStatus}`)}
|
||||||
|
/>
|
||||||
|
<Tooltip id={tooltipId} />
|
||||||
|
|
||||||
<p className="repacks-modal__repack-title">
|
<p className="repacks-modal__repack-title">
|
||||||
{repack.title}
|
{repack.title}
|
||||||
{userPreferences?.enableNewDownloadOptionsBadges !==
|
{userPreferences?.enableNewDownloadOptionsBadges !==
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export function LocalNotificationItem({
|
|||||||
return <SyncIcon size={24} />;
|
return <SyncIcon size={24} />;
|
||||||
case "ACHIEVEMENT_UNLOCKED":
|
case "ACHIEVEMENT_UNLOCKED":
|
||||||
return <TrophyIcon size={24} />;
|
return <TrophyIcon size={24} />;
|
||||||
|
case "SCAN_GAMES_COMPLETE":
|
||||||
|
return <SyncIcon size={24} />;
|
||||||
default:
|
default:
|
||||||
return <DownloadIcon size={24} />;
|
return <DownloadIcon size={24} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,7 +330,8 @@ export type LocalNotificationType =
|
|||||||
| "EXTRACTION_COMPLETE"
|
| "EXTRACTION_COMPLETE"
|
||||||
| "DOWNLOAD_COMPLETE"
|
| "DOWNLOAD_COMPLETE"
|
||||||
| "UPDATE_AVAILABLE"
|
| "UPDATE_AVAILABLE"
|
||||||
| "ACHIEVEMENT_UNLOCKED";
|
| "ACHIEVEMENT_UNLOCKED"
|
||||||
|
| "SCAN_GAMES_COMPLETE";
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
18
yarn.lock
18
yarn.lock
@@ -6459,10 +6459,10 @@ keyv@^4.0.0, keyv@^4.5.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
json-buffer "3.0.1"
|
json-buffer "3.0.1"
|
||||||
|
|
||||||
ky@^1.14.2:
|
ky@^1.11.0:
|
||||||
version "1.14.3"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/ky/-/ky-1.14.3.tgz#d7fcc6ac93d1588accee72cc44dcc6cf84c0bdaa"
|
resolved "https://registry.yarnpkg.com/ky/-/ky-1.14.1.tgz#16f20b3bf3939abcc04e2a9613f47360fe5f64c9"
|
||||||
integrity sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==
|
integrity sha512-hYje4L9JCmpEQBtudo+v52X5X8tgWXUYyPcxKSuxQNboqufecl9VMWjGiucAFH060AwPXHZuH+WB2rrqfkmafw==
|
||||||
|
|
||||||
language-subtag-registry@^0.3.20:
|
language-subtag-registry@^0.3.20:
|
||||||
version "0.3.23"
|
version "0.3.23"
|
||||||
@@ -9233,12 +9233,12 @@ word-wrap@^1.2.5:
|
|||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||||
|
|
||||||
workwonders-sdk@0.1.3:
|
workwonders-sdk@0.0.10:
|
||||||
version "0.1.3"
|
version "0.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/workwonders-sdk/-/workwonders-sdk-0.1.3.tgz#0fcf4797870ad6c469eaad452e4feb785001e719"
|
resolved "https://registry.yarnpkg.com/workwonders-sdk/-/workwonders-sdk-0.0.10.tgz#377167370a39c905c5228f8972c37c19004b7b21"
|
||||||
integrity sha512-QJ+YSsUh0MzsUDD2SGSsplpZgsgKFRBHeBHjupW2z+/yhEWewQbob060pQJeaB+zMpbBOako9NW0GykTqCFSvg==
|
integrity sha512-bnswhlLRz1TCiqGV8l+VEOBej7u1SAkzLMEv6A60Sp0+S4j4pnmSve92KeOts/GYtUeNDuNM7fLPwZwMKY3sAg==
|
||||||
dependencies:
|
dependencies:
|
||||||
ky "^1.14.2"
|
ky "^1.11.0"
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user