mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 16:53:57 +00:00
ci: fix lint errors
This commit is contained in:
@@ -17,9 +17,9 @@ const uploadImageToCDN = async (imagePath: string): Promise<string> => {
|
||||
const fileSizeInBytes = stat.size;
|
||||
|
||||
// Get presigned URL for achievement image
|
||||
const response = await HydraApi.post<{
|
||||
presignedUrl: string;
|
||||
achievementImageUrl: string;
|
||||
const response = await HydraApi.post<{
|
||||
presignedUrl: string;
|
||||
achievementImageUrl: string;
|
||||
}>("/presigned-urls/achievement-image", {
|
||||
imageExt: path.extname(imagePath).slice(1),
|
||||
imageLength: fileSizeInBytes,
|
||||
@@ -42,11 +42,11 @@ const uploadImageToCDN = async (imagePath: string): Promise<string> => {
|
||||
*/
|
||||
const storeImageLocally = async (imagePath: string): Promise<string> => {
|
||||
const fileBuffer = fs.readFileSync(imagePath);
|
||||
const base64Image = fileBuffer.toString('base64');
|
||||
const base64Image = fileBuffer.toString("base64");
|
||||
const mimeType = await fileTypeFromFile(imagePath);
|
||||
|
||||
|
||||
// Create a data URL for local storage
|
||||
return `data:${mimeType?.mime || 'image/jpeg'};base64,${base64Image}`;
|
||||
return `data:${mimeType?.mime || "image/jpeg"};base64,${base64Image}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -77,31 +77,42 @@ export const uploadAchievementImage = async (
|
||||
let imageUrl: string;
|
||||
|
||||
// Check if user has active subscription
|
||||
const hasSubscription = await db
|
||||
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
|
||||
.then((user) => {
|
||||
const expiresAt = new Date(user?.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
})
|
||||
.catch(() => false);
|
||||
const hasSubscription = await db
|
||||
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
|
||||
.then((user) => {
|
||||
const expiresAt = new Date(user?.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
})
|
||||
.catch(() => false);
|
||||
|
||||
if (hasSubscription) {
|
||||
// Upload to CDN and update via API
|
||||
imageUrl = await uploadImageToCDN(imagePath);
|
||||
if (shop) {
|
||||
await updateAchievementWithImageUrl(shop, gameId, achievementName, imageUrl);
|
||||
await updateAchievementWithImageUrl(
|
||||
shop,
|
||||
gameId,
|
||||
achievementName,
|
||||
imageUrl
|
||||
);
|
||||
}
|
||||
logger.log(`Achievement image uploaded to CDN for ${gameId}:${achievementName}`);
|
||||
logger.log(
|
||||
`Achievement image uploaded to CDN for ${gameId}:${achievementName}`
|
||||
);
|
||||
} else {
|
||||
// Store locally
|
||||
imageUrl = await storeImageLocally(imagePath);
|
||||
logger.log(`Achievement image stored locally for ${gameId}:${achievementName}`);
|
||||
logger.log(
|
||||
`Achievement image stored locally for ${gameId}:${achievementName}`
|
||||
);
|
||||
}
|
||||
|
||||
return { success: true, imageUrl };
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Failed to upload achievement image for ${gameId}:${achievementName}:`, error);
|
||||
logger.error(
|
||||
`Failed to upload achievement image for ${gameId}:${achievementName}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -121,12 +132,19 @@ const uploadAchievementImageEvent = async (
|
||||
const { imagePath, gameId, achievementName, shop } = params;
|
||||
|
||||
try {
|
||||
const result = await uploadAchievementImage(gameId, achievementName, imagePath, shop);
|
||||
const result = await uploadAchievementImage(
|
||||
gameId,
|
||||
achievementName,
|
||||
imagePath,
|
||||
shop
|
||||
);
|
||||
|
||||
// Update local database with image URL
|
||||
const achievementKey = levelKeys.game(shop, gameId);
|
||||
const existingData = await gameAchievementsSublevel.get(achievementKey).catch(() => null);
|
||||
|
||||
const existingData = await gameAchievementsSublevel
|
||||
.get(achievementKey)
|
||||
.catch(() => null);
|
||||
|
||||
if (existingData) {
|
||||
await gameAchievementsSublevel.put(achievementKey, {
|
||||
...existingData,
|
||||
@@ -142,17 +160,19 @@ const uploadAchievementImageEvent = async (
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
// Clean up the temporary screenshot file even on error
|
||||
try {
|
||||
fs.unlinkSync(imagePath);
|
||||
} catch (cleanupError) {
|
||||
logger.error(`Failed to cleanup screenshot file ${imagePath}:`, cleanupError);
|
||||
logger.error(
|
||||
`Failed to cleanup screenshot file ${imagePath}:`,
|
||||
cleanupError
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("uploadAchievementImage", uploadAchievementImageEvent);
|
||||
registerEvent("uploadAchievementImage", uploadAchievementImageEvent);
|
||||
|
||||
@@ -39,7 +39,7 @@ export const getUnlockedAchievements = async (
|
||||
const userDetails = await db.get<string, any>(levelKeys.user, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
|
||||
if (userDetails?.id) {
|
||||
remoteUserAchievements = await HydraApi.get<UserAchievement[]>(
|
||||
`/users/${userDetails.id}/games/achievements`,
|
||||
@@ -87,7 +87,8 @@ export const getUnlockedAchievements = async (
|
||||
...achievementData,
|
||||
unlocked: true,
|
||||
unlockTime: unlockedAchievementData.unlockTime,
|
||||
achievementImageUrl: remoteAchievementData?.achievementImageUrl || null,
|
||||
achievementImageUrl:
|
||||
remoteAchievementData?.achievementImageUrl || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -104,8 +104,6 @@ export const mergeAchievements = async (
|
||||
publishNotification &&
|
||||
userPreferences.achievementNotificationsEnabled !== false
|
||||
) {
|
||||
|
||||
|
||||
const filteredAchievements = newAchievements
|
||||
.toSorted((a, b) => {
|
||||
return a.unlockTime - b.unlockTime;
|
||||
@@ -192,37 +190,57 @@ export const mergeAchievements = async (
|
||||
}
|
||||
|
||||
// Capture and upload screenshot AFTER achievements are synced to server
|
||||
if (newAchievements.length && userPreferences.enableAchievementScreenshots === true) {
|
||||
if (
|
||||
newAchievements.length &&
|
||||
userPreferences.enableAchievementScreenshots === true
|
||||
) {
|
||||
try {
|
||||
// Import and trigger the upload process
|
||||
const { uploadAchievementImage } = await import("@main/events/achievements/upload-achievement-image");
|
||||
|
||||
const { uploadAchievementImage } = await import(
|
||||
"@main/events/achievements/upload-achievement-image"
|
||||
);
|
||||
|
||||
// Upload the screenshot for each new achievement
|
||||
for (const achievement of newAchievements) {
|
||||
try {
|
||||
// Find the achievement data to get the display name
|
||||
const achievementData = achievementsData.find((steamAchievement) => {
|
||||
return (
|
||||
achievement.name.toUpperCase() ===
|
||||
steamAchievement.name.toUpperCase()
|
||||
);
|
||||
});
|
||||
|
||||
const achievementDisplayName = achievementData?.displayName || achievement.name;
|
||||
|
||||
// Capture screenshot with game title and achievement name
|
||||
const screenshotPath = await ScreenshotService.captureDesktopScreenshot(
|
||||
game.title,
|
||||
achievementDisplayName
|
||||
const achievementData = achievementsData.find(
|
||||
(steamAchievement) => {
|
||||
return (
|
||||
achievement.name.toUpperCase() ===
|
||||
steamAchievement.name.toUpperCase()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const achievementDisplayName =
|
||||
achievementData?.displayName || achievement.name;
|
||||
|
||||
// Capture screenshot with game title and achievement name
|
||||
const screenshotPath =
|
||||
await ScreenshotService.captureDesktopScreenshot(
|
||||
game.title,
|
||||
achievementDisplayName
|
||||
);
|
||||
|
||||
await uploadAchievementImage(
|
||||
game.objectId,
|
||||
achievement.name,
|
||||
screenshotPath,
|
||||
game.shop
|
||||
);
|
||||
|
||||
await uploadAchievementImage(game.objectId, achievement.name, screenshotPath, game.shop);
|
||||
} catch (error) {
|
||||
achievementsLogger.error("Failed to upload achievement image", error);
|
||||
achievementsLogger.error(
|
||||
"Failed to upload achievement image",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
achievementsLogger.error("Failed to capture screenshot for achievement", error);
|
||||
achievementsLogger.error(
|
||||
"Failed to capture screenshot for achievement",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,20 +10,21 @@ export class ScreenshotService {
|
||||
private static readonly MAX_WIDTH = 1280; // Maximum width for compression
|
||||
private static readonly MAX_HEIGHT = 720; // Maximum height for compression
|
||||
|
||||
|
||||
/**
|
||||
* Compresses an image by resizing and adjusting quality
|
||||
*/
|
||||
private static compressImage(image: Electron.NativeImage): Electron.NativeImage {
|
||||
private static compressImage(
|
||||
image: Electron.NativeImage
|
||||
): Electron.NativeImage {
|
||||
const size = image.getSize();
|
||||
|
||||
|
||||
// Calculate new dimensions while maintaining aspect ratio
|
||||
let newWidth = size.width;
|
||||
let newHeight = size.height;
|
||||
|
||||
|
||||
if (newWidth > this.MAX_WIDTH || newHeight > this.MAX_HEIGHT) {
|
||||
const aspectRatio = newWidth / newHeight;
|
||||
|
||||
|
||||
if (newWidth > newHeight) {
|
||||
newWidth = this.MAX_WIDTH;
|
||||
newHeight = Math.round(newWidth / aspectRatio);
|
||||
@@ -32,21 +33,24 @@ export class ScreenshotService {
|
||||
newWidth = Math.round(newHeight * aspectRatio);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Resize the image if dimensions changed
|
||||
if (newWidth !== size.width || newHeight !== size.height) {
|
||||
return image.resize({ width: newWidth, height: newHeight });
|
||||
}
|
||||
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
public static async captureDesktopScreenshot(gameTitle?: string, achievementName?: string): Promise<string> {
|
||||
public static async captureDesktopScreenshot(
|
||||
gameTitle?: string,
|
||||
achievementName?: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Get all available desktop sources
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ["screen"],
|
||||
thumbnailSize: { width: 1920, height: 1080 }
|
||||
thumbnailSize: { width: 1920, height: 1080 },
|
||||
});
|
||||
|
||||
if (sources.length === 0) {
|
||||
@@ -55,74 +59,79 @@ export class ScreenshotService {
|
||||
|
||||
// Use the primary screen (first source)
|
||||
const primaryScreen = sources[0];
|
||||
|
||||
|
||||
// Convert the thumbnail to a higher quality image
|
||||
const originalImage = nativeImage.createFromDataURL(primaryScreen.thumbnail.toDataURL());
|
||||
|
||||
const originalImage = nativeImage.createFromDataURL(
|
||||
primaryScreen.thumbnail.toDataURL()
|
||||
);
|
||||
|
||||
// Compress the image to reduce file size
|
||||
const compressedImage = this.compressImage(originalImage);
|
||||
|
||||
|
||||
// Create screenshots directory structure
|
||||
const userDataPath = app.getPath("userData");
|
||||
const screenshotsDir = path.join(userDataPath, "screenshots");
|
||||
|
||||
|
||||
let finalDir = screenshotsDir;
|
||||
let filename: string;
|
||||
|
||||
|
||||
if (gameTitle && achievementName) {
|
||||
// Create game-specific directory
|
||||
const sanitizedGameTitle = gameTitle.replace(/[<>:"/\\|?*]/g, '_');
|
||||
const sanitizedGameTitle = gameTitle.replace(/[<>:"/\\|?*]/g, "_");
|
||||
const gameDir = path.join(screenshotsDir, sanitizedGameTitle);
|
||||
finalDir = gameDir;
|
||||
|
||||
|
||||
// Use achievement name as filename (sanitized)
|
||||
const sanitizedAchievementName = achievementName.replace(/[<>:"/\\|?*]/g, '_');
|
||||
const sanitizedAchievementName = achievementName.replace(
|
||||
/[<>:"/\\|?*]/g,
|
||||
"_"
|
||||
);
|
||||
filename = `${sanitizedAchievementName}.${this.SCREENSHOT_FORMAT}`;
|
||||
} else {
|
||||
// Fallback to timestamp-based naming
|
||||
const timestamp = Date.now();
|
||||
filename = `achievement_screenshot_${timestamp}.${this.SCREENSHOT_FORMAT}`;
|
||||
}
|
||||
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(finalDir)) {
|
||||
fs.mkdirSync(finalDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
const screenshotPath = path.join(finalDir, filename);
|
||||
|
||||
|
||||
// Save the compressed screenshot as JPEG with specified quality
|
||||
const jpegBuffer = compressedImage.toJPEG(this.SCREENSHOT_QUALITY);
|
||||
fs.writeFileSync(screenshotPath, jpegBuffer);
|
||||
|
||||
|
||||
logger.log(`Compressed screenshot saved to: ${screenshotPath}`);
|
||||
return screenshotPath;
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Failed to capture desktop screenshot:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async cleanupOldScreenshots(): Promise<void> {
|
||||
try {
|
||||
const userDataPath = app.getPath("userData");
|
||||
const screenshotsDir = path.join(userDataPath, "screenshots");
|
||||
|
||||
|
||||
if (!fs.existsSync(screenshotsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get all files recursively from screenshots directory and subdirectories
|
||||
const getAllFiles = (dir: string): Array<{name: string, path: string, mtime: Date}> => {
|
||||
const files: Array<{name: string, path: string, mtime: Date}> = [];
|
||||
|
||||
const getAllFiles = (
|
||||
dir: string
|
||||
): Array<{ name: string; path: string; mtime: Date }> => {
|
||||
const files: Array<{ name: string; path: string; mtime: Date }> = [];
|
||||
|
||||
const items = fs.readdirSync(dir);
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dir, item);
|
||||
const stat = fs.statSync(itemPath);
|
||||
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// Recursively get files from subdirectories
|
||||
files.push(...getAllFiles(itemPath));
|
||||
@@ -130,20 +139,21 @@ export class ScreenshotService {
|
||||
files.push({
|
||||
name: item,
|
||||
path: itemPath,
|
||||
mtime: stat.mtime
|
||||
mtime: stat.mtime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
const allFiles = getAllFiles(screenshotsDir)
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
|
||||
const allFiles = getAllFiles(screenshotsDir).sort(
|
||||
(a, b) => b.mtime.getTime() - a.mtime.getTime()
|
||||
);
|
||||
|
||||
// Keep only the 50 most recent screenshots (increased from 10 to accommodate multiple games)
|
||||
const filesToDelete = allFiles.slice(50);
|
||||
|
||||
|
||||
for (const file of filesToDelete) {
|
||||
try {
|
||||
fs.unlinkSync(file.path);
|
||||
@@ -152,11 +162,11 @@ export class ScreenshotService {
|
||||
logger.error(`Failed to delete screenshot ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clean up empty directories
|
||||
const cleanupEmptyDirs = (dir: string) => {
|
||||
if (dir === screenshotsDir) return; // Don't delete the main screenshots directory
|
||||
|
||||
|
||||
try {
|
||||
const items = fs.readdirSync(dir);
|
||||
if (items.length === 0) {
|
||||
@@ -167,24 +177,24 @@ export class ScreenshotService {
|
||||
// Directory might not be empty or might not exist, ignore
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Check for empty game directories and clean them up
|
||||
const gameDirectories = fs.readdirSync(screenshotsDir)
|
||||
.map(item => path.join(screenshotsDir, item))
|
||||
.filter(itemPath => {
|
||||
const gameDirectories = fs
|
||||
.readdirSync(screenshotsDir)
|
||||
.map((item) => path.join(screenshotsDir, item))
|
||||
.filter((itemPath) => {
|
||||
try {
|
||||
return fs.statSync(itemPath).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
for (const gameDir of gameDirectories) {
|
||||
cleanupEmptyDirs(gameDir);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Failed to cleanup old screenshots:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { User, type ProfileVisibility, type UserDetails, type UserPreferences } from "@types";
|
||||
import {
|
||||
User,
|
||||
type ProfileVisibility,
|
||||
type UserDetails,
|
||||
type UserPreferences,
|
||||
} from "@types";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { logger } from "../logger";
|
||||
@@ -13,7 +18,7 @@ export const getUserData = async () => {
|
||||
levelKeys.userPreferences,
|
||||
{ valueEncoding: "json" }
|
||||
);
|
||||
|
||||
|
||||
if (userPreferences?.language) {
|
||||
// Map supported languages (pt, ru, es) or fallback to en
|
||||
const supportedLanguages = ["pt", "ru", "es"];
|
||||
|
||||
@@ -103,4 +103,4 @@
|
||||
padding: 60px 20px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,21 @@ export function FullscreenImageModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackdropKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fullscreen-image-modal" onClick={handleBackdropClick}>
|
||||
<div
|
||||
className="fullscreen-image-modal"
|
||||
onClick={handleBackdropClick}
|
||||
onKeyDown={handleBackdropKeyDown}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="fullscreen-image-modal__container">
|
||||
<button
|
||||
className="fullscreen-image-modal__close-button"
|
||||
@@ -51,7 +64,7 @@ export function FullscreenImageModal({
|
||||
>
|
||||
<XIcon size={24} />
|
||||
</button>
|
||||
|
||||
|
||||
<div className="fullscreen-image-modal__image-container">
|
||||
<img
|
||||
src={imageUrl}
|
||||
@@ -63,4 +76,4 @@ export function FullscreenImageModal({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { FullscreenImageModal } from "./fullscreen-image-modal";
|
||||
export { FullscreenImageModal } from "./fullscreen-image-modal";
|
||||
|
||||
@@ -134,8 +134,10 @@ export function UserProfileContextProvider({
|
||||
// Get current language for API call
|
||||
const currentLanguage = i18n.language.split("-")[0];
|
||||
const supportedLanguages = ["pt", "ru", "es"];
|
||||
const language = supportedLanguages.includes(currentLanguage) ? currentLanguage : "en";
|
||||
|
||||
const language = supportedLanguages.includes(currentLanguage)
|
||||
? currentLanguage
|
||||
: "en";
|
||||
|
||||
const params = new URLSearchParams({ language });
|
||||
|
||||
return window.electron.hydraApi
|
||||
@@ -153,7 +155,15 @@ export function UserProfileContextProvider({
|
||||
showErrorToast(t("user_not_found"));
|
||||
navigate(-1);
|
||||
});
|
||||
}, [navigate, getUserStats, getUserLibraryGames, showErrorToast, userId, t, i18n]);
|
||||
}, [
|
||||
navigate,
|
||||
getUserStats,
|
||||
getUserLibraryGames,
|
||||
showErrorToast,
|
||||
userId,
|
||||
t,
|
||||
i18n,
|
||||
]);
|
||||
|
||||
const getBadges = useCallback(async () => {
|
||||
const language = i18n.language.split("-")[0];
|
||||
|
||||
@@ -71,8 +71,25 @@ export function AchievementList({
|
||||
src={achievement.achievementImageUrl}
|
||||
alt={`${achievement.displayName} screenshot`}
|
||||
loading="lazy"
|
||||
onClick={() => handleImageClick(achievement.achievementImageUrl!, achievement.displayName)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
handleImageClick(
|
||||
achievement.achievementImageUrl,
|
||||
achievement.displayName
|
||||
)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleImageClick(
|
||||
achievement.achievementImageUrl,
|
||||
achievement.displayName
|
||||
);
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`View ${achievement.displayName} screenshot in fullscreen`}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
<div className="achievements__item-custom-image-overlay">
|
||||
<SearchIcon size={20} />
|
||||
@@ -80,7 +97,7 @@ export function AchievementList({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{achievement.points != undefined ? (
|
||||
<div
|
||||
className="achievements__item-points"
|
||||
@@ -103,7 +120,7 @@ export function AchievementList({
|
||||
<p className="achievements__item-points-value">???</p>
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
{achievement.unlockTime != null && (
|
||||
<div
|
||||
className="achievements__item-unlock-time"
|
||||
@@ -117,7 +134,7 @@ export function AchievementList({
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
|
||||
<FullscreenImageModal
|
||||
isOpen={fullscreenImage !== null}
|
||||
imageUrl={fullscreenImage?.url || ""}
|
||||
|
||||
@@ -185,20 +185,20 @@
|
||||
display: flex;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
padding-bottom: calc(globals.$spacing-unit);
|
||||
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
@@ -213,7 +213,7 @@
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all ease 0.2s;
|
||||
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
@@ -233,7 +233,7 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
|
||||
&:hover .profile-content__souvenir-achievement-image-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -244,8 +244,6 @@
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
|
||||
}
|
||||
|
||||
&__souvenir-achievement-image-overlay {
|
||||
@@ -261,7 +259,7 @@
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
}
|
||||
@@ -356,7 +354,7 @@
|
||||
&__souvenir-unlock-time {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
small {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
@@ -370,7 +368,7 @@
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: transform ease 0.2s;
|
||||
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
@@ -383,7 +381,7 @@
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
transition: border-color ease 0.2s;
|
||||
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||
import { useAppDispatch, useFormat, useDate } from "@renderer/hooks";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { TelescopeIcon, ChevronRightIcon, SearchIcon } from "@primer/octicons-react";
|
||||
import {
|
||||
TelescopeIcon,
|
||||
ChevronRightIcon,
|
||||
SearchIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LockedProfile } from "./locked-profile";
|
||||
import { ReportProfile } from "../report-profile/report-profile";
|
||||
@@ -202,79 +206,96 @@ export function ProfileContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userProfile?.achievements && userProfile.achievements.length > 0 && (
|
||||
<div className="profile-content__souvenirs-section">
|
||||
<div className="profile-content__section-header">
|
||||
<div className="profile-content__section-title-group">
|
||||
<h2>{t("souvenirs")}</h2>
|
||||
<span className="profile-content__section-badge">
|
||||
{userProfile.achievements.length}
|
||||
</span>
|
||||
{userProfile?.achievements &&
|
||||
userProfile.achievements.length > 0 && (
|
||||
<div className="profile-content__souvenirs-section">
|
||||
<div className="profile-content__section-header">
|
||||
<div className="profile-content__section-title-group">
|
||||
<h2>{t("souvenirs")}</h2>
|
||||
<span className="profile-content__section-badge">
|
||||
{userProfile.achievements.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-content__souvenirs-grid">
|
||||
{userProfile.achievements.map((achievement, index) => (
|
||||
<div
|
||||
key={`${achievement.gameTitle}-${achievement.name}-${index}`}
|
||||
className="profile-content__souvenir-card"
|
||||
>
|
||||
<div className="profile-content__souvenir-card-header">
|
||||
<div className="profile-content__souvenir-achievement-image-wrapper">
|
||||
<img
|
||||
src={achievement.achievementImageUrl}
|
||||
alt={achievement.name}
|
||||
className="profile-content__souvenir-achievement-image"
|
||||
loading="lazy"
|
||||
onClick={() => handleImageClick(achievement.achievementImageUrl, achievement.name)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
<div className="profile-content__souvenir-achievement-image-overlay">
|
||||
<SearchIcon size={20} />
|
||||
<div className="profile-content__souvenirs-grid">
|
||||
{userProfile.achievements.map((achievement, index) => (
|
||||
<div
|
||||
key={`${achievement.gameTitle}-${achievement.name}-${index}`}
|
||||
className="profile-content__souvenir-card"
|
||||
>
|
||||
<div className="profile-content__souvenir-card-header">
|
||||
<div className="profile-content__souvenir-achievement-image-wrapper">
|
||||
<img
|
||||
src={achievement.achievementImageUrl}
|
||||
alt={achievement.name}
|
||||
className="profile-content__souvenir-achievement-image"
|
||||
loading="lazy"
|
||||
onClick={() =>
|
||||
handleImageClick(
|
||||
achievement.achievementImageUrl,
|
||||
achievement.name
|
||||
)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleImageClick(achievement.achievementImageUrl, achievement.name);
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`View ${achievement.name} screenshot in fullscreen`}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
<div className="profile-content__souvenir-achievement-image-overlay">
|
||||
<SearchIcon size={20} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-content__souvenir-card-content">
|
||||
<div className="profile-content__souvenir-achievement-info">
|
||||
<img
|
||||
src={achievement.achievementIcon}
|
||||
alt=""
|
||||
className="profile-content__souvenir-achievement-icon"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span className="profile-content__souvenir-achievement-name">
|
||||
{achievement.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-content__souvenir-game-info">
|
||||
<div className="profile-content__souvenir-game-left">
|
||||
|
||||
<div className="profile-content__souvenir-card-content">
|
||||
<div className="profile-content__souvenir-achievement-info">
|
||||
<img
|
||||
src={achievement.gameIconUrl}
|
||||
src={achievement.achievementIcon}
|
||||
alt=""
|
||||
className="profile-content__souvenir-game-icon"
|
||||
className="profile-content__souvenir-achievement-icon"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span className="profile-content__souvenir-game-title">
|
||||
{achievement.gameTitle}
|
||||
<span className="profile-content__souvenir-achievement-name">
|
||||
{achievement.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{achievement.unlockTime && (
|
||||
<div className="profile-content__souvenir-unlock-time">
|
||||
<small>{formatDateTime(achievement.unlockTime)}</small>
|
||||
|
||||
<div className="profile-content__souvenir-game-info">
|
||||
<div className="profile-content__souvenir-game-left">
|
||||
<img
|
||||
src={achievement.gameIconUrl}
|
||||
alt=""
|
||||
className="profile-content__souvenir-game-icon"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span className="profile-content__souvenir-game-title">
|
||||
{achievement.gameTitle}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{achievement.unlockTime && (
|
||||
<div className="profile-content__souvenir-unlock-time">
|
||||
<small>
|
||||
{formatDateTime(achievement.unlockTime)}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="profile-content__souvenir-card-gradient-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div className="profile-content__souvenir-card-gradient-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{hasAnyGames && (
|
||||
<SortOptions sortBy={sortBy} onSortChange={setSortBy} />
|
||||
@@ -343,7 +364,7 @@ export function ProfileContent() {
|
||||
<ProfileHero />
|
||||
|
||||
{content}
|
||||
|
||||
|
||||
<FullscreenImageModal
|
||||
isOpen={fullscreenImage !== null}
|
||||
imageUrl={fullscreenImage?.url || ""}
|
||||
|
||||
Reference in New Issue
Block a user