From f08ad361eda5bea57ecf561a87fdf52ffa10a877 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 2 Oct 2025 00:43:49 +0300 Subject: [PATCH 01/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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: Mon, 6 Oct 2025 15:41:12 +0300 Subject: [PATCH 24/38] 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 25/38] 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 26/38] 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 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 27/38] 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 28/38] 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 29/38] 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 30/38] 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 31/38] 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 32/38] 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 33/38] 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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 |_%gzbZ+qO%ANCsR6RiU; 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 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 34/38] 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 35/38] 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 36/38] 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 37/38] 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 38/38] 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 {