mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-27 04:41:03 +00:00
feat: cloud modal
This commit is contained in:
@@ -29,6 +29,8 @@ import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||
import { downloadSourcesWorker } from "./workers";
|
||||
import { repacksContext } from "./context";
|
||||
import { logger } from "./logger";
|
||||
import { useSubscription } from "./hooks/use-subscription";
|
||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
@@ -47,21 +49,21 @@ export function App() {
|
||||
const { indexRepacks } = useContext(repacksContext);
|
||||
|
||||
const {
|
||||
userDetails,
|
||||
hasActiveSubscription,
|
||||
isFriendsModalVisible,
|
||||
friendRequetsModalTab,
|
||||
friendModalUserId,
|
||||
syncFriendRequests,
|
||||
hideFriendsModal,
|
||||
} = useUserDetails();
|
||||
|
||||
const {
|
||||
userDetails,
|
||||
hasActiveSubscription,
|
||||
fetchUserDetails,
|
||||
updateUserDetails,
|
||||
clearUserDetails,
|
||||
} = useUserDetails();
|
||||
|
||||
const { hideHydraCloudModal, showHydraCloudModal, isHydraCloudModalVisible } =
|
||||
useSubscription();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -308,6 +310,11 @@ export function App() {
|
||||
onClose={handleToastClose}
|
||||
/>
|
||||
|
||||
<HydraCloudModal
|
||||
visible={isHydraCloudModalVisible}
|
||||
onClose={hideHydraCloudModal}
|
||||
/>
|
||||
|
||||
{userDetails && (
|
||||
<UserFriendModal
|
||||
visible={isFriendsModalVisible}
|
||||
|
||||
@@ -53,7 +53,6 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||
setShowGameOptionsModal: () => {},
|
||||
setShowRepacksModal: () => {},
|
||||
setHasNSFWContentBlocked: () => {},
|
||||
handleClickOpenCheckout: () => {},
|
||||
});
|
||||
|
||||
const { Provider } = gameDetailsContext;
|
||||
@@ -111,11 +110,6 @@ export function GameDetailsContextProvider({
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
||||
const handleClickOpenCheckout = () => {
|
||||
// TODO: show modal before redirecting to checkout page
|
||||
window.electron.openCheckout();
|
||||
};
|
||||
|
||||
const updateGame = useCallback(async () => {
|
||||
return window.electron
|
||||
.getGameByObjectId(objectId!)
|
||||
@@ -290,7 +284,6 @@ export function GameDetailsContextProvider({
|
||||
updateGame,
|
||||
setShowRepacksModal,
|
||||
setShowGameOptionsModal,
|
||||
handleClickOpenCheckout,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -29,5 +29,4 @@ export interface GameDetailsContext {
|
||||
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setShowGameOptionsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setHasNSFWContentBlocked: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleClickOpenCheckout: () => void;
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from "./window-slice";
|
||||
export * from "./toast-slice";
|
||||
export * from "./user-details-slice";
|
||||
export * from "./running-game-slice";
|
||||
export * from "./subscription-slice";
|
||||
|
||||
25
src/renderer/src/features/subscription-slice.ts
Normal file
25
src/renderer/src/features/subscription-slice.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export interface SubscriptionState {
|
||||
isHydraCloudModalVisible: boolean;
|
||||
}
|
||||
|
||||
const initialState: SubscriptionState = {
|
||||
isHydraCloudModalVisible: false,
|
||||
};
|
||||
|
||||
export const subscriptionSlice = createSlice({
|
||||
name: "subscription",
|
||||
initialState,
|
||||
reducers: {
|
||||
setHydraCloudModalVisible: (state) => {
|
||||
state.isHydraCloudModalVisible = true;
|
||||
},
|
||||
setHydraCloudModalHidden: (state) => {
|
||||
state.isHydraCloudModalVisible = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setHydraCloudModalVisible, setHydraCloudModalHidden } =
|
||||
subscriptionSlice.actions;
|
||||
28
src/renderer/src/hooks/use-subscription.ts
Normal file
28
src/renderer/src/hooks/use-subscription.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useCallback } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "./redux";
|
||||
import {
|
||||
setHydraCloudModalVisible,
|
||||
setHydraCloudModalHidden,
|
||||
} from "@renderer/features";
|
||||
|
||||
export function useSubscription() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { isHydraCloudModalVisible } = useAppSelector(
|
||||
(state) => state.subscription
|
||||
);
|
||||
|
||||
const showHydraCloudModal = useCallback(() => {
|
||||
dispatch(setHydraCloudModalVisible());
|
||||
}, [dispatch]);
|
||||
|
||||
const hideHydraCloudModal = useCallback(() => {
|
||||
dispatch(setHydraCloudModalHidden());
|
||||
}, [dispatch]);
|
||||
|
||||
return {
|
||||
isHydraCloudModalVisible,
|
||||
showHydraCloudModal,
|
||||
hideHydraCloudModal,
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./achievements.css";
|
||||
import { EyeClosedIcon } from "@primer/octicons-react";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
interface AchievementListProps {
|
||||
achievements: UserAchievement[];
|
||||
@@ -11,6 +13,7 @@ interface AchievementListProps {
|
||||
|
||||
export function AchievementList({ achievements }: AchievementListProps) {
|
||||
const { t } = useTranslation("achievement");
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
const { formatDateTime } = useDate();
|
||||
|
||||
return (
|
||||
@@ -41,7 +44,7 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
||||
<p>{achievement.description}</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
{achievement.points && (
|
||||
{achievement.points != undefined ? (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: "4px" }}
|
||||
title={t("achievement_earn_points", {
|
||||
@@ -51,6 +54,23 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
||||
<HydraIcon width={20} height={20} />
|
||||
<p style={{ fontSize: "1.1em" }}>{achievement.points}</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={showHydraCloudModal}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
cursor: "pointer",
|
||||
color: vars.color.warning,
|
||||
}}
|
||||
title={t("achievement_earn_points", {
|
||||
points: "???",
|
||||
})}
|
||||
>
|
||||
<HydraIcon width={20} height={20} />
|
||||
<p style={{ fontSize: "1.1em" }}>???</p>
|
||||
</button>
|
||||
)}
|
||||
{achievement.unlockTime && (
|
||||
<div
|
||||
|
||||
@@ -19,6 +19,7 @@ import * as styles from "./achievements.css";
|
||||
import { AchievementList } from "./achievement-list";
|
||||
import { AchievementPanel } from "./achievement-panel";
|
||||
import { ComparedAchievementPanel } from "./compared-achievement-panel";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
@@ -41,7 +42,7 @@ interface AchievementSummaryProps {
|
||||
function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
const { t } = useTranslation("achievement");
|
||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||
const { handleClickOpenCheckout } = useContext(gameDetailsContext);
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
|
||||
const getProfileImage = (
|
||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||
@@ -92,7 +93,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
<h3>
|
||||
<button
|
||||
className={styles.subscriptionRequiredButton}
|
||||
onClick={handleClickOpenCheckout}
|
||||
onClick={showHydraCloudModal}
|
||||
>
|
||||
{t("subscription_needed")}
|
||||
</button>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { steamUrlBuilder } from "@shared";
|
||||
|
||||
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
|
||||
const HERO_ANIMATION_THRESHOLD = 25;
|
||||
|
||||
@@ -31,9 +32,10 @@ export function GameDetailsContent() {
|
||||
gameColor,
|
||||
setGameColor,
|
||||
hasNSFWContentBlocked,
|
||||
handleClickOpenCheckout,
|
||||
} = useContext(gameDetailsContext);
|
||||
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
|
||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||
|
||||
const { setShowCloudSyncModal, getGameArtifacts } =
|
||||
@@ -104,7 +106,7 @@ export function GameDetailsContent() {
|
||||
}
|
||||
|
||||
if (!hasActiveSubscription) {
|
||||
handleClickOpenCheckout();
|
||||
showHydraCloudModal();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useSubmit } from "react-router-dom";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
|
||||
const fakeAchievements: UserAchievement[] = [
|
||||
{
|
||||
@@ -67,15 +69,10 @@ export function Sidebar() {
|
||||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
const {
|
||||
gameTitle,
|
||||
shopDetails,
|
||||
objectId,
|
||||
shop,
|
||||
stats,
|
||||
achievements,
|
||||
handleClickOpenCheckout,
|
||||
} = useContext(gameDetailsContext);
|
||||
const { gameTitle, shopDetails, objectId, shop, stats, achievements } =
|
||||
useContext(gameDetailsContext);
|
||||
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
const { formatDateTime } = useDate();
|
||||
@@ -179,7 +176,7 @@ export function Sidebar() {
|
||||
{!hasActiveSubscription && (
|
||||
<button
|
||||
className={styles.subscriptionRequiredButton}
|
||||
onClick={handleClickOpenCheckout}
|
||||
onClick={showHydraCloudModal}
|
||||
>
|
||||
<CloudOfflineIcon size={16} />
|
||||
<span>{t("achievements_not_sync")}</span>
|
||||
|
||||
@@ -203,3 +203,12 @@ export const achievementsProgressBar = style({
|
||||
borderRadius: "4px",
|
||||
},
|
||||
});
|
||||
|
||||
export const link = style({
|
||||
textAlign: "start",
|
||||
color: vars.color.body,
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,8 +5,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { useFormat } from "@renderer/hooks";
|
||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
|
||||
export function UserStatsBox() {
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
|
||||
const { userStats } = useContext(userProfileContext);
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
@@ -40,37 +43,49 @@ export function UserStatsBox() {
|
||||
|
||||
<div className={styles.box}>
|
||||
<ul className={styles.list}>
|
||||
{userStats.achievementsPointsEarnedSum && (
|
||||
<li>
|
||||
<h3 className={styles.listItemTitle}>{t("achievements")}</h3>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<p
|
||||
style={{ display: "flex", alignItems: "center", gap: "4px" }}
|
||||
<li>
|
||||
<h3 className={styles.listItemTitle}>{t("achievements")}</h3>
|
||||
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
||||
<>
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "space-between" }}
|
||||
>
|
||||
<HydraIcon width={20} height={20} />
|
||||
{userStats.achievementsPointsEarnedSum.value}
|
||||
</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile:
|
||||
userStats.achievementsPointsEarnedSum.topPercentile,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<p>Unlock count: {userStats.unlockedAchievementSum}</p>
|
||||
</li>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
}}
|
||||
>
|
||||
<HydraIcon width={20} height={20} />
|
||||
{userStats.achievementsPointsEarnedSum.value}
|
||||
</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile:
|
||||
userStats.achievementsPointsEarnedSum.topPercentile,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<p>Unlock count: {userStats.unlockedAchievementSum}</p>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={showHydraCloudModal}
|
||||
className={styles.link}
|
||||
>
|
||||
<small>
|
||||
Saiba como exibir suas conquistas e pontos no perfil
|
||||
</small>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h3 className={styles.listItemTitle}>{t("games")}</h3>
|
||||
<h3 className={styles.listItemTitle}>{t("total_play_time")}</h3>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<p>
|
||||
{t("total_play_time", {
|
||||
amount: formatPlayTime(
|
||||
userStats.totalPlayTimeInSeconds.value
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<p>{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile: userStats.totalPlayTimeInSeconds.topPercentile,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const friendListDisplayName = style({
|
||||
fontWeight: "bold",
|
||||
fontSize: vars.size.body,
|
||||
textAlign: "left",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
export const friendListContainer = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
alignItems: "center",
|
||||
borderRadius: "4px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
width: "100%",
|
||||
height: "54px",
|
||||
minHeight: "54px",
|
||||
transition: "all ease 0.2s",
|
||||
position: "relative",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const friendListButton = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
cursor: "pointer",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
color: vars.color.body,
|
||||
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||
padding: `0 ${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const friendRequestItem = style({
|
||||
color: vars.color.body,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
},
|
||||
});
|
||||
|
||||
export const acceptRequestButton = style({
|
||||
cursor: "pointer",
|
||||
color: vars.color.body,
|
||||
width: "28px",
|
||||
height: "28px",
|
||||
":hover": {
|
||||
color: vars.color.success,
|
||||
},
|
||||
});
|
||||
|
||||
export const cancelRequestButton = style({
|
||||
cursor: "pointer",
|
||||
color: vars.color.body,
|
||||
width: "28px",
|
||||
height: "28px",
|
||||
":hover": {
|
||||
color: vars.color.danger,
|
||||
},
|
||||
});
|
||||
|
||||
export const friendCodeButton = style({
|
||||
color: vars.color.body,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
alignItems: "center",
|
||||
transition: "all ease 0.2s",
|
||||
":hover": {
|
||||
color: vars.color.muted,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface HydraCloudModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const HydraCloudModal = ({ visible, onClose }: HydraCloudModalProps) => {
|
||||
const { t } = useTranslation("hydra_cloud");
|
||||
|
||||
const handleClickOpenCheckout = () => {
|
||||
window.electron.openCheckout();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("subscription_tour_title")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "500px",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
Você descobriu uma funcionalidade Hydra Cloud!
|
||||
<Button onClick={handleClickOpenCheckout}>Saiba mais</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
toastSlice,
|
||||
userDetailsSlice,
|
||||
gameRunningSlice,
|
||||
subscriptionSlice,
|
||||
} from "@renderer/features";
|
||||
|
||||
export const store = configureStore({
|
||||
@@ -20,6 +21,7 @@ export const store = configureStore({
|
||||
toast: toastSlice.reducer,
|
||||
userDetails: userDetailsSlice.reducer,
|
||||
gameRunning: gameRunningSlice.reducer,
|
||||
subscription: subscriptionSlice.reducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user