mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 16:53:57 +00:00
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:
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export interface UserDetails {
|
||||
|
||||
export interface ProfileAchievement {
|
||||
name: string;
|
||||
achievementImageUrl: string;
|
||||
imageUrl: string;
|
||||
unlockTime: number;
|
||||
gameTitle: string;
|
||||
gameIconUrl: string;
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface GameAchievement {
|
||||
achievements: SteamAchievement[];
|
||||
unlockedAchievements: UnlockedAchievement[];
|
||||
updatedAt: number | undefined;
|
||||
achievementImageUrl?: string | null;
|
||||
imageUrl?: string | null;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user