ci: changed css variables to image from souvenirs, made image add in a single api call with updating achievements, renamed variables

This commit is contained in:
Moyasee
2025-10-24 23:50:59 +03:00
parent f1f69e6dbd
commit a1f419957f
9 changed files with 136 additions and 141 deletions

View File

@@ -85,8 +85,7 @@ export const getUnlockedAchievements = async (
...achievementData,
unlocked: true,
unlockTime: unlockedAchievementData.unlockTime,
achievementImageUrl:
remoteAchievementData?.achievementImageUrl || null,
imageUrl: remoteAchievementData?.imageUrl || null,
};
}
@@ -99,7 +98,7 @@ export const getUnlockedAchievements = async (
!achievementData.hidden || showHiddenAchievementsDescription
? achievementData.description
: undefined,
achievementImageUrl: remoteAchievementData?.achievementImageUrl || null,
imageUrl: remoteAchievementData?.imageUrl || null,
};
})
.sort((a, b) => {

View File

@@ -15,7 +15,7 @@ export class AchievementImageService {
const response = await HydraApi.post<{
presignedUrl: string;
achievementImageUrl: string;
imageUrl: string;
}>("/presigned-urls/achievement-image", {
imageExt: path.extname(imagePath).slice(1),
imageLength: fileSizeInBytes,
@@ -29,7 +29,7 @@ export class AchievementImageService {
},
});
return response.achievementImageUrl;
return response.imageUrl;
}
private static async storeImageLocally(imagePath: string): Promise<string> {
@@ -40,17 +40,6 @@ export class AchievementImageService {
return `data:${mimeType?.mime || "image/jpeg"};base64,${base64Image}`;
}
private static async updateAchievementWithImageUrl(
shop: GameShop,
gameId: string,
achievementName: string,
imageUrl: string
): Promise<void> {
await HydraApi.patch(
`/profile/games/achievements/${shop}/${gameId}/${achievementName}/image`,
{ achievementImageUrl: imageUrl }
);
}
private static async hasActiveSubscription(): Promise<boolean> {
return db
@@ -75,7 +64,7 @@ export class AchievementImageService {
if (existingData) {
await gameAchievementsSublevel.put(achievementKey, {
...existingData,
achievementImageUrl: imageUrl,
imageUrl,
});
}
}
@@ -99,8 +88,7 @@ export class AchievementImageService {
static async uploadAchievementImage(
gameId: string,
achievementName: string,
imagePath: string,
shop?: GameShop
imagePath: string
): Promise<{ success: boolean; imageUrl: string }> {
try {
let imageUrl: string;
@@ -109,14 +97,9 @@ export class AchievementImageService {
if (hasSubscription) {
imageUrl = await this.uploadImageToCDN(imagePath);
if (shop) {
await this.updateAchievementWithImageUrl(
shop,
gameId,
achievementName,
imageUrl
);
}
// Removed per new single-call sync: image URL will be included
// in the PUT /profile/games/achievements payload later.
// No direct API call here anymore.
logger.log(
`Achievement image uploaded to CDN for ${gameId}:${achievementName}`
);
@@ -155,8 +138,7 @@ export class AchievementImageService {
const result = await this.uploadAchievementImage(
gameId,
achievementName,
imagePath,
shop
imagePath
);
await this.updateLocalAchievementData(shop, gameId, result.imageUrl);

View File

@@ -161,6 +161,65 @@ export const mergeAchievements = async (
}
}
// For subscribers, capture and upload screenshots first to get image URLs
let achievementsWithImages = [...mergedLocalAchievements];
if (
newAchievements.length &&
userPreferences.enableAchievementScreenshots === true
) {
try {
for (const achievement of newAchievements) {
try {
const achievementData = achievementsData.find(
(steamAchievement) => {
return (
achievement.name.toUpperCase() ===
steamAchievement.name.toUpperCase()
);
}
);
const achievementDisplayName =
achievementData?.displayName || achievement.name;
const screenshotPath =
await ScreenshotService.captureDesktopScreenshot(
game.title,
achievementDisplayName
);
const uploadResult = await AchievementImageService.uploadAchievementImage(
game.objectId,
achievement.name,
screenshotPath
);
// Update the achievement with the image URL for API sync
const achievementIndex = achievementsWithImages.findIndex(
(a) => a.name.toUpperCase() === achievement.name.toUpperCase()
);
if (achievementIndex !== -1 && uploadResult.imageUrl) {
achievementsWithImages[achievementIndex] = {
...achievementsWithImages[achievementIndex],
imageUrl: uploadResult.imageUrl,
};
}
} catch (error) {
achievementsLogger.error(
"Failed to upload achievement image",
error
);
}
}
} catch (error) {
achievementsLogger.error(
"Failed to capture screenshot for achievement",
error
);
}
}
const shouldSyncWithRemote =
game.remoteId &&
(newAchievements.length || AchievementWatcherManager.hasFinishedPreSearch);
@@ -170,7 +229,7 @@ export const mergeAchievements = async (
"/profile/games/achievements",
{
id: game.remoteId,
achievements: mergedLocalAchievements,
achievements: achievementsWithImages,
},
{ needsSubscription: !newAchievements.length }
)
@@ -186,56 +245,10 @@ export const mergeAchievements = async (
await saveAchievementsOnLocal(
game.objectId,
game.shop,
mergedLocalAchievements,
achievementsWithImages,
publishNotification
);
}
if (
newAchievements.length &&
userPreferences.enableAchievementScreenshots === true
) {
try {
for (const achievement of newAchievements) {
try {
const achievementData = achievementsData.find(
(steamAchievement) => {
return (
achievement.name.toUpperCase() ===
steamAchievement.name.toUpperCase()
);
}
);
const achievementDisplayName =
achievementData?.displayName || achievement.name;
const screenshotPath =
await ScreenshotService.captureDesktopScreenshot(
game.title,
achievementDisplayName
);
await AchievementImageService.uploadAchievementImage(
game.objectId,
achievement.name,
screenshotPath,
game.shop
);
} catch (error) {
achievementsLogger.error(
"Failed to upload achievement image",
error
);
}
}
} catch (error) {
achievementsLogger.error(
"Failed to capture screenshot for achievement",
error
);
}
}
})
.catch((err) => {
if (err instanceof SubscriptionRequiredError) {
@@ -249,7 +262,7 @@ export const mergeAchievements = async (
return saveAchievementsOnLocal(
game.objectId,
game.shop,
mergedLocalAchievements,
achievementsWithImages,
publishNotification
);
})
@@ -260,7 +273,7 @@ export const mergeAchievements = async (
await saveAchievementsOnLocal(
game.objectId,
game.shop,
mergedLocalAchievements,
achievementsWithImages,
publishNotification
);
}

View File

@@ -63,16 +63,16 @@ export function AchievementList({
</div>
<div className="achievements__item-meta">
{achievement.achievementImageUrl && achievement.unlocked && (
{achievement.imageUrl && achievement.unlocked && (
<div className="achievements__item-image-container">
<div className="achievements__item-custom-image-wrapper">
<button
type="button"
className="achievements__item-image-button"
onClick={() =>
achievement.achievementImageUrl &&
achievement.imageUrl &&
handleImageClick(
achievement.achievementImageUrl,
achievement.imageUrl,
achievement.displayName
)
}
@@ -86,7 +86,7 @@ export function AchievementList({
>
<img
className="achievements__item-custom-image"
src={achievement.achievementImageUrl}
src={achievement.imageUrl}
alt={`${achievement.displayName} screenshot`}
loading="lazy"
/>

View File

@@ -177,11 +177,11 @@
}
}
&__souvenirs-section {
&__images-section {
margin-bottom: calc(globals.$spacing-unit * 3);
}
&__souvenirs-grid {
&__images-grid {
display: grid;
gap: calc(globals.$spacing-unit * 2);
padding-bottom: calc(globals.$spacing-unit);
@@ -208,7 +208,7 @@
}
}
&__souvenir-card {
&__image-card {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
@@ -216,60 +216,60 @@
transition: all ease 0.2s;
position: relative;
container-type: inline-size;
&:hover {
transform: translateY(-4px);
background: rgba(0, 0, 0, 0.4);
border-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
@container (max-width: 240px) {
.profile-content__souvenir-achievement-icon {
.profile-content__image-achievement-icon {
width: 20px;
height: 20px;
}
.profile-content__souvenir-achievement-name {
.profile-content__image-achievement-name {
font-size: 13px;
}
.profile-content__souvenir-game-title {
.profile-content__image-game-title {
font-size: 11px;
}
}
@container (max-width: 280px) {
.profile-content__souvenir-card-content {
.profile-content__image-card-content {
gap: calc(globals.$spacing-unit * 0.75);
}
}
}
&__souvenir-card-header {
&__image-card-header {
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
position: relative;
}
&__souvenir-achievement-image-wrapper {
&__image-achievement-image-wrapper {
position: relative;
width: 100%;
height: 100%;
.profile-content__souvenir-image-button {
.profile-content__image-button {
width: 100%;
height: 100%;
display: block;
}
&:hover .profile-content__souvenir-achievement-image-overlay {
&:hover .profile-content__image-achievement-image-overlay {
opacity: 1;
}
}
&__souvenir-achievement-image {
&__image-achievement-image {
width: 100%;
height: 100%;
object-fit: cover;
@@ -277,7 +277,7 @@
display: block;
}
&__souvenir-achievement-image-overlay {
&__image-achievement-image-overlay {
position: absolute;
top: 0;
left: 0;
@@ -290,19 +290,19 @@
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
svg {
color: white;
}
}
// Show overlay on keyboard focus for accessibility
&__souvenir-image-button:focus-visible
+ &__souvenir-achievement-image-overlay {
&__image-button:focus-visible
+ &__image-achievement-image-overlay {
opacity: 1;
}
&__souvenir-card-content {
&__image-card-content {
padding: 16px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
@@ -312,7 +312,7 @@
overflow: hidden;
}
&__souvenir-card-gradient-overlay {
&__image-card-gradient-overlay {
position: absolute;
bottom: 0;
left: 0;
@@ -328,7 +328,7 @@
border-bottom-right-radius: 12px;
}
&__souvenir-achievement-info {
&__image-achievement-info {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 1);
@@ -336,14 +336,14 @@
z-index: 2;
}
&__souvenir-achievement-icon {
&__image-achievement-icon {
width: 24px;
height: 24px;
border-radius: 4px;
flex-shrink: 0;
}
&__souvenir-achievement-name {
&__image-achievement-name {
font-size: 14px;
font-weight: 600;
color: white;
@@ -356,7 +356,7 @@
min-width: 0;
}
&__souvenir-game-info {
&__image-game-info {
display: flex;
align-items: center;
justify-content: space-between;
@@ -366,7 +366,7 @@
min-width: 0;
}
&__souvenir-game-left {
&__image-game-left {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 0.75);
@@ -374,14 +374,14 @@
min-width: 0;
}
&__souvenir-game-icon {
&__image-game-icon {
width: 16px;
height: 16px;
border-radius: 2px;
flex-shrink: 0;
}
&__souvenir-game-title {
&__image-game-title {
font-size: 12px;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
@@ -391,26 +391,26 @@
text-overflow: ellipsis;
}
&__souvenir-item {
&__image-item {
flex-shrink: 0;
position: relative;
border-radius: 8px;
overflow: hidden;
transition: transform ease 0.2s;
&:hover {
transform: scale(1.05);
}
}
&__souvenir-image {
&__image {
width: 64px;
height: 64px;
object-fit: cover;
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);
}

View File

@@ -207,7 +207,7 @@ export function ProfileContent() {
{userProfile?.achievements &&
userProfile.achievements.length > 0 && (
<div className="profile-content__souvenirs-section">
<div className="profile-content__images-section">
<div className="profile-content__section-header">
<div className="profile-content__section-title-group">
<h2>{t("souvenirs")}</h2>
@@ -217,20 +217,20 @@ export function ProfileContent() {
</div>
</div>
<div className="profile-content__souvenirs-grid">
<div className="profile-content__images-grid">
{userProfile.achievements.map((achievement, index) => (
<div
key={`${achievement.gameTitle}-${achievement.name}-${index}`}
className="profile-content__souvenir-card"
className="profile-content__image-card"
>
<div className="profile-content__souvenir-card-header">
<div className="profile-content__souvenir-achievement-image-wrapper">
<div className="profile-content__image-card-header">
<div className="profile-content__image-achievement-image-wrapper">
<button
type="button"
className="profile-content__souvenir-image-button"
className="profile-content__image-button"
onClick={() =>
handleImageClick(
achievement.achievementImageUrl,
achievement.imageUrl,
achievement.name
)
}
@@ -243,47 +243,47 @@ export function ProfileContent() {
}}
>
<img
src={achievement.achievementImageUrl}
src={achievement.imageUrl}
alt={achievement.name}
className="profile-content__souvenir-achievement-image"
className="profile-content__image-achievement-image"
loading="lazy"
/>
</button>
<div className="profile-content__souvenir-achievement-image-overlay">
<div className="profile-content__image-achievement-image-overlay">
<SearchIcon size={20} />
</div>
</div>
</div>
<div className="profile-content__souvenir-card-content">
<div className="profile-content__souvenir-achievement-info">
<div className="profile-content__image-card-content">
<div className="profile-content__image-achievement-info">
<img
src={achievement.achievementIcon}
alt=""
className="profile-content__souvenir-achievement-icon"
className="profile-content__image-achievement-icon"
loading="lazy"
/>
<span className="profile-content__souvenir-achievement-name">
<span className="profile-content__image-achievement-name">
{achievement.name}
</span>
</div>
<div className="profile-content__souvenir-game-info">
<div className="profile-content__souvenir-game-left">
<div className="profile-content__image-game-info">
<div className="profile-content__image-game-left">
<img
src={achievement.gameIconUrl}
alt=""
className="profile-content__souvenir-game-icon"
className="profile-content__image-game-icon"
loading="lazy"
/>
<span className="profile-content__souvenir-game-title">
<span className="profile-content__image-game-title">
{achievement.gameTitle}
</span>
</div>
</div>
</div>
<div className="profile-content__souvenir-card-gradient-overlay"></div>
<div className="profile-content__image-card-gradient-overlay"></div>
</div>
))}
</div>

View File

@@ -5,6 +5,7 @@ export type ShortcutLocation = "desktop" | "start_menu";
export interface UnlockedAchievement {
name: string;
unlockTime: number;
imageUrl?: string | null;
}
export interface SteamAchievement {
@@ -20,5 +21,5 @@ export interface SteamAchievement {
export interface UserAchievement extends SteamAchievement {
unlocked: boolean;
unlockTime: number | null;
achievementImageUrl?: string | null;
imageUrl?: string | null;
}

View File

@@ -196,7 +196,7 @@ export interface UserDetails {
export interface ProfileAchievement {
name: string;
achievementImageUrl: string;
imageUrl: string;
unlockTime: number;
gameTitle: string;
gameIconUrl: string;

View File

@@ -83,7 +83,7 @@ export interface GameAchievement {
achievements: SteamAchievement[];
unlockedAchievements: UnlockedAchievement[];
updatedAt: number | undefined;
achievementImageUrl?: string | null;
imageUrl?: string | null;
language?: string;
}