mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
feat: added new badge to repacks-modal, set up badge clearing
This commit is contained in:
@@ -19,6 +19,7 @@ import "./library/delete-game-folder";
|
||||
import "./library/get-game-by-object-id";
|
||||
import "./library/get-library";
|
||||
import "./library/extract-game-download";
|
||||
import "./library/clear-new-download-options";
|
||||
import "./library/open-game";
|
||||
import "./library/open-game-executable-path";
|
||||
import "./library/open-game-installer";
|
||||
|
||||
27
src/main/events/library/clear-new-download-options.ts
Normal file
27
src/main/events/library/clear-new-download-options.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { logger } from "@main/services";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const clearNewDownloadOptions = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
newDownloadOptionsCount: undefined,
|
||||
});
|
||||
logger.info(`Cleared newDownloadOptionsCount for game ${gameKey}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to clear newDownloadOptionsCount: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("clearNewDownloadOptions", clearNewDownloadOptions);
|
||||
@@ -73,10 +73,6 @@ export class DownloadSourcesChecker {
|
||||
gameId: `${game.shop}:${game.objectId}`,
|
||||
count: gameUpdate.newDownloadOptionsCount,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Game ${game.title} has ${gameUpdate.newDownloadOptionsCount} new download options`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +117,6 @@ export class DownloadSourcesChecker {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get download sources
|
||||
const downloadSources = await downloadSourcesSublevel.values().all();
|
||||
const downloadSourceIds = downloadSources.map((source) => source.id);
|
||||
logger.info(
|
||||
@@ -135,7 +130,6 @@ export class DownloadSourcesChecker {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get when we LAST started the app (for this check's 'since' parameter)
|
||||
const previousBaseline = await getDownloadSourcesCheckBaseline();
|
||||
const since =
|
||||
previousBaseline ||
|
||||
@@ -143,10 +137,8 @@ export class DownloadSourcesChecker {
|
||||
|
||||
logger.info(`Using since: ${since} (from last app start)`);
|
||||
|
||||
// Clear any previously stored new download option counts so badges don't persist across restarts
|
||||
const clearedPayload = await this.clearStaleBadges(nonCustomGames);
|
||||
|
||||
// Prepare games array for API call (excluding custom games)
|
||||
const games = nonCustomGames.map((game: Game) => ({
|
||||
shop: game.shop,
|
||||
objectId: game.objectId,
|
||||
@@ -164,7 +156,6 @@ export class DownloadSourcesChecker {
|
||||
}
|
||||
);
|
||||
|
||||
// Call the API
|
||||
const response = await HydraApi.checkDownloadSourcesChanges(
|
||||
downloadSourceIds,
|
||||
games,
|
||||
@@ -173,24 +164,20 @@ export class DownloadSourcesChecker {
|
||||
|
||||
logger.info("API call completed, response:", response);
|
||||
|
||||
// Save the 'since' value we just used (for modal to compare against)
|
||||
await updateDownloadSourcesSinceValue(since);
|
||||
logger.info(`Saved 'since' value: ${since} (for modal comparison)`);
|
||||
|
||||
// Update baseline to NOW (for next app start's 'since')
|
||||
const now = new Date().toISOString();
|
||||
await updateDownloadSourcesCheckBaseline(now);
|
||||
logger.info(
|
||||
`Updated baseline to: ${now} (will be 'since' on next app start)`
|
||||
);
|
||||
|
||||
// Process the response and store newDownloadOptionsCount for games with new options
|
||||
const gamesWithNewOptions = await this.processApiResponse(
|
||||
response,
|
||||
nonCustomGames
|
||||
);
|
||||
|
||||
// Send IPC event to renderer to clear stale badges and set fresh counts from response
|
||||
this.sendNewDownloadOptionsEvent(clearedPayload, gamesWithNewOptions);
|
||||
|
||||
logger.info("Download sources check completed successfully");
|
||||
|
||||
@@ -183,6 +183,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||
clearNewDownloadOptions: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("clearNewDownloadOptions", shop, objectId),
|
||||
toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) =>
|
||||
ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned),
|
||||
updateLaunchOptions: (
|
||||
|
||||
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
@@ -142,6 +142,10 @@ declare global {
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<void>;
|
||||
clearNewDownloadOptions: (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<void>;
|
||||
toggleGamePin: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
|
||||
@@ -55,15 +55,15 @@
|
||||
}
|
||||
|
||||
&__new-badge {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 4px 8px;
|
||||
background-color: rgba(34, 197, 94, 0.15);
|
||||
color: rgb(187, 247, 208);
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
&__no-results {
|
||||
|
||||
@@ -21,7 +21,8 @@ import { DownloadSettingsModal } from "./download-settings-modal";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { Downloader } from "@shared";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { useDate, useFeature } from "@renderer/hooks";
|
||||
import { useDate, useFeature, useAppDispatch } from "@renderer/hooks";
|
||||
import { clearNewDownloadOptions } from "@renderer/features";
|
||||
import "./repacks-modal.scss";
|
||||
|
||||
export interface RepacksModalProps {
|
||||
@@ -56,6 +57,9 @@ export function RepacksModal({
|
||||
null
|
||||
);
|
||||
const [isLoadingTimestamp, setIsLoadingTimestamp] = useState(true);
|
||||
const [viewedRepackIds, setViewedRepackIds] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
const { game, repacks } = useContext(gameDetailsContext);
|
||||
|
||||
@@ -63,14 +67,15 @@ export function RepacksModal({
|
||||
|
||||
const { formatDate } = useDate();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getHashFromMagnet = (magnet: string) => {
|
||||
if (!magnet || typeof magnet !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hashRegex = /xt=urn:btih:([a-f0-9]+)/i;
|
||||
const match = hashRegex.exec(magnet);
|
||||
const hashRegex = /xt=urn:btih:([a-zA-Z0-9]+)/i;
|
||||
const match = magnet.match(hashRegex);
|
||||
|
||||
return match ? match[1].toLowerCase() : null;
|
||||
};
|
||||
@@ -115,6 +120,21 @@ export function RepacksModal({
|
||||
}
|
||||
}, [visible, repacks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
visible &&
|
||||
game?.newDownloadOptionsCount &&
|
||||
game.newDownloadOptionsCount > 0
|
||||
) {
|
||||
// Clear the badge in the database
|
||||
globalThis.electron.clearNewDownloadOptions(game.shop, game.objectId);
|
||||
|
||||
// Clear the badge in Redux store
|
||||
const gameId = `${game.shop}:${game.objectId}`;
|
||||
dispatch(clearNewDownloadOptions({ gameId }));
|
||||
}
|
||||
}, [visible, game, dispatch]);
|
||||
|
||||
const sortedRepacks = useMemo(() => {
|
||||
return orderBy(
|
||||
repacks,
|
||||
@@ -157,6 +177,8 @@ export function RepacksModal({
|
||||
const handleRepackClick = (repack: GameRepack) => {
|
||||
setRepack(repack);
|
||||
setShowSelectFolderModal(true);
|
||||
// Mark this repack as viewed to hide the "NEW" badge
|
||||
setViewedRepackIds((prev) => new Set(prev).add(repack.id));
|
||||
};
|
||||
|
||||
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
@@ -180,6 +202,9 @@ export function RepacksModal({
|
||||
// Don't show badge while loading timestamp
|
||||
if (isLoadingTimestamp) return false;
|
||||
|
||||
// Don't show badge if user has already clicked this repack in current session
|
||||
if (viewedRepackIds.has(repack.id)) return false;
|
||||
|
||||
if (!lastCheckTimestamp || !repack.createdAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user