From f08ad361eda5bea57ecf561a87fdf52ffa10a877 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 2 Oct 2025 00:43:49 +0300 Subject: [PATCH 01/52] feat: added review functionality --- package.json | 7 + src/locales/en/translation.json | 23 + .../events/catalogue/check-game-review.ts | 17 + .../events/catalogue/create-game-review.ts | 18 + src/main/events/catalogue/delete-review.ts | 14 + src/main/events/catalogue/get-game-reviews.ts | 26 + src/main/events/catalogue/vote-review.ts | 15 + src/main/events/index.ts | 5 + src/preload/index.ts | 26 + .../src/components/game-card/game-card.tsx | 10 +- src/renderer/src/declaration.d.ts | 28 + .../gallery-slider/gallery-slider.scss | 2 +- .../game-details/game-details-content.tsx | 474 ++++++++++++- .../game-details/game-details-skeleton.tsx | 1 + .../src/pages/game-details/game-details.scss | 656 ++++++++++++++++++ .../game-details/review-prompt-banner.scss | 46 ++ .../game-details/review-prompt-banner.tsx | 44 ++ .../game-details/review-sort-options.scss | 72 ++ .../game-details/review-sort-options.tsx | 60 ++ src/types/index.ts | 19 + yarn.lock | 446 ++++++++++++ 21 files changed, 2003 insertions(+), 6 deletions(-) create mode 100644 src/main/events/catalogue/check-game-review.ts create mode 100644 src/main/events/catalogue/create-game-review.ts create mode 100644 src/main/events/catalogue/delete-review.ts create mode 100644 src/main/events/catalogue/get-game-reviews.ts create mode 100644 src/main/events/catalogue/vote-review.ts create mode 100644 src/renderer/src/pages/game-details/review-prompt-banner.scss create mode 100644 src/renderer/src/pages/game-details/review-prompt-banner.tsx create mode 100644 src/renderer/src/pages/game-details/review-sort-options.scss create mode 100644 src/renderer/src/pages/game-details/review-sort-options.tsx diff --git a/package.json b/package.json index e21c962a..e2f750bd 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,12 @@ "@primer/octicons-react": "^19.9.0", "@radix-ui/react-dropdown-menu": "^2.1.2", "@reduxjs/toolkit": "^2.2.3", + "@tiptap/extension-bold": "^3.6.2", + "@tiptap/extension-italic": "^3.6.2", + "@tiptap/extension-link": "^3.6.2", + "@tiptap/extension-underline": "^3.6.2", + "@tiptap/react": "^3.6.2", + "@tiptap/starter-kit": "^3.6.2", "auto-launch": "^5.0.6", "axios": "^1.7.9", "axios-cookiejar-support": "^5.0.5", @@ -63,6 +69,7 @@ "jsdom": "^24.0.0", "jsonwebtoken": "^9.0.2", "lodash-es": "^4.17.21", + "lucide-react": "^0.544.0", "parse-torrent": "^11.0.18", "rc-virtual-list": "^3.18.3", "react-dnd": "^16.0.1", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index bcf8cf54..b0fee465 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -198,6 +198,29 @@ "hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util it's completed. If Hydra closes before completing, you will lose your progress.", "achievements": "Achievements", "achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}", + "show_more": "Show more", + "show_less": "Show less", + "reviews": "Reviews", + "leave_a_review": "Leave a Review", + "write_review_placeholder": "Share your thoughts about this game...", + "sort_newest": "Newest", + "sort_by": "Sort by", + "no_reviews_yet": "No reviews yet", + "be_first_to_review": "Be the first to share your thoughts about this game!", + "sort_oldest": "Oldest", + "sort_highest_score": "Highest Score", + "sort_lowest_score": "Lowest Score", + "sort_most_voted": "Most Voted", + "rating": "Rating", + "submit_review": "Submit Review", + "submitting": "Submitting...", + "loading_reviews": "Loading reviews...", + "loading_more_reviews": "Loading more reviews...", + "load_more_reviews": "Load More Reviews", + "youve_played_for_hours": "You've played for {{hours}} hours", + "would_you_recommend_this_game": "Would you like to leave a review to this game?", + "yes": "Yes", + "maybe_later": "Maybe Later", "cloud_save": "Cloud save", "cloud_save_description": "Save your progress in the cloud and continue playing on any device", "backups": "Backups", diff --git a/src/main/events/catalogue/check-game-review.ts b/src/main/events/catalogue/check-game-review.ts new file mode 100644 index 00000000..c46ede07 --- /dev/null +++ b/src/main/events/catalogue/check-game-review.ts @@ -0,0 +1,17 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import type { GameShop } from "@types"; + +const checkGameReview = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string +) => { + return HydraApi.get( + `/games/${shop}/${objectId}/reviews/check`, + null, + { needsAuth: true } + ); +}; + +registerEvent("checkGameReview", checkGameReview); \ No newline at end of file diff --git a/src/main/events/catalogue/create-game-review.ts b/src/main/events/catalogue/create-game-review.ts new file mode 100644 index 00000000..7f29b639 --- /dev/null +++ b/src/main/events/catalogue/create-game-review.ts @@ -0,0 +1,18 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import type { GameShop } from "@types"; + +const createGameReview = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + reviewHtml: string, + score: number +) => { + return HydraApi.post(`/games/${shop}/${objectId}/reviews`, { + reviewHtml, + score, + }); +}; + +registerEvent("createGameReview", createGameReview); \ No newline at end of file diff --git a/src/main/events/catalogue/delete-review.ts b/src/main/events/catalogue/delete-review.ts new file mode 100644 index 00000000..2048b3e7 --- /dev/null +++ b/src/main/events/catalogue/delete-review.ts @@ -0,0 +1,14 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import type { GameShop } from "@types"; + +const deleteReview = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + reviewId: string +) => { + return HydraApi.delete(`/games/${shop}/${objectId}/reviews/${reviewId}`); +}; + +registerEvent("deleteReview", deleteReview); \ No newline at end of file diff --git a/src/main/events/catalogue/get-game-reviews.ts b/src/main/events/catalogue/get-game-reviews.ts new file mode 100644 index 00000000..d3c31780 --- /dev/null +++ b/src/main/events/catalogue/get-game-reviews.ts @@ -0,0 +1,26 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import type { GameShop } from "@types"; + +const getGameReviews = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + take: number = 20, + skip: number = 0, + sortBy: string = "newest" +) => { + const params = new URLSearchParams({ + take: take.toString(), + skip: skip.toString(), + sortBy, + }); + + return HydraApi.get( + `/games/${shop}/${objectId}/reviews?${params.toString()}`, + null, + { needsAuth: false } + ); +}; + +registerEvent("getGameReviews", getGameReviews); \ No newline at end of file diff --git a/src/main/events/catalogue/vote-review.ts b/src/main/events/catalogue/vote-review.ts new file mode 100644 index 00000000..b60062c3 --- /dev/null +++ b/src/main/events/catalogue/vote-review.ts @@ -0,0 +1,15 @@ +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import type { GameShop } from "@types"; + +const voteReview = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + reviewId: string, + voteType: 'upvote' | 'downvote' +) => { + return HydraApi.put(`/games/${shop}/${objectId}/reviews/${reviewId}/${voteType}`, {}); +}; + +registerEvent("voteReview", voteReview); \ No newline at end of file diff --git a/src/main/events/index.ts b/src/main/events/index.ts index d4c461f8..378a3b6e 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -11,6 +11,11 @@ import "./catalogue/get-game-stats"; import "./catalogue/get-trending-games"; import "./catalogue/get-publishers"; import "./catalogue/get-developers"; +import "./catalogue/create-game-review"; +import "./catalogue/get-game-reviews"; +import "./catalogue/vote-review"; +import "./catalogue/delete-review"; +import "./catalogue/check-game-review"; import "./hardware/get-disk-free-space"; import "./hardware/check-folder-write-permission"; import "./library/add-game-to-library"; diff --git a/src/preload/index.ts b/src/preload/index.ts index 17c1225f..eda43369 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -77,6 +77,32 @@ contextBridge.exposeInMainWorld("electron", { getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"), + createGameReview: ( + shop: GameShop, + objectId: string, + reviewHtml: string, + score: number + ) => ipcRenderer.invoke("createGameReview", shop, objectId, reviewHtml, score), + getGameReviews: ( + shop: GameShop, + objectId: string, + take?: number, + skip?: number, + sortBy?: string + ) => ipcRenderer.invoke("getGameReviews", shop, objectId, take, skip, sortBy), + voteReview: ( + shop: GameShop, + objectId: string, + reviewId: string, + voteType: "upvote" | "downvote" + ) => ipcRenderer.invoke("voteReview", shop, objectId, reviewId, voteType), + deleteReview: ( + shop: GameShop, + objectId: string, + reviewId: string + ) => ipcRenderer.invoke("deleteReview", shop, objectId, reviewId), + checkGameReview: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("checkGameReview", shop, objectId), onUpdateAchievements: ( objectId: string, shop: GameShop, diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index 3cdefc19..cb9a060c 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -1,4 +1,4 @@ -import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; +import { DownloadIcon, PeopleIcon, StarIcon } from "@primer/octicons-react"; import type { GameStats } from "@types"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; @@ -107,6 +107,14 @@ export function GameCard({ game, ...props }: GameCardProps) { {stats ? numberFormatter.format(stats.playerCount) : "…"} + {stats?.averageScore && ( +
+ + + {stats.averageScore.toFixed(1)} + +
+ )} diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index e6277888..752a1115 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -93,6 +93,34 @@ declare global { ) => Promise; getGameStats: (objectId: string, shop: GameShop) => Promise; getTrendingGames: () => Promise; + createGameReview: ( + shop: GameShop, + objectId: string, + reviewHtml: string, + score: number + ) => Promise; + getGameReviews: ( + shop: GameShop, + objectId: string, + take?: number, + skip?: number, + sortBy?: string + ) => Promise; + voteReview: ( + shop: GameShop, + objectId: string, + reviewId: string, + voteType: 'upvote' | 'downvote' + ) => Promise; + deleteReview: ( + shop: GameShop, + objectId: string, + reviewId: string + ) => Promise; + checkGameReview: ( + shop: GameShop, + objectId: string + ) => Promise<{ hasReviewed: boolean }>; onUpdateAchievements: ( objectId: string, shop: GameShop, diff --git a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss index 9483b50e..d1ae2481 100644 --- a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss @@ -65,7 +65,7 @@ &__preview { width: 100%; padding: globals.$spacing-unit 0; - height: 100%; + height: auto; display: flex; position: relative; overflow-x: auto; diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index ca2ca023..48228e8e 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -1,33 +1,45 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { PencilIcon } from "@primer/octicons-react"; +import { PencilIcon, TrashIcon, ClockIcon } from "@primer/octicons-react"; +import { ThumbsUp, ThumbsDown } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Bold from '@tiptap/extension-bold'; +import Italic from '@tiptap/extension-italic'; +import Underline from '@tiptap/extension-underline'; +import type { GameReview } from "@types"; import { HeroPanel } from "./hero"; import { DescriptionHeader } from "./description-header/description-header"; import { GallerySlider } from "./gallery-slider/gallery-slider"; import { Sidebar } from "./sidebar/sidebar"; import { EditGameModal } from "./modals"; +import { ReviewSortOptions } from "./review-sort-options"; +import { ReviewPromptBanner } from "./review-prompt-banner"; import { useTranslation } from "react-i18next"; import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; import { AuthPage } from "@shared"; import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif"; -import { useUserDetails, useLibrary } from "@renderer/hooks"; +import { useUserDetails, useLibrary, useDate } from "@renderer/hooks"; import { useSubscription } from "@renderer/hooks/use-subscription"; import "./game-details.scss"; export function GameDetailsContent() { const heroRef = useRef(null); + const navigate = useNavigate(); const { t } = useTranslation("game_details"); - const { objectId, shopDetails, game, hasNSFWContentBlocked, updateGame } = + const { objectId, shopDetails, game, hasNSFWContentBlocked, updateGame, shop } = useContext(gameDetailsContext); const { showHydraCloudModal } = useSubscription(); const { userDetails, hasActiveSubscription } = useUserDetails(); const { updateLibrary } = useLibrary(); + const { formatDistance } = useDate(); const { setShowCloudSyncModal, getGameArtifacts } = useContext(cloudSyncContext); @@ -80,6 +92,41 @@ export function GameDetailsContent() { const [backdropOpacity, setBackdropOpacity] = useState(1); const [showEditGameModal, setShowEditGameModal] = useState(false); + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); + + // Reviews state management + const [reviews, setReviews] = useState([]); + const [reviewsLoading, setReviewsLoading] = useState(false); + const [reviewScore, setReviewScore] = useState(5); + const [submittingReview, setSubmittingReview] = useState(false); + const [reviewsSortBy, setReviewsSortBy] = useState("newest"); + const [reviewsPage, setReviewsPage] = useState(0); + const [hasMoreReviews, setHasMoreReviews] = useState(true); + const [visibleBlockedReviews, setVisibleBlockedReviews] = useState>(new Set()); + const [totalReviewCount, setTotalReviewCount] = useState(0); + const [showReviewForm, setShowReviewForm] = useState(false); + + // Review prompt banner state + const [showReviewPrompt, setShowReviewPrompt] = useState(false); + const [hasUserReviewed, setHasUserReviewed] = useState(false); + const [reviewCheckLoading, setReviewCheckLoading] = useState(false); + + // Tiptap editor for review input + const editor = useEditor({ + extensions: [ + StarterKit, + Bold, + Italic, + Underline, + ], + content: '', + editorProps: { + attributes: { + class: 'game-details__review-editor', + 'data-placeholder': t("write_review_placeholder"), + }, + }, + }); useEffect(() => { setBackdropOpacity(1); @@ -114,6 +161,188 @@ export function GameDetailsContent() { const isCustomGame = game?.shop === "custom"; + // Reviews functions + const checkUserReview = async () => { + if (!objectId || !userDetails) return; + + setReviewCheckLoading(true); + try { + const response = await window.electron.checkGameReview(shop, objectId); + const hasReviewed = (response as any)?.hasReviewed || false; + setHasUserReviewed(hasReviewed); + + // Show prompt only if user hasn't reviewed and has played the game + if (!hasReviewed && game?.playTimeInMilliseconds && game.playTimeInMilliseconds > 0) { + setShowReviewPrompt(true); + } + } catch (error) { + console.error("Failed to check user review:", error); + } finally { + setReviewCheckLoading(false); + } + }; + + const loadReviews = async (reset = false) => { + if (!objectId) return; + + setReviewsLoading(true); + try { + const skip = reset ? 0 : reviewsPage * 20; + const response = await window.electron.getGameReviews( + shop, + objectId, + 20, + skip, + reviewsSortBy + ); + + // Handle the response structure: { totalCount: number, reviews: Review[] } + const reviewsData = (response as any)?.reviews || []; + const reviewCount = (response as any)?.totalCount || 0; + + if (reset) { + setReviews(reviewsData); + setReviewsPage(0); + setTotalReviewCount(reviewCount); + } else { + setReviews(prev => [...prev, ...reviewsData]); + } + + setHasMoreReviews(reviewsData.length === 20); + } catch (error) { + console.error("Failed to load reviews:", error); + } finally { + setReviewsLoading(false); + } + }; + + const handleVoteReview = async (reviewId: string, voteType: 'upvote' | 'downvote') => { + if (!objectId) return; + + try { + await window.electron.voteReview(shop, objectId, reviewId, voteType); + // Reload reviews to get updated vote counts + loadReviews(true); + } catch (error) { + console.error(`Failed to ${voteType} review:`, error); + } + }; + + const handleDeleteReview = async (reviewId: string) => { + if (!objectId) return; + + try { + await window.electron.deleteReview(shop, objectId, reviewId); + // Reload reviews after deletion + loadReviews(true); + } catch (error) { + console.error('Failed to delete review:', error); + } + }; + + const handleSubmitReview = async () => { + console.log("handleSubmitReview called"); + console.log("game:", game); + console.log("objectId:", objectId); + + const reviewHtml = editor?.getHTML() || ''; + console.log("reviewHtml:", reviewHtml); + console.log("reviewScore:", reviewScore); + console.log("submittingReview:", submittingReview); + + if (!objectId || !reviewHtml.trim() || submittingReview) { + console.log("Early return - validation failed"); + return; + } + + console.log("Starting review submission..."); + setSubmittingReview(true); + try { + console.log("Calling window.electron.createGameReview..."); + await window.electron.createGameReview( + shop, + objectId, + reviewHtml, + reviewScore + ); + + console.log("Review submitted successfully"); + editor?.commands.clearContent(); + setReviewScore(5); + await loadReviews(true); // Reload reviews after submission + setShowReviewForm(false); // Hide the review form after successful submission + setShowReviewPrompt(false); // Hide the prompt banner + setHasUserReviewed(true); // Update the review status + } catch (error) { + console.error("Failed to submit review:", error); + } finally { + setSubmittingReview(false); + console.log("Review submission completed"); + } + }; + + // Review prompt banner handlers + const handleReviewPromptYes = () => { + setShowReviewPrompt(false); + setShowReviewForm(true); + + // Scroll to review form + setTimeout(() => { + const reviewFormElement = document.querySelector('.game-details__review-form'); + if (reviewFormElement) { + reviewFormElement.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }, 100); + }; + + const handleReviewPromptLater = () => { + setShowReviewPrompt(false); + }; + + const handleSortChange = (newSortBy: string) => { + setReviewsSortBy(newSortBy); + setReviewsPage(0); + setHasMoreReviews(true); + loadReviews(true); + }; + + const toggleBlockedReview = (reviewId: string) => { + setVisibleBlockedReviews(prev => { + const newSet = new Set(prev); + if (newSet.has(reviewId)) { + newSet.delete(reviewId); + } else { + newSet.add(reviewId); + } + return newSet; + }); + }; + + const loadMoreReviews = () => { + if (!reviewsLoading && hasMoreReviews) { + setReviewsPage(prev => prev + 1); + loadReviews(false); + } + }; + + // Load reviews when component mounts or sort changes + useEffect(() => { + if (objectId && (game || shop)) { + loadReviews(true); + checkUserReview(); // Check if user has reviewed this game + } + }, [game, shop, objectId, reviewsSortBy, userDetails]); + + // Load more reviews when page changes + useEffect(() => { + if (reviewsPage > 0) { + loadReviews(false); + } + }, [reviewsPage]); + // Helper function to get image with custom asset priority const getImageWithCustomPriority = ( customUrl: string | null | undefined, @@ -227,6 +456,14 @@ export function GameDetailsContent() {
+ {/* Review Prompt Banner */} + {showReviewPrompt && userDetails && game?.playTimeInMilliseconds && !hasUserReviewed && !reviewCheckLoading && ( + + )} + @@ -234,8 +471,237 @@ export function GameDetailsContent() { dangerouslySetInnerHTML={{ __html: aboutTheGame, }} - className="game-details__description" + className={`game-details__description ${ + isDescriptionExpanded ? 'game-details__description--expanded' : 'game-details__description--collapsed' + }`} /> + + {aboutTheGame && aboutTheGame.length > 500 && ( + + )} + +
+ {showReviewForm && ( + <> +
+

{t("leave_a_review")}

+
+ +
+
+ +
+
+ + + +
+ + +
+
+ +
+
+ + +
+
+
+ + )} + + {showReviewForm && ( +
+ )} + +
+
+
+

+ {t("reviews")} +

+ + {totalReviewCount} + +
+ +
+ + {reviewsLoading && reviews.length === 0 && ( +
+ {t("loading_reviews")} +
+ )} + + {!reviewsLoading && reviews.length === 0 && ( +
+
📝
+

+ {t("no_reviews_yet")} +

+

+ {t("be_first_to_review")} +

+
+ )} + + {reviews.map((review, index) => ( +
+ {review.isBlocked && !visibleBlockedReviews.has(review.id) ? ( +
+ Review from blocked user — + +
+ ) : ( + <> +
+
+ {review.user?.profileImageUrl && ( + {review.user.displayName + )} +
+
review.user?.id && navigate(`/profile/${review.user.id}`)} + > + {review.user?.displayName || 'Anonymous'} +
+
+ + {formatDistance(new Date(review.createdAt), new Date(), { addSuffix: true })} +
+
+
+
+ {review.score}/10 +
+
+
+
+
+ + +
+ {userDetails?.id === review.user?.id && ( + + )} + {review.isBlocked && visibleBlockedReviews.has(review.id) && ( + + )} +
+ + )} +
+ ))} + + {hasMoreReviews && !reviewsLoading && ( + + )} + + {reviewsLoading && reviews.length > 0 && ( +
+ {t("loading_more_reviews")} +
+ )} +
+
{game?.shop !== "custom" && } diff --git a/src/renderer/src/pages/game-details/game-details-skeleton.tsx b/src/renderer/src/pages/game-details/game-details-skeleton.tsx index c39da3bb..adaf4ab2 100644 --- a/src/renderer/src/pages/game-details/game-details-skeleton.tsx +++ b/src/renderer/src/pages/game-details/game-details-skeleton.tsx @@ -35,6 +35,7 @@ export function GameDetailsSkeleton() { ))} +
diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index e1140d31..b82bd6b1 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -27,6 +27,418 @@ $hero-height: 300px; } } + &__review-form { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2); + margin-bottom: calc(globals.$spacing-unit * 3); + padding: calc(globals.$spacing-unit * 2); + background: rgba(255, 255, 255, 0.02); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.05); + } + + &__review-form-controls { + display: flex; + gap: calc(globals.$spacing-unit * 2); + align-items: flex-end; + flex-wrap: wrap; + + @media (max-width: 768px) { + flex-direction: column; + align-items: stretch; + gap: calc(globals.$spacing-unit * 1.5); + } + } + + &__review-form-bottom { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: calc(globals.$spacing-unit * 2); + + @media (max-width: 768px) { + flex-direction: column; + align-items: stretch; + gap: calc(globals.$spacing-unit * 1.5); + } + } + + &__review-score-container { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 0.75); + min-width: 120px; + } + + &__review-score-label { + display: block; + font-size: globals.$body-font-size; + color: globals.$body-color; + } + + &__review-score-select { + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid globals.$border-color; + border-radius: 4px; + padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1); + color: globals.$body-color; + font-size: globals.$body-font-size; + font-family: inherit; + cursor: pointer; + transition: border-color 0.2s ease, background-color 0.2s ease; + + &:focus { + outline: none; + background-color: rgba(255, 255, 255, 0.08); + border-color: globals.$brand-teal; + } + + &:hover { + border-color: rgba(255, 255, 255, 0.15); + } + + option { + background-color: globals.$dark-background-color; + color: globals.$body-color; + } + } + + &__reviews-sort { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 0.75); + min-width: 150px; + } + + &__reviews-sort-label { + display: block; + font-size: globals.$body-font-size; + color: globals.$body-color; + } + + &__reviews-sort-select { + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid globals.$border-color; + border-radius: 4px; + padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1); + color: globals.$body-color; + font-size: globals.$body-font-size; + font-family: inherit; + cursor: pointer; + transition: border-color 0.2s ease, background-color 0.2s ease; + + &:focus { + outline: none; + background-color: rgba(255, 255, 255, 0.08); + border-color: globals.$brand-teal; + } + + &:hover { + border-color: rgba(255, 255, 255, 0.15); + } + + option { + background-color: globals.$dark-background-color; + color: globals.$body-color; + } + } + + &__review-submit-button { + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid globals.$border-color; + color: globals.$body-color; + padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5); + border-radius: 6px; + cursor: pointer; + font-size: globals.$small-font-size; + font-family: inherit; + transition: all ease 0.2s; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; + + &:hover:not(:disabled) { + background-color: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.15); + } + + &:active { + opacity: 0.9; + } + + &:disabled { + background: rgba(255, 255, 255, 0.1); + cursor: not-allowed; + color: rgba(255, 255, 255, 0.5); + } + } + + &__reviews-list { + margin-top: calc(globals.$spacing-unit * 3); + } + + &__reviews-separator { + height: 1px; + background: rgba(255, 255, 255, 0.1); + margin: calc(globals.$spacing-unit * 3) 0; + width: 100%; + } + + &__reviews-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: calc(globals.$spacing-unit * 2); + padding-bottom: calc(globals.$spacing-unit * 1); + } + + &__reviews-empty { + text-align: center; + padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 2); + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 8px; + margin-bottom: calc(globals.$spacing-unit * 2); + } + + &__reviews-empty-icon { + font-size: 48px; + margin-bottom: calc(globals.$spacing-unit * 2); + opacity: 0.6; + } + + &__reviews-empty-title { + color: rgba(255, 255, 255, 0.9); + font-weight: 600; + margin: 0 0 calc(globals.$spacing-unit * 1) 0; + } + + &__reviews-empty-message { + color: rgba(255, 255, 255, 0.6); + font-size: globals.$small-font-size; + margin: 0; + line-height: 1.4; + } + + &__review-item { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + padding: calc(globals.$spacing-unit * 2); + margin-bottom: calc(globals.$spacing-unit * 2); + } + + &__review-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: calc(globals.$spacing-unit * 1.5); + } + + &__review-user { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit * 1); + } + + &__review-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(255, 255, 255, 0.1); + } + + &__review-user-info { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 0.25); + } + + &__review-display-name { + color: rgba(255, 255, 255, 0.9); + font-size: globals.$small-font-size; + font-weight: 600; + + &--clickable { + cursor: pointer; + transition: color 0.2s ease; + + &:hover { + text-decoration: underline; + } + } + } + + &__review-actions { + margin-top: 12px; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + } + + &__review-votes { + display: flex; + gap: 12px; + } + + &__vote-button { + display: flex; + align-items: center; + gap: 6px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + padding: 6px 12px; + color: #ccc; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + } + + &--upvote:hover { + color: #4caf50; + border-color: #4caf50; + } + + &--downvote:hover { + color: #f44336; + border-color: #f44336; + } + + &--active { + &.game-details__vote-button--upvote { + svg { + fill: white; + } + } + + &.game-details__vote-button--downvote { + svg { + fill: white; + } + } + } + + span { + font-weight: 500; + } + } + + &__delete-review-button { + display: flex; + align-items: center; + justify-content: center; + background: rgba(244, 67, 54, 0.1); + border: 1px solid rgba(244, 67, 54, 0.3); + border-radius: 6px; + padding: 6px; + color: #f44336; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: rgba(244, 67, 54, 0.2); + border-color: #f44336; + color: #ff5722; + } + } + + &__blocked-review-simple { + color: rgba(255, 255, 255, 0.6); + font-size: globals.$small-font-size; + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit * 0.5); + } + + &__blocked-review-show-link { + background: none; + border: none; + color: #ffc107; + font-size: globals.$small-font-size; + cursor: pointer; + text-decoration: underline; + padding: 0; + transition: color 0.2s ease; + + &:hover { + color: #ffeb3b; + } + } + + &__blocked-review-hide-link { + background: none; + border: none; + color: rgba(255, 255, 255, 0.5); + font-size: globals.$small-font-size; + cursor: pointer; + text-decoration: underline; + padding: 0; + transition: color 0.2s ease; + + &:hover { + color: rgba(255, 255, 255, 0.8); + } + } + + &__review-score { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.9); + padding: calc(globals.$spacing-unit * 0.5) calc(globals.$spacing-unit * 1); + border-radius: 4px; + font-size: globals.$small-font-size; + font-weight: 600; + border: 1px solid rgba(255, 255, 255, 0.15); + } + + &__review-date { + display: flex; + align-items: center; + gap: 4px; + color: rgba(255, 255, 255, 0.6); + font-size: globals.$small-font-size; + } + + &__review-content { + color: globals.$body-color; + line-height: 1.5; + } + + &__reviews-loading { + text-align: center; + color: rgba(255, 255, 255, 0.6); + padding: calc(globals.$spacing-unit * 2); + } + + &__load-more-reviews { + background: rgba(255, 255, 255, 0.05); + border: 1px solid globals.$border-color; + color: globals.$body-color; + padding: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 2); + border-radius: 4px; + cursor: pointer; + font-size: globals.$body-font-size; + font-family: inherit; + transition: all 0.2s ease; + width: 100%; + margin-top: calc(globals.$spacing-unit * 2); + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + border-color: globals.$brand-teal; + } + } + &__hero { width: 100%; height: $hero-height; @@ -192,6 +604,8 @@ $hero-height: 300px; min-width: 0; flex: 1; overflow-x: hidden; + display: flex; + flex-direction: column; } &__description { @@ -203,6 +617,7 @@ $hero-height: 300px; margin-right: auto; overflow-x: auto; min-height: auto; + transition: max-height 0.3s ease-in-out; @media (min-width: 1280px) { width: 60%; @@ -212,6 +627,27 @@ $hero-height: 300px; width: 50%; } + &--collapsed { + max-height: 300px; + overflow: hidden; + position: relative; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 60px; + background: linear-gradient(transparent, globals.$background-color); + pointer-events: none; + } + } + + &--expanded { + max-height: none; + } + img, video { border-radius: 5px; @@ -237,6 +673,24 @@ $hero-height: 300px; } } + &__description-toggle { + background: none; + border: 1px solid globals.$border-color; + color: globals.$body-color; + padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5); + border-radius: 4px; + cursor: pointer; + font-size: globals.$body-font-size; + margin-top: calc(globals.$spacing-unit * 1.5); + transition: all 0.2s ease; + align-self: center; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + border-color: globals.$brand-teal; + } + } + &__description-skeleton { display: flex; flex-direction: column; @@ -367,4 +821,206 @@ $hero-height: 300px; flex: 1; transition: opacity 0.2s ease; } + + &__reviews-section { + margin-top: calc(globals.$spacing-unit * 3); + padding-top: calc(globals.$spacing-unit * 3); + border-top: 1px solid rgba(255, 255, 255, 0.1); + width: 100%; + margin-left: auto; + margin-right: auto; + + @media (min-width: 1280px) { + width: 60%; + } + + @media (min-width: 1536px) { + width: 50%; + } + } + + &__reviews-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: calc(globals.$spacing-unit * 2); + gap: calc(globals.$spacing-unit * 2); + + @media (max-width: 768px) { + flex-direction: column; + align-items: flex-start; + gap: calc(globals.$spacing-unit * 1.5); + } + } + + &__reviews-title { + font-size: 1.25rem; + font-weight: 600; + color: globals.$muted-color; + margin: 0; + } + + &__reviews-title-group { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit); + flex: 1; + } + + &__reviews-badge { + background-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + min-width: 24px; + text-align: center; + flex-shrink: 0; + } + + &__leave-review-cta { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit * 0.5); + padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5); + background: linear-gradient(135deg, globals.$brand-teal, globals.$brand-blue); + color: white; + border: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + margin-bottom: calc(globals.$spacing-unit); + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(globals.$brand-teal, 0.3); + } + + &:active { + transform: translateY(0); + } + + svg { + flex-shrink: 0; + } + } + + &__review-input-container { + width: 100%; + position: relative; + } + + &__review-input-bottom { + position: absolute; + bottom: 8px; + left: 8px; + right: 8px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + z-index: 10; + } + + &__review-editor-toolbar { + display: flex; + gap: 4px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + padding: 4px; + backdrop-filter: blur(10px); + background: rgba(0, 0, 0, 0.3); + } + + &__editor-button { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 3px; + color: globals.$body-color; + padding: 4px 6px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + transition: all 0.2s ease; + min-width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + + &:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &.is-active { + background: globals.$brand-teal; + border-color: globals.$brand-teal; + color: white; + } + } + + &__review-input { + width: 100%; + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid globals.$border-color; + border-radius: 6px; + padding: calc(globals.$spacing-unit * 1.5); + padding-bottom: calc(globals.$spacing-unit * 3.5); + color: globals.$body-color; + font-size: globals.$body-font-size; + font-family: inherit; + line-height: 1.5; + min-height: 100px; + transition: border-color 0.2s ease, background-color 0.2s ease; + + &:focus-within { + outline: none; + background-color: rgba(255, 255, 255, 0.08); + border-color: globals.$brand-teal; + } + + &:hover { + border-color: rgba(255, 255, 255, 0.15); + } + + .ProseMirror { + outline: none; + min-height: 80px; + + &:empty:before { + content: attr(data-placeholder); + color: rgba(208, 209, 215, 0.6); + pointer-events: none; + } + + p { + margin: 0 0 8px 0; + + &:last-child { + margin-bottom: 0; + } + } + + strong { + font-weight: 700; + } + + em { + font-style: italic; + } + + u { + text-decoration: underline; + } + } + } } diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.scss b/src/renderer/src/pages/game-details/review-prompt-banner.scss new file mode 100644 index 00000000..28ba1e47 --- /dev/null +++ b/src/renderer/src/pages/game-details/review-prompt-banner.scss @@ -0,0 +1,46 @@ +@use "../../scss/globals.scss"; + +.review-prompt-banner { + background: rgba(255, 255, 255, 0.02); + border-radius: 8px; + padding: calc(globals.$spacing-unit * 2); + margin-bottom: calc(globals.$spacing-unit * 3); + border: 1px solid rgba(255, 255, 255, 0.05); + + &__content { + display: flex; + align-items: center; + justify-content: space-between; + gap: calc(globals.$spacing-unit * 2.5); + + @media (max-width: 768px) { + flex-direction: column; + align-items: flex-start; + gap: calc(globals.$spacing-unit * 2); + } + } + + &__text { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 0.5); + } + + &__playtime { + font-size: globals.$body-font-size; + color: globals.$body-color; + font-weight: 600; + } + + &__question { + font-size: globals.$small-font-size; + color: globals.$muted-color; + font-weight: 400; + } + + &__actions { + display: flex; + gap: globals.$spacing-unit; + align-items: center; + } +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.tsx b/src/renderer/src/pages/game-details/review-prompt-banner.tsx new file mode 100644 index 00000000..87c1b170 --- /dev/null +++ b/src/renderer/src/pages/game-details/review-prompt-banner.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Button } from "@renderer/components"; +import "./review-prompt-banner.scss"; + +interface ReviewPromptBannerProps { + onYesClick: () => void; + onLaterClick: () => void; +} + +export function ReviewPromptBanner({ + onYesClick, + onLaterClick, +}: ReviewPromptBannerProps) { + const { t } = useTranslation("game_details"); + + return ( +
+
+
+ + You've seemed to enjoy this game + + + {t("would_you_recommend_this_game")} + +
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/review-sort-options.scss b/src/renderer/src/pages/game-details/review-sort-options.scss new file mode 100644 index 00000000..e982cb24 --- /dev/null +++ b/src/renderer/src/pages/game-details/review-sort-options.scss @@ -0,0 +1,72 @@ +@use "../../scss/globals.scss"; + +.review-sort-options { + &__container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: calc(globals.$spacing-unit); + } + + &__label { + color: rgba(255, 255, 255, 0.6); + font-size: 14px; + font-weight: 400; + } + + &__options { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit); + font-size: 14px; + flex-wrap: wrap; + + @media (max-width: 768px) { + gap: calc(globals.$spacing-unit * 0.75); + } + } + + &__option { + background: none; + border: none; + color: rgba(255, 255, 255, 0.4); + cursor: pointer; + padding: 4px 0; + font-size: 14px; + font-weight: 300; + transition: all ease 0.2s; + display: flex; + align-items: center; + gap: 6px; + + &:hover:not(:disabled) { + color: rgba(255, 255, 255, 0.6); + } + + &.active { + color: rgba(255, 255, 255, 0.9); + font-weight: 500; + } + + span { + display: inline-block; + + @media (max-width: 480px) { + display: none; + } + } + + @media (max-width: 480px) { + gap: 0; + } + } + + &__separator { + color: rgba(255, 255, 255, 0.3); + font-size: 14px; + + @media (max-width: 480px) { + display: none; + } + } +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/review-sort-options.tsx b/src/renderer/src/pages/game-details/review-sort-options.tsx new file mode 100644 index 00000000..5ec25c31 --- /dev/null +++ b/src/renderer/src/pages/game-details/review-sort-options.tsx @@ -0,0 +1,60 @@ +import { CalendarIcon, StarIcon, ThumbsupIcon, ClockIcon } from "@primer/octicons-react"; +import { useTranslation } from "react-i18next"; +import "./review-sort-options.scss"; + +type ReviewSortOption = "newest" | "oldest" | "score_high" | "score_low" | "most_voted"; + +interface ReviewSortOptionsProps { + sortBy: ReviewSortOption; + onSortChange: (sortBy: ReviewSortOption) => void; +} + +export function ReviewSortOptions({ sortBy, onSortChange }: ReviewSortOptionsProps) { + const { t } = useTranslation("game_details"); + + return ( +
+
+ + | + + | + + | + + | + +
+
+ ); +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 593c45be..f4b0645b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -234,6 +234,25 @@ export interface GameStats { downloadCount: number; playerCount: number; assets: ShopAssets | null; + averageScore: number | null; +} + +export interface GameReview { + id: string; + reviewHtml: string; + score: number; + createdAt: string; + updatedAt: string; + upvotes: number; + downvotes: number; + isBlocked: boolean; + hasUpvoted: boolean; + hasDownvoted: boolean; + user: { + id: string; + displayName: string; + profileImageUrl: string | null; + } | null; } export interface TrendingGame extends ShopAssets { diff --git a/yarn.lock b/yarn.lock index 2c321857..71551858 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2073,6 +2073,11 @@ redux-thunk "^3.1.0" reselect "^5.1.0" +"@remirror/core-constants@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f" + integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg== + "@remix-run/router@1.19.2": version "1.19.2" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273" @@ -2840,6 +2845,201 @@ dependencies: uint8-util "^2.2.5" +"@tiptap/core@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.6.2.tgz#abda4116e4a39779fca7070e316b9ed9fdcded7e" + integrity sha512-XKZYrCVFsyQGF6dXQR73YR222l/76wkKfZ+2/4LCrem5qtcOarmv5pYxjUBG8mRuBPskTTBImSFTeQltJIUNCg== + +"@tiptap/extension-blockquote@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-3.6.2.tgz#01b589565c87a691e586e189ddcbcdc5f35618fc" + integrity sha512-TSl41UZhi3ugJMDaf91CA4F5NeFylgTSm6GqnZAHOE6IREdCpAK3qej2zaW3EzfpzxW7sRGLlytkZRvpeyjgJA== + +"@tiptap/extension-bold@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.6.2.tgz#ed721961daf3210c7ba4433a5aeae981043c2d77" + integrity sha512-Q9KO8CCPCAXYqHzIw8b/ookVmrfqfCg2cyh9h9Hvw6nhO4LOOnJMcGVmWsrpFItbwCGMafI5iY9SbSj7RpCyuw== + +"@tiptap/extension-bubble-menu@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.6.2.tgz#237d84f217c8da52c0bc5265a36557fb27d64eaf" + integrity sha512-OF5CxCmYExcXZjcectwAeujSeDZ4IltPy+SsqBZLbQRDts9PQhzv5azGDvYdL2eMMkT3yhO2gWkXxSHMxI3O6w== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@tiptap/extension-bullet-list@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.6.2.tgz#be20b6c795c53bc0d199bdc4dd9f01b6270a1bee" + integrity sha512-Y5Uhir+za7xMm6RAe592aNNlLvCayVSQt2HfSckOr+c/v/Zd2bFUHv0ef6l/nUzUhDBs32Bg9SvfWx/yyMyNEw== + +"@tiptap/extension-code-block@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-3.6.2.tgz#cb3f6f607dcfb36e3eff25255fdcfdedfb3940a7" + integrity sha512-5jfoiQ/3AUrIyuVU1NmEXar6sZFnY7wDFf3ZU2zpcBUG++yg/CmpOe5bXpoolczhl58cM/jyBG5gumQjyOxLNg== + +"@tiptap/extension-code@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.6.2.tgz#5c6500d748fd4f52ddbe01ff114d4933c7a09e8f" + integrity sha512-U6jilbcpCxtLZAgJrTapXzzVJTXnS78kJITFSOLyGCTyGSm6PXatQ4hnaxVGmNet66GySONGjhwAVZ8+l94Rwg== + +"@tiptap/extension-document@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.6.2.tgz#5c3f3a85d12868f5d4e6d6d258b8fa0b8000b778" + integrity sha512-4qg3KWL3aO1M7hfDpZR6/vSo7Cfqr3McyGUfqb/BXqYDW1DwT8jJkDTcHrGU7WUKRlWgoyPyzM8pZiGlP0uQHg== + +"@tiptap/extension-dropcursor@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.6.2.tgz#22a64a4da25ac17cf0cd33e1e924762000152817" + integrity sha512-6R5sma/i2TKd5h9OpIcy3a0wOGp5BNT/zIgnE/1HTmKi40eNcCAVe8sxd6+iWA5ETONP1E48kDy4hqA5ZzZCiQ== + +"@tiptap/extension-floating-menu@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.6.2.tgz#cc9c97cdd5fa55407631d3135e00ca8051516444" + integrity sha512-ym7YMKGY3QhFUKUS6JYOwtdi8s2PeGmOhu7TwI9/U0LmGbELeKJBJl2BP1yB+Sjpv25pVL++CwJQ6dsrjDlZ8g== + +"@tiptap/extension-gapcursor@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.6.2.tgz#790c94d20a5b8ded4c0d38960254d24704a2bc08" + integrity sha512-gXg+EvUKlv3ZO1GxKkRmAsi/V4yyA8AzLW6ppOcYrM2CKf6epmPaVRgAjdwHCA6cm3QuCBJyWeGTCAjhjNakhw== + +"@tiptap/extension-hard-break@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.6.2.tgz#3c379d9104cd7d9e942277f22ba62c57fae267ad" + integrity sha512-ncuPBHhGY58QjluJvEH6vXotaa1QZ/vphXBGAr55kiATZwMIEHgwh2Hgc6AiFTcw057gabGn6jNFDfRB+HjbmA== + +"@tiptap/extension-heading@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.6.2.tgz#3884c309de60c9d61f1bb60c521410b3a0d88ed7" + integrity sha512-JQ2yjwXGAiwGc+MhS1mULBr354MHfmWqVDQLRg8ey6LkdXggTDDJ1Ni3GrUS7B5YcA/ICdhr4krXaQpNkT5Syw== + +"@tiptap/extension-horizontal-rule@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.6.2.tgz#f5680b3209bc48bf8635f3674355bd3d47f15622" + integrity sha512-3TlPqedPDM9QkRTUPhOTxNxQVPSsBwlsuLrAZOgyM1y871Xi7M1DFX0h9LLXuqzPndYzUY16NjrfBGFJX+O56w== + +"@tiptap/extension-italic@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.6.2.tgz#ea314f5e723499c9e7a1021ad7836693db9c653c" + integrity sha512-46zYKqM3o9w1A2G9hWr0ERGbJpqIncoH45XIfLdAI6ZldZVVf+NeXMGwjOPf4+03cZ5/emk3MRTnVp9vF4ToIg== + +"@tiptap/extension-link@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.6.2.tgz#5577d100cd3b735247db327b15d91de025cc76b6" + integrity sha512-3yiRDWa187h30e6iUOJeejZLsbzbJthLfBwTeJGx7pHh7RngsEW82npBRuqLoI3udhJGTkXbzwAFZ9qOGOjl1Q== + dependencies: + linkifyjs "^4.3.2" + +"@tiptap/extension-list-item@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-3.6.2.tgz#705f782a872e4bbb6f0e125fe277c45aeefe8161" + integrity sha512-ma/D2GKylpNB04FfNI3tDMY+C9nz7Yk85H21YTIGv8QL5KlDK97L6orydmx6IVRc2nNMZQVitBIEKDOXcczX9w== + +"@tiptap/extension-list-keymap@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.6.2.tgz#f14e173325b443a89dbbca7f418b76ec3d5c9a21" + integrity sha512-1kl/lggH+LL/FUwcSx8p761ebk9L5ZGK06mGyDDU9XiGLS310CktZYLnpEuFgn/oMPbRHo26oNl9SXLn1/U53A== + +"@tiptap/extension-list@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list/-/extension-list-3.6.2.tgz#beb4d965f48085fa7f69197e10109cde8c175046" + integrity sha512-ZLaEHGVq4eL26hZZFE9e7RArk2rEjcVstN/YTRTKElTnLaf58kLTKN3nlgy1PWGwzfWGUuXURBuEBLaq5l6djg== + +"@tiptap/extension-ordered-list@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.6.2.tgz#43b83757f67264ff0050c03825e780da43680c1d" + integrity sha512-KdJ5MLIw19N+XiqQ2COXGtaq9TzUbtlLE5dgYCJQ2EumeZKIGELvUnHjrnIB9gH/gRlMs+hprLTh23xVUDJovg== + +"@tiptap/extension-paragraph@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.6.2.tgz#d6cc89cdc369e463dd7dd4eb9121718441c984a0" + integrity sha512-jeJWj2xKib3392iHQEcB7wYZ30dUgXuwqpCTwtN9eANor+Zvv6CpDKBs1R2al6BYFbIJCgKeTulqxce0yoC80g== + +"@tiptap/extension-strike@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.6.2.tgz#2dab3f253a4ecfd525c5609ab5edb9325a6364c2" + integrity sha512-976u5WaioIN/0xCjl/UIEypmzACzxgVz6OGgfIsYyreMUiPjhhgzXb0A/2Po5p3nZpKcaMcxifOdhqdw+lDpIQ== + +"@tiptap/extension-text@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.6.2.tgz#77313173a9f91208e40d298bc2d40b39371b8fca" + integrity sha512-fFSUEv1H3lM92yr6jZdELk0gog8rPTK5hTf08kP8RsY8pA80Br1ADVenejrMV4UNTmT1JWTXGBGhMqfQFHUvAQ== + +"@tiptap/extension-underline@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.6.2.tgz#9f0dfb9722bd3d0cd144fc955bcb94a3fcf5eac2" + integrity sha512-IrG6vjxTMI2EeyhZCtx0sNTEu83PsAvzIh4vxmG1fUi/RYokks+sFbgGMuq0jtO96iVNEszlpAC/vaqfxFJwew== + +"@tiptap/extensions@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.6.2.tgz#591fbd5b9fa41f98f69dbd7d21d5d38a2241d94b" + integrity sha512-tg7/DgaI6SpkeawryapUtNoBxsJUMJl3+nSjTfTvsaNXed+BHzLPsvmPbzlF9ScrAbVEx8nj6CCkneECYIQ4CQ== + +"@tiptap/pm@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.6.2.tgz#2121d4917f92d11229529a26955a7033aa8a8843" + integrity sha512-g+NXjqjbj6NfHOMl22uNWVYIu8oCq7RFfbnpohPMsSKJLaHYE8mJR++7T6P5R9FoqhIFdwizg1jTpwRU5CHqXQ== + dependencies: + prosemirror-changeset "^2.3.0" + prosemirror-collab "^1.3.1" + prosemirror-commands "^1.6.2" + prosemirror-dropcursor "^1.8.1" + prosemirror-gapcursor "^1.3.2" + prosemirror-history "^1.4.1" + prosemirror-inputrules "^1.4.0" + prosemirror-keymap "^1.2.2" + prosemirror-markdown "^1.13.1" + prosemirror-menu "^1.2.4" + prosemirror-model "^1.24.1" + prosemirror-schema-basic "^1.2.3" + prosemirror-schema-list "^1.5.0" + prosemirror-state "^1.4.3" + prosemirror-tables "^1.6.4" + prosemirror-trailing-node "^3.0.0" + prosemirror-transform "^1.10.2" + prosemirror-view "^1.38.1" + +"@tiptap/react@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-3.6.2.tgz#5495776c9051a60ece7522da176c9f211a67c7df" + integrity sha512-jgG+bM/GDvI6jnqW3YyLtr/vOR6iO2ta9PYVzoWqNYIxISsMOJeRfinsIqB8l6hkiGZApn9bQji6oUXTc59fgA== + dependencies: + "@types/use-sync-external-store" "^0.0.6" + fast-deep-equal "^3.1.3" + use-sync-external-store "^1.4.0" + optionalDependencies: + "@tiptap/extension-bubble-menu" "^3.6.2" + "@tiptap/extension-floating-menu" "^3.6.2" + +"@tiptap/starter-kit@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-3.6.2.tgz#ddd5612d4836a87082254779c9f152bb51e757bc" + integrity sha512-nPzraIx/f1cOUNqG1LSC0OTnEu3mudcN3jQVuyGh3dvdOnik7FUciJEVfHKnloAyeoijidEeiLpiGHInp2uREg== + dependencies: + "@tiptap/core" "^3.6.2" + "@tiptap/extension-blockquote" "^3.6.2" + "@tiptap/extension-bold" "^3.6.2" + "@tiptap/extension-bullet-list" "^3.6.2" + "@tiptap/extension-code" "^3.6.2" + "@tiptap/extension-code-block" "^3.6.2" + "@tiptap/extension-document" "^3.6.2" + "@tiptap/extension-dropcursor" "^3.6.2" + "@tiptap/extension-gapcursor" "^3.6.2" + "@tiptap/extension-hard-break" "^3.6.2" + "@tiptap/extension-heading" "^3.6.2" + "@tiptap/extension-horizontal-rule" "^3.6.2" + "@tiptap/extension-italic" "^3.6.2" + "@tiptap/extension-link" "^3.6.2" + "@tiptap/extension-list" "^3.6.2" + "@tiptap/extension-list-item" "^3.6.2" + "@tiptap/extension-list-keymap" "^3.6.2" + "@tiptap/extension-ordered-list" "^3.6.2" + "@tiptap/extension-paragraph" "^3.6.2" + "@tiptap/extension-strike" "^3.6.2" + "@tiptap/extension-text" "^3.6.2" + "@tiptap/extension-underline" "^3.6.2" + "@tiptap/extensions" "^3.6.2" + "@tiptap/pm" "^3.6.2" + "@tokenizer/inflate@^0.2.6": version "0.2.7" resolved "https://registry.yarnpkg.com/@tokenizer/inflate/-/inflate-0.2.7.tgz#32dd9dfc9abe457c89b3d9b760fc0690c85a103b" @@ -3014,6 +3214,11 @@ dependencies: "@types/node" "*" +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + "@types/lodash-es@^4.17.12": version "4.17.12" resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz" @@ -3033,6 +3238,19 @@ dependencies: "@types/node" "*" +"@types/markdown-it@^14.0.0": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" @@ -3125,6 +3343,11 @@ resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + "@types/user-agents@^1.0.4": version "1.0.4" resolved "https://registry.npmjs.org/@types/user-agents/-/user-agents-1.0.4.tgz" @@ -4209,6 +4432,11 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +crelt@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" + integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== + cross-fetch-ponyfill@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz#5c5524e3bd3374e71d5016c2327e416369a57527" @@ -6614,6 +6842,18 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + +linkifyjs@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1" + integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA== + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -6779,6 +7019,11 @@ lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== +lucide-react@^0.544.0: + version "0.544.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.544.0.tgz#4719953c10fd53a64dd8343bb0ed16ec79f3eeef" + integrity sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw== + magic-string@^0.30.17: version "0.30.17" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" @@ -6822,6 +7067,18 @@ make-fetch-happen@^10.2.1: socks-proxy-agent "^7.0.0" ssri "^9.0.0" +markdown-it@^14.0.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -6844,6 +7101,11 @@ maybe-combine-errors@^1.0.0: resolved "https://registry.yarnpkg.com/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz#e9592832e61fc47643a92cff3c1f33e27211e5be" integrity sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A== +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + meow@^12.0.1: version "12.1.1" resolved "https://registry.yarnpkg.com/meow/-/meow-12.1.1.tgz#e558dddbab12477b69b2e9a2728c327f191bace6" @@ -7264,6 +7526,11 @@ ora@^5.1.0: strip-ansi "^6.0.0" wcwidth "^1.0.1" +orderedmap@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" + integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== + own-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" @@ -7499,6 +7766,160 @@ property-expr@^2.0.5: resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== +prosemirror-changeset@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz#eee3299cfabc7a027694e9abdc4e85505e9dd5e7" + integrity sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ== + dependencies: + prosemirror-transform "^1.0.0" + +prosemirror-collab@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33" + integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ== + dependencies: + prosemirror-state "^1.0.0" + +prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2: + version "1.7.1" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz#d101fef85618b1be53d5b99ea17bee5600781b38" + integrity sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.10.2" + +prosemirror-dropcursor@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz#2ed30c4796109ddeb1cf7282372b3850528b7228" + integrity sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw== + dependencies: + prosemirror-state "^1.0.0" + prosemirror-transform "^1.1.0" + prosemirror-view "^1.1.0" + +prosemirror-gapcursor@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4" + integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ== + dependencies: + prosemirror-keymap "^1.0.0" + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-view "^1.0.0" + +prosemirror-history@^1.0.0, prosemirror-history@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.4.1.tgz#cc370a46fb629e83a33946a0e12612e934ab8b98" + integrity sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ== + dependencies: + prosemirror-state "^1.2.2" + prosemirror-transform "^1.0.0" + prosemirror-view "^1.31.0" + rope-sequence "^1.3.0" + +prosemirror-inputrules@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz#e22bfaf1d6ea4fe240ad447c184af3d520d43c37" + integrity sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA== + dependencies: + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz#c0f6ab95f75c0b82c97e44eb6aaf29cbfc150472" + integrity sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw== + dependencies: + prosemirror-state "^1.0.0" + w3c-keyname "^2.2.0" + +prosemirror-markdown@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz#863eb3fd5f57a444e4378174622b562735b1c503" + integrity sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g== + dependencies: + "@types/markdown-it" "^14.0.0" + markdown-it "^14.0.0" + prosemirror-model "^1.25.0" + +prosemirror-menu@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz#dea00e7b623cea89f4d76963bee22d2ac2343250" + integrity sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ== + dependencies: + crelt "^1.0.0" + prosemirror-commands "^1.0.0" + prosemirror-history "^1.0.0" + prosemirror-state "^1.0.0" + +prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0: + version "1.25.3" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.3.tgz#c657c60a361cb1e9c9f683d19118c0af50a6f7a9" + integrity sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA== + dependencies: + orderedmap "^2.0.0" + +prosemirror-schema-basic@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz#389ce1ec09b8a30ea9bbb92c58569cb690c2d695" + integrity sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ== + dependencies: + prosemirror-model "^1.25.0" + +prosemirror-schema-list@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz#5869c8f749e8745c394548bb11820b0feb1e32f5" + integrity sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.7.3" + +prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080" + integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-transform "^1.0.0" + prosemirror-view "^1.27.0" + +prosemirror-tables@^1.6.4: + version "1.8.1" + resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.8.1.tgz#896a234e3e18240b629b747a871369dae78c8a9a" + integrity sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug== + dependencies: + prosemirror-keymap "^1.2.2" + prosemirror-model "^1.25.0" + prosemirror-state "^1.4.3" + prosemirror-transform "^1.10.3" + prosemirror-view "^1.39.1" + +prosemirror-trailing-node@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz#5bc223d4fc1e8d9145e4079ec77a932b54e19e04" + integrity sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ== + dependencies: + "@remirror/core-constants" "3.0.0" + escape-string-regexp "^4.0.0" + +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.3, prosemirror-transform@^1.7.3: + version "1.10.4" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz#56419eac14f9f56612c806ae46f9238648f3f02e" + integrity sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw== + dependencies: + prosemirror-model "^1.21.0" + +prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.38.1, prosemirror-view@^1.39.1: + version "1.41.2" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.2.tgz#e69ad3883bfd3c9f3c9cf6da5cee940210df0b6f" + integrity sha512-PGS/jETmh+Qjmre/6vcG7SNHAKiGc4vKOJmHMPRmvcUl7ISuVtrtHmH06UDUwaim4NDJfZfVMl7U7JkMMETa6g== + dependencies: + prosemirror-model "^1.20.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.1.0" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -7517,6 +7938,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -7901,6 +8327,11 @@ rollup@^4.20.0: "@rollup/rollup-win32-x64-msvc" "4.23.0" fsevents "~2.3.2" +rope-sequence@^1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425" + integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ== + rrweb-cssom@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b" @@ -8902,6 +9333,11 @@ typescript@^5.3.3, typescript@^5.4.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + uint8-util@^2.2.2, uint8-util@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/uint8-util/-/uint8-util-2.2.5.tgz#f1a8ff800e4e10a3ac1c82ee3667c99245123896" @@ -9021,6 +9457,11 @@ use-sync-external-store@^1.0.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== +use-sync-external-store@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" + integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== + user-agents@^1.1.387: version "1.1.387" resolved "https://registry.yarnpkg.com/user-agents/-/user-agents-1.1.387.tgz#afc69da00b50eee7ffa17724890e755a6672b99f" @@ -9087,6 +9528,11 @@ void-elements@3.1.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== +w3c-keyname@^2.2.0: + version "2.2.8" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" + integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== + w3c-xmlserializer@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" From 461da55070f1bf9384d58519cc53e5cf60be3762 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 2 Oct 2025 00:45:33 +0300 Subject: [PATCH 02/52] fix: push fix --- .../events/catalogue/check-game-review.ts | 10 +- .../events/catalogue/create-game-review.ts | 2 +- src/main/events/catalogue/delete-review.ts | 2 +- src/main/events/catalogue/get-game-reviews.ts | 2 +- src/main/events/catalogue/vote-review.ts | 9 +- src/preload/index.ts | 10 +- .../src/components/game-card/game-card.tsx | 4 +- src/renderer/src/declaration.d.ts | 2 +- .../game-details/game-details-content.tsx | 284 +++++++++++------- .../game-details/game-details-skeleton.tsx | 6 +- .../src/pages/game-details/game-details.scss | 35 ++- .../game-details/review-prompt-banner.scss | 2 +- .../game-details/review-prompt-banner.tsx | 14 +- .../game-details/review-sort-options.scss | 4 +- .../game-details/review-sort-options.tsx | 21 +- 15 files changed, 242 insertions(+), 165 deletions(-) diff --git a/src/main/events/catalogue/check-game-review.ts b/src/main/events/catalogue/check-game-review.ts index c46ede07..5fa71e29 100644 --- a/src/main/events/catalogue/check-game-review.ts +++ b/src/main/events/catalogue/check-game-review.ts @@ -7,11 +7,9 @@ const checkGameReview = async ( shop: GameShop, objectId: string ) => { - return HydraApi.get( - `/games/${shop}/${objectId}/reviews/check`, - null, - { needsAuth: true } - ); + return HydraApi.get(`/games/${shop}/${objectId}/reviews/check`, null, { + needsAuth: true, + }); }; -registerEvent("checkGameReview", checkGameReview); \ No newline at end of file +registerEvent("checkGameReview", checkGameReview); diff --git a/src/main/events/catalogue/create-game-review.ts b/src/main/events/catalogue/create-game-review.ts index 7f29b639..57c74d45 100644 --- a/src/main/events/catalogue/create-game-review.ts +++ b/src/main/events/catalogue/create-game-review.ts @@ -15,4 +15,4 @@ const createGameReview = async ( }); }; -registerEvent("createGameReview", createGameReview); \ No newline at end of file +registerEvent("createGameReview", createGameReview); diff --git a/src/main/events/catalogue/delete-review.ts b/src/main/events/catalogue/delete-review.ts index 2048b3e7..e617a288 100644 --- a/src/main/events/catalogue/delete-review.ts +++ b/src/main/events/catalogue/delete-review.ts @@ -11,4 +11,4 @@ const deleteReview = async ( return HydraApi.delete(`/games/${shop}/${objectId}/reviews/${reviewId}`); }; -registerEvent("deleteReview", deleteReview); \ No newline at end of file +registerEvent("deleteReview", deleteReview); diff --git a/src/main/events/catalogue/get-game-reviews.ts b/src/main/events/catalogue/get-game-reviews.ts index d3c31780..8f29db3f 100644 --- a/src/main/events/catalogue/get-game-reviews.ts +++ b/src/main/events/catalogue/get-game-reviews.ts @@ -23,4 +23,4 @@ const getGameReviews = async ( ); }; -registerEvent("getGameReviews", getGameReviews); \ No newline at end of file +registerEvent("getGameReviews", getGameReviews); diff --git a/src/main/events/catalogue/vote-review.ts b/src/main/events/catalogue/vote-review.ts index b60062c3..a562eada 100644 --- a/src/main/events/catalogue/vote-review.ts +++ b/src/main/events/catalogue/vote-review.ts @@ -7,9 +7,12 @@ const voteReview = async ( shop: GameShop, objectId: string, reviewId: string, - voteType: 'upvote' | 'downvote' + voteType: "upvote" | "downvote" ) => { - return HydraApi.put(`/games/${shop}/${objectId}/reviews/${reviewId}/${voteType}`, {}); + return HydraApi.put( + `/games/${shop}/${objectId}/reviews/${reviewId}/${voteType}`, + {} + ); }; -registerEvent("voteReview", voteReview); \ No newline at end of file +registerEvent("voteReview", voteReview); diff --git a/src/preload/index.ts b/src/preload/index.ts index eda43369..7596fd11 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -82,7 +82,8 @@ contextBridge.exposeInMainWorld("electron", { objectId: string, reviewHtml: string, score: number - ) => ipcRenderer.invoke("createGameReview", shop, objectId, reviewHtml, score), + ) => + ipcRenderer.invoke("createGameReview", shop, objectId, reviewHtml, score), getGameReviews: ( shop: GameShop, objectId: string, @@ -96,11 +97,8 @@ contextBridge.exposeInMainWorld("electron", { reviewId: string, voteType: "upvote" | "downvote" ) => ipcRenderer.invoke("voteReview", shop, objectId, reviewId, voteType), - deleteReview: ( - shop: GameShop, - objectId: string, - reviewId: string - ) => ipcRenderer.invoke("deleteReview", shop, objectId, reviewId), + deleteReview: (shop: GameShop, objectId: string, reviewId: string) => + ipcRenderer.invoke("deleteReview", shop, objectId, reviewId), checkGameReview: (shop: GameShop, objectId: string) => ipcRenderer.invoke("checkGameReview", shop, objectId), onUpdateAchievements: ( diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index cb9a060c..15b5439b 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -110,9 +110,7 @@ export function GameCard({ game, ...props }: GameCardProps) { {stats?.averageScore && (
- - {stats.averageScore.toFixed(1)} - + {stats.averageScore.toFixed(1)}
)}
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 752a1115..c1e06a89 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -110,7 +110,7 @@ declare global { shop: GameShop, objectId: string, reviewId: string, - voteType: 'upvote' | 'downvote' + voteType: "upvote" | "downvote" ) => Promise; deleteReview: ( shop: GameShop, diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 48228e8e..0c53177f 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -2,11 +2,11 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { PencilIcon, TrashIcon, ClockIcon } from "@primer/octicons-react"; import { ThumbsUp, ThumbsDown } from "lucide-react"; import { useNavigate } from "react-router-dom"; -import { useEditor, EditorContent } from '@tiptap/react'; -import StarterKit from '@tiptap/starter-kit'; -import Bold from '@tiptap/extension-bold'; -import Italic from '@tiptap/extension-italic'; -import Underline from '@tiptap/extension-underline'; +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Bold from "@tiptap/extension-bold"; +import Italic from "@tiptap/extension-italic"; +import Underline from "@tiptap/extension-underline"; import type { GameReview } from "@types"; import { HeroPanel } from "./hero"; @@ -32,8 +32,14 @@ export function GameDetailsContent() { const { t } = useTranslation("game_details"); - const { objectId, shopDetails, game, hasNSFWContentBlocked, updateGame, shop } = - useContext(gameDetailsContext); + const { + objectId, + shopDetails, + game, + hasNSFWContentBlocked, + updateGame, + shop, + } = useContext(gameDetailsContext); const { showHydraCloudModal } = useSubscription(); @@ -93,7 +99,7 @@ export function GameDetailsContent() { const [backdropOpacity, setBackdropOpacity] = useState(1); const [showEditGameModal, setShowEditGameModal] = useState(false); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); - + // Reviews state management const [reviews, setReviews] = useState([]); const [reviewsLoading, setReviewsLoading] = useState(false); @@ -102,10 +108,12 @@ export function GameDetailsContent() { const [reviewsSortBy, setReviewsSortBy] = useState("newest"); const [reviewsPage, setReviewsPage] = useState(0); const [hasMoreReviews, setHasMoreReviews] = useState(true); - const [visibleBlockedReviews, setVisibleBlockedReviews] = useState>(new Set()); + const [visibleBlockedReviews, setVisibleBlockedReviews] = useState< + Set + >(new Set()); const [totalReviewCount, setTotalReviewCount] = useState(0); const [showReviewForm, setShowReviewForm] = useState(false); - + // Review prompt banner state const [showReviewPrompt, setShowReviewPrompt] = useState(false); const [hasUserReviewed, setHasUserReviewed] = useState(false); @@ -113,17 +121,12 @@ export function GameDetailsContent() { // Tiptap editor for review input const editor = useEditor({ - extensions: [ - StarterKit, - Bold, - Italic, - Underline, - ], - content: '', + extensions: [StarterKit, Bold, Italic, Underline], + content: "", editorProps: { attributes: { - class: 'game-details__review-editor', - 'data-placeholder': t("write_review_placeholder"), + class: "game-details__review-editor", + "data-placeholder": t("write_review_placeholder"), }, }, }); @@ -164,15 +167,19 @@ export function GameDetailsContent() { // Reviews functions const checkUserReview = async () => { if (!objectId || !userDetails) return; - + setReviewCheckLoading(true); try { const response = await window.electron.checkGameReview(shop, objectId); const hasReviewed = (response as any)?.hasReviewed || false; setHasUserReviewed(hasReviewed); - + // Show prompt only if user hasn't reviewed and has played the game - if (!hasReviewed && game?.playTimeInMilliseconds && game.playTimeInMilliseconds > 0) { + if ( + !hasReviewed && + game?.playTimeInMilliseconds && + game.playTimeInMilliseconds > 0 + ) { setShowReviewPrompt(true); } } catch (error) { @@ -184,7 +191,7 @@ export function GameDetailsContent() { const loadReviews = async (reset = false) => { if (!objectId) return; - + setReviewsLoading(true); try { const skip = reset ? 0 : reviewsPage * 20; @@ -195,19 +202,19 @@ export function GameDetailsContent() { skip, reviewsSortBy ); - + // Handle the response structure: { totalCount: number, reviews: Review[] } const reviewsData = (response as any)?.reviews || []; const reviewCount = (response as any)?.totalCount || 0; - + if (reset) { setReviews(reviewsData); setReviewsPage(0); setTotalReviewCount(reviewCount); } else { - setReviews(prev => [...prev, ...reviewsData]); + setReviews((prev) => [...prev, ...reviewsData]); } - + setHasMoreReviews(reviewsData.length === 20); } catch (error) { console.error("Failed to load reviews:", error); @@ -216,9 +223,12 @@ export function GameDetailsContent() { } }; - const handleVoteReview = async (reviewId: string, voteType: 'upvote' | 'downvote') => { + const handleVoteReview = async ( + reviewId: string, + voteType: "upvote" | "downvote" + ) => { if (!objectId) return; - + try { await window.electron.voteReview(shop, objectId, reviewId, voteType); // Reload reviews to get updated vote counts @@ -230,13 +240,13 @@ export function GameDetailsContent() { const handleDeleteReview = async (reviewId: string) => { if (!objectId) return; - + try { await window.electron.deleteReview(shop, objectId, reviewId); // Reload reviews after deletion loadReviews(true); } catch (error) { - console.error('Failed to delete review:', error); + console.error("Failed to delete review:", error); } }; @@ -244,17 +254,17 @@ export function GameDetailsContent() { console.log("handleSubmitReview called"); console.log("game:", game); console.log("objectId:", objectId); - - const reviewHtml = editor?.getHTML() || ''; + + const reviewHtml = editor?.getHTML() || ""; console.log("reviewHtml:", reviewHtml); console.log("reviewScore:", reviewScore); console.log("submittingReview:", submittingReview); - + if (!objectId || !reviewHtml.trim() || submittingReview) { console.log("Early return - validation failed"); return; } - + console.log("Starting review submission..."); setSubmittingReview(true); try { @@ -265,7 +275,7 @@ export function GameDetailsContent() { reviewHtml, reviewScore ); - + console.log("Review submitted successfully"); editor?.commands.clearContent(); setReviewScore(5); @@ -285,14 +295,16 @@ export function GameDetailsContent() { const handleReviewPromptYes = () => { setShowReviewPrompt(false); setShowReviewForm(true); - + // Scroll to review form setTimeout(() => { - const reviewFormElement = document.querySelector('.game-details__review-form'); + const reviewFormElement = document.querySelector( + ".game-details__review-form" + ); if (reviewFormElement) { - reviewFormElement.scrollIntoView({ - behavior: 'smooth', - block: 'start' + reviewFormElement.scrollIntoView({ + behavior: "smooth", + block: "start", }); } }, 100); @@ -310,7 +322,7 @@ export function GameDetailsContent() { }; const toggleBlockedReview = (reviewId: string) => { - setVisibleBlockedReviews(prev => { + setVisibleBlockedReviews((prev) => { const newSet = new Set(prev); if (newSet.has(reviewId)) { newSet.delete(reviewId); @@ -323,7 +335,7 @@ export function GameDetailsContent() { const loadMoreReviews = () => { if (!reviewsLoading && hasMoreReviews) { - setReviewsPage(prev => prev + 1); + setReviewsPage((prev) => prev + 1); loadReviews(false); } }; @@ -457,13 +469,17 @@ export function GameDetailsContent() {
{/* Review Prompt Banner */} - {showReviewPrompt && userDetails && game?.playTimeInMilliseconds && !hasUserReviewed && !reviewCheckLoading && ( - - )} - + {showReviewPrompt && + userDetails && + game?.playTimeInMilliseconds && + !hasUserReviewed && + !reviewCheckLoading && ( + + )} + @@ -472,10 +488,12 @@ export function GameDetailsContent() { __html: aboutTheGame, }} className={`game-details__description ${ - isDescriptionExpanded ? 'game-details__description--expanded' : 'game-details__description--collapsed' + isDescriptionExpanded + ? "game-details__description--expanded" + : "game-details__description--collapsed" }`} /> - + {aboutTheGame && aboutTheGame.length > 500 && (
- +
- +
- +
@@ -578,7 +610,7 @@ export function GameDetailsContent() { {totalReviewCount} - @@ -589,7 +621,7 @@ export function GameDetailsContent() { {t("loading_reviews")} )} - + {!reviewsLoading && reviews.length === 0 && (
📝
@@ -601,13 +633,14 @@ export function GameDetailsContent() {

)} - + {reviews.map((review, index) => (
- {review.isBlocked && !visibleBlockedReviews.has(review.id) ? ( + {review.isBlocked && + !visibleBlockedReviews.has(review.id) ? (
- Review from blocked user — -
-
- -
{userDetails?.id === review.user?.id && ( - )} - {review.isBlocked && visibleBlockedReviews.has(review.id) && ( - - )} + {review.isBlocked && + visibleBlockedReviews.has(review.id) && ( + + )}
)}
))} - + {hasMoreReviews && !reviewsLoading && (
diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index b82bd6b1..f6b724ab 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -86,7 +86,9 @@ $hero-height: 300px; font-size: globals.$body-font-size; font-family: inherit; cursor: pointer; - transition: border-color 0.2s ease, background-color 0.2s ease; + transition: + border-color 0.2s ease, + background-color 0.2s ease; &:focus { outline: none; @@ -126,7 +128,9 @@ $hero-height: 300px; font-size: globals.$body-font-size; font-family: inherit; cursor: pointer; - transition: border-color 0.2s ease, background-color 0.2s ease; + transition: + border-color 0.2s ease, + background-color 0.2s ease; &:focus { outline: none; @@ -148,7 +152,8 @@ $hero-height: 300px; background-color: rgba(255, 255, 255, 0.05); border: 1px solid globals.$border-color; color: globals.$body-color; - padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5); + padding: calc(globals.$spacing-unit * 0.75) + calc(globals.$spacing-unit * 1.5); border-radius: 6px; cursor: pointer; font-size: globals.$small-font-size; @@ -631,9 +636,9 @@ $hero-height: 300px; max-height: 300px; overflow: hidden; position: relative; - + &::after { - content: ''; + content: ""; position: absolute; bottom: 0; left: 0; @@ -677,7 +682,8 @@ $hero-height: 300px; background: none; border: 1px solid globals.$border-color; color: globals.$body-color; - padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5); + padding: calc(globals.$spacing-unit * 0.75) + calc(globals.$spacing-unit * 1.5); border-radius: 4px; cursor: pointer; font-size: globals.$body-font-size; @@ -883,8 +889,13 @@ $hero-height: 300px; display: flex; align-items: center; gap: calc(globals.$spacing-unit * 0.5); - padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1.5); - background: linear-gradient(135deg, globals.$brand-teal, globals.$brand-blue); + padding: calc(globals.$spacing-unit * 0.75) + calc(globals.$spacing-unit * 1.5); + background: linear-gradient( + 135deg, + globals.$brand-teal, + globals.$brand-blue + ); color: white; border: none; border-radius: 8px; @@ -980,7 +991,9 @@ $hero-height: 300px; font-family: inherit; line-height: 1.5; min-height: 100px; - transition: border-color 0.2s ease, background-color 0.2s ease; + transition: + border-color 0.2s ease, + background-color 0.2s ease; &:focus-within { outline: none; @@ -995,7 +1008,7 @@ $hero-height: 300px; .ProseMirror { outline: none; min-height: 80px; - + &:empty:before { content: attr(data-placeholder); color: rgba(208, 209, 215, 0.6); @@ -1004,7 +1017,7 @@ $hero-height: 300px; p { margin: 0 0 8px 0; - + &:last-child { margin-bottom: 0; } diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.scss b/src/renderer/src/pages/game-details/review-prompt-banner.scss index 28ba1e47..b8f7557b 100644 --- a/src/renderer/src/pages/game-details/review-prompt-banner.scss +++ b/src/renderer/src/pages/game-details/review-prompt-banner.scss @@ -43,4 +43,4 @@ gap: globals.$spacing-unit; align-items: center; } -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.tsx b/src/renderer/src/pages/game-details/review-prompt-banner.tsx index 87c1b170..7bd96613 100644 --- a/src/renderer/src/pages/game-details/review-prompt-banner.tsx +++ b/src/renderer/src/pages/game-details/review-prompt-banner.tsx @@ -18,27 +18,21 @@ export function ReviewPromptBanner({
- You've seemed to enjoy this game + You've seemed to enjoy this game {t("would_you_recommend_this_game")}
- -
); -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/game-details/review-sort-options.scss b/src/renderer/src/pages/game-details/review-sort-options.scss index e982cb24..5b374728 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.scss +++ b/src/renderer/src/pages/game-details/review-sort-options.scss @@ -50,7 +50,7 @@ span { display: inline-block; - + @media (max-width: 480px) { display: none; } @@ -69,4 +69,4 @@ display: none; } } -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/game-details/review-sort-options.tsx b/src/renderer/src/pages/game-details/review-sort-options.tsx index 5ec25c31..858faefd 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.tsx +++ b/src/renderer/src/pages/game-details/review-sort-options.tsx @@ -1,15 +1,28 @@ -import { CalendarIcon, StarIcon, ThumbsupIcon, ClockIcon } from "@primer/octicons-react"; +import { + CalendarIcon, + StarIcon, + ThumbsupIcon, + ClockIcon, +} from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import "./review-sort-options.scss"; -type ReviewSortOption = "newest" | "oldest" | "score_high" | "score_low" | "most_voted"; +type ReviewSortOption = + | "newest" + | "oldest" + | "score_high" + | "score_low" + | "most_voted"; interface ReviewSortOptionsProps { sortBy: ReviewSortOption; onSortChange: (sortBy: ReviewSortOption) => void; } -export function ReviewSortOptions({ sortBy, onSortChange }: ReviewSortOptionsProps) { +export function ReviewSortOptions({ + sortBy, + onSortChange, +}: ReviewSortOptionsProps) { const { t } = useTranslation("game_details"); return ( @@ -57,4 +70,4 @@ export function ReviewSortOptions({ sortBy, onSortChange }: ReviewSortOptionsPro ); -} \ No newline at end of file +} From 19cf24ef489e04045c9b9d3c8e3facb8e4c23f06 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 2 Oct 2025 01:30:44 +0300 Subject: [PATCH 03/52] feat: changed profile pictures in reviews to squares, changed sorting buttons --- .../game-details/game-details-content.tsx | 2 +- .../src/pages/game-details/game-details.scss | 2 +- .../game-details/review-sort-options.scss | 15 +++++ .../game-details/review-sort-options.tsx | 59 ++++++++++--------- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 0c53177f..6f2558ef 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -666,7 +666,7 @@ export function GameDetailsContent() { navigate(`/profile/${review.user.id}`) } onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); review.user?.id && navigate(`/profile/${review.user.id}`); diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index f6b724ab..fe097839 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -252,7 +252,7 @@ $hero-height: 300px; &__review-avatar { width: 32px; height: 32px; - border-radius: 50%; + border-radius: 4px; object-fit: cover; border: 2px solid rgba(255, 255, 255, 0.1); } diff --git a/src/renderer/src/pages/game-details/review-sort-options.scss b/src/renderer/src/pages/game-details/review-sort-options.scss index 5b374728..eafe9972 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.scss +++ b/src/renderer/src/pages/game-details/review-sort-options.scss @@ -61,6 +61,21 @@ } } + &__toggle-option { + &.active { + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + padding: 6px 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + &:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.03); + border-radius: 4px; + padding: 6px 8px; + } + } + &__separator { color: rgba(255, 255, 255, 0.3); font-size: 14px; diff --git a/src/renderer/src/pages/game-details/review-sort-options.tsx b/src/renderer/src/pages/game-details/review-sort-options.tsx index 858faefd..fc7f431a 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.tsx +++ b/src/renderer/src/pages/game-details/review-sort-options.tsx @@ -1,8 +1,7 @@ import { - CalendarIcon, - StarIcon, ThumbsupIcon, - ClockIcon, + ChevronUpIcon, + ChevronDownIcon, } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; import "./review-sort-options.scss"; @@ -25,44 +24,46 @@ export function ReviewSortOptions({ }: ReviewSortOptionsProps) { const { t } = useTranslation("game_details"); + const handleDateToggle = () => { + const newSort = sortBy === "newest" ? "oldest" : "newest"; + onSortChange(newSort); + }; + + const handleScoreToggle = () => { + const newSort = sortBy === "score_high" ? "score_low" : "score_high"; + onSortChange(newSort); + }; + + const handleMostVotedClick = () => { + onSortChange("most_voted"); + }; + + const isDateActive = sortBy === "newest" || sortBy === "oldest"; + const isScoreActive = sortBy === "score_high" || sortBy === "score_low"; + const isMostVotedActive = sortBy === "most_voted"; + return (
| | - | - - | - )} -
- {showReviewForm && ( - <> -
-

- {t("leave_a_review")} -

-
+ {game?.shop !== "custom" && ( +
+ {showReviewForm && ( + <> +
+

+ {t("leave_a_review")} +

+
+ +
+
+ +
+
+ + + +
-
-
- -
-
- -
+
- +
+
+ + +
+ + )} -
-
- - -
+ {showReviewForm && ( +
+ )} + +
+
+
+

+ {t("reviews")} +

+ + {totalReviewCount} +
- - )} - - {showReviewForm && ( -
- )} - -
-
-
-

- {t("reviews")} -

- - {totalReviewCount} - -
-
- {reviewsLoading && reviews.length === 0 && ( -
- {t("loading_reviews")} -
- )} + {reviewsLoading && reviews.length === 0 && ( +
+ {t("loading_reviews")} +
+ )} - {!reviewsLoading && reviews.length === 0 && ( -
-
📝
-

- {t("no_reviews_yet")} -

-

- {t("be_first_to_review")} -

-
- )} + {!reviewsLoading && reviews.length === 0 && ( +
+
📝
+

+ {t("no_reviews_yet")} +

+

+ {t("be_first_to_review")} +

+
+ )} - {reviews.map((review, index) => ( -
- {review.isBlocked && - !visibleBlockedReviews.has(review.id) ? ( -
- Review from blocked user — - -
- ) : ( - <> -
-
- {review.user?.profileImageUrl && ( - {review.user.displayName - )} -
-
- review.user?.id && - navigate(`/profile/${review.user.id}`) - } - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + {reviews.map((review, index) => ( +
+ {review.isBlocked && + !visibleBlockedReviews.has(review.id) ? ( +
+ Review from blocked user — + +
+ ) : ( + <> +
+
+ {review.user?.profileImageUrl && ( + {review.user.displayName + )} +
+
review.user?.id && - navigate(`/profile/${review.user.id}`); + navigate(`/profile/${review.user.id}`) } - }} - role="button" - tabIndex={0} - > - {review.user?.displayName || "Anonymous"} -
-
- - {formatDistance( - new Date(review.createdAt), - new Date(), - { addSuffix: true } - )} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + review.user?.id && + navigate(`/profile/${review.user.id}`); + } + }} + role="button" + tabIndex={0} + > + {review.user?.displayName || "Anonymous"} +
+
+ + {formatDistance( + new Date(review.createdAt), + new Date(), + { addSuffix: true } + )} +
+
+ {review.score}/10 +
-
- {review.score}/10 -
-
-
-
-
- - -
- {userDetails?.id === review.user?.id && ( - - )} - {review.isBlocked && - visibleBlockedReviews.has(review.id) && ( +
+
+
+ +
+ {userDetails?.id === review.user?.id && ( + )} -
- - )} -
- ))} + {review.isBlocked && + visibleBlockedReviews.has(review.id) && ( + + )} +
+ + )} +
+ ))} - {hasMoreReviews && !reviewsLoading && ( - - )} + {hasMoreReviews && !reviewsLoading && ( + + )} - {reviewsLoading && reviews.length > 0 && ( -
- {t("loading_more_reviews")} -
- )} + {reviewsLoading && reviews.length > 0 && ( +
+ {t("loading_more_reviews")} +
+ )} +
-
+ )}
{game?.shop !== "custom" && } @@ -773,6 +782,15 @@ export function GameDetailsContent() { onGameUpdated={handleGameUpdated} /> )} + + { + setShowDeleteReviewModal(false); + setReviewToDelete(null); + }} + onConfirm={confirmDeleteReview} + />
); } diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index fe097839..da3745e9 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -196,7 +196,6 @@ $hero-height: 300px; display: flex; justify-content: space-between; align-items: center; - margin-bottom: calc(globals.$spacing-unit * 2); padding-bottom: calc(globals.$spacing-unit * 1); } @@ -850,7 +849,6 @@ $hero-height: 300px; justify-content: space-between; align-items: center; margin-bottom: calc(globals.$spacing-unit * 2); - gap: calc(globals.$spacing-unit * 2); @media (max-width: 768px) { flex-direction: column; diff --git a/src/renderer/src/pages/game-details/modals/delete-review-modal.scss b/src/renderer/src/pages/game-details/modals/delete-review-modal.scss new file mode 100644 index 00000000..40ad6e59 --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/delete-review-modal.scss @@ -0,0 +1,19 @@ +@use "../../../scss/globals.scss"; + +.delete-review-modal { + &__karma-warning { + background-color: rgba(255, 193, 7, 0.1); + border: 1px solid rgba(255, 193, 7, 0.3); + border-radius: 4px; + padding: 12px; + margin-bottom: 16px; + color: #ffc107; + font-size: 14px; + font-weight: 500; + } + + &__actions { + display: flex; + justify-content: flex-end; + } +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx b/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx new file mode 100644 index 00000000..45501b88 --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx @@ -0,0 +1,45 @@ +import { useTranslation } from "react-i18next"; +import { Button, Modal } from "@renderer/components"; +import "./delete-review-modal.scss"; + +interface DeleteReviewModalProps { + visible: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export function DeleteReviewModal({ + visible, + onClose, + onConfirm, +}: DeleteReviewModalProps) { + const { t } = useTranslation("game_details"); + + const handleDeleteReview = () => { + onConfirm(); + onClose(); + }; + + return ( + +
+ {t("delete_review_karma_warning")} +
+ +
+ + + +
+
+ ); +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/modals/index.ts b/src/renderer/src/pages/game-details/modals/index.ts index 724e0003..d0f27b0f 100644 --- a/src/renderer/src/pages/game-details/modals/index.ts +++ b/src/renderer/src/pages/game-details/modals/index.ts @@ -2,3 +2,4 @@ export * from "./repacks-modal"; export * from "./download-settings-modal"; export * from "./game-options-modal"; export * from "./edit-game-modal"; +export * from "./delete-review-modal"; diff --git a/src/renderer/src/pages/game-details/review-sort-options.scss b/src/renderer/src/pages/game-details/review-sort-options.scss index eafe9972..fba6c50f 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.scss +++ b/src/renderer/src/pages/game-details/review-sort-options.scss @@ -6,6 +6,7 @@ flex-direction: column; align-items: flex-start; gap: calc(globals.$spacing-unit); + margin-bottom: calc(globals.$spacing-unit * 3); } &__label { @@ -37,7 +38,7 @@ transition: all ease 0.2s; display: flex; align-items: center; - gap: 6px; + gap: 4px; &:hover:not(:disabled) { color: rgba(255, 255, 255, 0.6); @@ -61,21 +62,6 @@ } } - &__toggle-option { - &.active { - background: rgba(255, 255, 255, 0.05); - border-radius: 4px; - padding: 6px 8px; - border: 1px solid rgba(255, 255, 255, 0.1); - } - - &:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.03); - border-radius: 4px; - padding: 6px 8px; - } - } - &__separator { color: rgba(255, 255, 255, 0.3); font-size: 14px; diff --git a/src/renderer/src/pages/game-details/review-sort-options.tsx b/src/renderer/src/pages/game-details/review-sort-options.tsx index fc7f431a..ca11d056 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.tsx +++ b/src/renderer/src/pages/game-details/review-sort-options.tsx @@ -49,16 +49,30 @@ export function ReviewSortOptions({ className={`review-sort-options__option review-sort-options__toggle-option ${isDateActive ? "active" : ""}`} onClick={handleDateToggle} > - {sortBy === "newest" ? : } - {sortBy === "oldest" ? t("sort_oldest") : t("sort_newest")} + {sortBy === "newest" ? ( + + ) : ( + + )} + + {sortBy === "oldest" ? t("sort_oldest") : t("sort_newest")} + | |
); -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/profile/profile-content/user-karma-box.scss b/src/renderer/src/pages/profile/profile-content/user-karma-box.scss index 5f5610e3..63015b4d 100644 --- a/src/renderer/src/pages/profile/profile-content/user-karma-box.scss +++ b/src/renderer/src/pages/profile/profile-content/user-karma-box.scss @@ -44,4 +44,4 @@ font-size: 0.85rem; line-height: 1.4; } -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx b/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx index 4668bf28..fa69d88f 100644 --- a/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx @@ -25,7 +25,8 @@ export function UserKarmaBox() {

- {numberFormatter.format(userDetails.karma)} {t("karma_count")} + {numberFormatter.format(userDetails.karma)}{" "} + {t("karma_count")}

@@ -37,4 +38,4 @@ export function UserKarmaBox() {
); -} \ No newline at end of file +} From 80275dc08fb466d5983f029c695818031445c2a1 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 2 Oct 2025 19:29:50 +0300 Subject: [PATCH 06/52] Fix: Review delete modal button color + added missing translation --- src/locales/en/translation.json | 5 +++-- .../src/pages/game-details/modals/delete-review-modal.tsx | 8 ++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 5326b24b..670dda4f 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -319,8 +319,9 @@ "filter_by_source": "Filter by source", "no_repacks_found": "No sources found for this game", "delete_review": "Delete review", - "delete_review_modal_title": "Delete Review", - "delete_review_modal_description": "Are you sure you want to delete your review? This action cannot be undone.", + "delete_review_modal_title": "Are you sure you want to delete your review?", + "delete_review_modal_description": "This action cannot be undone.", + "delete_review_button": "Delete", "delete_review_karma_warning": "You will lose any karma points earned from this review." }, "activation": { diff --git a/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx b/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx index fb1ef992..958c23db 100644 --- a/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx @@ -27,17 +27,13 @@ export function DeleteReviewModal({ description={t("delete_review_modal_description")} onClose={onClose} > -
- {t("delete_review_karma_warning")} -
-
-
From 8d5b169166ae984bc7f37da26bcd0d1dc04b15b2 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 2 Oct 2025 21:22:09 +0300 Subject: [PATCH 07/52] Feat: updated input field design, fixed text overflow --- .../confirm-modal/confirm-modal.tsx | 13 +- .../game-details/game-details.context.tsx | 3 +- .../game-details/game-details-content.tsx | 68 +++++-- .../src/pages/game-details/game-details.scss | 185 ++++++------------ .../game-details/hero/hero-panel-actions.tsx | 32 ++- .../game-details/modals/repacks-modal.tsx | 2 +- 6 files changed, 153 insertions(+), 150 deletions(-) diff --git a/src/renderer/src/components/confirm-modal/confirm-modal.tsx b/src/renderer/src/components/confirm-modal/confirm-modal.tsx index d210c035..75a8f5c9 100644 --- a/src/renderer/src/components/confirm-modal/confirm-modal.tsx +++ b/src/renderer/src/components/confirm-modal/confirm-modal.tsx @@ -33,9 +33,18 @@ export function ConfirmModal({ }; return ( - +
- diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index 23ea3845..5be5cf98 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -201,7 +201,8 @@ export function GameDetailsContextProvider({ }, [objectId, gameTitle, dispatch]); useEffect(() => { - const state = (location && (location.state as Record)) || {}; + const state = + (location && (location.state as Record)) || {}; if (state.openRepacks) { setShowRepacksModal(true); try { diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index de66e5a6..bb6c2e85 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -104,6 +104,8 @@ export function GameDetailsContent() { const [reviewsLoading, setReviewsLoading] = useState(false); const [reviewScore, setReviewScore] = useState(5); const [submittingReview, setSubmittingReview] = useState(false); + const [reviewCharCount, setReviewCharCount] = useState(0); + const MAX_REVIEW_CHARS = 1000; const [reviewsSortBy, setReviewsSortBy] = useState("newest"); const [reviewsPage, setReviewsPage] = useState(0); const [hasMoreReviews, setHasMoreReviews] = useState(true); @@ -127,6 +129,31 @@ export function GameDetailsContent() { class: "game-details__review-editor", "data-placeholder": t("write_review_placeholder"), }, + handlePaste: (view, event) => { + // Strip formatting from pasted content to prevent overflow issues + const text = event.clipboardData?.getData('text/plain') || ''; + const currentText = view.state.doc.textContent; + const remainingChars = MAX_REVIEW_CHARS - currentText.length; + + if (text && remainingChars > 0) { + event.preventDefault(); + const truncatedText = text.slice(0, remainingChars); + view.dispatch(view.state.tr.insertText(truncatedText)); + return true; + } + return false; + }, + }, + onUpdate: ({ editor }) => { + const text = editor.getText(); + setReviewCharCount(text.length); + + // Prevent typing beyond character limit + if (text.length > MAX_REVIEW_CHARS) { + const truncatedContent = text.slice(0, MAX_REVIEW_CHARS); + editor.commands.setContent(truncatedContent); + setReviewCharCount(MAX_REVIEW_CHARS); + } }, }); @@ -266,7 +293,7 @@ export function GameDetailsContent() { console.log("reviewScore:", reviewScore); console.log("submittingReview:", submittingReview); - if (!objectId || !reviewHtml.trim() || submittingReview) { + if (!objectId || !reviewHtml.trim() || submittingReview || reviewCharCount > MAX_REVIEW_CHARS) { console.log("Early return - validation failed"); return; } @@ -523,11 +550,7 @@ export function GameDetailsContent() {
- -
+
- - +
+ MAX_REVIEW_CHARS ? "over-limit" : ""}> + {reviewCharCount}/{MAX_REVIEW_CHARS} + +
+
@@ -599,6 +619,18 @@ export function GameDetailsContent() {
+ +
diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index da3745e9..e9e94aea 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -30,12 +30,8 @@ $hero-height: 300px; &__review-form { display: flex; flex-direction: column; - gap: calc(globals.$spacing-unit * 2); - margin-bottom: calc(globals.$spacing-unit * 3); - padding: calc(globals.$spacing-unit * 2); - background: rgba(255, 255, 255, 0.02); - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.05); + gap: 16px; + margin-bottom: 24px; } &__review-form-controls { @@ -54,55 +50,35 @@ $hero-height: 300px; &__review-form-bottom { display: flex; justify-content: space-between; - align-items: flex-end; - gap: calc(globals.$spacing-unit * 2); - - @media (max-width: 768px) { - flex-direction: column; - align-items: stretch; - gap: calc(globals.$spacing-unit * 1.5); - } + align-items: center; + gap: 16px; + flex-wrap: wrap; } &__review-score-container { display: flex; - flex-direction: column; - gap: calc(globals.$spacing-unit * 0.75); - min-width: 120px; + align-items: center; + gap: 8px; } &__review-score-label { - display: block; - font-size: globals.$body-font-size; - color: globals.$body-color; + font-size: 14px; + color: #ffffff; + font-weight: 500; } &__review-score-select { - background-color: rgba(255, 255, 255, 0.05); - border: 1px solid globals.$border-color; + background-color: #2a2a2a; + border: 1px solid #3a3a3a; border-radius: 4px; - padding: calc(globals.$spacing-unit * 0.75) calc(globals.$spacing-unit * 1); - color: globals.$body-color; - font-size: globals.$body-font-size; - font-family: inherit; + color: #ffffff; + padding: 6px 12px; + font-size: 14px; cursor: pointer; - transition: - border-color 0.2s ease, - background-color 0.2s ease; &:focus { outline: none; - background-color: rgba(255, 255, 255, 0.08); - border-color: globals.$brand-teal; - } - - &:hover { - border-color: rgba(255, 255, 255, 0.15); - } - - option { - background-color: globals.$dark-background-color; - color: globals.$body-color; + border-color: #0078d4; } } @@ -150,19 +126,14 @@ $hero-height: 300px; &__review-submit-button { background-color: rgba(255, 255, 255, 0.05); - border: 1px solid globals.$border-color; - color: globals.$body-color; - padding: calc(globals.$spacing-unit * 0.75) - calc(globals.$spacing-unit * 1.5); + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 6px; + color: #ffffff; + padding: 10px 20px; + font-size: 14px; + font-weight: 500; cursor: pointer; - font-size: globals.$small-font-size; - font-family: inherit; - transition: all ease 0.2s; - height: 32px; - display: flex; - align-items: center; - justify-content: center; + transition: all 0.2s ease; white-space: nowrap; &:hover:not(:disabled) { @@ -170,12 +141,8 @@ $hero-height: 300px; border-color: rgba(255, 255, 255, 0.15); } - &:active { - opacity: 0.9; - } - &:disabled { - background: rgba(255, 255, 255, 0.1); + background-color: rgba(255, 255, 255, 0.1); cursor: not-allowed; color: rgba(255, 255, 255, 0.5); } @@ -918,111 +885,87 @@ $hero-height: 300px; } &__review-input-container { - width: 100%; - position: relative; + display: flex; + flex-direction: column; + border: 1px solid #3a3a3a; + border-radius: 8px; + background-color: #1e1e1e; + overflow: hidden; } - &__review-input-bottom { - position: absolute; - bottom: 8px; - left: 8px; - right: 8px; + &__review-input-header { display: flex; justify-content: space-between; align-items: center; - gap: 8px; - z-index: 10; + padding: 8px 12px; + background-color: #2a2a2a; + border-bottom: 1px solid #3a3a3a; } &__review-editor-toolbar { display: flex; gap: 4px; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 4px; - padding: 4px; - backdrop-filter: blur(10px); - background: rgba(0, 0, 0, 0.3); } &__editor-button { - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 3px; - color: globals.$body-color; - padding: 4px 6px; + background: none; + border: 1px solid #4a4a4a; + border-radius: 4px; + color: #ffffff; + padding: 4px 8px; cursor: pointer; font-size: 12px; - font-weight: 600; transition: all 0.2s ease; - min-width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - &:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.1); - border-color: rgba(255, 255, 255, 0.2); + &:hover { + background-color: #3a3a3a; + border-color: #5a5a5a; + } + + &.is-active { + background-color: #0078d4; + border-color: #0078d4; } &:disabled { opacity: 0.5; cursor: not-allowed; } + } - &.is-active { - background: globals.$brand-teal; - border-color: globals.$brand-teal; - color: white; + &__review-char-counter { + font-size: 12px; + color: #888888; + + .over-limit { + color: #ff6b6b; } } &__review-input { - width: 100%; - background-color: rgba(255, 255, 255, 0.05); - border: 1px solid globals.$border-color; - border-radius: 6px; - padding: calc(globals.$spacing-unit * 1.5); - padding-bottom: calc(globals.$spacing-unit * 3.5); - color: globals.$body-color; - font-size: globals.$body-font-size; - font-family: inherit; - line-height: 1.5; - min-height: 100px; - transition: - border-color 0.2s ease, - background-color 0.2s ease; - - &:focus-within { - outline: none; - background-color: rgba(255, 255, 255, 0.08); - border-color: globals.$brand-teal; - } - - &:hover { - border-color: rgba(255, 255, 255, 0.15); - } - + min-height: 120px; + padding: 12px; + .ProseMirror { outline: none; - min-height: 80px; - - &:empty:before { - content: attr(data-placeholder); - color: rgba(208, 209, 215, 0.6); - pointer-events: none; + color: #ffffff; + font-size: 14px; + line-height: 1.5; + + &:focus { + outline: none; } p { margin: 0 0 8px 0; - + &:last-child { margin-bottom: 0; } } strong { - font-weight: 700; + font-weight: bold; } em { diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index ac8a1615..e23120a8 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -69,14 +69,32 @@ export function HeroPanelActions() { updateGame(); }; - window.addEventListener("hydra:game-favorite-toggled", onFavoriteToggled as EventListener); - window.addEventListener("hydra:game-removed-from-library", onGameRemoved as EventListener); - window.addEventListener("hydra:game-files-removed", onFilesRemoved as EventListener); + window.addEventListener( + "hydra:game-favorite-toggled", + onFavoriteToggled as EventListener + ); + window.addEventListener( + "hydra:game-removed-from-library", + onGameRemoved as EventListener + ); + window.addEventListener( + "hydra:game-files-removed", + onFilesRemoved as EventListener + ); return () => { - window.removeEventListener("hydra:game-favorite-toggled", onFavoriteToggled as EventListener); - window.removeEventListener("hydra:game-removed-from-library", onGameRemoved as EventListener); - window.removeEventListener("hydra:game-files-removed", onFilesRemoved as EventListener); + window.removeEventListener( + "hydra:game-favorite-toggled", + onFavoriteToggled as EventListener + ); + window.removeEventListener( + "hydra:game-removed-from-library", + onGameRemoved as EventListener + ); + window.removeEventListener( + "hydra:game-files-removed", + onFilesRemoved as EventListener + ); }; }, [updateLibrary, updateGame]); @@ -226,7 +244,7 @@ export function HeroPanelActions() { onClick={() => setShowRepacksModal(true)} theme="outline" disabled={isGameDownloading} - className={`hero-panel-actions__action ${!repacks.length ? 'hero-panel-actions__action--disabled' : ''}`} + className={`hero-panel-actions__action ${!repacks.length ? "hero-panel-actions__action--disabled" : ""}`} > {t("download")} diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index ec7dc3f8..97b8b1b5 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -277,4 +277,4 @@ export function RepacksModal({ ); -} \ No newline at end of file +} From fab02c4d16daafffba34bf006777d1f5a2fc3f1a Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 3 Oct 2025 02:55:41 +0300 Subject: [PATCH 08/52] Fix: Format-check fail and translations. Feat: added animations to upvote and downvote buttons --- src/locales/en/translation.json | 6 +- .../game-details/game-details-content.tsx | 68 ++++++++++++++++--- .../src/pages/game-details/game-details.scss | 8 +-- .../modals/delete-review-modal.scss | 1 + .../modals/delete-review-modal.tsx | 4 +- 5 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 22c54234..22bb9380 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -213,7 +213,6 @@ "leave_a_review": "Leave a Review", "write_review_placeholder": "Share your thoughts about this game...", "sort_newest": "Newest", - "sort_by": "Sort by", "no_reviews_yet": "No reviews yet", "be_first_to_review": "Be the first to share your thoughts about this game!", "sort_oldest": "Oldest", @@ -226,7 +225,6 @@ "loading_reviews": "Loading reviews...", "loading_more_reviews": "Loading more reviews...", "load_more_reviews": "Load More Reviews", - "youve_played_for_hours": "You've played for {{hours}} hours", "would_you_recommend_this_game": "Would you like to leave a review to this game?", "yes": "Yes", "maybe_later": "Maybe Later", @@ -330,8 +328,8 @@ "delete_review": "Delete review", "delete_review_modal_title": "Are you sure you want to delete your review?", "delete_review_modal_description": "This action cannot be undone.", - "delete_review_button": "Delete", - "delete_review_karma_warning": "You will lose any karma points earned from this review." + "delete_review_modal_delete_button": "Delete", + "delete_review_modal_cancel_button": "Cancel" }, "activation": { "title": "Activate Hydra", diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index bb6c2e85..f3db5164 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -4,6 +4,7 @@ import { ThumbsUp, ThumbsDown } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; +import { motion } from "framer-motion"; import type { GameReview } from "@types"; import { HeroPanel } from "./hero"; @@ -131,10 +132,10 @@ export function GameDetailsContent() { }, handlePaste: (view, event) => { // Strip formatting from pasted content to prevent overflow issues - const text = event.clipboardData?.getData('text/plain') || ''; + const text = event.clipboardData?.getData("text/plain") || ""; const currentText = view.state.doc.textContent; const remainingChars = MAX_REVIEW_CHARS - currentText.length; - + if (text && remainingChars > 0) { event.preventDefault(); const truncatedText = text.slice(0, remainingChars); @@ -147,7 +148,7 @@ export function GameDetailsContent() { onUpdate: ({ editor }) => { const text = editor.getText(); setReviewCharCount(text.length); - + // Prevent typing beyond character limit if (text.length > MAX_REVIEW_CHARS) { const truncatedContent = text.slice(0, MAX_REVIEW_CHARS); @@ -293,7 +294,12 @@ export function GameDetailsContent() { console.log("reviewScore:", reviewScore); console.log("submittingReview:", submittingReview); - if (!objectId || !reviewHtml.trim() || submittingReview || reviewCharCount > MAX_REVIEW_CHARS) { + if ( + !objectId || + !reviewHtml.trim() || + submittingReview || + reviewCharCount > MAX_REVIEW_CHARS + ) { console.log("Early return - validation failed"); return; } @@ -584,7 +590,13 @@ export function GameDetailsContent() {
- MAX_REVIEW_CHARS ? "over-limit" : ""}> + MAX_REVIEW_CHARS + ? "over-limit" + : "" + } + > {reviewCharCount}/{MAX_REVIEW_CHARS}
@@ -619,12 +631,14 @@ export function GameDetailsContent() {
- + - +
{userDetails?.id === review.user?.id && (
From 1b5f70a075bea27415e76fa907b7204a534b0268 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 3 Oct 2025 03:01:37 +0300 Subject: [PATCH 09/52] Fix: replaced array index with review.id and marked props as read-only --- .../pages/game-details/game-details-content.tsx | 17 ++++------------- .../game-details/modals/delete-review-modal.tsx | 2 +- .../pages/game-details/review-prompt-banner.tsx | 2 +- .../pages/game-details/review-sort-options.tsx | 2 +- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index f3db5164..8a09712e 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -688,8 +688,8 @@ export function GameDetailsContent() {
)} - {reviews.map((review, index) => ( -
+ {reviews.map((review) => ( +
{review.isBlocked && !visibleBlockedReviews.has(review.id) ? (
@@ -713,24 +713,15 @@ export function GameDetailsContent() { /> )}
-
review.user?.id && navigate(`/profile/${review.user.id}`) } - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - review.user?.id && - navigate(`/profile/${review.user.id}`); - } - }} - role="button" - tabIndex={0} > {review.user?.displayName || "Anonymous"} -
+
{formatDistance( diff --git a/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx b/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx index 2ed352c5..fe612bbd 100644 --- a/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/delete-review-modal.tsx @@ -12,7 +12,7 @@ export function DeleteReviewModal({ visible, onClose, onConfirm, -}: DeleteReviewModalProps) { +}: Readonly) { const { t } = useTranslation("game_details"); const handleDeleteReview = () => { diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.tsx b/src/renderer/src/pages/game-details/review-prompt-banner.tsx index 7bd96613..aeddaaad 100644 --- a/src/renderer/src/pages/game-details/review-prompt-banner.tsx +++ b/src/renderer/src/pages/game-details/review-prompt-banner.tsx @@ -10,7 +10,7 @@ interface ReviewPromptBannerProps { export function ReviewPromptBanner({ onYesClick, onLaterClick, -}: ReviewPromptBannerProps) { +}: Readonly) { const { t } = useTranslation("game_details"); return ( diff --git a/src/renderer/src/pages/game-details/review-sort-options.tsx b/src/renderer/src/pages/game-details/review-sort-options.tsx index ca11d056..75ec0f39 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.tsx +++ b/src/renderer/src/pages/game-details/review-sort-options.tsx @@ -21,7 +21,7 @@ interface ReviewSortOptionsProps { export function ReviewSortOptions({ sortBy, onSortChange, -}: ReviewSortOptionsProps) { +}: Readonly) { const { t } = useTranslation("game_details"); const handleDateToggle = () => { From 899f68318fd6fc8bb60315fe1a3e58f136b4e544 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 3 Oct 2025 03:06:37 +0300 Subject: [PATCH 10/52] Fix: multiple imports, ambigious spacing and unexpected negated condition --- src/renderer/src/pages/game-details/game-details-content.tsx | 2 +- .../src/pages/game-details/hero/hero-panel-actions.tsx | 2 +- .../src/pages/profile/profile-content/user-karma-box.tsx | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 8a09712e..a66971b6 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -693,7 +693,7 @@ export function GameDetailsContent() { {review.isBlocked && !visibleBlockedReviews.has(review.id) ? (
- Review from blocked user — + Review from blocked user —{" "}
@@ -618,12 +631,23 @@ export function GameDetailsContent() { {t("rating")} - setReviewScore(e.target.value ? Number(e.target.value) : null) + setReviewScore( + e.target.value ? Number(e.target.value) : null + ) } >
-
+
{review.score}/10
diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index 76b6cdcb..83758524 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -75,7 +75,9 @@ $hero-height: 300px; padding: 6px 12px; font-size: 14px; cursor: pointer; - transition: border-color 0.2s ease, background-color 0.2s ease; + transition: + border-color 0.2s ease, + background-color 0.2s ease; &:focus { outline: none; diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.scss b/src/renderer/src/pages/game-details/sidebar/sidebar.scss index b48e8a8f..1330d278 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.scss +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.scss @@ -115,7 +115,6 @@ @media (min-width: 1024px) { flex-direction: column; } - } &__category-title { From 1f7947f50f31a427ab6672eb88fb3a52391084fc Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 4 Oct 2025 20:20:48 +0300 Subject: [PATCH 19/52] fix: refactoring function, using proper attributes and extracted ternary operation --- .../game-details/game-details-content.tsx | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 9e0ea490..7cf67e4c 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -32,6 +32,45 @@ const getScoreColorClass = (score: number): string => { return ""; }; +// Helper function to process media elements for responsive display +const processMediaElements = (document: Document) => { + const $images = Array.from(document.querySelectorAll("img")); + $images.forEach(($image) => { + $image.loading = "lazy"; + // Remove any inline width/height styles that might cause overflow + $image.removeAttribute("width"); + $image.removeAttribute("height"); + $image.removeAttribute("style"); + // Set max-width to prevent overflow + $image.style.maxWidth = "100%"; + $image.style.width = "auto"; + $image.style.height = "auto"; + $image.style.boxSizing = "border-box"; + }); + + // Handle videos the same way + const $videos = Array.from(document.querySelectorAll("video")); + $videos.forEach(($video) => { + // Remove any inline width/height styles that might cause overflow + $video.removeAttribute("width"); + $video.removeAttribute("height"); + $video.removeAttribute("style"); + // Set max-width to prevent overflow + $video.style.maxWidth = "100%"; + $video.style.width = "auto"; + $video.style.height = "auto"; + $video.style.boxSizing = "border-box"; + }); +}; + +// Helper function to get score color class for select element +const getSelectScoreColorClass = (score: number): string => { + if (score >= 0 && score <= 3) return "game-details__review-score-select--red"; + if (score >= 4 && score <= 7) return "game-details__review-score-select--yellow"; + if (score >= 8 && score <= 10) return "game-details__review-score-select--green"; + return ""; +}; + export function GameDetailsContent() { const heroRef = useRef(null); const navigate = useNavigate(); @@ -64,33 +103,7 @@ export function GameDetailsContent() { "text/html" ); - const $images = Array.from(document.querySelectorAll("img")); - $images.forEach(($image) => { - $image.loading = "lazy"; - // Remove any inline width/height styles that might cause overflow - $image.removeAttribute("width"); - $image.removeAttribute("height"); - $image.removeAttribute("style"); - // Set max-width to prevent overflow - $image.style.maxWidth = "100%"; - $image.style.width = "auto"; - $image.style.height = "auto"; - $image.style.boxSizing = "border-box"; - }); - - // Handle videos the same way - const $videos = Array.from(document.querySelectorAll("video")); - $videos.forEach(($video) => { - // Remove any inline width/height styles that might cause overflow - $video.removeAttribute("width"); - $video.removeAttribute("height"); - $video.removeAttribute("style"); - // Set max-width to prevent overflow - $video.style.maxWidth = "100%"; - $video.style.width = "auto"; - $video.style.height = "auto"; - $video.style.boxSizing = "border-box"; - }); + processMediaElements(document); return document.body.outerHTML; } @@ -619,14 +632,11 @@ export function GameDetailsContent() { className="game-details__review-input" onClick={() => editor?.commands.focus()} onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); editor?.commands.focus(); } }} - role="textbox" - tabIndex={0} - aria-label={t("write_review_placeholder")} >
@@ -639,13 +649,7 @@ export function GameDetailsContent() { From 8653e62dce475d1f91aeff2e3820e83f9e83a8eb Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 4 Oct 2025 20:25:28 +0300 Subject: [PATCH 21/52] fix: using proper input type instead of the button role --- .../pages/game-details/game-details-content.tsx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 13a41121..9da59d4c 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -630,19 +630,7 @@ export function GameDetailsContent() {
-
editor?.commands.focus()} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - editor?.commands.focus(); - } - }} - role="button" - tabIndex={0} - aria-label="Click to focus review editor" - > +
From 6667e00c9104737434361398ff4cf103be53b881 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 5 Oct 2025 20:32:41 +0300 Subject: [PATCH 22/52] Feat: added rating showing in game card in categories, fixed maybe later button, changed empty state, fixed copy issue, added karma showing, added remove review text, added empty state for games with no reviews, fixed sorting buttons, fixed shift in the page --- src/locales/en/translation.json | 18 +- src/locales/ru/translation.json | 50 +++- .../src/components/game-card/game-card.scss | 7 +- .../src/components/game-card/game-card.tsx | 17 +- src/renderer/src/components/index.ts | 1 + .../src/components/star-rating/index.ts | 1 + .../components/star-rating/star-rating.scss | 54 +++++ .../components/star-rating/star-rating.tsx | 64 +++++ .../game-details/game-details-content.tsx | 220 ++++++++++-------- .../src/pages/game-details/game-details.scss | 131 ++++++++--- .../game-details/review-prompt-banner.scss | 2 +- .../game-details/review-prompt-banner.tsx | 2 +- .../game-details/review-sort-options.tsx | 4 +- .../pages/game-details/sidebar/sidebar.tsx | 23 +- .../profile-content/user-karma-box.tsx | 11 +- src/types/index.ts | 1 + 16 files changed, 448 insertions(+), 158 deletions(-) create mode 100644 src/renderer/src/components/star-rating/index.ts create mode 100644 src/renderer/src/components/star-rating/star-rating.scss create mode 100644 src/renderer/src/components/star-rating/star-rating.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b54fe2fb..1af953fe 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -222,12 +222,22 @@ "sort_most_voted": "Most Voted", "rating": "Rating", "rating_stats": "Rating", - "select_rating": "Select Rating", + "rating_very_negative": "Very Negative", + "rating_negative": "Negative", + "rating_neutral": "Neutral", + "rating_positive": "Positive", + "rating_very_positive": "Very Positive", "submit_review": "Submit Review", "submitting": "Submitting...", + "review_submitted_successfully": "Review submitted successfully!", + "review_submission_failed": "Failed to submit review. Please try again.", + "review_cannot_be_empty": "Review text field cannot be empty.", + "review_deleted_successfully": "Review deleted successfully.", + "review_deletion_failed": "Failed to delete review. Please try again.", "loading_reviews": "Loading reviews...", "loading_more_reviews": "Loading more reviews...", "load_more_reviews": "Load More Reviews", + "you_seemed_to_enjoy_this_game": "You've seemed to enjoy this game", "would_you_recommend_this_game": "Would you like to leave a review to this game?", "yes": "Yes", "maybe_later": "Maybe Later", @@ -329,6 +339,7 @@ "filter_by_source": "Filter by source", "no_repacks_found": "No sources found for this game", "delete_review": "Delete review", + "remove_review": "Remove Review", "delete_review_modal_title": "Are you sure you want to delete your review?", "delete_review_modal_description": "This action cannot be undone.", "delete_review_modal_delete_button": "Delete", @@ -548,7 +559,8 @@ "game_card": { "available_one": "Available", "available_other": "Available", - "no_downloads": "No downloads available" + "no_downloads": "No downloads available", + "calculating": "Calculating" }, "binary_not_found_modal": { "title": "Programs not installed", @@ -654,7 +666,7 @@ "game_added_to_pinned": "Game added to pinned", "karma": "Karma", "karma_count": "karma", - "karma_description": "Earned from positive likes on your reviews" + "karma_description": "Earned from positive likes on reviews" }, "achievement": { "achievement_unlocked": "Achievement unlocked", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 8992a4a0..45177eaf 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -189,10 +189,14 @@ "refuse_nsfw_content": "Назад", "stats": "Статистика", "player_count": "Активные игроки", + "rating_count": "Рейтинг", "warning": "Внимание:", "hydra_needs_to_remain_open": "Для этой загрузки Hydra должна оставаться открытой до завершения. Если Hydra закроется до завершения, вы потеряете прогресс.", "achievements": "Достижения", "achievements_count": "Достижения {{unlockedCount}}/{{achievementsCount}}", + "show_more": "Показать больше", + "reviews": "Отзывы", + "leave_a_review": "Оставить отзыв", "cloud_save": "Облачное сохранение", "cloud_save_description": "Сохраняйте ваш прогресс в облаке и продолжайте играть на любом устройстве", "backups": "Резервные копии", @@ -271,7 +275,41 @@ "backup_unfrozen": "Резервная копия откреплена", "backup_freeze_failed": "Не удалось закрепить резервную копию", "backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий", - "manual_playtime_tooltip": "Это время игры было обновлено вручную" + "manual_playtime_tooltip": "Это время игры было обновлено вручную", + "write_review_placeholder": "Поделитесь своими мыслями об этой игре...", + "sort_newest": "Новые", + "no_reviews_yet": "Пока нет отзывов", + "be_first_to_review": "Будьте первым, кто поделится своими мыслями об этой игре!", + "sort_oldest": "Старые", + "sort_highest_score": "Высший балл", + "sort_lowest_score": "Низший балл", + "sort_most_voted": "Самые популярные", + "rating": "Рейтинг", + "rating_stats": "Рейтинг", + "rating_very_negative": "Очень негативный", + "rating_negative": "Негативный", + "rating_neutral": "Нейтральный", + "rating_positive": "Позитивный", + "rating_very_positive": "Очень позитивный", + "submit_review": "Отправить отзыв", + "submitting": "Отправка...", + "remove_review": "Удалить отзыв", + "delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?", + "delete_review_modal_description": "Это действие нельзя отменить.", + "delete_review_modal_delete_button": "Удалить", + "delete_review_modal_cancel_button": "Отмена", + "review_submitted_successfully": "Отзыв успешно отправлен!", + "review_submission_failed": "Не удалось отправить отзыв. Попробуйте еще раз.", + "review_cannot_be_empty": "Поле отзыва не может быть пустым.", + "review_deleted_successfully": "Отзыв успешно удален.", + "review_deletion_failed": "Не удалось удалить отзыв. Попробуйте еще раз.", + "loading_reviews": "Загрузка отзывов...", + "loading_more_reviews": "Загрузка дополнительных отзывов...", + "load_more_reviews": "Загрузить больше отзывов", + "you_seemed_to_enjoy_this_game": "Похоже, вам понравилась эта игра", + "would_you_recommend_this_game": "Хотели бы вы оставить отзыв об этой игре?", + "yes": "Да", + "maybe_later": "Может быть позже" }, "activation": { "title": "Активировать Hydra", @@ -475,7 +513,8 @@ "game_card": { "available_one": "Доступный", "available_other": "Доступный", - "no_downloads": "Нет доступных источников" + "no_downloads": "Нет доступных источников", + "calculating": "Вычисление" }, "binary_not_found_modal": { "title": "Программы не установлены", @@ -572,7 +611,12 @@ "show_achievements_on_profile": "Покажите свои достижения в профиле", "show_points_on_profile": "Показывать заработанные очки в своем профиле", "error_adding_friend": "Не удалось отправить запрос в друзья. Пожалуйста, проверьте код друга", - "friend_code_length_error": "Код друга должен содержать 8 символов" + "friend_code_length_error": "Код друга должен содержать 8 символов", + "game_removed_from_pinned": "Игра удалена из закрепленных", + "game_added_to_pinned": "Игра добавлена в закрепленные", + "karma": "Карма", + "karma_count": "карма", + "karma_description": "Заработано от положительных лайков на отзывах" }, "achievement": { "achievement_unlocked": "Достижение разблокировано", diff --git a/src/renderer/src/components/game-card/game-card.scss b/src/renderer/src/components/game-card/game-card.scss index ee4a22b1..99aa866e 100644 --- a/src/renderer/src/components/game-card/game-card.scss +++ b/src/renderer/src/components/game-card/game-card.scss @@ -72,7 +72,12 @@ display: flex; color: globals.$muted-color; font-size: 12px; - align-items: flex-end; + align-items: center; + + // Ensure star rating is properly aligned + .star-rating { + align-items: center; + } } &__title-container { diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index 15b5439b..1aa58ba7 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -1,4 +1,4 @@ -import { DownloadIcon, PeopleIcon, StarIcon } from "@primer/octicons-react"; +import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; import type { GameStats } from "@types"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; @@ -7,6 +7,7 @@ import "./game-card.scss"; import { useTranslation } from "react-i18next"; import { Badge } from "../badge/badge"; +import { StarRating } from "../star-rating/star-rating"; import { useCallback, useState, useMemo } from "react"; import { useFormat, useRepacks } from "@renderer/hooks"; @@ -107,12 +108,14 @@ export function GameCard({ game, ...props }: GameCardProps) { {stats ? numberFormatter.format(stats.playerCount) : "…"}
- {stats?.averageScore && ( -
- - {stats.averageScore.toFixed(1)} -
- )} +
+ +
diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index 9970be42..89dccdbc 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -18,3 +18,4 @@ export * from "./debrid-badge/debrid-badge"; export * from "./context-menu/context-menu"; export * from "./game-context-menu/game-context-menu"; export * from "./game-context-menu/use-game-actions"; +export * from "./star-rating/star-rating"; diff --git a/src/renderer/src/components/star-rating/index.ts b/src/renderer/src/components/star-rating/index.ts new file mode 100644 index 00000000..0f153ca4 --- /dev/null +++ b/src/renderer/src/components/star-rating/index.ts @@ -0,0 +1 @@ +export * from "./star-rating"; \ No newline at end of file diff --git a/src/renderer/src/components/star-rating/star-rating.scss b/src/renderer/src/components/star-rating/star-rating.scss new file mode 100644 index 00000000..4fa7ba2a --- /dev/null +++ b/src/renderer/src/components/star-rating/star-rating.scss @@ -0,0 +1,54 @@ +@use "../../scss/globals.scss"; + +.star-rating { + display: flex; + align-items: center; + gap: 2px; + + &__star { + color: globals.$muted-color; + transition: color ease 0.2s; + + &--filled { + color: #ffffff; + } + + &--empty { + color: globals.$muted-color; + } + + &--half { + color: #ffffff; + position: absolute; + top: 0; + left: 0; + clip-path: polygon(0 0, 50% 0, 50% 100%, 0 100%); + } + } + + &__half-star { + position: relative; + display: inline-block; + } + + &__value { + margin-left: 4px; + font-size: 12px; + color: globals.$muted-color; + font-weight: 500; + } + + &__calculating-text, + &__no-rating-text { + margin-left: 4px; + font-size: 12px; + color: globals.$muted-color; + } + + &--calculating, + &--no-rating { + .star-rating__star { + color: globals.$muted-color; + } + } +} \ No newline at end of file diff --git a/src/renderer/src/components/star-rating/star-rating.tsx b/src/renderer/src/components/star-rating/star-rating.tsx new file mode 100644 index 00000000..5aa2a5ee --- /dev/null +++ b/src/renderer/src/components/star-rating/star-rating.tsx @@ -0,0 +1,64 @@ +import { StarIcon, StarFillIcon } from "@primer/octicons-react"; +import "./star-rating.scss"; + +export interface StarRatingProps { + rating: number | null; + maxStars?: number; + size?: number; + showCalculating?: boolean; + calculatingText?: string; +} + +export function StarRating({ + rating, + maxStars = 5, + size = 12, + showCalculating = false, + calculatingText = "Calculating" +}: StarRatingProps) { + if (rating === null && showCalculating) { + return ( +
+ + {calculatingText} +
+ ); + } + + if (rating === null || rating === undefined) { + return ( +
+ + +
+ ); + } + + const filledStars = Math.floor(rating); + const hasHalfStar = rating % 1 >= 0.5; + const emptyStars = maxStars - filledStars - (hasHalfStar ? 1 : 0); + + return ( +
+ + {Array.from({ length: filledStars }, (_, index) => ( + + ))} + + + {hasHalfStar && ( +
+ + +
+ )} + + + {Array.from({ length: emptyStars }, (_, index) => ( + + ))} + + {rating.toFixed(1)} +
+ ); +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 9da59d4c..2b6de1d8 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -1,6 +1,6 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { PencilIcon, TrashIcon, ClockIcon } from "@primer/octicons-react"; -import { ThumbsUp, ThumbsDown } from "lucide-react"; +import { PencilIcon, TrashIcon, ClockIcon, NoteIcon } from "@primer/octicons-react"; +import { ThumbsUp, ThumbsDown, Star } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; @@ -20,28 +20,24 @@ import { useTranslation } from "react-i18next"; import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif"; -import { useUserDetails, useLibrary, useDate } from "@renderer/hooks"; +import { useUserDetails, useLibrary, useDate, useToast } from "@renderer/hooks"; import { useSubscription } from "@renderer/hooks/use-subscription"; import "./game-details.scss"; -// Helper function to get score color class const getScoreColorClass = (score: number): string => { - if (score >= 0 && score <= 3) return "game-details__review-score--red"; - if (score >= 4 && score <= 6) return "game-details__review-score--yellow"; - if (score >= 7 && score <= 10) return "game-details__review-score--green"; + if (score >= 1 && score <= 2) return "game-details__review-score--red"; + if (score >= 3 && score <= 3) return "game-details__review-score--yellow"; + if (score >= 4 && score <= 5) return "game-details__review-score--green"; return ""; }; -// Helper function to process media elements for responsive display const processMediaElements = (document: Document) => { const $images = Array.from(document.querySelectorAll("img")); $images.forEach(($image) => { $image.loading = "lazy"; - // Remove any inline width/height styles that might cause overflow $image.removeAttribute("width"); $image.removeAttribute("height"); $image.removeAttribute("style"); - // Set max-width to prevent overflow $image.style.maxWidth = "100%"; $image.style.width = "auto"; $image.style.height = "auto"; @@ -51,11 +47,9 @@ const processMediaElements = (document: Document) => { // Handle videos the same way const $videos = Array.from(document.querySelectorAll("video")); $videos.forEach(($video) => { - // Remove any inline width/height styles that might cause overflow $video.removeAttribute("width"); $video.removeAttribute("height"); $video.removeAttribute("style"); - // Set max-width to prevent overflow $video.style.maxWidth = "100%"; $video.style.width = "auto"; $video.style.height = "auto"; @@ -63,16 +57,26 @@ const processMediaElements = (document: Document) => { }); }; -// Helper function to get score color class for select element const getSelectScoreColorClass = (score: number): string => { - if (score >= 0 && score <= 3) return "game-details__review-score-select--red"; - if (score >= 4 && score <= 7) + if (score >= 1 && score <= 2) return "game-details__review-score-select--red"; + if (score >= 3 && score <= 3) return "game-details__review-score-select--yellow"; - if (score >= 8 && score <= 10) + if (score >= 4 && score <= 5) return "game-details__review-score-select--green"; return ""; }; +const getRatingText = (score: number, t: (key: string) => string): string => { + switch (score) { + case 1: return t("rating_very_negative"); + case 2: return t("rating_negative"); + case 3: return t("rating_neutral"); + case 4: return t("rating_positive"); + case 5: return t("rating_very_positive"); + default: return ""; + } +}; + export function GameDetailsContent() { const heroRef = useRef(null); const navigate = useNavigate(); @@ -93,6 +97,7 @@ export function GameDetailsContent() { const { userDetails, hasActiveSubscription } = useUserDetails(); const { updateLibrary } = useLibrary(); const { formatDistance } = useDate(); + const { showSuccessToast, showErrorToast } = useToast(); const { setShowCloudSyncModal, getGameArtifacts } = useContext(cloudSyncContext); @@ -139,16 +144,13 @@ export function GameDetailsContent() { const [totalReviewCount, setTotalReviewCount] = useState(0); const [showReviewForm, setShowReviewForm] = useState(false); - // Review prompt banner state const [showReviewPrompt, setShowReviewPrompt] = useState(false); const [hasUserReviewed, setHasUserReviewed] = useState(false); const [reviewCheckLoading, setReviewCheckLoading] = useState(false); - // Tiptap editor for review input const editor = useEditor({ extensions: [ StarterKit.configure({ - // Disable link extension to prevent automatic link rendering and XSS link: false, }), ], @@ -159,14 +161,26 @@ export function GameDetailsContent() { "data-placeholder": t("write_review_placeholder"), }, handlePaste: (view, event) => { - // Strip formatting from pasted content to prevent overflow issues - const text = event.clipboardData?.getData("text/plain") || ""; + const htmlContent = event.clipboardData?.getData("text/html") || ""; + const plainText = event.clipboardData?.getData("text/plain") || ""; + const currentText = view.state.doc.textContent; const remainingChars = MAX_REVIEW_CHARS - currentText.length; - if (text && remainingChars > 0) { + if ((htmlContent || plainText) && remainingChars > 0) { event.preventDefault(); - const truncatedText = text.slice(0, remainingChars); + + if (htmlContent) { + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlContent; + const textLength = tempDiv.textContent?.length || 0; + + if (textLength <= remainingChars) { + return false; + } + } + + const truncatedText = plainText.slice(0, remainingChars); view.dispatch(view.state.tr.insertText(truncatedText)); return true; } @@ -177,7 +191,6 @@ export function GameDetailsContent() { const text = editor.getText(); setReviewCharCount(text.length); - // Prevent typing beyond character limit if (text.length > MAX_REVIEW_CHARS) { const truncatedContent = text.slice(0, MAX_REVIEW_CHARS); editor.commands.setContent(truncatedContent); @@ -219,7 +232,6 @@ export function GameDetailsContent() { const isCustomGame = game?.shop === "custom"; - // Reviews functions const checkUserReview = async () => { if (!objectId || !userDetails) return; @@ -229,11 +241,9 @@ export function GameDetailsContent() { const hasReviewed = (response as any)?.hasReviewed || false; setHasUserReviewed(hasReviewed); - // Show prompt only if user hasn't reviewed and has played the game if ( !hasReviewed && - game?.playTimeInMilliseconds && - game.playTimeInMilliseconds > 0 + !sessionStorage.getItem(`reviewPromptDismissed_${objectId}`) ) { setShowReviewPrompt(true); } @@ -258,7 +268,6 @@ export function GameDetailsContent() { reviewsSortBy ); - // Handle the response structure: { totalCount: number, reviews: Review[] } const reviewsData = (response as any)?.reviews || []; const reviewCount = (response as any)?.totalCount || 0; @@ -286,7 +295,6 @@ export function GameDetailsContent() { try { await window.electron.voteReview(shop, objectId, reviewId, voteType); - // Reload reviews to get updated vote counts loadReviews(true); } catch (error) { console.error(`Failed to ${voteType} review:`, error); @@ -303,40 +311,40 @@ export function GameDetailsContent() { try { await window.electron.deleteReview(shop, objectId, reviewToDelete); - // Reload reviews after deletion loadReviews(true); setShowDeleteReviewModal(false); setReviewToDelete(null); + showSuccessToast(t("review_deleted_successfully")); } catch (error) { console.error("Failed to delete review:", error); + showErrorToast(t("review_deletion_failed")); } }; const handleSubmitReview = async () => { - console.log("handleSubmitReview called"); - console.log("game:", game); - console.log("objectId:", objectId); - const reviewHtml = editor?.getHTML() || ""; - console.log("reviewHtml:", reviewHtml); - console.log("reviewScore:", reviewScore); - console.log("submittingReview:", submittingReview); + const reviewText = editor?.getText() || ""; - if ( - !objectId || - !reviewHtml.trim() || - reviewScore === null || - submittingReview || - reviewCharCount > MAX_REVIEW_CHARS - ) { - console.log("Early return - validation failed"); + if (!objectId) { + return; + } + + if (!reviewText.trim()) { + showErrorToast(t("review_cannot_be_empty")); + return; + } + + if (submittingReview || reviewCharCount > MAX_REVIEW_CHARS) { + return; + } + + if (reviewScore === null) { return; } - console.log("Starting review submission..."); setSubmittingReview(true); + try { - console.log("Calling window.electron.createGameReview..."); await window.electron.createGameReview( shop, objectId, @@ -344,27 +352,25 @@ export function GameDetailsContent() { reviewScore ); - console.log("Review submitted successfully"); editor?.commands.clearContent(); setReviewScore(null); - await loadReviews(true); // Reload reviews after submission - setShowReviewForm(false); // Hide the review form after successful submission - setShowReviewPrompt(false); // Hide the prompt banner - setHasUserReviewed(true); // Update the review status + showSuccessToast(t("review_submitted_successfully")); + + await loadReviews(true); + setShowReviewForm(false); + setShowReviewPrompt(false); + setHasUserReviewed(true); } catch (error) { - console.error("Failed to submit review:", error); + showErrorToast(t("review_submission_failed")); } finally { setSubmittingReview(false); - console.log("Review submission completed"); } }; - // Review prompt banner handlers const handleReviewPromptYes = () => { setShowReviewPrompt(false); setShowReviewForm(true); - // Scroll to review form setTimeout(() => { const reviewFormElement = document.querySelector( ".game-details__review-form" @@ -380,13 +386,18 @@ export function GameDetailsContent() { const handleReviewPromptLater = () => { setShowReviewPrompt(false); + if (objectId) { + sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, 'true'); + } }; const handleSortChange = (newSortBy: string) => { - setReviewsSortBy(newSortBy); - setReviewsPage(0); - setHasMoreReviews(true); - loadReviews(true); + if (newSortBy !== reviewsSortBy) { + setReviewsSortBy(newSortBy); + setReviewsPage(0); + setHasMoreReviews(true); + loadReviews(true); + } }; const toggleBlockedReview = (reviewId: string) => { @@ -408,22 +419,19 @@ export function GameDetailsContent() { } }; - // Load reviews when component mounts or sort changes useEffect(() => { if (objectId && (game || shop)) { loadReviews(true); - checkUserReview(); // Check if user has reviewed this game + checkUserReview(); } }, [game, shop, objectId, reviewsSortBy, userDetails]); - // Load more reviews when page changes useEffect(() => { if (reviewsPage > 0) { loadReviews(false); } }, [reviewsPage]); - // Helper function to get image with custom asset priority const getImageWithCustomPriority = ( customUrl: string | null | undefined, originalUrl: string | null | undefined, @@ -540,7 +548,6 @@ export function GameDetailsContent() { {game?.shop !== "custom" && showReviewPrompt && userDetails && - game?.playTimeInMilliseconds && !hasUserReviewed && !reviewCheckLoading && (
- - +
+ {[1, 2, 3, 4, 5].map((starValue) => ( + + ))} +
@@ -716,7 +718,9 @@ export function GameDetailsContent() { {!reviewsLoading && reviews.length === 0 && (
-
📝
+
+ +

{t("no_reviews_yet")}

@@ -770,10 +774,23 @@ export function GameDetailsContent() {
-
- {review.score}/10 +
+ {[1, 2, 3, 4, 5].map((starValue) => ( + + ))}
+ {t("remove_review")} )} {review.isBlocked && diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index 83758524..b0726655 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -55,10 +55,31 @@ $hero-height: 300px; flex-wrap: wrap; } + &__review-message { + padding: calc(globals.$spacing-unit * 1); + border-radius: 4px; + font-size: globals.$small-font-size; + font-weight: 500; + margin-top: calc(globals.$spacing-unit * 1); + border: 1px solid; + + &--success { + background: rgba(34, 197, 94, 0.1); + color: #86efac; + border-color: rgba(34, 197, 94, 0.3); + } + + &--error { + background: rgba(239, 68, 68, 0.1); + color: #fca5a5; + border-color: rgba(239, 68, 68, 0.3); + } + } + &__review-score-container { display: flex; align-items: center; - gap: 8px; + gap: 4px; } &__review-score-label { @@ -104,6 +125,59 @@ $hero-height: 300px; } } + &__star-rating { + display: flex; + align-items: center; + gap: 4px; + } + + &__star { + background: none; + border: none; + color: #666666; + cursor: pointer; + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + + &:hover { + color: #ffffff; + background-color: rgba(255, 255, 255, 0.1); + transform: scale(1.1); + } + + &--filled { + color: #ffffff; + + &.game-details__review-score-select--red { + color: #e74c3c; + } + + &.game-details__review-score-select--yellow { + color: #f39c12; + } + + &.game-details__review-score-select--green { + color: #27ae60; + } + } + + &--empty { + color: #666666; + + &:hover { + color: #ffffff; + } + } + + svg { + fill: currentColor; + } + } + &__reviews-sort { display: flex; flex-direction: column; @@ -191,16 +265,13 @@ $hero-height: 300px; &__reviews-empty { text-align: center; padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 2); - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 8px; margin-bottom: calc(globals.$spacing-unit * 2); } &__reviews-empty-icon { font-size: 48px; margin-bottom: calc(globals.$spacing-unit * 2); - opacity: 0.6; + color: rgba(255, 255, 255, 0.6); } &__reviews-empty-title { @@ -341,6 +412,7 @@ $hero-height: 300px; color: #f44336; cursor: pointer; transition: all 0.2s ease; + gap: 6px; &:hover { background: rgba(244, 67, 54, 0.2); @@ -387,32 +459,39 @@ $hero-height: 300px; } } - &__review-score { - background: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.9); - padding: calc(globals.$spacing-unit * 0.5) calc(globals.$spacing-unit * 1); - border-radius: 4px; - font-size: globals.$small-font-size; - font-weight: 600; - border: 1px solid rgba(255, 255, 255, 0.15); + &__review-score-stars { + display: flex; + align-items: center; + gap: 2px; + } - // Color variants based on score - &--red { - background: rgba(239, 68, 68, 0.2); - color: #fca5a5; - border-color: rgba(239, 68, 68, 0.4); + &__review-star { + color: #666666; + transition: color 0.2s ease; + cursor: default; + + &--filled { + color: #ffffff; + + &.game-details__review-score--red { + color: #fca5a5; + } + + &.game-details__review-score--yellow { + color: #fcd34d; + } + + &.game-details__review-score--green { + color: #86efac; + } } - &--yellow { - background: rgba(245, 158, 11, 0.2); - color: #fcd34d; - border-color: rgba(245, 158, 11, 0.4); + &--empty { + color: #666666; } - &--green { - background: rgba(34, 197, 94, 0.2); - color: #86efac; - border-color: rgba(34, 197, 94, 0.4); + svg { + fill: currentColor; } } diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.scss b/src/renderer/src/pages/game-details/review-prompt-banner.scss index b8f7557b..f9358e52 100644 --- a/src/renderer/src/pages/game-details/review-prompt-banner.scss +++ b/src/renderer/src/pages/game-details/review-prompt-banner.scss @@ -4,7 +4,7 @@ background: rgba(255, 255, 255, 0.02); border-radius: 8px; padding: calc(globals.$spacing-unit * 2); - margin-bottom: calc(globals.$spacing-unit * 3); + margin-bottom: calc(globals.$spacing-unit * 1.5); border: 1px solid rgba(255, 255, 255, 0.05); &__content { diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.tsx b/src/renderer/src/pages/game-details/review-prompt-banner.tsx index aeddaaad..01fdd075 100644 --- a/src/renderer/src/pages/game-details/review-prompt-banner.tsx +++ b/src/renderer/src/pages/game-details/review-prompt-banner.tsx @@ -18,7 +18,7 @@ export function ReviewPromptBanner({
- You've seemed to enjoy this game + {t("you_seemed_to_enjoy_this_game")} {t("would_you_recommend_this_game")} diff --git a/src/renderer/src/pages/game-details/review-sort-options.tsx b/src/renderer/src/pages/game-details/review-sort-options.tsx index 75ec0f39..0944e58e 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.tsx +++ b/src/renderer/src/pages/game-details/review-sort-options.tsx @@ -35,7 +35,9 @@ export function ReviewSortOptions({ }; const handleMostVotedClick = () => { - onSortChange("most_voted"); + if (sortBy !== "most_voted") { + onSortChange("most_voted"); + } }; const isDateActive = sortBy === "newest" || sortBy === "oldest"; diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index d8aa2128..febb6a8b 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -5,7 +5,7 @@ import type { UserAchievement, } from "@types"; import { useTranslation } from "react-i18next"; -import { Button, Link } from "@renderer/components"; +import { Button, Link, StarRating } from "@renderer/components"; import { gameDetailsContext } from "@renderer/context"; import { useDate, useFormat, useUserDetails } from "@renderer/hooks"; @@ -227,15 +227,18 @@ export function Sidebar() {

{numberFormatter.format(stats?.playerCount)}

- {stats?.averageScore && ( -
-

- - {t("rating_count")} -

-

{stats.averageScore.toFixed(1)}/10

-
- )} +
+

+ + {t("rating_count")} +

+ +
)} diff --git a/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx b/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx index 8c85217d..d2232276 100644 --- a/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx @@ -6,13 +6,16 @@ import { Award } from "lucide-react"; import "./user-karma-box.scss"; export function UserKarmaBox() { - const { isMe } = useContext(userProfileContext); + const { isMe, userProfile } = useContext(userProfileContext); const { userDetails } = useUserDetails(); const { t } = useTranslation("user_profile"); const { numberFormatter } = useFormat(); - // Only show karma for the current user (me) - if (!isMe || !userDetails) return null; + // Get karma from userDetails (for current user) or userProfile (for other users) + const karma = isMe ? userDetails?.karma : userProfile?.karma; + + // Don't show if karma is not available + if (karma === undefined || karma === null) return null; return (
@@ -24,7 +27,7 @@ export function UserKarmaBox() {

- {numberFormatter.format(userDetails.karma)}{" "} + {numberFormatter.format(karma)}{" "} {t("karma_count")}

diff --git a/src/types/index.ts b/src/types/index.ts index 17ed08cc..0c6af89b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -203,6 +203,7 @@ export interface UserProfile { currentGame: UserProfileCurrentGame | null; bio: string; hasActiveSubscription: boolean; + karma: number; quirks: { backupsPerGameLimit: number; }; From 063e97e0ec8a3cdff31c7c1e6e0dc85d73cc529e Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 5 Oct 2025 20:36:20 +0300 Subject: [PATCH 23/52] Fix: marked props read-only and catch error --- src/locales/en/translation.json | 2 +- .../src/components/game-card/game-card.scss | 2 +- .../src/components/game-card/game-card.tsx | 2 +- .../src/components/star-rating/index.ts | 2 +- .../components/star-rating/star-rating.scss | 2 +- .../components/star-rating/star-rating.tsx | 39 ++++++---- .../game-details/game-details-content.tsx | 72 ++++++++++++------- .../src/pages/game-details/game-details.scss | 4 +- 8 files changed, 79 insertions(+), 46 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 1af953fe..d118b20b 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -223,7 +223,7 @@ "rating": "Rating", "rating_stats": "Rating", "rating_very_negative": "Very Negative", - "rating_negative": "Negative", + "rating_negative": "Negative", "rating_neutral": "Neutral", "rating_positive": "Positive", "rating_very_positive": "Very Positive", diff --git a/src/renderer/src/components/game-card/game-card.scss b/src/renderer/src/components/game-card/game-card.scss index 99aa866e..1830762f 100644 --- a/src/renderer/src/components/game-card/game-card.scss +++ b/src/renderer/src/components/game-card/game-card.scss @@ -73,7 +73,7 @@ color: globals.$muted-color; font-size: 12px; align-items: center; - + // Ensure star rating is properly aligned .star-rating { align-items: center; diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index 1aa58ba7..6e790500 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -109,7 +109,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
- ) { if (rating === null && showCalculating) { return (
@@ -40,25 +40,36 @@ export function StarRating({ return (
- {Array.from({ length: filledStars }, (_, index) => ( - + ))} - {hasHalfStar && (
- - + +
)} - {Array.from({ length: emptyStars }, (_, index) => ( - + ))} - + {rating.toFixed(1)}
); -} \ No newline at end of file +} diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 2b6de1d8..9060dc39 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -1,5 +1,10 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { PencilIcon, TrashIcon, ClockIcon, NoteIcon } from "@primer/octicons-react"; +import { + PencilIcon, + TrashIcon, + ClockIcon, + NoteIcon, +} from "@primer/octicons-react"; import { ThumbsUp, ThumbsDown, Star } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { useEditor, EditorContent } from "@tiptap/react"; @@ -68,12 +73,18 @@ const getSelectScoreColorClass = (score: number): string => { const getRatingText = (score: number, t: (key: string) => string): string => { switch (score) { - case 1: return t("rating_very_negative"); - case 2: return t("rating_negative"); - case 3: return t("rating_neutral"); - case 4: return t("rating_positive"); - case 5: return t("rating_very_positive"); - default: return ""; + case 1: + return t("rating_very_negative"); + case 2: + return t("rating_negative"); + case 3: + return t("rating_neutral"); + case 4: + return t("rating_positive"); + case 5: + return t("rating_very_positive"); + default: + return ""; } }; @@ -163,23 +174,23 @@ export function GameDetailsContent() { handlePaste: (view, event) => { const htmlContent = event.clipboardData?.getData("text/html") || ""; const plainText = event.clipboardData?.getData("text/plain") || ""; - + const currentText = view.state.doc.textContent; const remainingChars = MAX_REVIEW_CHARS - currentText.length; if ((htmlContent || plainText) && remainingChars > 0) { event.preventDefault(); - + if (htmlContent) { const tempDiv = document.createElement("div"); tempDiv.innerHTML = htmlContent; const textLength = tempDiv.textContent?.length || 0; - + if (textLength <= remainingChars) { - return false; + return false; } } - + const truncatedText = plainText.slice(0, remainingChars); view.dispatch(view.state.tr.insertText(truncatedText)); return true; @@ -343,7 +354,7 @@ export function GameDetailsContent() { } setSubmittingReview(true); - + try { await window.electron.createGameReview( shop, @@ -355,12 +366,13 @@ export function GameDetailsContent() { editor?.commands.clearContent(); setReviewScore(null); showSuccessToast(t("review_submitted_successfully")); - - await loadReviews(true); - setShowReviewForm(false); - setShowReviewPrompt(false); + + await loadReviews(true); + setShowReviewForm(false); + setShowReviewPrompt(false); setHasUserReviewed(true); } catch (error) { + console.error("Failed to submit review:", error); showErrorToast(t("review_submission_failed")); } finally { setSubmittingReview(false); @@ -387,7 +399,7 @@ export function GameDetailsContent() { const handleReviewPromptLater = () => { setShowReviewPrompt(false); if (objectId) { - sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, 'true'); + sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, "true"); } }; @@ -422,7 +434,7 @@ export function GameDetailsContent() { useEffect(() => { if (objectId && (game || shop)) { loadReviews(true); - checkUserReview(); + checkUserReview(); } }, [game, shop, objectId, reviewsSortBy, userDetails]); @@ -661,9 +673,13 @@ export function GameDetailsContent() { onClick={() => setReviewScore(starValue)} title={getRatingText(starValue, t)} > - ))} @@ -684,7 +700,6 @@ export function GameDetailsContent() { ? t("submitting") : t("submit_review")} -
@@ -774,12 +789,19 @@ export function GameDetailsContent() {
-
+
{[1, 2, 3, 4, 5].map((starValue) => ( Date: Sun, 5 Oct 2025 21:38:43 +0300 Subject: [PATCH 24/52] Update translation.json --- src/locales/ru/translation.json | 136 ++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 43 deletions(-) diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 8992a4a0..c316977a 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -6,8 +6,8 @@ "home": { "surprise_me": "Удиви меня", "no_results": "Ничего не найдено", - "hot": "Сейчас популярно", "start_typing": "Начинаю вводить текст...", + "hot": "Сейчас популярно", "weekly": "📅 Лучшие игры недели", "achievements": "🏆 Игры с достижениями" }, @@ -28,6 +28,8 @@ "need_help": "Нужна помощь?", "favorites": "Избранное", "playable_button_title": "Показать только установленные игры.", + "add_custom_game_tooltip": "Добавить пользовательскую игру", + "show_playable_only_tooltip": "Показать только доступные для игры", "custom_game_modal": "Добавить пользовательскую игру", "custom_game_modal_description": "Добавьте пользовательскую игру в библиотеку, выбрав исполняемый файл", "custom_game_modal_executable_path": "Путь к исполняемому файлу", @@ -135,6 +137,7 @@ "amount_minutes": "{{amount}} минут", "accuracy": "точность {{accuracy}}%", "add_to_library": "Добавить в библиотеку", + "already_in_library": "Уже в библиотеке", "remove_from_library": "Удалить из библиотеки", "no_downloads": "Нет доступных источников", "play_time": "Сыграно {{amount}}", @@ -163,11 +166,13 @@ "open_folder": "Открыть папку", "open_download_location": "Просмотреть папку загрузок", "create_shortcut": "Создать ярлык на рабочем столе", + "create_shortcut_simple": "Создать ярлык", "clear": "Очистить", "remove_files": "Удалить файлы", "remove_from_library_title": "Вы уверены?", "remove_from_library_description": "{{game}} будет удалена из вашей библиотеки.", "options": "Настройки", + "properties": "Свойства", "executable_section_title": "Файл", "executable_section_description": "Путь к файлу, который будет запущен при нажатии на \"Play\"", "downloads_section_title": "Загрузки", @@ -177,18 +182,27 @@ "download_in_progress": "Идёт загрузка", "download_paused": "Загрузка приостановлена", "last_downloaded_option": "Последний вариант загрузки", + "create_steam_shortcut": "Создать ярлык Steam", "create_shortcut_success": "Ярлык создан", + "you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения", "create_shortcut_error": "Не удалось создать ярлык", - "allow_nsfw_content": "Продолжить", - "download": "Скачать", - "download_count": "Загрузки", - "download_error": "Этот вариант загрузки недоступен", - "executable_path_in_use": "Исполняемый файл уже используется \"{{game}}\"", - "nsfw_content_description": "{{title}} содержит контент, который может не подходить для всех возрастов. \nВы уверены, что хотите продолжить?", + "add_to_favorites": "Добавить в избранное", + "remove_from_favorites": "Удалить из избранного", + "failed_update_favorites": "Не удалось обновить избранное", + "game_removed_from_library": "Игра удалена из библиотеки", + "failed_remove_from_library": "Не удалось удалить из библиотеки", + "files_removed_success": "Файлы успешно удалены", + "failed_remove_files": "Не удалось удалить файлы", "nsfw_content_title": "Эта игра содержит неприемлемый контент", + "nsfw_content_description": "{{title}} содержит контент, который может не подходить для всех возрастов. \nВы уверены, что хотите продолжить?", + "allow_nsfw_content": "Продолжить", "refuse_nsfw_content": "Назад", "stats": "Статистика", + "download_count": "Загрузки", "player_count": "Активные игроки", + "download_error": "Этот вариант загрузки недоступен", + "download": "Скачать", + "executable_path_in_use": "Исполняемый файл уже используется \"{{game}}\"", "warning": "Внимание:", "hydra_needs_to_remain_open": "Для этой загрузки Hydra должна оставаться открытой до завершения. Если Hydra закроется до завершения, вы потеряете прогресс.", "achievements": "Достижения", @@ -244,26 +258,29 @@ "update_playtime_title": "Обновить время игры", "update_playtime_description": "Вручную обновите время игры для {{game}}", "update_playtime": "Обновить время игры", + "update_playtime_success": "Время игры успешно обновлено", + "update_playtime_error": "Не удалось обновить время игры", "update_game_playtime": "Обновить время игры", + "manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную. Это действие нельзя отменить.", + "manual_playtime_tooltip": "Это время игры было обновлено вручную", "download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.", - "game_added_to_favorites": "Игра добавлена в избранное", + "download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus.", "game_removed_from_favorites": "Игра удалена из избранного", + "game_added_to_favorites": "Игра добавлена в избранное", + "game_removed_from_pinned": "Игра удалена из закрепленных", + "game_added_to_pinned": "Игра добавлена в закрепленные", "automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов", - "create_steam_shortcut": "Создать ярлык Steam", - "you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения", "create_start_menu_shortcut": "Создать ярлык в меню «Пуск»", "invalid_wine_prefix_path": "Недопустимый путь префикса Wine", "invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.", "missing_wine_prefix": "Префикс Wine необходим для создания резервной копии в Linux", - "download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus.", - "update_playtime_success": "Время игры успешно обновлено", - "update_playtime_error": "Не удалось обновить время игры", - "manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную. Это действие нельзя отменить.", "artifact_renamed": "Резервная копия успешно переименована", "rename_artifact": "Переименовать резервную копию", "rename_artifact_description": "Переименуйте резервную копию, присвоив ей более описательное имя.", "artifact_name_label": "Название резервной копии", "artifact_name_placeholder": "Введите название для резервной копии", + "save_changes": "Сохранить изменения", + "required_field": "Это поле обязательно к заполнению", "max_length_field": "Это поле должно содержать менее {{length}} символов", "freeze_backup": "Закрепить, чтобы она не была перезаписана автоматическими резервными копиями", "unfreeze_backup": "Открепить", @@ -271,7 +288,22 @@ "backup_unfrozen": "Резервная копия откреплена", "backup_freeze_failed": "Не удалось закрепить резервную копию", "backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий", - "manual_playtime_tooltip": "Это время игры было обновлено вручную" + "edit_game_modal_button": "Настроить ресурсы игры", + "game_details": "Детали игры", + "currency_symbol": "₽", + "currency_country": "ru", + "prices": "Цены", + "no_prices_found": "Цены не найдены", + "view_all_prices": "Нажмите, чтобы посмотреть все цены", + "retail_price": "Розничная цена", + "keyshop_price": "Цена в магазине ключей", + "historical_retail": "Исторические розничные цены", + "historical_keyshop": "Исторические цены в магазинах ключей", + "language": "Язык", + "caption": "Субтитры", + "audio": "Аудио", + "filter_by_source": "Фильтр по источнику", + "no_repacks_found": "Источники для этой игры не найдены" }, "activation": { "title": "Активировать Hydra", @@ -309,6 +341,7 @@ "stop_seeding": "Остановить раздачу", "resume_seeding": "Продолжить раздачу", "options": "Управлять", + "alldebrid_size_not_supported": "Информация о загрузке для AllDebrid пока не поддерживается", "extract": "Распаковать файлы", "extracting": "Распаковка файлов…" }, @@ -317,13 +350,10 @@ "change": "Изменить", "notifications": "Уведомления", "enable_download_notifications": "По завершении загрузки", - "enable_achievement_notifications": "Когда достижение разблокировано", "enable_repack_list_notifications": "При добавлении нового репака", "real_debrid_api_token_label": "Real-Debrid API-токен", "quit_app_instead_hiding": "Закрывать приложение вместо сворачивания в трей", "launch_with_system": "Запускать Hydra вместе с системой", - "launch_minimized": "Запустить Hydra в свернутом виде", - "disable_nsfw_alert": "Отключить предупреждение о непристойном контенте", "general": "Основные", "behavior": "Поведение", "download_sources": "Источники загрузки", @@ -350,11 +380,11 @@ "download_source_errored": "Ошибка", "sync_download_sources": "Обновить источники", "removed_download_source": "Источник удален", + "removed_download_sources": "Источники удалены", "cancel_button_confirmation_delete_all_sources": "Нет", "confirm_button_confirmation_delete_all_sources": "Да, удалить все", - "description_confirmation_delete_all_sources": "Вы удалите все источники", "title_confirmation_delete_all_sources": "Удалить все источники", - "removed_download_sources": "Источники удалены", + "description_confirmation_delete_all_sources": "Вы удалите все источники", "button_delete_all_sources": "Удалить все источники", "added_download_source": "Источник добавлен", "download_sources_synced": "Все источники обновлены", @@ -363,17 +393,20 @@ "found_download_option_one": "Найден {{countFormatted}} вариант загрузки", "found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки", "import": "Импортировать", - "blocked_users": "Заблокированные пользователи", - "friends_only": "Только для друзей", - "must_be_valid_url": "У источника должен быть правильный URL", - "privacy": "Конфиденциальность", + "public": "Публичный", "private": "Частный", + "friends_only": "Только для друзей", + "privacy": "Конфиденциальность", "profile_visibility": "Видимость профиля", "profile_visibility_description": "Выберите, кто может видеть ваш профиль и библиотеку", - "public": "Публичный", "required_field": "Это поле обязательно к заполнению", "source_already_exists": "Этот источник уже добавлен", + "must_be_valid_url": "У источника должен быть правильный URL", + "blocked_users": "Заблокированные пользователи", "user_unblocked": "Пользователь разблокирован", + "enable_achievement_notifications": "Когда достижение разблокировано", + "launch_minimized": "Запустить Hydra в свернутом виде", + "disable_nsfw_alert": "Отключить предупреждение о непристойном контенте", "seed_after_download_complete": "Раздавать после завершения загрузки", "show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением", "account": "Аккаунт", @@ -415,9 +448,20 @@ "enable_torbox": "Включить TorBox", "torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.", "torbox_account_linked": "Аккаунт TorBox привязан", - "real_debrid_account_linked": "Аккаунт Real-Debrid привязан", "create_real_debrid_account": "Нажмите здесь, если у вас еще нет аккаунта Real-Debrid", "create_torbox_account": "Нажмите здесь, если у вас еще нет учетной записи TorBox", + "real_debrid_account_linked": "Аккаунт Real-Debrid привязан", + "enable_all_debrid": "Включить All-Debrid", + "all_debrid_description": "All-Debrid - это неограниченный загрузчик, который позволяет быстро скачивать файлы из различных источников.", + "all_debrid_free_account_error": "Аккаунт \"{{username}}\" является бесплатным. Пожалуйста, оформите подписку на All-Debrid", + "all_debrid_account_linked": "Аккаунт All-Debrid успешно привязан", + "alldebrid_missing_key": "Пожалуйста, предоставьте API ключ", + "alldebrid_invalid_key": "Неверный API ключ", + "alldebrid_blocked": "Ваш API ключ заблокирован по геолокации или IP", + "alldebrid_banned": "Этот аккаунт был заблокирован", + "alldebrid_unknown_error": "Произошла неизвестная ошибка", + "alldebrid_invalid_response": "Неверный ответ от All-Debrid", + "alldebrid_network_error": "Ошибка сети. Пожалуйста, проверьте соединение", "name_min_length": "Название темы должно содержать не менее 3 символов", "import_theme": "Импортировать тему", "import_theme_description": "Вы импортируете {{theme}} из магазина тем", @@ -431,6 +475,7 @@ "installing_common_redist": "Установка…", "show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду", "extract_files_by_default": "Извлекать файлы по умолчанию после загрузки", + "enable_steam_achievements": "Включить поиск достижений Steam", "achievement_custom_notification_position": "Позиция уведомлений достижений", "top-left": "Верхний левый угол", "top-center": "Верхний центр", @@ -447,8 +492,7 @@ "hidden": "Скрытый", "test_notification": "Тестовое уведомление", "notification_preview": "Предварительный просмотр уведомления о достижении", - "enable_friend_start_game_notifications": "Когда друг начинает играть в игру", - "enable_steam_achievements": "Включить поиск достижений Steam" + "enable_friend_start_game_notifications": "Когда друг начинает играть в игру" }, "notifications": { "download_complete": "Загрузка завершена", @@ -460,13 +504,13 @@ "restart_to_install_update": "Перезапустите Hydra для установки обновления", "notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}", "notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}", + "new_friend_request_description": "{{displayName}} отправил вам запрос в друзья", "new_friend_request_title": "Новый запрос на добавление в друзья", "extraction_complete": "Распаковка завершена", "game_extracted": "{{title}} успешно распакован", "friend_started_playing_game": "{{displayName}} начал играть в игру", "test_achievement_notification_title": "Это тестовое уведомление", - "test_achievement_notification_description": "Довольно круто, да?", - "new_friend_request_description": "{{displayName}} отправил вам запрос в друзья" + "test_achievement_notification_description": "Довольно круто, да?" }, "system_tray": { "open": "Открыть Hydra", @@ -496,6 +540,10 @@ "last_time_played": "Последняя игра {{period}}", "activity": "Недавняя активность", "library": "Библиотека", + "pinned": "Закрепленные", + "achievements_earned": "Заработанные достижения", + "played_recently": "Недавно сыгранные", + "playtime": "Время игры", "total_play_time": "Всего сыграно", "manual_playtime_tooltip": "Время игры было обновлено вручную", "no_recent_activity_title": "Хммм... Тут ничего нет", @@ -539,24 +587,24 @@ "no_pending_invites": "У вас нет запросов ожидающих ответа", "no_blocked_users": "Вы не заблокировали ни одного пользователя", "friend_code_copied": "Код друга скопирован", - "displayname_max_length": "Отображаемое имя должно содержать не более 50 символов.", - "displayname_min_length": "Отображаемое имя должно содержать не менее 3 символов.", - "image_process_failure": "Сбой при обработке изображения", - "locked_profile": "Этот профиль является частным", + "undo_friendship_modal_text": "Это отменит вашу дружбу с {{displayName}}.", "privacy_hint": "Чтобы указать, кто может это видеть, перейдите в <0>Настройки.", - "profile_reported": "Профиль сообщил", - "report": "Отчет", - "report_description": "Дополнительная информация", - "report_description_placeholder": "Дополнительная информация", + "locked_profile": "Этот профиль является частным", + "image_process_failure": "Сбой при обработке изображения", + "required_field": "Это поле обязательно к заполнению", + "displayname_min_length": "Отображаемое имя должно содержать не менее 3 символов.", + "displayname_max_length": "Отображаемое имя должно содержать не более 50 символов.", "report_profile": "Пожаловаться на этот профиль", "report_reason": "Почему вы жалуетесь на этот профиль?", + "report_description": "Дополнительная информация", + "report_description_placeholder": "Дополнительная информация", + "report": "Отчет", "report_reason_hate": "Разжигание ненависти", - "report_reason_other": "Другой", "report_reason_sexual_content": "Сексуальный контент", - "report_reason_spam": "Спам", "report_reason_violence": "Насилие", - "required_field": "Это поле обязательно к заполнению", - "undo_friendship_modal_text": "Это отменит вашу дружбу с {{displayName}}.", + "report_reason_spam": "Спам", + "report_reason_other": "Другой", + "profile_reported": "Профиль сообщил", "your_friend_code": "Код вашего друга:", "upload_banner": "Загрузить баннер", "uploading_banner": "Загрузка баннера...", @@ -572,7 +620,9 @@ "show_achievements_on_profile": "Покажите свои достижения в профиле", "show_points_on_profile": "Показывать заработанные очки в своем профиле", "error_adding_friend": "Не удалось отправить запрос в друзья. Пожалуйста, проверьте код друга", - "friend_code_length_error": "Код друга должен содержать 8 символов" + "friend_code_length_error": "Код друга должен содержать 8 символов", + "game_removed_from_pinned": "Игра удалена из закрепленных", + "game_added_to_pinned": "Игра добавлена в закрепленные" }, "achievement": { "achievement_unlocked": "Достижение разблокировано", From b0d9d18c6cc04c94274b01ca8840ba8912257261 Mon Sep 17 00:00:00 2001 From: Wkeynhk <86107421+Wkeynhk@users.noreply.github.com> Date: Mon, 6 Oct 2025 00:35:24 +0300 Subject: [PATCH 25/52] Update translation.json --- src/locales/ru/translation.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index c316977a..bf9bf751 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -288,7 +288,7 @@ "backup_unfrozen": "Резервная копия откреплена", "backup_freeze_failed": "Не удалось закрепить резервную копию", "backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий", - "edit_game_modal_button": "Настроить ресурсы игры", + "edit_game_modal_button": "Изменить детали игры", "game_details": "Детали игры", "currency_symbol": "₽", "currency_country": "ru", @@ -405,7 +405,7 @@ "blocked_users": "Заблокированные пользователи", "user_unblocked": "Пользователь разблокирован", "enable_achievement_notifications": "Когда достижение разблокировано", - "launch_minimized": "Запустить Hydra в свернутом виде", + "launch_minimized": "Запускать Hydra в свернутом виде", "disable_nsfw_alert": "Отключить предупреждение о непристойном контенте", "seed_after_download_complete": "Раздавать после завершения загрузки", "show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением", @@ -598,13 +598,13 @@ "report_reason": "Почему вы жалуетесь на этот профиль?", "report_description": "Дополнительная информация", "report_description_placeholder": "Дополнительная информация", - "report": "Отчет", + "report": "Пожаловаться", "report_reason_hate": "Разжигание ненависти", "report_reason_sexual_content": "Сексуальный контент", "report_reason_violence": "Насилие", "report_reason_spam": "Спам", - "report_reason_other": "Другой", - "profile_reported": "Профиль сообщил", + "report_reason_other": "Другое", + "profile_reported": "Жалоба на профиль отправлена", "your_friend_code": "Код вашего друга:", "upload_banner": "Загрузить баннер", "uploading_banner": "Загрузка баннера...", From 055be6b10a2c149bd3316cdf049192eb925c2ee9 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 6 Oct 2025 15:41:12 +0300 Subject: [PATCH 26/52] Fix: review prompt banner appearing in all games --- .../src/components/game-card/game-card.scss | 3 +-- .../src/pages/game-details/game-details-content.tsx | 13 +++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/game-card/game-card.scss b/src/renderer/src/components/game-card/game-card.scss index 1830762f..46c6bec9 100644 --- a/src/renderer/src/components/game-card/game-card.scss +++ b/src/renderer/src/components/game-card/game-card.scss @@ -73,8 +73,7 @@ color: globals.$muted-color; font-size: 12px; align-items: center; - - // Ensure star rating is properly aligned + .star-rating { align-items: center; } diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 9060dc39..16ee7386 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -106,7 +106,7 @@ export function GameDetailsContent() { const { showHydraCloudModal } = useSubscription(); const { userDetails, hasActiveSubscription } = useUserDetails(); - const { updateLibrary } = useLibrary(); + const { updateLibrary, library } = useLibrary(); const { formatDistance } = useDate(); const { showSuccessToast, showErrorToast } = useToast(); @@ -159,6 +159,14 @@ export function GameDetailsContent() { const [hasUserReviewed, setHasUserReviewed] = useState(false); const [reviewCheckLoading, setReviewCheckLoading] = useState(false); + // Check if the current game is in the user's library + const isGameInLibrary = useMemo(() => { + if (!library || !shop || !objectId) return false; + return library.some( + (libItem) => libItem.shop === shop && libItem.objectId === objectId + ); + }, [library, shop, objectId]); + const editor = useEditor({ extensions: [ StarterKit.configure({ @@ -561,7 +569,8 @@ export function GameDetailsContent() { showReviewPrompt && userDetails && !hasUserReviewed && - !reviewCheckLoading && ( + !reviewCheckLoading && + isGameInLibrary && ( Date: Mon, 6 Oct 2025 15:48:19 +0300 Subject: [PATCH 27/52] fix: formatting --- src/renderer/src/components/game-card/game-card.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/components/game-card/game-card.scss b/src/renderer/src/components/game-card/game-card.scss index 46c6bec9..9d1eaf93 100644 --- a/src/renderer/src/components/game-card/game-card.scss +++ b/src/renderer/src/components/game-card/game-card.scss @@ -73,7 +73,7 @@ color: globals.$muted-color; font-size: 12px; align-items: center; - + .star-rating { align-items: center; } From 47ac8e63acdcbe150bacc564dd35851b7b2efa50 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 6 Oct 2025 18:30:56 +0300 Subject: [PATCH 28/52] fix: fixed button layout in prompt message and fixed rating display in stats in game page --- src/renderer/src/components/star-rating/star-rating.tsx | 6 ++++-- .../src/pages/game-details/review-prompt-banner.tsx | 6 +++--- src/renderer/src/pages/game-details/sidebar/sidebar.tsx | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/star-rating/star-rating.tsx b/src/renderer/src/components/star-rating/star-rating.tsx index cdd04e03..50d08afd 100644 --- a/src/renderer/src/components/star-rating/star-rating.tsx +++ b/src/renderer/src/components/star-rating/star-rating.tsx @@ -7,6 +7,7 @@ export interface StarRatingProps { size?: number; showCalculating?: boolean; calculatingText?: string; + hideIcon?: boolean; } export function StarRating({ @@ -15,11 +16,12 @@ export function StarRating({ size = 12, showCalculating = false, calculatingText = "Calculating", + hideIcon = false, }: Readonly) { if (rating === null && showCalculating) { return (
- + {!hideIcon && } {calculatingText}
); @@ -28,7 +30,7 @@ export function StarRating({ if (rating === null || rating === undefined) { return (
- + {!hideIcon && }
); diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.tsx b/src/renderer/src/pages/game-details/review-prompt-banner.tsx index 01fdd075..053c97e8 100644 --- a/src/renderer/src/pages/game-details/review-prompt-banner.tsx +++ b/src/renderer/src/pages/game-details/review-prompt-banner.tsx @@ -25,12 +25,12 @@ export function ReviewPromptBanner({
- +
diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index febb6a8b..33009508 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -233,10 +233,11 @@ export function Sidebar() { {t("rating_count")}

From a9b67ad1e6062d43b76848c8369d1d90fff06218 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:25:09 -0300 Subject: [PATCH 29/52] feat: handle and log game backup errors --- src/locales/en/translation.json | 1 + src/locales/es/translation.json | 1 + src/locales/pt-PT/translation.json | 1 + src/main/services/steam.ts | 6 +++- .../confirm-modal/confirm-modal.tsx | 13 ++++++-- .../context/cloud-sync/cloud-sync.context.tsx | 12 +++++-- .../game-details/game-details.context.tsx | 3 +- .../game-details/hero/hero-panel-actions.tsx | 32 +++++++++++++++---- .../game-details/modals/repacks-modal.tsx | 2 +- 9 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 0c1ed914..d5a0708c 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -219,6 +219,7 @@ "uploading_backup": "Uploading backup…", "no_backups": "You haven't created any backups for this game yet", "backup_uploaded": "Backup uploaded", + "backup_failed": "Backup failed", "backup_deleted": "Backup deleted", "backup_restored": "Backup restored", "see_all_achievements": "See all achievements", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 6f0fc9f1..226f77af 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -204,6 +204,7 @@ "uploading_backup": "Subiendo copia de seguridad…", "no_backups": "No has creado ninguna copia de seguridad para este juego todavía", "backup_uploaded": "Copia de seguridad subida", + "backup_failed": "Copia de seguridad fallida", "backup_deleted": "Copia de seguridad eliminada", "backup_restored": "Copia de seguridad restaurada", "see_all_achievements": "Ver todos los logros", diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index 654e94ec..d36e3083 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -142,6 +142,7 @@ "uploading_backup": "A criar backup…", "no_backups": "Ainda não fizeste nenhum backup deste jogo", "backup_uploaded": "Backup criado", + "backup_failed": "Falha ao criar backup", "backup_deleted": "Backup apagado", "backup_restored": "Backup restaurado", "see_all_achievements": "Ver todas as conquistas", diff --git a/src/main/services/steam.ts b/src/main/services/steam.ts index bc867111..e3aad8d9 100644 --- a/src/main/services/steam.ts +++ b/src/main/services/steam.ts @@ -76,7 +76,11 @@ export const getSteamAppDetails = async ( return null; }) .catch((err) => { - logger.error(err, { method: "getSteamAppDetails" }); + logger.error("Error on getSteamAppDetails", { + message: err?.message, + code: err?.code, + name: err?.name, + }); return null; }); }; diff --git a/src/renderer/src/components/confirm-modal/confirm-modal.tsx b/src/renderer/src/components/confirm-modal/confirm-modal.tsx index d210c035..75a8f5c9 100644 --- a/src/renderer/src/components/confirm-modal/confirm-modal.tsx +++ b/src/renderer/src/components/confirm-modal/confirm-modal.tsx @@ -33,9 +33,18 @@ export function ConfirmModal({ }; return ( - +
- diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx index f9287a11..e1ea9e2f 100644 --- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -87,7 +87,7 @@ export function CloudSyncContextProvider({ const [loadingPreview, setLoadingPreview] = useState(false); const [freezingArtifact, setFreezingArtifact] = useState(false); - const { showSuccessToast } = useToast(); + const { showSuccessToast, showErrorToast } = useToast(); const downloadGameArtifact = useCallback( async (gameArtifactId: string) => { @@ -122,9 +122,15 @@ export function CloudSyncContextProvider({ const uploadSaveGame = useCallback( async (downloadOptionTitle: string | null) => { setUploadingBackup(true); - window.electron.uploadSaveGame(objectId, shop, downloadOptionTitle); + window.electron + .uploadSaveGame(objectId, shop, downloadOptionTitle) + .catch((err) => { + setUploadingBackup(false); + logger.error("Failed to upload save game", { objectId, shop, err }); + showErrorToast(t("backup_failed")); + }); }, - [objectId, shop] + [objectId, shop, t, showErrorToast] ); const toggleArtifactFreeze = useCallback( diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index 23ea3845..5be5cf98 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -201,7 +201,8 @@ export function GameDetailsContextProvider({ }, [objectId, gameTitle, dispatch]); useEffect(() => { - const state = (location && (location.state as Record)) || {}; + const state = + (location && (location.state as Record)) || {}; if (state.openRepacks) { setShowRepacksModal(true); try { diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index ac8a1615..e23120a8 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -69,14 +69,32 @@ export function HeroPanelActions() { updateGame(); }; - window.addEventListener("hydra:game-favorite-toggled", onFavoriteToggled as EventListener); - window.addEventListener("hydra:game-removed-from-library", onGameRemoved as EventListener); - window.addEventListener("hydra:game-files-removed", onFilesRemoved as EventListener); + window.addEventListener( + "hydra:game-favorite-toggled", + onFavoriteToggled as EventListener + ); + window.addEventListener( + "hydra:game-removed-from-library", + onGameRemoved as EventListener + ); + window.addEventListener( + "hydra:game-files-removed", + onFilesRemoved as EventListener + ); return () => { - window.removeEventListener("hydra:game-favorite-toggled", onFavoriteToggled as EventListener); - window.removeEventListener("hydra:game-removed-from-library", onGameRemoved as EventListener); - window.removeEventListener("hydra:game-files-removed", onFilesRemoved as EventListener); + window.removeEventListener( + "hydra:game-favorite-toggled", + onFavoriteToggled as EventListener + ); + window.removeEventListener( + "hydra:game-removed-from-library", + onGameRemoved as EventListener + ); + window.removeEventListener( + "hydra:game-files-removed", + onFilesRemoved as EventListener + ); }; }, [updateLibrary, updateGame]); @@ -226,7 +244,7 @@ export function HeroPanelActions() { onClick={() => setShowRepacksModal(true)} theme="outline" disabled={isGameDownloading} - className={`hero-panel-actions__action ${!repacks.length ? 'hero-panel-actions__action--disabled' : ''}`} + className={`hero-panel-actions__action ${!repacks.length ? "hero-panel-actions__action--disabled" : ""}`} > {t("download")} diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index ec7dc3f8..97b8b1b5 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -277,4 +277,4 @@ export function RepacksModal({ ); -} \ No newline at end of file +} From 91fd5932dabce08ed2c4e6fa9cdef8b14b6b0754 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:40:57 -0300 Subject: [PATCH 30/52] fix: lastTimePlayed not updating correctly when merging games --- src/main/services/cloud-sync.ts | 4 ++-- src/main/services/library-sync/merge-with-remote-games.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/services/cloud-sync.ts b/src/main/services/cloud-sync.ts index 6da24ce1..200a5ee3 100644 --- a/src/main/services/cloud-sync.ts +++ b/src/main/services/cloud-sync.ts @@ -80,7 +80,7 @@ export class CloudSync { try { await fs.promises.rm(backupPath, { recursive: true }); } catch (error) { - logger.error("Failed to remove backup path", error); + logger.error("Failed to remove backup path", { backupPath, error }); } } @@ -163,7 +163,7 @@ export class CloudSync { try { await fs.promises.unlink(bundleLocation); } catch (error) { - logger.error("Failed to remove tar file", error); + logger.error("Failed to remove tar file", { bundleLocation, error }); } } } diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index b5b2d551..68b4b835 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -22,7 +22,8 @@ export const mergeWithRemoteGames = async () => { const updatedLastTimePlayed = localGame.lastTimePlayed == null || (game.lastTimePlayed && - new Date(game.lastTimePlayed) > localGame.lastTimePlayed) + new Date(game.lastTimePlayed) > + new Date(localGame.lastTimePlayed)) ? game.lastTimePlayed : localGame.lastTimePlayed; From 7b8f7fc0703f0feec513610cbcfd9931a247b2f5 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:17:27 -0300 Subject: [PATCH 31/52] feat: add alternative steam path on linux --- src/main/services/steam.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/services/steam.ts b/src/main/services/steam.ts index e3aad8d9..886772b5 100644 --- a/src/main/services/steam.ts +++ b/src/main/services/steam.ts @@ -19,7 +19,12 @@ export interface SteamAppDetailsResponse { export const getSteamLocation = async () => { if (process.platform === "linux") { - return path.join(SystemPath.getPath("home"), ".local", "share", "Steam"); + const possiblePaths = [ + path.join(SystemPath.getPath("home"), ".steam", "steam"), + path.join(SystemPath.getPath("home"), ".local", "share", "Steam"), + ]; + + return possiblePaths.find((p) => fs.existsSync(p)) || possiblePaths[0]; } if (process.platform === "darwin") { From 9bada771df3a0027946de57f1d59c869485c00bc Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:26:05 -0300 Subject: [PATCH 32/52] feat: separate game assets from game stats --- src/main/events/catalogue/get-game-assets.ts | 51 +++++++++++++++++++ .../events/catalogue/save-game-shop-assets.ts | 25 --------- .../library/add-custom-game-to-library.ts | 1 + .../events/library/create-steam-shortcut.ts | 10 ++-- src/main/level/sublevels/game-shop-assets.ts | 12 ++--- .../library-sync/merge-with-remote-games.ts | 4 ++ src/main/services/notifications/index.ts | 7 ++- .../services/ws/events/friend-game-session.ts | 11 ++-- src/preload/index.ts | 5 +- .../game-details/game-details.context.tsx | 40 +++++++-------- src/renderer/src/declaration.d.ts | 10 ++-- .../pages/game-details/sidebar/sidebar.tsx | 13 ++++- .../user-library-game-card.tsx | 2 +- src/types/index.ts | 4 +- 14 files changed, 111 insertions(+), 84 deletions(-) create mode 100644 src/main/events/catalogue/get-game-assets.ts delete mode 100644 src/main/events/catalogue/save-game-shop-assets.ts diff --git a/src/main/events/catalogue/get-game-assets.ts b/src/main/events/catalogue/get-game-assets.ts new file mode 100644 index 00000000..04a03808 --- /dev/null +++ b/src/main/events/catalogue/get-game-assets.ts @@ -0,0 +1,51 @@ +import type { GameShop, ShopAssets } from "@types"; +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import { gamesShopAssetsSublevel, levelKeys } from "@main/level"; + +const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 30; // 30 minutes + +export const getGameAssets = async (objectId: string, shop: GameShop) => { + const cachedAssets = await gamesShopAssetsSublevel.get( + levelKeys.game(shop, objectId) + ); + + if ( + cachedAssets && + cachedAssets.updatedAt + LOCAL_CACHE_EXPIRATION > Date.now() + ) { + return cachedAssets; + } + + return HydraApi.get( + `/games/${shop}/${objectId}/assets`, + null, + { + needsAuth: false, + } + ).then(async (assets) => { + if (!assets) return null; + + // Preserve existing title if it differs from the incoming title (indicating it was customized) + const shouldPreserveTitle = + cachedAssets?.title && cachedAssets.title !== assets.title; + + await gamesShopAssetsSublevel.put(levelKeys.game(shop, objectId), { + ...assets, + title: shouldPreserveTitle ? cachedAssets.title : assets.title, + updatedAt: Date.now(), + }); + + return assets; + }); +}; + +const getGameAssetsEvent = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop +) => { + return getGameAssets(objectId, shop); +}; + +registerEvent("getGameAssets", getGameAssetsEvent); diff --git a/src/main/events/catalogue/save-game-shop-assets.ts b/src/main/events/catalogue/save-game-shop-assets.ts deleted file mode 100644 index bf5f8b81..00000000 --- a/src/main/events/catalogue/save-game-shop-assets.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { GameShop, ShopAssets } from "@types"; -import { gamesShopAssetsSublevel, levelKeys } from "@main/level"; -import { registerEvent } from "../register-event"; - -const saveGameShopAssets = async ( - _event: Electron.IpcMainInvokeEvent, - objectId: string, - shop: GameShop, - assets: ShopAssets -): Promise => { - const key = levelKeys.game(shop, objectId); - const existingAssets = await gamesShopAssetsSublevel.get(key); - - // Preserve existing title if it differs from the incoming title (indicating it was customized) - const shouldPreserveTitle = - existingAssets?.title && existingAssets.title !== assets.title; - - return gamesShopAssetsSublevel.put(key, { - ...existingAssets, - ...assets, - title: shouldPreserveTitle ? existingAssets.title : assets.title, - }); -}; - -registerEvent("saveGameShopAssets", saveGameShopAssets); diff --git a/src/main/events/library/add-custom-game-to-library.ts b/src/main/events/library/add-custom-game-to-library.ts index 47fd3436..f85c008b 100644 --- a/src/main/events/library/add-custom-game-to-library.ts +++ b/src/main/events/library/add-custom-game-to-library.ts @@ -27,6 +27,7 @@ const addCustomGameToLibrary = async ( } const assets = { + updatedAt: Date.now(), objectId, shop, title, diff --git a/src/main/events/library/create-steam-shortcut.ts b/src/main/events/library/create-steam-shortcut.ts index f83dd675..d5434d7f 100644 --- a/src/main/events/library/create-steam-shortcut.ts +++ b/src/main/events/library/create-steam-shortcut.ts @@ -1,12 +1,11 @@ import { registerEvent } from "../register-event"; -import type { GameShop, GameStats } from "@types"; +import type { GameShop, ShopAssets } from "@types"; import { gamesSublevel, levelKeys } from "@main/level"; import { composeSteamShortcut, getSteamLocation, getSteamShortcuts, getSteamUsersIds, - HydraApi, logger, SystemPath, writeSteamShortcuts, @@ -15,6 +14,7 @@ import fs from "node:fs"; import axios from "axios"; import path from "node:path"; import { ASSETS_PATH } from "@main/constants"; +import { getGameAssets } from "../catalogue/get-game-assets"; const downloadAsset = async (downloadPath: string, url?: string | null) => { try { @@ -41,7 +41,7 @@ const downloadAsset = async (downloadPath: string, url?: string | null) => { const downloadAssetsFromSteam = async ( shop: GameShop, objectId: string, - assets: GameStats["assets"] + assets: ShopAssets | null ) => { const gameAssetsPath = path.join(ASSETS_PATH, `${shop}-${objectId}`); @@ -86,9 +86,7 @@ const createSteamShortcut = async ( throw new Error("No executable path found for game"); } - const { assets } = await HydraApi.get( - `/games/${shop}/${objectId}/stats` - ); + const assets = await getGameAssets(objectId, shop); const steamUserIds = await getSteamUsersIds(); diff --git a/src/main/level/sublevels/game-shop-assets.ts b/src/main/level/sublevels/game-shop-assets.ts index 561d85df..806e041f 100644 --- a/src/main/level/sublevels/game-shop-assets.ts +++ b/src/main/level/sublevels/game-shop-assets.ts @@ -3,9 +3,9 @@ import type { ShopAssets } from "@types"; import { db } from "../level"; import { levelKeys } from "./keys"; -export const gamesShopAssetsSublevel = db.sublevel( - levelKeys.gameShopAssets, - { - valueEncoding: "json", - } -); +export const gamesShopAssetsSublevel = db.sublevel< + string, + ShopAssets & { updatedAt: number } +>(levelKeys.gameShopAssets, { + valueEncoding: "json", +}); diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index 68b4b835..f7ea2744 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -58,7 +58,11 @@ export const mergeWithRemoteGames = async () => { }); } + const localGameShopAsset = await gamesShopAssetsSublevel.get(gameKey); + await gamesShopAssetsSublevel.put(gameKey, { + updatedAt: Date.now(), + ...localGameShopAsset, shop: game.shop, objectId: game.objectId, title: localGame?.title || game.title, // Preserve local title if it exists diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts index fa9ac593..d28c3cd7 100644 --- a/src/main/services/notifications/index.ts +++ b/src/main/services/notifications/index.ts @@ -10,7 +10,7 @@ import icon from "@resources/icon.png?asset"; import { NotificationOptions, toXmlString } from "./xml"; import { logger } from "../logger"; import { WindowManager } from "../window-manager"; -import type { Game, GameStats, UserPreferences, UserProfile } from "@types"; +import type { Game, UserPreferences, UserProfile } from "@types"; import { db, levelKeys } from "@main/level"; import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update"; import { SystemPath } from "../system-path"; @@ -108,15 +108,14 @@ export const publishNewFriendRequestNotification = async ( }; export const publishFriendStartedPlayingGameNotification = async ( - friend: UserProfile, - game: GameStats + friend: UserProfile ) => { new Notification({ title: t("friend_started_playing_game", { ns: "notifications", displayName: friend.displayName, }), - body: game.assets?.title, + body: friend?.currentGame?.title, icon: friend?.profileImageUrl ? await downloadImage(friend.profileImageUrl) : trayIcon, diff --git a/src/main/services/ws/events/friend-game-session.ts b/src/main/services/ws/events/friend-game-session.ts index 67967b3c..b089c421 100644 --- a/src/main/services/ws/events/friend-game-session.ts +++ b/src/main/services/ws/events/friend-game-session.ts @@ -2,7 +2,7 @@ import type { FriendGameSession } from "@main/generated/envelope"; import { db, levelKeys } from "@main/level"; import { HydraApi } from "@main/services/hydra-api"; import { publishFriendStartedPlayingGameNotification } from "@main/services/notifications"; -import type { GameStats, UserPreferences, UserProfile } from "@types"; +import type { UserPreferences, UserProfile } from "@types"; export const friendGameSessionEvent = async (payload: FriendGameSession) => { const userPreferences = await db.get( @@ -14,12 +14,9 @@ export const friendGameSessionEvent = async (payload: FriendGameSession) => { if (userPreferences?.friendStartGameNotificationsEnabled === false) return; - const [friend, gameStats] = await Promise.all([ - HydraApi.get(`/users/${payload.friendId}`), - HydraApi.get(`/games/steam/${payload.objectId}/stats`), - ]).catch(() => [null, null]); + const friend = await HydraApi.get(`/users/${payload.friendId}`); - if (friend && gameStats) { - publishFriendStartedPlayingGameNotification(friend, gameStats); + if (friend) { + publishFriendStartedPlayingGameNotification(friend); } }; diff --git a/src/preload/index.ts b/src/preload/index.ts index 813758f0..e26909d4 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -17,7 +17,6 @@ import type { Theme, FriendRequestSync, ShortcutLocation, - ShopAssets, AchievementCustomNotificationPosition, AchievementNotificationInfo, } from "@types"; @@ -67,8 +66,6 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("searchGames", payload, take, skip), getCatalogue: (category: CatalogueCategory) => ipcRenderer.invoke("getCatalogue", category), - saveGameShopAssets: (objectId: string, shop: GameShop, assets: ShopAssets) => - ipcRenderer.invoke("saveGameShopAssets", objectId, shop, assets), getGameShopDetails: (objectId: string, shop: GameShop, language: string) => ipcRenderer.invoke("getGameShopDetails", objectId, shop, language), getRandomGame: () => ipcRenderer.invoke("getRandomGame"), @@ -76,6 +73,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("getHowLongToBeat", objectId, shop), getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), + getGameAssets: (objectId: string, shop: GameShop) => + ipcRenderer.invoke("getGameAssets", objectId, shop), getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"), createGameReview: ( shop: GameShop, diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index 5be5cf98..778fa3fe 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -142,29 +142,23 @@ export function GameDetailsContextProvider({ } }); - const statsPromise = window.electron - .getGameStats(objectId, shop) - .then((result) => { - if (abortController.signal.aborted) return null; - setStats(result); - return result; - }); + window.electron.getGameStats(objectId, shop).then((result) => { + if (abortController.signal.aborted) return; + setStats(result); + }); - Promise.all([shopDetailsPromise, statsPromise]) - .then(([_, stats]) => { - if (stats) { - const assets = stats.assets; - if (assets) { - window.electron.saveGameShopAssets(objectId, shop, assets); + const assetsPromise = window.electron.getGameAssets(objectId, shop); - setShopDetails((prev) => { - if (!prev) return null; - return { - ...prev, - assets, - }; - }); - } + Promise.all([shopDetailsPromise, assetsPromise]) + .then(([_, assets]) => { + if (assets) { + setShopDetails((prev) => { + if (!prev) return null; + return { + ...prev, + assets, + }; + }); } }) .finally(() => { @@ -207,8 +201,8 @@ export function GameDetailsContextProvider({ setShowRepacksModal(true); try { window.history.replaceState({}, document.title, location.pathname); - } catch (_e) { - void _e; + } catch (e) { + console.error(e); } } }, [location]); diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 82bbeb28..9f9e4177 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -39,6 +39,7 @@ import type { AchievementCustomNotificationPosition, AchievementNotificationInfo, UserLibraryResponse, + Game, } from "@types"; import type { AxiosProgressEvent } from "axios"; import type disk from "diskusage"; @@ -77,11 +78,6 @@ declare global { skip: number ) => Promise<{ edges: CatalogueSearchResult[]; count: number }>; getCatalogue: (category: CatalogueCategory) => Promise; - saveGameShopAssets: ( - objectId: string, - shop: GameShop, - assets: ShopAssets - ) => Promise; getGameShopDetails: ( objectId: string, shop: GameShop, @@ -93,6 +89,10 @@ declare global { shop: GameShop ) => Promise; getGameStats: (objectId: string, shop: GameShop) => Promise; + getGameAssets: ( + objectId: string, + shop: GameShop + ) => Promise; getTrendingGames: () => Promise; createGameReview: ( shop: GameShop, diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 33009508..df1429ec 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -233,9 +233,18 @@ export function Sidebar() { {t("rating_count")}

diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index a3d24958..72b48a8c 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -234,7 +234,7 @@ export function UserLibraryGameCard({
{game.title} diff --git a/src/types/index.ts b/src/types/index.ts index 0c6af89b..6a864f3a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -45,7 +45,7 @@ export interface ShopAssets { libraryImageUrl: string; logoImageUrl: string; logoPosition: string | null; - coverImageUrl: string; + coverImageUrl: string | null; } export type ShopDetails = SteamAppDetails & { @@ -235,8 +235,8 @@ export interface DownloadSourceValidationResult { export interface GameStats { downloadCount: number; playerCount: number; - assets: ShopAssets | null; averageScore: number | null; + reviewCount: number; } export interface GameReview { From b21c97ea66d6e68bb6b87cdc2ba0ae32f0364972 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:59:54 -0300 Subject: [PATCH 33/52] feat: remove import --- src/main/events/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 1d537db3..ecea6463 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -3,7 +3,6 @@ import { ipcMain } from "electron"; import "./catalogue/get-catalogue"; import "./catalogue/get-game-shop-details"; -import "./catalogue/save-game-shop-assets"; import "./catalogue/get-how-long-to-beat"; import "./catalogue/get-random-game"; import "./catalogue/search-games"; From df6d9df31d453c08e8b83d96c74a9d5d7fe8a26d Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 12 Oct 2025 12:18:43 -0300 Subject: [PATCH 34/52] feat: update cache time --- src/main/events/catalogue/get-game-assets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/events/catalogue/get-game-assets.ts b/src/main/events/catalogue/get-game-assets.ts index 04a03808..de1d2b1f 100644 --- a/src/main/events/catalogue/get-game-assets.ts +++ b/src/main/events/catalogue/get-game-assets.ts @@ -3,7 +3,7 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { gamesShopAssetsSublevel, levelKeys } from "@main/level"; -const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 30; // 30 minutes +const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60 * 8; // 8 hours export const getGameAssets = async (objectId: string, shop: GameShop) => { const cachedAssets = await gamesShopAssetsSublevel.get( From 741f9de85c5d320054803ee59fef43b35f7b1b55 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 12 Oct 2025 19:51:26 +0300 Subject: [PATCH 35/52] Fix: formatting --- .../src/pages/game-details/sidebar/sidebar.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 33009508..df1429ec 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -233,9 +233,18 @@ export function Sidebar() { {t("rating_count")}

From 2240a8c9fb5be0261c0dd64b2fcbe862311b1fc8 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 12 Oct 2025 19:54:45 +0300 Subject: [PATCH 36/52] Fix: TipTap formatting not displaying on the review message --- src/shared/html-sanitizer.ts | 48 ++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/shared/html-sanitizer.ts b/src/shared/html-sanitizer.ts index 4f8042e3..9cd50fc6 100644 --- a/src/shared/html-sanitizer.ts +++ b/src/shared/html-sanitizer.ts @@ -43,19 +43,51 @@ export function sanitizeHtml(html: string): string { return ""; } - let cleanText = removeHtmlTags(html); + // Use DOM-based sanitization to preserve safe formatting while removing dangerous content. + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = html; - cleanText = decodeHtmlEntities(cleanText); + // Remove clearly unsafe elements entirely. + const disallowedSelectors = [ + "script", + "style", + "iframe", + "object", + "embed", + "link", + "meta", + ]; + disallowedSelectors.forEach((sel) => { + tempDiv.querySelectorAll(sel).forEach((el) => el.remove()); + }); - cleanText = removeZalgoText(cleanText); + // Strip potentially dangerous attributes from remaining elements. + tempDiv.querySelectorAll("*").forEach((el) => { + Array.from(el.attributes).forEach((attr) => { + const name = attr.name.toLowerCase(); + if ( + name.startsWith("on") || // Event handlers + name === "style" || + name === "src" || + name === "href" // Links disabled in editor; avoid javascript: URLs + ) { + el.removeAttribute(attr.name); + } + }); + }); - cleanText = cleanText.replaceAll(/\s+/g, " ").trim(); - - if (!cleanText || cleanText.length === 0) { - return ""; + // Clean Zalgo text characters within text nodes. + const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT); + let node: Node | null; + // eslint-disable-next-line no-cond-assign + while ((node = walker.nextNode())) { + const textNode = node as Text; + const value = textNode.nodeValue || ""; + textNode.nodeValue = removeZalgoText(value); } - return cleanText; + const cleanHtml = tempDiv.innerHTML.trim(); + return cleanHtml; } export function stripHtml(html: string): string { From 602b2fef91e108275de43a8d279643cc1b9f8a17 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 12 Oct 2025 19:57:12 +0300 Subject: [PATCH 37/52] Fix: TipTap formatting not displaying on the review message --- src/shared/html-sanitizer.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/shared/html-sanitizer.ts b/src/shared/html-sanitizer.ts index 9cd50fc6..ea3d475b 100644 --- a/src/shared/html-sanitizer.ts +++ b/src/shared/html-sanitizer.ts @@ -6,37 +6,7 @@ function removeZalgoText(text: string): string { return text.replaceAll(zalgoRegex, ""); } -function decodeHtmlEntities(text: string): string { - const entityMap: { [key: string]: string } = { - "&": "&", - "<": "<", - ">": ">", - """: '"', - "'": "'", - " ": " ", - }; - return text.replaceAll(/&[#\w]+;/g, (entity) => { - return entityMap[entity] || entity; - }); -} - -function removeHtmlTags(html: string): string { - let result = ""; - let inTag = false; - - for (const char of html) { - if (char === "<") { - inTag = true; - } else if (char === ">") { - inTag = false; - } else if (!inTag) { - result += char; - } - } - - return result; -} export function sanitizeHtml(html: string): string { if (!html || typeof html !== "string") { From 14204f1fbebbec7a0cd5b76de318b410243e04c4 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 12 Oct 2025 18:39:41 +0100 Subject: [PATCH 38/52] feat: adding review styling --- src/locales/en/translation.json | 17 +- src/locales/pt-BR/translation.json | 117 ++++++- src/main/constants.ts | 11 + src/main/events/index.ts | 3 + .../misc/check-homebrew-folder-exists.ts | 13 + .../misc/get-hydra-decky-plugin-info.ts | 63 ++++ .../events/misc/install-hydra-decky-plugin.ts | 50 +++ src/main/main.ts | 5 + src/main/services/decky-plugin.ts | 313 ++++++++++++++++++ src/main/services/index.ts | 1 + src/preload/index.ts | 4 + src/renderer/src/assets/icons/decky.png | Bin 0 -> 215327 bytes .../confirm-modal/confirm-modal.scss | 11 - .../confirm-modal/confirm-modal.tsx | 57 ---- .../confirmation-modal.scss | 2 +- .../confirmation-modal/confirmation-modal.tsx | 2 +- .../game-context-menu/game-context-menu.tsx | 32 +- .../src/components/sidebar/sidebar.scss | 22 ++ .../src/components/sidebar/sidebar.tsx | 131 +++++++- src/renderer/src/declaration.d.ts | 13 + src/renderer/src/helpers.ts | 8 + .../description-header.scss | 18 +- .../game-details/game-details-content.tsx | 132 ++++++-- .../src/pages/game-details/game-details.scss | 84 +++-- .../src/pages/settings/settings-debrid.scss | 71 ++++ .../src/pages/settings/settings-debrid.tsx | 228 +++++++++++++ src/renderer/src/pages/settings/settings.tsx | 33 +- 27 files changed, 1226 insertions(+), 215 deletions(-) create mode 100644 src/main/events/misc/check-homebrew-folder-exists.ts create mode 100644 src/main/events/misc/get-hydra-decky-plugin-info.ts create mode 100644 src/main/events/misc/install-hydra-decky-plugin.ts create mode 100644 src/main/services/decky-plugin.ts create mode 100644 src/renderer/src/assets/icons/decky.png delete mode 100644 src/renderer/src/components/confirm-modal/confirm-modal.scss delete mode 100644 src/renderer/src/components/confirm-modal/confirm-modal.tsx create mode 100644 src/renderer/src/pages/settings/settings-debrid.scss create mode 100644 src/renderer/src/pages/settings/settings-debrid.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index c3b3e452..3dc93d90 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -76,7 +76,18 @@ "edit_game_modal_drop_hero_image_here": "Drop hero image here", "edit_game_modal_drop_to_replace_icon": "Drop to replace icon", "edit_game_modal_drop_to_replace_logo": "Drop to replace logo", - "edit_game_modal_drop_to_replace_hero": "Drop to replace hero" + "edit_game_modal_drop_to_replace_hero": "Drop to replace hero", + "install_decky_plugin": "Install Decky Plugin", + "decky_plugin_installed_version": "Decky Plugin (v{{version}})", + "install_decky_plugin_title": "Install Hydra Decky Plugin", + "install_decky_plugin_message": "This will download and install the Hydra plugin for Decky Loader. This may require elevated permissions. Continue?", + "update_decky_plugin_title": "Update Hydra Decky Plugin", + "update_decky_plugin_message": "A new version of the Hydra Decky plugin is available. Would you like to update it now?", + "decky_plugin_installed": "Decky plugin v{{version}} installed successfully", + "decky_plugin_installation_failed": "Failed to install Decky plugin: {{error}}", + "decky_plugin_installation_error": "Error installing Decky plugin: {{error}}", + "confirm": "Confirm", + "cancel": "Cancel" }, "header": { "search": "Search games", @@ -227,7 +238,7 @@ "rating_neutral": "Neutral", "rating_positive": "Positive", "rating_very_positive": "Very Positive", - "submit_review": "Submit Review", + "submit_review": "Submit", "submitting": "Submitting...", "review_submitted_successfully": "Review submitted successfully!", "review_submission_failed": "Failed to submit review. Please try again.", @@ -486,6 +497,8 @@ "delete_theme_description": "This will delete the theme {{theme}}", "cancel": "Cancel", "appearance": "Appearance", + "debrid": "Debrid", + "debrid_description": "Debrid services are premium unrestricted downloaders that allow you to quickly download files hosted on various file hosting services, only limited by your internet speed.", "enable_torbox": "Enable TorBox", "torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.", "torbox_account_linked": "TorBox account linked", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 37569701..e9a84c89 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -27,21 +27,67 @@ "friends": "Amigos", "need_help": "Precisa de ajuda?", "favorites": "Favoritos", + "playable_button_title": "Mostrar apenas jogos que você pode jogar agora", "add_custom_game_tooltip": "Adicionar jogo personalizado", + "show_playable_only_tooltip": "Mostrar Apenas Jogáveis", "custom_game_modal": "Adicionar jogo personalizado", + "custom_game_modal_description": "Adicione um jogo personalizado à sua biblioteca selecionando um arquivo executável", + "custom_game_modal_executable_path": "Caminho do Executável", + "custom_game_modal_select_executable": "Selecionar arquivo executável", + "custom_game_modal_title": "Título", + "custom_game_modal_enter_title": "Insira o título", "edit_game_modal_title": "Título", - "playable_button_title": "", - "custom_game_modal_add": "Adicionar Jogo", - "custom_game_modal_adding": "Adicionando...", "custom_game_modal_browse": "Buscar", "custom_game_modal_cancel": "Cancelar", - "edit_game_modal_assets": "Imagens", - "edit_game_modal_icon": "Ícone", - "edit_game_modal_browse": "Buscar", - "edit_game_modal_cancel": "Cancelar", + "custom_game_modal_add": "Adicionar Jogo", + "custom_game_modal_adding": "Adicionando...", + "custom_game_modal_success": "Jogo personalizado adicionado com sucesso", + "custom_game_modal_failed": "Falha ao adicionar jogo personalizado", + "custom_game_modal_executable": "Executável", + "edit_game_modal": "Personalizar detalhes", + "edit_game_modal_description": "Personalize os recursos e detalhes do jogo", "edit_game_modal_enter_title": "Insira o título", + "edit_game_modal_image": "Imagem", + "edit_game_modal_select_image": "Selecionar imagem", + "edit_game_modal_browse": "Buscar", + "edit_game_modal_image_preview": "Visualização da imagem", + "edit_game_modal_icon": "Ícone", + "edit_game_modal_select_icon": "Selecionar ícone", + "edit_game_modal_icon_preview": "Visualização do ícone", "edit_game_modal_logo": "Logo", - "edit_game_modal": "Personalizar detalhes" + "edit_game_modal_select_logo": "Selecionar logo", + "edit_game_modal_logo_preview": "Visualização do logo", + "edit_game_modal_hero": "Hero da Biblioteca", + "edit_game_modal_select_hero": "Selecionar imagem hero da biblioteca", + "edit_game_modal_hero_preview": "Visualização da imagem hero da biblioteca", + "edit_game_modal_cancel": "Cancelar", + "edit_game_modal_update": "Atualizar", + "edit_game_modal_updating": "Atualizando...", + "edit_game_modal_fill_required": "Por favor, preencha todos os campos obrigatórios", + "edit_game_modal_success": "Recursos atualizados com sucesso", + "edit_game_modal_failed": "Falha ao atualizar recursos", + "edit_game_modal_image_filter": "Imagem", + "edit_game_modal_icon_resolution": "Resolução recomendada: 256x256px", + "edit_game_modal_logo_resolution": "Resolução recomendada: 640x360px", + "edit_game_modal_hero_resolution": "Resolução recomendada: 1920x620px", + "edit_game_modal_assets": "Imagens", + "edit_game_modal_drop_icon_image_here": "Solte a imagem do ícone aqui", + "edit_game_modal_drop_logo_image_here": "Solte a imagem do logo aqui", + "edit_game_modal_drop_hero_image_here": "Solte a imagem hero aqui", + "edit_game_modal_drop_to_replace_icon": "Solte para substituir o ícone", + "edit_game_modal_drop_to_replace_logo": "Solte para substituir o logo", + "edit_game_modal_drop_to_replace_hero": "Solte para substituir o hero", + "install_decky_plugin": "Instalar Plugin Decky", + "decky_plugin_installed_version": "Plugin Decky (v{{version}})", + "install_decky_plugin_title": "Instalar Plugin Hydra Decky", + "install_decky_plugin_message": "Isso irá baixar e instalar o plugin Hydra para Decky Loader. Pode ser necessário permissões elevadas. Continuar?", + "update_decky_plugin_title": "Atualizar Plugin Hydra Decky", + "update_decky_plugin_message": "Uma nova versão do plugin Hydra Decky está disponível. Gostaria de atualizar agora?", + "decky_plugin_installed": "Plugin Decky v{{version}} instalado com sucesso", + "decky_plugin_installation_failed": "Falha ao instalar plugin Decky: {{error}}", + "decky_plugin_installation_error": "Erro ao instalar plugin Decky: {{error}}", + "confirm": "Confirmar", + "cancel": "Cancelar" }, "header": { "search": "Buscar jogos", @@ -256,7 +302,48 @@ "update_playtime": "Modificar tempo de jogo", "update_playtime_description": "Atualizar manualmente o tempo de jogo de {{game}}", "update_playtime_error": "Falha ao atualizar tempo de jogo", - "update_playtime_title": "Atualizar tempo de jogo" + "update_playtime_title": "Atualizar tempo de jogo", + "update_playtime_success": "Tempo de jogo atualizado com sucesso", + "show_more": "Mostrar mais", + "show_less": "Mostrar menos", + "reviews": "Avaliações", + "leave_a_review": "Deixar uma Avaliação", + "write_review_placeholder": "Compartilhe seus pensamentos sobre este jogo...", + "sort_newest": "Mais Recentes", + "sort_oldest": "Mais Antigas", + "sort_highest_score": "Maior Nota", + "sort_lowest_score": "Menor Nota", + "sort_most_voted": "Mais Votadas", + "no_reviews_yet": "Ainda não há avaliações", + "be_first_to_review": "Seja o primeiro a compartilhar seus pensamentos sobre este jogo!", + "rating": "Avaliação", + "rating_stats": "Avaliação", + "rating_very_negative": "Muito Negativo", + "rating_negative": "Negativo", + "rating_neutral": "Neutro", + "rating_positive": "Positivo", + "rating_very_positive": "Muito Positivo", + "submit_review": "Enviar", + "submitting": "Enviando...", + "review_submitted_successfully": "Avaliação enviada com sucesso!", + "review_submission_failed": "Falha ao enviar avaliação. Por favor, tente novamente.", + "review_cannot_be_empty": "O campo de texto da avaliação não pode estar vazio.", + "review_deleted_successfully": "Avaliação excluída com sucesso.", + "review_deletion_failed": "Falha ao excluir avaliação. Por favor, tente novamente.", + "loading_reviews": "Carregando avaliações...", + "loading_more_reviews": "Carregando mais avaliações...", + "load_more_reviews": "Carregar Mais Avaliações", + "you_seemed_to_enjoy_this_game": "Parece que você gostou deste jogo", + "would_you_recommend_this_game": "Gostaria de deixar uma avaliação para este jogo?", + "yes": "Sim", + "maybe_later": "Talvez Mais Tarde", + "delete_review": "Excluir avaliação", + "remove_review": "Remover Avaliação", + "delete_review_modal_title": "Tem certeza de que deseja excluir sua avaliação?", + "delete_review_modal_description": "Esta ação não pode ser desfeita.", + "delete_review_modal_delete_button": "Excluir", + "delete_review_modal_cancel_button": "Cancelar", + "rating_count": "Avaliação" }, "activation": { "title": "Ativação", @@ -395,6 +482,8 @@ "delete_theme_description": "Isso irá deletar o tema {{theme}}", "cancel": "Cancelar", "appearance": "Aparência", + "debrid": "Debrid", + "debrid_description": "Serviços Debrid são downloaders premium sem restrições que permitem baixar rapidamente arquivos hospedados em vários serviços de hospedagem de arquivos, limitados apenas pela sua velocidade de internet.", "enable_torbox": "Habilitar TorBox", "torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.", "torbox_account_linked": "Conta do TorBox vinculada", @@ -457,7 +546,8 @@ "game_card": { "available_one": "Disponível", "available_other": "Disponíveis", - "no_downloads": "Sem downloads disponíveis" + "no_downloads": "Sem downloads disponíveis", + "calculating": "Calculando" }, "binary_not_found_modal": { "title": "Programas não instalados", @@ -569,7 +659,12 @@ "amount_minutes_short": "{{amount}}m", "amount_hours_short": "{{amount}}h", "game_added_to_pinned": "Jogo adicionado aos fixados", - "achievements_earned": "Conquistas recebidas" + "game_removed_from_pinned": "Jogo removido dos fixados", + "achievements_earned": "Conquistas recebidas", + "karma": "Karma", + "karma_count": "karma", + "karma_description": "Ganho a partir de curtidas positivas em avaliações", + "manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente" }, "achievement": { "achievement_unlocked": "Conquista desbloqueada", diff --git a/src/main/constants.ts b/src/main/constants.ts index b067be80..82b99b2a 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -42,3 +42,14 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : ""); export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets"); export const MAIN_LOOP_INTERVAL = 2000; + +export const DECKY_PLUGINS_LOCATION = path.join( + SystemPath.getPath("home"), + "homebrew", + "plugins" +); + +export const HYDRA_DECKY_PLUGIN_LOCATION = path.join( + DECKY_PLUGINS_LOCATION, + "Hydra" +); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 1d537db3..6146da22 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -58,6 +58,9 @@ import "./misc/install-common-redist"; import "./misc/can-install-common-redist"; import "./misc/save-temp-file"; import "./misc/delete-temp-file"; +import "./misc/install-hydra-decky-plugin"; +import "./misc/get-hydra-decky-plugin-info"; +import "./misc/check-homebrew-folder-exists"; import "./torrenting/cancel-game-download"; import "./torrenting/pause-game-download"; import "./torrenting/resume-game-download"; diff --git a/src/main/events/misc/check-homebrew-folder-exists.ts b/src/main/events/misc/check-homebrew-folder-exists.ts new file mode 100644 index 00000000..32e09754 --- /dev/null +++ b/src/main/events/misc/check-homebrew-folder-exists.ts @@ -0,0 +1,13 @@ +import { registerEvent } from "../register-event"; +import { DECKY_PLUGINS_LOCATION } from "@main/constants"; +import fs from "node:fs"; +import path from "node:path"; + +const checkHomebrewFolderExists = async ( + _event: Electron.IpcMainInvokeEvent +): Promise => { + const homebrewPath = path.dirname(DECKY_PLUGINS_LOCATION); + return fs.existsSync(homebrewPath); +}; + +registerEvent("checkHomebrewFolderExists", checkHomebrewFolderExists); diff --git a/src/main/events/misc/get-hydra-decky-plugin-info.ts b/src/main/events/misc/get-hydra-decky-plugin-info.ts new file mode 100644 index 00000000..da72033e --- /dev/null +++ b/src/main/events/misc/get-hydra-decky-plugin-info.ts @@ -0,0 +1,63 @@ +import { registerEvent } from "../register-event"; +import { logger } from "@main/services"; +import { HYDRA_DECKY_PLUGIN_LOCATION } from "@main/constants"; +import fs from "node:fs"; +import path from "node:path"; + +const getHydraDeckyPluginInfo = async ( + _event: Electron.IpcMainInvokeEvent +): Promise<{ + installed: boolean; + version: string | null; + path: string; +}> => { + try { + // Check if plugin folder exists + if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) { + logger.log("Hydra Decky plugin not installed"); + return { + installed: false, + version: null, + path: HYDRA_DECKY_PLUGIN_LOCATION, + }; + } + + // Check if package.json exists + const packageJsonPath = path.join( + HYDRA_DECKY_PLUGIN_LOCATION, + "package.json" + ); + + if (!fs.existsSync(packageJsonPath)) { + logger.log("Hydra Decky plugin package.json not found"); + return { + installed: false, + version: null, + path: HYDRA_DECKY_PLUGIN_LOCATION, + }; + } + + // Read and parse package.json + const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(packageJsonContent); + const version = packageJson.version; + + logger.log(`Hydra Decky plugin installed, version: ${version}`); + + return { + installed: true, + version, + path: HYDRA_DECKY_PLUGIN_LOCATION, + }; + } catch (error) { + logger.error("Failed to get plugin info:", error); + return { + installed: false, + version: null, + path: HYDRA_DECKY_PLUGIN_LOCATION, + }; + } +}; + +registerEvent("getHydraDeckyPluginInfo", getHydraDeckyPluginInfo); + diff --git a/src/main/events/misc/install-hydra-decky-plugin.ts b/src/main/events/misc/install-hydra-decky-plugin.ts new file mode 100644 index 00000000..3ddbbd64 --- /dev/null +++ b/src/main/events/misc/install-hydra-decky-plugin.ts @@ -0,0 +1,50 @@ +import { registerEvent } from "../register-event"; +import { logger, DeckyPlugin } from "@main/services"; +import { HYDRA_DECKY_PLUGIN_LOCATION } from "@main/constants"; + +const installHydraDeckyPlugin = async ( + _event: Electron.IpcMainInvokeEvent +): Promise<{ + success: boolean; + path: string; + currentVersion: string | null; + expectedVersion: string; + error?: string; +}> => { + try { + logger.log("Installing/updating Hydra Decky plugin..."); + + const result = await DeckyPlugin.checkPluginVersion(); + + if (result.exists && !result.outdated) { + logger.log("Plugin installed successfully"); + return { + success: true, + path: HYDRA_DECKY_PLUGIN_LOCATION, + currentVersion: result.currentVersion, + expectedVersion: result.expectedVersion, + }; + } else { + logger.error("Failed to install plugin"); + return { + success: false, + path: HYDRA_DECKY_PLUGIN_LOCATION, + currentVersion: result.currentVersion, + expectedVersion: result.expectedVersion, + error: "Plugin installation failed", + }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error("Failed to install plugin:", error); + return { + success: false, + path: HYDRA_DECKY_PLUGIN_LOCATION, + currentVersion: null, + expectedVersion: "0.0.3", + error: errorMessage, + }; + } +}; + +registerEvent("installHydraDeckyPlugin", installHydraDeckyPlugin); diff --git a/src/main/main.ts b/src/main/main.ts index 67391057..9b8ecc2b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -16,6 +16,7 @@ import { startMainLoop, Ludusavi, Lock, + DeckyPlugin, } from "@main/services"; export const loadState = async () => { @@ -49,6 +50,10 @@ export const loadState = async () => { Ludusavi.copyConfigFileToUserData(); Ludusavi.copyBinaryToUserData(); + if (process.platform === "linux") { + DeckyPlugin.checkAndUpdateIfOutdated(); + } + await HydraApi.setupApi().then(() => { uploadGamesBatch(); // WSClient.connect(); diff --git a/src/main/services/decky-plugin.ts b/src/main/services/decky-plugin.ts new file mode 100644 index 00000000..7e178189 --- /dev/null +++ b/src/main/services/decky-plugin.ts @@ -0,0 +1,313 @@ +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import axios from "axios"; +import sudo from "sudo-prompt"; +import { app } from "electron"; +import { + HYDRA_DECKY_PLUGIN_LOCATION, + DECKY_PLUGINS_LOCATION, +} from "@main/constants"; +import { logger } from "./logger"; +import { SevenZip } from "./7zip"; +import { SystemPath } from "./system-path"; + +export class DeckyPlugin { + private static readonly EXPECTED_VERSION = "0.0.3"; + private static readonly DOWNLOAD_URL = + "https://github.com/hydralauncher/decky-hydra-launcher/releases/download/0.0.3/Hydra.zip"; + + private static getPackageJsonPath(): string { + return path.join(HYDRA_DECKY_PLUGIN_LOCATION, "package.json"); + } + + private static async downloadPlugin(): Promise { + logger.log("Downloading Hydra Decky plugin..."); + + const tempDir = SystemPath.getPath("temp"); + const zipPath = path.join(tempDir, "Hydra.zip"); + + const response = await axios.get(this.DOWNLOAD_URL, { + responseType: "arraybuffer", + }); + + await fs.promises.writeFile(zipPath, response.data); + logger.log(`Plugin downloaded to: ${zipPath}`); + + return zipPath; + } + + private static async extractPlugin(zipPath: string): Promise { + logger.log("Extracting Hydra Decky plugin..."); + + const tempDir = SystemPath.getPath("temp"); + const extractPath = path.join(tempDir, "hydra-decky-plugin"); + + if (fs.existsSync(extractPath)) { + await fs.promises.rm(extractPath, { recursive: true, force: true }); + } + + await fs.promises.mkdir(extractPath, { recursive: true }); + + return new Promise((resolve, reject) => { + SevenZip.extractFile( + { + filePath: zipPath, + outputPath: extractPath, + }, + () => { + logger.log(`Plugin extracted to: ${extractPath}`); + resolve(extractPath); + }, + () => { + reject(new Error("Failed to extract plugin")); + } + ); + }); + } + + private static needsSudo(): boolean { + try { + if (fs.existsSync(DECKY_PLUGINS_LOCATION)) { + fs.accessSync(DECKY_PLUGINS_LOCATION, fs.constants.W_OK); + return false; + } + + const parentDir = path.dirname(DECKY_PLUGINS_LOCATION); + if (fs.existsSync(parentDir)) { + fs.accessSync(parentDir, fs.constants.W_OK); + return false; + } + + return true; + } catch (error) { + if ( + error && + typeof error === "object" && + "code" in error && + (error.code === "EACCES" || error.code === "EPERM") + ) { + return true; + } + throw error; + } + } + + private static async installPluginWithSudo( + extractPath: string + ): Promise { + logger.log("Installing plugin with sudo..."); + + const username = os.userInfo().username; + const sourcePath = path.join(extractPath, "Hydra"); + + return new Promise((resolve, reject) => { + const command = `mkdir -p "${DECKY_PLUGINS_LOCATION}" && rm -rf "${HYDRA_DECKY_PLUGIN_LOCATION}" && cp -r "${sourcePath}" "${HYDRA_DECKY_PLUGIN_LOCATION}" && chown -R ${username}: "${DECKY_PLUGINS_LOCATION}"`; + + sudo.exec( + command, + { name: app.getName() }, + (sudoError, _stdout, stderr) => { + if (sudoError) { + logger.error("Failed to install plugin with sudo:", sudoError); + reject(sudoError); + } else { + logger.log("Plugin installed successfully with sudo"); + if (stderr) { + logger.log("Sudo stderr:", stderr); + } + resolve(); + } + } + ); + }); + } + + private static async installPluginWithoutSudo( + extractPath: string + ): Promise { + logger.log("Installing plugin without sudo..."); + + const sourcePath = path.join(extractPath, "Hydra"); + + if (!fs.existsSync(DECKY_PLUGINS_LOCATION)) { + await fs.promises.mkdir(DECKY_PLUGINS_LOCATION, { recursive: true }); + } + + if (fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) { + await fs.promises.rm(HYDRA_DECKY_PLUGIN_LOCATION, { + recursive: true, + force: true, + }); + } + + await fs.promises.cp(sourcePath, HYDRA_DECKY_PLUGIN_LOCATION, { + recursive: true, + }); + + logger.log("Plugin installed successfully"); + } + + private static async installPlugin(extractPath: string): Promise { + if (this.needsSudo()) { + await this.installPluginWithSudo(extractPath); + } else { + await this.installPluginWithoutSudo(extractPath); + } + } + + private static async updatePlugin(): Promise { + let zipPath: string | null = null; + let extractPath: string | null = null; + + try { + zipPath = await this.downloadPlugin(); + extractPath = await this.extractPlugin(zipPath); + await this.installPlugin(extractPath); + + logger.log("Plugin update completed successfully"); + } catch (error) { + logger.error("Failed to update plugin:", error); + throw error; + } finally { + if (zipPath) { + try { + await fs.promises.rm(zipPath, { force: true }); + logger.log("Cleaned up downloaded zip file"); + } catch (cleanupError) { + logger.error("Failed to clean up zip file:", cleanupError); + } + } + + if (extractPath) { + try { + await fs.promises.rm(extractPath, { recursive: true, force: true }); + logger.log("Cleaned up extraction directory"); + } catch (cleanupError) { + logger.error( + "Failed to clean up extraction directory:", + cleanupError + ); + } + } + } + } + + public static async checkAndUpdateIfOutdated(): Promise { + if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) { + logger.log("Hydra Decky plugin not installed, skipping update check"); + return; + } + + const packageJsonPath = this.getPackageJsonPath(); + + try { + if (!fs.existsSync(packageJsonPath)) { + logger.log( + "Hydra Decky plugin package.json not found, skipping update" + ); + return; + } + + const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(packageJsonContent); + const currentVersion = packageJson.version; + const isOutdated = currentVersion !== this.EXPECTED_VERSION; + + if (isOutdated) { + logger.log( + `Hydra Decky plugin is outdated. Current: ${currentVersion}, Expected: ${this.EXPECTED_VERSION}. Updating...` + ); + + await this.updatePlugin(); + logger.log("Hydra Decky plugin updated successfully"); + } else { + logger.log(`Hydra Decky plugin is up to date (${currentVersion})`); + } + } catch (error) { + logger.error(`Error checking/updating Hydra Decky plugin: ${error}`); + } + } + + public static async checkPluginVersion(): Promise<{ + exists: boolean; + outdated: boolean; + currentVersion: string | null; + expectedVersion: string; + }> { + if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) { + logger.log("Hydra Decky plugin folder not found, installing..."); + + try { + await this.updatePlugin(); + return { + exists: true, + outdated: false, + currentVersion: this.EXPECTED_VERSION, + expectedVersion: this.EXPECTED_VERSION, + }; + } catch (error) { + logger.error("Failed to install plugin:", error); + return { + exists: false, + outdated: true, + currentVersion: null, + expectedVersion: this.EXPECTED_VERSION, + }; + } + } + + const packageJsonPath = this.getPackageJsonPath(); + + try { + if (!fs.existsSync(packageJsonPath)) { + logger.log("Hydra Decky plugin package.json not found, installing..."); + + await this.updatePlugin(); + return { + exists: true, + outdated: false, + currentVersion: this.EXPECTED_VERSION, + expectedVersion: this.EXPECTED_VERSION, + }; + } + + const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(packageJsonContent); + const currentVersion = packageJson.version; + const isOutdated = currentVersion !== this.EXPECTED_VERSION; + + if (isOutdated) { + logger.log( + `Hydra Decky plugin is outdated. Current: ${currentVersion}, Expected: ${this.EXPECTED_VERSION}` + ); + + await this.updatePlugin(); + + return { + exists: true, + outdated: false, + currentVersion: this.EXPECTED_VERSION, + expectedVersion: this.EXPECTED_VERSION, + }; + } else { + logger.log(`Hydra Decky plugin is up to date (${currentVersion})`); + } + + return { + exists: true, + outdated: isOutdated, + currentVersion, + expectedVersion: this.EXPECTED_VERSION, + }; + } catch (error) { + logger.error(`Error checking Hydra Decky plugin version: ${error}`); + return { + exists: false, + outdated: true, + currentVersion: null, + expectedVersion: this.EXPECTED_VERSION, + }; + } + } +} diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 727805c7..88b39d1b 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -17,3 +17,4 @@ export * from "./system-path"; export * from "./library-sync"; export * from "./wine"; export * from "./lock"; +export * from "./decky-plugin"; diff --git a/src/preload/index.ts b/src/preload/index.ts index 813758f0..700561ac 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -386,6 +386,10 @@ contextBridge.exposeInMainWorld("electron", { getBadges: () => ipcRenderer.invoke("getBadges"), canInstallCommonRedist: () => ipcRenderer.invoke("canInstallCommonRedist"), installCommonRedist: () => ipcRenderer.invoke("installCommonRedist"), + installHydraDeckyPlugin: () => ipcRenderer.invoke("installHydraDeckyPlugin"), + getHydraDeckyPluginInfo: () => ipcRenderer.invoke("getHydraDeckyPluginInfo"), + checkHomebrewFolderExists: () => + ipcRenderer.invoke("checkHomebrewFolderExists"), platform: process.platform, /* Auto update */ diff --git a/src/renderer/src/assets/icons/decky.png b/src/renderer/src/assets/icons/decky.png new file mode 100644 index 0000000000000000000000000000000000000000..205552dd3d7558e885ad4808e4345ac2d00738b6 GIT binary patch literal 215327 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%0A6s5g>U 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 AF8}}l literal 0 HcmV?d00001 diff --git a/src/renderer/src/components/confirm-modal/confirm-modal.scss b/src/renderer/src/components/confirm-modal/confirm-modal.scss deleted file mode 100644 index e5bda187..00000000 --- a/src/renderer/src/components/confirm-modal/confirm-modal.scss +++ /dev/null @@ -1,11 +0,0 @@ -@use "../../scss/globals.scss"; - -.confirm-modal { - &__actions { - display: flex; - width: 100%; - justify-content: flex-end; - align-items: center; - gap: globals.$spacing-unit; - } -} diff --git a/src/renderer/src/components/confirm-modal/confirm-modal.tsx b/src/renderer/src/components/confirm-modal/confirm-modal.tsx deleted file mode 100644 index 75a8f5c9..00000000 --- a/src/renderer/src/components/confirm-modal/confirm-modal.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Button, Modal } from "@renderer/components"; -import "./confirm-modal.scss"; - -export interface ConfirmModalProps { - visible: boolean; - title: string; - description?: string; - onClose: () => void; - onConfirm: () => Promise | void; - confirmLabel?: string; - cancelLabel?: string; - confirmTheme?: "primary" | "outline" | "danger"; - confirmDisabled?: boolean; -} - -export function ConfirmModal({ - visible, - title, - description, - onClose, - onConfirm, - confirmLabel, - cancelLabel, - confirmTheme = "outline", - confirmDisabled = false, -}: ConfirmModalProps) { - const { t } = useTranslation(); - - const handleConfirm = async () => { - await onConfirm(); - onClose(); - }; - - return ( - -
- - - -
-
- ); -} diff --git a/src/renderer/src/components/confirmation-modal/confirmation-modal.scss b/src/renderer/src/components/confirmation-modal/confirmation-modal.scss index 428818c4..7689ebcd 100644 --- a/src/renderer/src/components/confirmation-modal/confirmation-modal.scss +++ b/src/renderer/src/components/confirmation-modal/confirmation-modal.scss @@ -8,7 +8,7 @@ &__actions { display: flex; align-self: flex-end; - gap: calc(globals.$spacing-unit * 2); + gap: globals.$spacing-unit; } &__description { font-size: 16px; diff --git a/src/renderer/src/components/confirmation-modal/confirmation-modal.tsx b/src/renderer/src/components/confirmation-modal/confirmation-modal.tsx index 63256935..f81453fa 100644 --- a/src/renderer/src/components/confirmation-modal/confirmation-modal.tsx +++ b/src/renderer/src/components/confirmation-modal/confirmation-modal.tsx @@ -42,7 +42,7 @@ export function ConfirmationModal({ {cancelButtonLabel} ))} + {window.electron.platform === "linux" && homebrewFolderExists && ( +
  • + +
  • + )} @@ -321,18 +404,20 @@ export function Sidebar() { - {hasActiveSubscription && ( - - )} +
    + {hasActiveSubscription && ( + + )} +
    @@ -772,11 +791,20 @@ export function GameDetailsContent() {
    {review.user?.profileImageUrl && ( - {review.user.displayName + )}
    {userDetails?.id === review.user?.id && ( diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index 09c0f05f..aee2e639 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -128,7 +128,7 @@ $hero-height: 300px; &__star-rating { display: flex; align-items: center; - gap: 4px; + gap: 2px; } &__star { @@ -136,7 +136,7 @@ $hero-height: 300px; border: none; color: #666666; cursor: pointer; - padding: 4px; + padding: 2px; border-radius: 4px; display: flex; align-items: center; @@ -220,30 +220,6 @@ $hero-height: 300px; } } - &__review-submit-button { - background-color: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 6px; - color: #ffffff; - padding: 10px 20px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; - - &:hover:not(:disabled) { - background-color: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.15); - } - - &:disabled { - background-color: rgba(255, 255, 255, 0.1); - cursor: not-allowed; - color: rgba(255, 255, 255, 0.5); - } - } - &__reviews-list { margin-top: calc(globals.$spacing-unit * 3); } @@ -288,7 +264,12 @@ $hero-height: 300px; } &__review-item { - background: rgba(255, 255, 255, 0.03); + background: linear-gradient( + to right, + globals.$dark-background-color 0%, + globals.$dark-background-color 30%, + globals.$background-color 100% + ); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 6px; padding: calc(globals.$spacing-unit * 2); @@ -310,12 +291,29 @@ $hero-height: 300px; gap: calc(globals.$spacing-unit * 1); } + &__review-avatar-button { + background: none; + border: none; + padding: 0; + cursor: pointer; + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.8; + } + + &:active { + opacity: 0.6; + } + } + &__review-avatar { width: 32px; height: 32px; border-radius: 4px; object-fit: cover; border: 2px solid rgba(255, 255, 255, 0.1); + display: block; } &__review-user-info { @@ -370,16 +368,7 @@ $hero-height: 300px; &:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); - } - - &--upvote:hover { - color: #4caf50; - border-color: #4caf50; - } - - &--downvote:hover { - color: #f44336; - border-color: #f44336; + color: #ffffff; } &--active { @@ -398,6 +387,9 @@ $hero-height: 300px; span { font-weight: 500; + display: inline-block; + min-width: 1ch; + overflow: hidden; } } @@ -1015,9 +1007,9 @@ $hero-height: 300px; &__review-input-container { display: flex; flex-direction: column; - border: 1px solid #3a3a3a; + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; - background-color: #1e1e1e; + background-color: globals.$dark-background-color; overflow: hidden; } @@ -1026,8 +1018,8 @@ $hero-height: 300px; justify-content: space-between; align-items: center; padding: 8px 12px; - background-color: #2a2a2a; - border-bottom: 1px solid #3a3a3a; + background-color: globals.$background-color; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } &__review-editor-toolbar { @@ -1037,7 +1029,7 @@ $hero-height: 300px; &__editor-button { background: none; - border: 1px solid #4a4a4a; + border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 4px; color: #ffffff; padding: 4px 8px; @@ -1046,13 +1038,13 @@ $hero-height: 300px; transition: all 0.2s ease; &:hover { - background-color: #3a3a3a; - border-color: #5a5a5a; + background-color: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.2); } &.is-active { - background-color: #0078d4; - border-color: #0078d4; + background-color: globals.$brand-blue; + border-color: globals.$brand-blue; } &:disabled { diff --git a/src/renderer/src/pages/settings/settings-debrid.scss b/src/renderer/src/pages/settings/settings-debrid.scss new file mode 100644 index 00000000..749ddbbc --- /dev/null +++ b/src/renderer/src/pages/settings/settings-debrid.scss @@ -0,0 +1,71 @@ +@use "../../scss/globals.scss"; + +.settings-debrid { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2); + + &__description { + margin: 0 0 calc(globals.$spacing-unit * 2) 0; + color: var(--text-secondary); + line-height: 1.6; + } + + &__section { + display: flex; + flex-direction: column; + + &:not(:last-child) { + margin-bottom: calc(globals.$spacing-unit * 2); + } + } + + &__section-header { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit); + margin-bottom: calc(globals.$spacing-unit * 2); + } + + &__section-title { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + } + + &__collapse-button { + background: none; + border: none; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all ease 0.2s; + flex-shrink: 0; + + &:hover { + color: rgba(255, 255, 255, 0.9); + background-color: rgba(255, 255, 255, 0.1); + } + } + + &__check-icon { + color: white; + flex-shrink: 0; + } + + &__beta-badge { + background: linear-gradient(135deg, #c9aa71, #d4af37); + color: #1a1a1a; + font-size: 0.625rem; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; + letter-spacing: 0.5px; + flex-shrink: 0; + } +} diff --git a/src/renderer/src/pages/settings/settings-debrid.tsx b/src/renderer/src/pages/settings/settings-debrid.tsx new file mode 100644 index 00000000..4bb7d276 --- /dev/null +++ b/src/renderer/src/pages/settings/settings-debrid.tsx @@ -0,0 +1,228 @@ +import { useState, useCallback, useMemo } from "react"; +import { useFeature, useAppSelector } from "@renderer/hooks"; +import { SettingsTorBox } from "./settings-torbox"; +import { SettingsRealDebrid } from "./settings-real-debrid"; +import { SettingsAllDebrid } from "./settings-all-debrid"; +import { motion, AnimatePresence } from "framer-motion"; +import { ChevronRightIcon, CheckCircleFillIcon } from "@primer/octicons-react"; +import { useTranslation } from "react-i18next"; +import "./settings-debrid.scss"; + +interface CollapseState { + torbox: boolean; + realDebrid: boolean; + allDebrid: boolean; +} + +const sectionVariants = { + collapsed: { + opacity: 0, + y: -20, + height: 0, + transition: { + duration: 0.3, + ease: [0.25, 0.1, 0.25, 1], + opacity: { duration: 0.1 }, + y: { duration: 0.1 }, + height: { duration: 0.2 }, + }, + }, + expanded: { + opacity: 1, + y: 0, + height: "auto", + transition: { + duration: 0.3, + ease: [0.25, 0.1, 0.25, 1], + opacity: { duration: 0.2, delay: 0.1 }, + y: { duration: 0.3 }, + height: { duration: 0.3 }, + }, + }, +}; + +const chevronVariants = { + collapsed: { + rotate: 0, + transition: { + duration: 0.2, + ease: "easeInOut", + }, + }, + expanded: { + rotate: 90, + transition: { + duration: 0.2, + ease: "easeInOut", + }, + }, +}; + +export function SettingsDebrid() { + const { t } = useTranslation("settings"); + const { isFeatureEnabled, Feature } = useFeature(); + const isTorBoxEnabled = isFeatureEnabled(Feature.TorBox); + + const userPreferences = useAppSelector( + (state) => state.userPreferences.value + ); + + const initialCollapseState = useMemo(() => { + return { + torbox: !userPreferences?.torBoxApiToken, + realDebrid: !userPreferences?.realDebridApiToken, + allDebrid: !userPreferences?.allDebridApiKey, + }; + }, [userPreferences]); + + const [collapseState, setCollapseState] = + useState(initialCollapseState); + + const toggleSection = useCallback((section: keyof CollapseState) => { + setCollapseState((prevState) => ({ + ...prevState, + [section]: !prevState[section], + })); + }, []); + + return ( +
    +

    {t("debrid_description")}

    + +
    +
    + +

    Real-Debrid

    + {userPreferences?.realDebridApiToken && ( + + )} +
    + + + {!collapseState.realDebrid && ( + + + + )} + +
    + + {isTorBoxEnabled && ( +
    +
    + +

    TorBox

    + {userPreferences?.torBoxApiToken && ( + + )} +
    + + + {!collapseState.torbox && ( + + + + )} + +
    + )} + +
    +
    + +

    All-Debrid

    + BETA + {userPreferences?.allDebridApiKey && ( + + )} +
    + + + {!collapseState.allDebrid && ( + + + + )} + +
    +
    + ); +} diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index d609d218..eb19af31 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -1,7 +1,5 @@ import { Button } from "@renderer/components"; import { useTranslation } from "react-i18next"; -import { SettingsRealDebrid } from "./settings-real-debrid"; -import { SettingsAllDebrid } from "./settings-all-debrid"; import { SettingsGeneral } from "./settings-general"; import { SettingsBehavior } from "./settings-behavior"; import { SettingsDownloadSources } from "./settings-download-sources"; @@ -10,21 +8,17 @@ import { SettingsContextProvider, } from "@renderer/context"; import { SettingsAccount } from "./settings-account"; -import { useFeature, useUserDetails } from "@renderer/hooks"; +import { useUserDetails } from "@renderer/hooks"; import { useMemo } from "react"; import "./settings.scss"; import { SettingsAppearance } from "./aparence/settings-appearance"; -import { SettingsTorBox } from "./settings-torbox"; +import { SettingsDebrid } from "./settings-debrid"; export default function Settings() { const { t } = useTranslation("settings"); const { userDetails } = useUserDetails(); - const { isFeatureEnabled, Feature } = useFeature(); - - const isTorBoxEnabled = isFeatureEnabled(Feature.TorBox); - const categories = useMemo(() => { const categories = [ { tabLabel: t("general"), contentTitle: t("general") }, @@ -34,16 +28,7 @@ export default function Settings() { tabLabel: t("appearance"), contentTitle: t("appearance"), }, - ...(isTorBoxEnabled - ? [ - { - tabLabel: "TorBox", - contentTitle: "TorBox", - }, - ] - : []), - { tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" }, - { tabLabel: "All-Debrid", contentTitle: "All-Debrid" }, + { tabLabel: t("debrid"), contentTitle: t("debrid") }, ]; if (userDetails) @@ -52,7 +37,7 @@ export default function Settings() { { tabLabel: t("account"), contentTitle: t("account") }, ]; return categories; - }, [userDetails, t, isTorBoxEnabled]); + }, [userDetails, t]); return ( @@ -76,15 +61,7 @@ export default function Settings() { } if (currentCategoryIndex === 4) { - return ; - } - - if (currentCategoryIndex === 5) { - return ; - } - - if (currentCategoryIndex === 6) { - return ; + return ; } return ; From 5877c8c79807a1c2495a9267d649f88cf0c99812 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 12 Oct 2025 18:40:05 +0100 Subject: [PATCH 39/52] feat: adding review styling --- src/main/events/misc/get-hydra-decky-plugin-info.ts | 1 - .../src/pages/game-details/sidebar/sidebar.tsx | 13 +++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/events/misc/get-hydra-decky-plugin-info.ts b/src/main/events/misc/get-hydra-decky-plugin-info.ts index da72033e..6ff7b050 100644 --- a/src/main/events/misc/get-hydra-decky-plugin-info.ts +++ b/src/main/events/misc/get-hydra-decky-plugin-info.ts @@ -60,4 +60,3 @@ const getHydraDeckyPluginInfo = async ( }; registerEvent("getHydraDeckyPluginInfo", getHydraDeckyPluginInfo); - diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 33009508..df1429ec 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -233,9 +233,18 @@ export function Sidebar() { {t("rating_count")}

    From 34aea2b0c421286b285a58b35e9df86be28bdac6 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 12 Oct 2025 18:51:47 +0100 Subject: [PATCH 40/52] feat: adding api call for decky plugin --- src/locales/en/translation.json | 1 + src/locales/pt-BR/translation.json | 1 + .../misc/get-hydra-decky-plugin-info.ts | 36 ++- .../events/misc/install-hydra-decky-plugin.ts | 2 +- src/main/services/decky-plugin.ts | 209 +++++++++++++----- .../src/components/sidebar/sidebar.tsx | 17 +- src/renderer/src/declaration.d.ts | 2 + src/shared/html-sanitizer.ts | 2 - 8 files changed, 199 insertions(+), 71 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 3dc93d90..0ca77d87 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -78,6 +78,7 @@ "edit_game_modal_drop_to_replace_logo": "Drop to replace logo", "edit_game_modal_drop_to_replace_hero": "Drop to replace hero", "install_decky_plugin": "Install Decky Plugin", + "update_decky_plugin": "Update Decky Plugin", "decky_plugin_installed_version": "Decky Plugin (v{{version}})", "install_decky_plugin_title": "Install Hydra Decky Plugin", "install_decky_plugin_message": "This will download and install the Hydra plugin for Decky Loader. This may require elevated permissions. Continue?", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index e9a84c89..b9cee539 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -78,6 +78,7 @@ "edit_game_modal_drop_to_replace_logo": "Solte para substituir o logo", "edit_game_modal_drop_to_replace_hero": "Solte para substituir o hero", "install_decky_plugin": "Instalar Plugin Decky", + "update_decky_plugin": "Atualizar Plugin Decky", "decky_plugin_installed_version": "Plugin Decky (v{{version}})", "install_decky_plugin_title": "Instalar Plugin Hydra Decky", "install_decky_plugin_message": "Isso irá baixar e instalar o plugin Hydra para Decky Loader. Pode ser necessário permissões elevadas. Continuar?", diff --git a/src/main/events/misc/get-hydra-decky-plugin-info.ts b/src/main/events/misc/get-hydra-decky-plugin-info.ts index 6ff7b050..430bd691 100644 --- a/src/main/events/misc/get-hydra-decky-plugin-info.ts +++ b/src/main/events/misc/get-hydra-decky-plugin-info.ts @@ -1,17 +1,37 @@ import { registerEvent } from "../register-event"; -import { logger } from "@main/services"; +import { logger, HydraApi } from "@main/services"; import { HYDRA_DECKY_PLUGIN_LOCATION } from "@main/constants"; import fs from "node:fs"; import path from "node:path"; +interface DeckyReleaseInfo { + version: string; + downloadUrl: string; +} + const getHydraDeckyPluginInfo = async ( _event: Electron.IpcMainInvokeEvent ): Promise<{ installed: boolean; version: string | null; path: string; + outdated: boolean; + expectedVersion: string | null; }> => { try { + // Fetch the expected version from API + let expectedVersion: string | null = null; + try { + const releaseInfo = await HydraApi.get( + "/decky/release", + {}, + { needsAuth: false } + ); + expectedVersion = releaseInfo.version; + } catch (error) { + logger.error("Failed to fetch Decky release info:", error); + } + // Check if plugin folder exists if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) { logger.log("Hydra Decky plugin not installed"); @@ -19,6 +39,8 @@ const getHydraDeckyPluginInfo = async ( installed: false, version: null, path: HYDRA_DECKY_PLUGIN_LOCATION, + outdated: true, + expectedVersion, }; } @@ -34,6 +56,8 @@ const getHydraDeckyPluginInfo = async ( installed: false, version: null, path: HYDRA_DECKY_PLUGIN_LOCATION, + outdated: true, + expectedVersion, }; } @@ -42,12 +66,18 @@ const getHydraDeckyPluginInfo = async ( const packageJson = JSON.parse(packageJsonContent); const version = packageJson.version; - logger.log(`Hydra Decky plugin installed, version: ${version}`); + const outdated = expectedVersion ? version !== expectedVersion : false; + + logger.log( + `Hydra Decky plugin installed, version: ${version}, expected: ${expectedVersion}, outdated: ${outdated}` + ); return { installed: true, version, path: HYDRA_DECKY_PLUGIN_LOCATION, + outdated, + expectedVersion, }; } catch (error) { logger.error("Failed to get plugin info:", error); @@ -55,6 +85,8 @@ const getHydraDeckyPluginInfo = async ( installed: false, version: null, path: HYDRA_DECKY_PLUGIN_LOCATION, + outdated: true, + expectedVersion: null, }; } }; diff --git a/src/main/events/misc/install-hydra-decky-plugin.ts b/src/main/events/misc/install-hydra-decky-plugin.ts index 3ddbbd64..e14ea2ed 100644 --- a/src/main/events/misc/install-hydra-decky-plugin.ts +++ b/src/main/events/misc/install-hydra-decky-plugin.ts @@ -41,7 +41,7 @@ const installHydraDeckyPlugin = async ( success: false, path: HYDRA_DECKY_PLUGIN_LOCATION, currentVersion: null, - expectedVersion: "0.0.3", + expectedVersion: "unknown", error: errorMessage, }; } diff --git a/src/main/services/decky-plugin.ts b/src/main/services/decky-plugin.ts index 7e178189..4dc1fdad 100644 --- a/src/main/services/decky-plugin.ts +++ b/src/main/services/decky-plugin.ts @@ -11,11 +11,35 @@ import { import { logger } from "./logger"; import { SevenZip } from "./7zip"; import { SystemPath } from "./system-path"; +import { HydraApi } from "./hydra-api"; + +interface DeckyReleaseInfo { + version: string; + downloadUrl: string; +} export class DeckyPlugin { - private static readonly EXPECTED_VERSION = "0.0.3"; - private static readonly DOWNLOAD_URL = - "https://github.com/hydralauncher/decky-hydra-launcher/releases/download/0.0.3/Hydra.zip"; + private static releaseInfo: DeckyReleaseInfo | null = null; + + private static async getDeckyReleaseInfo(): Promise { + if (this.releaseInfo) { + return this.releaseInfo; + } + + try { + const response = await HydraApi.get( + "/decky/release", + {}, + { needsAuth: false } + ); + + this.releaseInfo = response; + return response; + } catch (error) { + logger.error("Failed to fetch Decky release info:", error); + throw error; + } + } private static getPackageJsonPath(): string { return path.join(HYDRA_DECKY_PLUGIN_LOCATION, "package.json"); @@ -24,10 +48,11 @@ export class DeckyPlugin { private static async downloadPlugin(): Promise { logger.log("Downloading Hydra Decky plugin..."); + const releaseInfo = await this.getDeckyReleaseInfo(); const tempDir = SystemPath.getPath("temp"); const zipPath = path.join(tempDir, "Hydra.zip"); - const response = await axios.get(this.DOWNLOAD_URL, { + const response = await axios.get(releaseInfo.downloadUrl, { responseType: "arraybuffer", }); @@ -209,14 +234,15 @@ export class DeckyPlugin { return; } + const releaseInfo = await this.getDeckyReleaseInfo(); const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonContent); const currentVersion = packageJson.version; - const isOutdated = currentVersion !== this.EXPECTED_VERSION; + const isOutdated = currentVersion !== releaseInfo.version; if (isOutdated) { logger.log( - `Hydra Decky plugin is outdated. Current: ${currentVersion}, Expected: ${this.EXPECTED_VERSION}. Updating...` + `Hydra Decky plugin is outdated. Current: ${currentVersion}, Expected: ${releaseInfo.version}. Updating...` ); await this.updatePlugin(); @@ -235,78 +261,139 @@ export class DeckyPlugin { currentVersion: string | null; expectedVersion: string; }> { - if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) { - logger.log("Hydra Decky plugin folder not found, installing..."); + try { + const releaseInfo = await this.getDeckyReleaseInfo(); + + if (!fs.existsSync(HYDRA_DECKY_PLUGIN_LOCATION)) { + logger.log("Hydra Decky plugin folder not found, installing..."); + + try { + await this.updatePlugin(); + + // Read the actual installed version from package.json + const packageJsonPath = this.getPackageJsonPath(); + if (fs.existsSync(packageJsonPath)) { + const packageJsonContent = fs.readFileSync( + packageJsonPath, + "utf-8" + ); + const packageJson = JSON.parse(packageJsonContent); + return { + exists: true, + outdated: false, + currentVersion: packageJson.version, + expectedVersion: releaseInfo.version, + }; + } + + return { + exists: true, + outdated: false, + currentVersion: releaseInfo.version, + expectedVersion: releaseInfo.version, + }; + } catch (error) { + logger.error("Failed to install plugin:", error); + return { + exists: false, + outdated: true, + currentVersion: null, + expectedVersion: releaseInfo.version, + }; + } + } + + const packageJsonPath = this.getPackageJsonPath(); try { - await this.updatePlugin(); + if (!fs.existsSync(packageJsonPath)) { + logger.log( + "Hydra Decky plugin package.json not found, installing..." + ); + + await this.updatePlugin(); + + // Read the actual installed version from package.json + if (fs.existsSync(packageJsonPath)) { + const packageJsonContent = fs.readFileSync( + packageJsonPath, + "utf-8" + ); + const packageJson = JSON.parse(packageJsonContent); + return { + exists: true, + outdated: false, + currentVersion: packageJson.version, + expectedVersion: releaseInfo.version, + }; + } + + return { + exists: true, + outdated: false, + currentVersion: releaseInfo.version, + expectedVersion: releaseInfo.version, + }; + } + + const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(packageJsonContent); + const currentVersion = packageJson.version; + const isOutdated = currentVersion !== releaseInfo.version; + + if (isOutdated) { + logger.log( + `Hydra Decky plugin is outdated. Current: ${currentVersion}, Expected: ${releaseInfo.version}` + ); + + await this.updatePlugin(); + + if (fs.existsSync(packageJsonPath)) { + const updatedPackageJsonContent = fs.readFileSync( + packageJsonPath, + "utf-8" + ); + const updatedPackageJson = JSON.parse(updatedPackageJsonContent); + return { + exists: true, + outdated: false, + currentVersion: updatedPackageJson.version, + expectedVersion: releaseInfo.version, + }; + } + + return { + exists: true, + outdated: false, + currentVersion: releaseInfo.version, + expectedVersion: releaseInfo.version, + }; + } else { + logger.log(`Hydra Decky plugin is up to date (${currentVersion})`); + } + return { exists: true, - outdated: false, - currentVersion: this.EXPECTED_VERSION, - expectedVersion: this.EXPECTED_VERSION, + outdated: isOutdated, + currentVersion, + expectedVersion: releaseInfo.version, }; } catch (error) { - logger.error("Failed to install plugin:", error); + logger.error(`Error checking Hydra Decky plugin version: ${error}`); return { exists: false, outdated: true, currentVersion: null, - expectedVersion: this.EXPECTED_VERSION, + expectedVersion: releaseInfo.version, }; } - } - - const packageJsonPath = this.getPackageJsonPath(); - - try { - if (!fs.existsSync(packageJsonPath)) { - logger.log("Hydra Decky plugin package.json not found, installing..."); - - await this.updatePlugin(); - return { - exists: true, - outdated: false, - currentVersion: this.EXPECTED_VERSION, - expectedVersion: this.EXPECTED_VERSION, - }; - } - - const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8"); - const packageJson = JSON.parse(packageJsonContent); - const currentVersion = packageJson.version; - const isOutdated = currentVersion !== this.EXPECTED_VERSION; - - if (isOutdated) { - logger.log( - `Hydra Decky plugin is outdated. Current: ${currentVersion}, Expected: ${this.EXPECTED_VERSION}` - ); - - await this.updatePlugin(); - - return { - exists: true, - outdated: false, - currentVersion: this.EXPECTED_VERSION, - expectedVersion: this.EXPECTED_VERSION, - }; - } else { - logger.log(`Hydra Decky plugin is up to date (${currentVersion})`); - } - - return { - exists: true, - outdated: isOutdated, - currentVersion, - expectedVersion: this.EXPECTED_VERSION, - }; } catch (error) { - logger.error(`Error checking Hydra Decky plugin version: ${error}`); + logger.error(`Error fetching release info: ${error}`); return { exists: false, outdated: true, currentVersion: null, - expectedVersion: this.EXPECTED_VERSION, + expectedVersion: "unknown", }; } } diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index 13647556..5dd6ae94 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -51,7 +51,8 @@ export function Sidebar() { const [deckyPluginInfo, setDeckyPluginInfo] = useState<{ installed: boolean; version: string | null; - }>({ installed: false, version: null }); + outdated: boolean; + }>({ installed: false, version: null, outdated: false }); const [homebrewFolderExists, setHomebrewFolderExists] = useState(false); const [showDeckyConfirmModal, setShowDeckyConfirmModal] = useState(false); const navigate = useNavigate(); @@ -102,6 +103,7 @@ export function Sidebar() { setDeckyPluginInfo({ installed: info.installed, version: info.version, + outdated: info.outdated, }); setHomebrewFolderExists(folderExists); } catch (error) { @@ -110,6 +112,9 @@ export function Sidebar() { }; const handleInstallHydraDeckyPlugin = () => { + if (deckyPluginInfo.installed && !deckyPluginInfo.outdated) { + return; + } setShowDeckyConfirmModal(true); }; @@ -318,11 +323,13 @@ export function Sidebar() { style={{ width: 16, height: 16 }} /> - {deckyPluginInfo.installed + {deckyPluginInfo.installed && !deckyPluginInfo.outdated ? t("decky_plugin_installed_version", { version: deckyPluginInfo.version, }) - : t("install_decky_plugin")} + : deckyPluginInfo.installed && deckyPluginInfo.outdated + ? t("update_decky_plugin") + : t("install_decky_plugin")} @@ -433,12 +440,12 @@ export function Sidebar() { ; checkHomebrewFolderExists: () => Promise; onCommonRedistProgress: ( diff --git a/src/shared/html-sanitizer.ts b/src/shared/html-sanitizer.ts index ea3d475b..c78d7dd8 100644 --- a/src/shared/html-sanitizer.ts +++ b/src/shared/html-sanitizer.ts @@ -6,8 +6,6 @@ function removeZalgoText(text: string): string { return text.replaceAll(zalgoRegex, ""); } - - export function sanitizeHtml(html: string): string { if (!html || typeof html !== "string") { return ""; From 38b04ee991d5317350a18c9077782d6af1237b20 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 12 Oct 2025 19:01:07 +0100 Subject: [PATCH 41/52] fix: fixing translations --- src/shared/html-sanitizer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/html-sanitizer.ts b/src/shared/html-sanitizer.ts index 01027b2f..4a243e44 100644 --- a/src/shared/html-sanitizer.ts +++ b/src/shared/html-sanitizer.ts @@ -1,4 +1,5 @@ function removeZalgoText(text: string): string { + // eslint-disable-next-line no-misleading-character-class const zalgoRegex = /[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/g; From 030b3b8f7c9c5995c936691e0e128948006a8a1b Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 12 Oct 2025 19:03:30 +0100 Subject: [PATCH 42/52] fix: fixing translations --- src/shared/html-sanitizer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shared/html-sanitizer.ts b/src/shared/html-sanitizer.ts index 4a243e44..778a5ee4 100644 --- a/src/shared/html-sanitizer.ts +++ b/src/shared/html-sanitizer.ts @@ -1,7 +1,8 @@ function removeZalgoText(text: string): string { - // eslint-disable-next-line no-misleading-character-class + // Match combining characters that are commonly used in Zalgo text + // Using alternation instead of character class to avoid misleading-character-class warning const zalgoRegex = - /[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/g; + /(\u0300-\u036F|\u1AB0-\u1AFF|\u1DC0-\u1DFF|\u20D0-\u20FF|\uFE20-\uFE2F)/g; return text.replaceAll(zalgoRegex, ""); } From c2e5bc0e91d14a692d7d60f06aa31ce97ceb03fe Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 12 Oct 2025 19:04:10 +0100 Subject: [PATCH 43/52] fix: fixing translations --- src/shared/html-sanitizer.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/shared/html-sanitizer.ts b/src/shared/html-sanitizer.ts index 778a5ee4..8f3ae932 100644 --- a/src/shared/html-sanitizer.ts +++ b/src/shared/html-sanitizer.ts @@ -1,10 +1,19 @@ function removeZalgoText(text: string): string { // Match combining characters that are commonly used in Zalgo text - // Using alternation instead of character class to avoid misleading-character-class warning - const zalgoRegex = - /(\u0300-\u036F|\u1AB0-\u1AFF|\u1DC0-\u1DFF|\u20D0-\u20FF|\uFE20-\uFE2F)/g; + // Using a more explicit approach to avoid misleading-character-class warning + const combiningMarks = [ + /\u0300-\u036F/g, // Combining Diacritical Marks + /\u1AB0-\u1AFF/g, // Combining Diacritical Marks Extended + /\u1DC0-\u1DFF/g, // Combining Diacritical Marks Supplement + /\u20D0-\u20FF/g, // Combining Diacritical Marks for Symbols + /\uFE20-\uFE2F/g, // Combining Half Marks + ]; - return text.replaceAll(zalgoRegex, ""); + let result = text; + for (const regex of combiningMarks) { + result = result.replace(regex, ""); + } + return result; } export function sanitizeHtml(html: string): string { From 1cba3f350c438e281a270d070ea25c8e1bee6959 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 12 Oct 2025 22:12:15 +0300 Subject: [PATCH 44/52] formatting --- src/shared/html-sanitizer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/shared/html-sanitizer.ts b/src/shared/html-sanitizer.ts index ea3d475b..c78d7dd8 100644 --- a/src/shared/html-sanitizer.ts +++ b/src/shared/html-sanitizer.ts @@ -6,8 +6,6 @@ function removeZalgoText(text: string): string { return text.replaceAll(zalgoRegex, ""); } - - export function sanitizeHtml(html: string): string { if (!html || typeof html !== "string") { return ""; From 7c33c43d9c3475d00b7a1a472a035ff7dec06319 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 12 Oct 2025 20:16:37 +0100 Subject: [PATCH 45/52] fix: fixing search on sources modal --- .../components/context-menu/context-menu.scss | 3 +- .../components/context-menu/context-menu.tsx | 4 +-- .../game-context-menu/game-context-menu.tsx | 18 ++++++++++-- .../game-context-menu/use-game-actions.ts | 28 ++++++++++++++++++- .../game-details/modals/repacks-modal.tsx | 14 +++++++++- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/components/context-menu/context-menu.scss b/src/renderer/src/components/context-menu/context-menu.scss index 673fa2da..e292b22d 100644 --- a/src/renderer/src/components/context-menu/context-menu.scss +++ b/src/renderer/src/components/context-menu/context-menu.scss @@ -19,7 +19,6 @@ &__item-container { position: relative; - padding-right: 8px; } &__item { @@ -97,7 +96,7 @@ &__submenu { position: absolute; - left: calc(100% - 2px); + left: 100%; top: 0; background-color: globals.$background-color; border: 1px solid globals.$border-color; diff --git a/src/renderer/src/components/context-menu/context-menu.tsx b/src/renderer/src/components/context-menu/context-menu.tsx index cb9e0347..34431728 100644 --- a/src/renderer/src/components/context-menu/context-menu.tsx +++ b/src/renderer/src/components/context-menu/context-menu.tsx @@ -144,9 +144,9 @@ export function ContextMenu({ if (parentRect.right + submenuWidth > viewportWidth - 8) { styles.left = "auto"; - styles.right = "calc(100% - 2px)"; + styles.right = "100%"; } else { - styles.left = "calc(100% - 2px)"; + styles.left = "100%"; styles.right = undefined; } diff --git a/src/renderer/src/components/game-context-menu/game-context-menu.tsx b/src/renderer/src/components/game-context-menu/game-context-menu.tsx index e424dac7..694012b7 100644 --- a/src/renderer/src/components/game-context-menu/game-context-menu.tsx +++ b/src/renderer/src/components/game-context-menu/game-context-menu.tsx @@ -40,9 +40,11 @@ export function GameContextMenu({ canPlay, isDeleting, isGameDownloading, + isGameRunning, hasRepacks, shouldShowCreateStartMenuShortcut, handlePlayGame, + handleCloseGame, handleToggleFavorite, handleCreateShortcut, handleCreateSteamShortcut, @@ -57,10 +59,20 @@ export function GameContextMenu({ const items: ContextMenuItemData[] = [ { id: "play", - label: canPlay ? t("play") : t("download"), - icon: canPlay ? : , + label: isGameRunning ? t("close") : canPlay ? t("play") : t("download"), + icon: isGameRunning ? ( + + ) : canPlay ? ( + + ) : ( + + ), onClick: () => { - void handlePlayGame(); + if (isGameRunning) { + void handleCloseGame(); + } else { + void handlePlayGame(); + } }, disabled: isDeleting, }, diff --git a/src/renderer/src/components/game-context-menu/use-game-actions.ts b/src/renderer/src/components/game-context-menu/use-game-actions.ts index 042b796b..c7224225 100644 --- a/src/renderer/src/components/game-context-menu/use-game-actions.ts +++ b/src/renderer/src/components/game-context-menu/use-game-actions.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { LibraryGame, ShortcutLocation } from "@types"; import { useDownload, useLibrary, useToast } from "@renderer/hooks"; @@ -21,6 +21,7 @@ export function useGameActions(game: LibraryGame) { } = useDownload(); const [creatingSteamShortcut, setCreatingSteamShortcut] = useState(false); + const [isGameRunning, setIsGameRunning] = useState(false); const canPlay = Boolean(game.executablePath); const isDeleting = isGameDeleting(game.id); @@ -30,6 +31,20 @@ export function useGameActions(game: LibraryGame) { const shouldShowCreateStartMenuShortcut = window.electron.platform === "win32"; + useEffect(() => { + const unsubscribe = window.electron.onGamesRunning((gamesIds) => { + const updatedIsGameRunning = + !!game?.id && + !!gamesIds.find((gameRunning) => gameRunning.id == game.id); + + setIsGameRunning(updatedIsGameRunning); + }); + + return () => { + unsubscribe(); + }; + }, [game?.id]); + const handlePlayGame = async () => { if (!canPlay) { const path = buildGameDetailsPath({ @@ -75,6 +90,15 @@ export function useGameActions(game: LibraryGame) { } }; + const handleCloseGame = async () => { + try { + await window.electron.closeGame(game.shop, game.objectId); + } catch (error) { + showErrorToast("Failed to close game"); + logger.error("Failed to close game", error); + } + }; + const handleToggleFavorite = async () => { try { if (game.favorite) { @@ -239,10 +263,12 @@ export function useGameActions(game: LibraryGame) { canPlay, isDeleting, isGameDownloading, + isGameRunning, hasRepacks, shouldShowCreateStartMenuShortcut, creatingSteamShortcut, handlePlayGame, + handleCloseGame, handleToggleFavorite, handleCreateShortcut, handleCreateSteamShortcut, diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index 97b8b1b5..f2d1f974 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -161,6 +161,14 @@ export function RepacksModal({ const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); + useEffect(() => { + if (!visible) { + setFilterTerm(""); + setSelectedFingerprints([]); + setIsFilterDrawerOpen(false); + } + }, [visible]); + return ( <>
    - + {downloadSources.length > 0 && (
    )} - {reviews.map((review) => ( -
    - {review.isBlocked && - !visibleBlockedReviews.has(review.id) ? ( -
    - Review from blocked user —{" "} - -
    - ) : ( - <> -
    -
    - {review.user?.profileImageUrl && ( - - )} -
    - -
    - - {formatDistance( - new Date(review.createdAt), - new Date(), - { addSuffix: true } - )} +
    0 ? 0.5 : 1, + transition: "opacity 0.2s ease", + }} + > + {reviews.map((review) => ( +
    + {review.isBlocked && + !visibleBlockedReviews.has(review.id) ? ( +
    + Review from blocked user —{" "} + +
    + ) : ( + <> +
    +
    + {review.user?.profileImageUrl && ( + + )} +
    + +
    + + {formatDistance( + new Date(review.createdAt), + new Date(), + { addSuffix: true } + )} +
    +
    + {[1, 2, 3, 4, 5].map((starValue) => ( + + ))} +
    - {[1, 2, 3, 4, 5].map((starValue) => ( - +
    +
    + + handleVoteReview(review.id, "upvote") + } + animate={ + review.hasUpvoted + ? { + scale: [1, 1.2, 1], + transition: { duration: 0.3 }, + } + : {} } - className={`game-details__review-star ${ - starValue <= review.score - ? "game-details__review-star--filled" - : "game-details__review-star--empty" - } ${ - starValue <= review.score - ? getScoreColorClass(review.score) - : "" - }`} - /> - ))} -
    -
    -
    -
    -
    - - handleVoteReview(review.id, "upvote") - } - animate={ - review.hasUpvoted - ? { - scale: [1, 1.2, 1], - transition: { duration: 0.3 }, - } - : {} - } - > - - - - (previousVotesRef.current.get(review.id) - ?.upvotes || 0) - } - variants={{ - enter: (isIncreasing: boolean) => ({ - y: isIncreasing ? 10 : -10, - opacity: 0, - }), - center: { y: 0, opacity: 1 }, - exit: (isIncreasing: boolean) => ({ - y: isIncreasing ? -10 : 10, - opacity: 0, - }), - }} - initial="enter" - animate="center" - exit="exit" - transition={{ duration: 0.2 }} - onAnimationComplete={() => { - previousVotesRef.current.set(review.id, { - upvotes: review.upvotes || 0, - downvotes: review.downvotes || 0, - }); - }} - > - {formatNumber(review.upvotes || 0)} - - - - - handleVoteReview(review.id, "downvote") - } - animate={ - review.hasDownvoted - ? { - scale: [1, 1.2, 1], - transition: { duration: 0.3 }, - } - : {} - } - > - - - - (previousVotesRef.current.get(review.id) - ?.downvotes || 0) - } - variants={{ - enter: (isIncreasing: boolean) => ({ - y: isIncreasing ? 10 : -10, - opacity: 0, - }), - center: { y: 0, opacity: 1 }, - exit: (isIncreasing: boolean) => ({ - y: isIncreasing ? -10 : 10, - opacity: 0, - }), - }} - initial="enter" - animate="center" - exit="exit" - transition={{ duration: 0.2 }} - onAnimationComplete={() => { - previousVotesRef.current.set(review.id, { - upvotes: review.upvotes || 0, - downvotes: review.downvotes || 0, - }); - }} - > - {formatNumber(review.downvotes || 0)} - - - -
    - {userDetails?.id === review.user?.id && ( - - )} - {review.isBlocked && - visibleBlockedReviews.has(review.id) && ( -
    + {userDetails?.id === review.user?.id && ( + )} -
    - - )} -
    - ))} + {review.isBlocked && + visibleBlockedReviews.has(review.id) && ( + + )} +
    + + )} +
    + ))} +
    {hasMoreReviews && !reviewsLoading && (