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