mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-15 16:33:02 -03:00
Feat: Added changing game playtime functionality
This commit is contained in:
@@ -4,13 +4,12 @@ import { GameShop } from "@types";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
import { levelKeys } from "@main/level";
|
||||
|
||||
|
||||
const changeGamePlaytime = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
playTimeInSeconds: number,
|
||||
) => {
|
||||
playTimeInSeconds: number
|
||||
) => {
|
||||
try {
|
||||
await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, {
|
||||
playTimeInSeconds,
|
||||
@@ -26,10 +25,6 @@ const changeGamePlaytime = async (
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update game favorite status: ${error}`);
|
||||
}
|
||||
;
|
||||
};
|
||||
|
||||
|
||||
registerEvent("changeGamePlayTime", changeGamePlaytime);
|
||||
|
||||
|
||||
|
||||
|
||||
6
src/renderer/src/declaration.d.ts
vendored
6
src/renderer/src/declaration.d.ts
vendored
@@ -162,7 +162,11 @@ declare global {
|
||||
) => () => Electron.IpcRenderer;
|
||||
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
resetGameAchievements: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
changeGamePlayTime: (shop: GameShop, objectId: string, playtimeInSeconds: number) => Promise<void>;
|
||||
changeGamePlayTime: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
playtimeInSeconds: number
|
||||
) => Promise<void>;
|
||||
/* User preferences */
|
||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||
|
||||
@@ -36,4 +36,4 @@
|
||||
align-items: center;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,12 @@ export function ChangeGamePlaytimeModal({
|
||||
// Prefill current playtime when modal becomes visible
|
||||
useEffect(() => {
|
||||
if (visible && game.playTimeInMilliseconds) {
|
||||
const totalMinutes = Math.floor(game.playTimeInMilliseconds / (1000 * 60));
|
||||
const totalMinutes = Math.floor(
|
||||
game.playTimeInMilliseconds / (1000 * 60)
|
||||
);
|
||||
const currentHours = Math.floor(totalMinutes / 60);
|
||||
const currentMinutes = totalMinutes % 60;
|
||||
|
||||
|
||||
setHours(currentHours.toString());
|
||||
setMinutes(currentMinutes.toString());
|
||||
} else if (visible) {
|
||||
@@ -51,19 +53,24 @@ export function ChangeGamePlaytimeModal({
|
||||
const currentMinutes = parseInt(minutes) || 0;
|
||||
|
||||
// Calculate maximum allowed values based on current input
|
||||
const maxAllowedHours = Math.min(MAX_TOTAL_HOURS, Math.floor(MAX_TOTAL_HOURS - (currentMinutes / 60)));
|
||||
const maxAllowedMinutes = currentHours >= MAX_TOTAL_HOURS ? 0 : Math.min(59, Math.floor((MAX_TOTAL_HOURS - currentHours) * 60));
|
||||
|
||||
const maxAllowedHours = Math.min(
|
||||
MAX_TOTAL_HOURS,
|
||||
Math.floor(MAX_TOTAL_HOURS - currentMinutes / 60)
|
||||
);
|
||||
const maxAllowedMinutes =
|
||||
currentHours >= MAX_TOTAL_HOURS
|
||||
? 0
|
||||
: Math.min(59, Math.floor((MAX_TOTAL_HOURS - currentHours) * 60));
|
||||
|
||||
const handleChangePlaytime = async () => {
|
||||
const hoursNum = parseInt(hours) || 0;
|
||||
const minutesNum = parseInt(minutes) || 0;
|
||||
const totalSeconds = (hoursNum * 3600) + (minutesNum * 60);
|
||||
const totalSeconds = hoursNum * 3600 + minutesNum * 60;
|
||||
|
||||
if (totalSeconds < 0) return;
|
||||
|
||||
// Prevent exceeding 10,000 hours total
|
||||
if (hoursNum + (minutesNum / 60) > MAX_TOTAL_HOURS) return;
|
||||
if (hoursNum + minutesNum / 60 > MAX_TOTAL_HOURS) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
@@ -80,14 +87,14 @@ export function ChangeGamePlaytimeModal({
|
||||
|
||||
const handleHoursChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = e.target.value;
|
||||
|
||||
|
||||
// Remove leading zeros and prevent multiple zeros
|
||||
if (value.length > 1 && value.startsWith('0')) {
|
||||
value = value.replace(/^0+/, '') || '0';
|
||||
if (value.length > 1 && value.startsWith("0")) {
|
||||
value = value.replace(/^0+/, "") || "0";
|
||||
}
|
||||
|
||||
|
||||
const numValue = parseInt(value) || 0;
|
||||
|
||||
|
||||
// Don't allow more than the calculated maximum
|
||||
if (numValue <= maxAllowedHours) {
|
||||
setHours(value);
|
||||
@@ -96,14 +103,14 @@ export function ChangeGamePlaytimeModal({
|
||||
|
||||
const handleMinutesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = e.target.value;
|
||||
|
||||
|
||||
// Remove leading zeros and prevent multiple zeros
|
||||
if (value.length > 1 && value.startsWith('0')) {
|
||||
value = value.replace(/^0+/, '') || '0';
|
||||
if (value.length > 1 && value.startsWith("0")) {
|
||||
value = value.replace(/^0+/, "") || "0";
|
||||
}
|
||||
|
||||
|
||||
const numValue = parseInt(value) || 0;
|
||||
|
||||
|
||||
// Don't allow more than the calculated maximum
|
||||
if (numValue <= maxAllowedMinutes) {
|
||||
setMinutes(value);
|
||||
@@ -128,7 +135,7 @@ export function ChangeGamePlaytimeModal({
|
||||
<span>{t("manual_playtime_warning")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="change-game-playtime-modal__inputs">
|
||||
<TextField
|
||||
label={t("hours")}
|
||||
@@ -153,8 +160,8 @@ export function ChangeGamePlaytimeModal({
|
||||
</div>
|
||||
|
||||
<div className="change-game-playtime-modal__actions">
|
||||
<Button
|
||||
onClick={handleChangePlaytime}
|
||||
<Button
|
||||
onClick={handleChangePlaytime}
|
||||
theme="outline"
|
||||
disabled={!isValid || isSubmitting}
|
||||
>
|
||||
@@ -169,4 +176,3 @@ export function ChangeGamePlaytimeModal({
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -232,7 +232,11 @@ export function GameOptionsModal({
|
||||
|
||||
const handleChangePlaytime = async (playtimeInSeconds: number) => {
|
||||
try {
|
||||
await window.electron.changeGamePlayTime(game.shop, game.objectId, playtimeInSeconds);
|
||||
await window.electron.changeGamePlayTime(
|
||||
game.shop,
|
||||
game.objectId,
|
||||
playtimeInSeconds
|
||||
);
|
||||
await updateGame();
|
||||
showSuccessToast(t("change_playtime_success"));
|
||||
} catch (error) {
|
||||
@@ -498,7 +502,6 @@ export function GameOptionsModal({
|
||||
<Button
|
||||
onClick={() => setShowChangePlaytimeModal(true)}
|
||||
theme="danger"
|
||||
|
||||
>
|
||||
{t("change_game_playtime")}
|
||||
</Button>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
formatDownloadProgress,
|
||||
} from "@renderer/helpers";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { ClockIcon, TrophyIcon, AlertFillIcon} from "@primer/octicons-react";
|
||||
import { ClockIcon, TrophyIcon, AlertFillIcon } from "@primer/octicons-react";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -85,98 +85,108 @@ export function UserLibraryGameCard({
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<li
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className="user-library-game__wrapper"
|
||||
title={isTooltipHovered ? undefined : game.title}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="user-library-game__cover"
|
||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||
<>
|
||||
<li
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className="user-library-game__wrapper"
|
||||
title={isTooltipHovered ? undefined : game.title}
|
||||
>
|
||||
<div className="user-library-game__overlay">
|
||||
|
||||
<small className="user-library-game__playtime" data-tooltip-place="top"
|
||||
data-tooltip-content={game.hasManuallyUpdatedPlaytime ? t("manual_playtime_tooltip") : undefined}
|
||||
data-tooltip-id={game.objectId} >
|
||||
{game.hasManuallyUpdatedPlaytime ? (
|
||||
<AlertFillIcon size={11} className="user-library-game__manual-playtime" />
|
||||
) : (
|
||||
<ClockIcon size={11} />
|
||||
)}
|
||||
{formatPlayTime(game.playTimeInSeconds)}
|
||||
|
||||
</small>
|
||||
<button
|
||||
type="button"
|
||||
className="user-library-game__cover"
|
||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||
>
|
||||
<div className="user-library-game__overlay">
|
||||
<small
|
||||
className="user-library-game__playtime"
|
||||
data-tooltip-place="top"
|
||||
data-tooltip-content={
|
||||
game.hasManuallyUpdatedPlaytime
|
||||
? t("manual_playtime_tooltip")
|
||||
: undefined
|
||||
}
|
||||
data-tooltip-id={game.objectId}
|
||||
>
|
||||
{game.hasManuallyUpdatedPlaytime ? (
|
||||
<AlertFillIcon
|
||||
size={11}
|
||||
className="user-library-game__manual-playtime"
|
||||
/>
|
||||
) : (
|
||||
<ClockIcon size={11} />
|
||||
)}
|
||||
{formatPlayTime(game.playTimeInSeconds)}
|
||||
</small>
|
||||
|
||||
{userProfile?.hasActiveSubscription &&
|
||||
game.achievementCount > 0 && (
|
||||
<div className="user-library-game__stats">
|
||||
<div className="user-library-game__stats-header">
|
||||
<div className="user-library-game__stats-content">
|
||||
<div
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{game.unlockedAchievementCount} /{" "}
|
||||
{game.achievementCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{game.achievementsPointsEarnedSum > 0 && (
|
||||
<div
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={16} height={16} />
|
||||
{formatAchievementPoints(
|
||||
game.achievementsPointsEarnedSum
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
||||
<div className="user-library-game__stats">
|
||||
<div className="user-library-game__stats-header">
|
||||
<div className="user-library-game__stats-content">
|
||||
<div
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<TrophyIcon size={13} />
|
||||
<span>
|
||||
{game.unlockedAchievementCount} / {game.achievementCount}
|
||||
{formatDownloadProgress(
|
||||
game.unlockedAchievementCount / game.achievementCount,
|
||||
1
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{game.achievementsPointsEarnedSum > 0 && (
|
||||
<div
|
||||
className="user-library-game__stats-item"
|
||||
style={{
|
||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={16} height={16} />
|
||||
{formatAchievementPoints(
|
||||
game.achievementsPointsEarnedSum
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<progress
|
||||
max={1}
|
||||
value={
|
||||
game.unlockedAchievementCount / game.achievementCount
|
||||
}
|
||||
className="user-library-game__achievements-progress"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
game.unlockedAchievementCount / game.achievementCount,
|
||||
1
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<progress
|
||||
max={1}
|
||||
value={game.unlockedAchievementCount / game.achievementCount}
|
||||
className="user-library-game__achievements-progress"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={game.coverImageUrl}
|
||||
alt={game.title}
|
||||
className="user-library-game__game-image"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
<Tooltip
|
||||
id={game.objectId}
|
||||
style={{
|
||||
zIndex: 9999,
|
||||
}}
|
||||
openOnClick={false}
|
||||
afterShow={() => setIsTooltipHovered(true)}
|
||||
afterHide={() => setIsTooltipHovered(false)}
|
||||
/>
|
||||
<img
|
||||
src={game.coverImageUrl}
|
||||
alt={game.title}
|
||||
className="user-library-game__game-image"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
<Tooltip
|
||||
id={game.objectId}
|
||||
style={{
|
||||
zIndex: 9999,
|
||||
}}
|
||||
openOnClick={false}
|
||||
afterShow={() => setIsTooltipHovered(true)}
|
||||
afterHide={() => setIsTooltipHovered(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export type UserGame = {
|
||||
unlockedAchievementCount: number;
|
||||
achievementCount: number;
|
||||
achievementsPointsEarnedSum: number;
|
||||
hasManuallyUpdatedPlaytime: boolean;
|
||||
} & ShopAssets;
|
||||
|
||||
export interface GameRunning {
|
||||
|
||||
Reference in New Issue
Block a user