From 8fb62af0cfc83c283eecf798de89328bcc514cf6 Mon Sep 17 00:00:00 2001 From: JackEnx Date: Wed, 8 May 2024 15:38:52 -0300 Subject: [PATCH 001/163] feature: wip-game-achievements refactor: rename files --- src/main/data-source.ts | 2 + src/main/entity/game-achievements.entity.ts | 21 ++++ src/main/entity/index.ts | 2 + .../game-achievements-observer.ts | 89 +++++++++++++++ .../get-game-achievements-to-watch.ts | 59 ++++++++++ .../save-all-local-steam-achivements.ts | 74 +++++++++++++ .../steam/steam-achievement-info.ts | 54 ++++++++++ .../steam/steam-achievement-merge.ts | 28 +++++ .../steam/steam-find-game-achivement-files.ts | 86 +++++++++++++++ .../steam/steam-get-achivement.ts | 48 +++++++++ .../steam-global-achievement-percentages.ts | 33 ++++++ src/main/events/achievements/types/index.ts | 52 +++++++++ .../util/check-unlocked-achievements.ts | 102 ++++++++++++++++++ .../achievements/util/parseAchievementFile.ts | 57 ++++++++++ src/main/knex-client.ts | 2 + .../20240919030940_create_game_achievement.ts | 18 ++++ src/main/repository.ts | 4 + src/main/services/process-watcher.ts | 8 ++ 18 files changed, 739 insertions(+) create mode 100644 src/main/entity/game-achievements.entity.ts create mode 100644 src/main/events/achievements/game-achievements-observer.ts create mode 100644 src/main/events/achievements/services/get-game-achievements-to-watch.ts create mode 100644 src/main/events/achievements/services/save-all-local-steam-achivements.ts create mode 100644 src/main/events/achievements/steam/steam-achievement-info.ts create mode 100644 src/main/events/achievements/steam/steam-achievement-merge.ts create mode 100644 src/main/events/achievements/steam/steam-find-game-achivement-files.ts create mode 100644 src/main/events/achievements/steam/steam-get-achivement.ts create mode 100644 src/main/events/achievements/steam/steam-global-achievement-percentages.ts create mode 100644 src/main/events/achievements/types/index.ts create mode 100644 src/main/events/achievements/util/check-unlocked-achievements.ts create mode 100644 src/main/events/achievements/util/parseAchievementFile.ts create mode 100644 src/main/migrations/20240919030940_create_game_achievement.ts diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 29c72f8c..9745abd8 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -7,6 +7,7 @@ import { Repack, UserPreferences, UserAuth, + GameAchievement, } from "@main/entity"; import { databasePath } from "./constants"; @@ -21,6 +22,7 @@ export const dataSource = new DataSource({ DownloadSource, DownloadQueue, UserAuth, + GameAchievement, ], synchronize: false, database: databasePath, diff --git a/src/main/entity/game-achievements.entity.ts b/src/main/entity/game-achievements.entity.ts new file mode 100644 index 00000000..e50d3294 --- /dev/null +++ b/src/main/entity/game-achievements.entity.ts @@ -0,0 +1,21 @@ +import { + Column, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from "typeorm"; +import { Game } from "./game.entity"; + +@Entity("game_achievement") +export class GameAchievement { + @PrimaryGeneratedColumn() + id: number; + + @OneToOne(() => Game) + @JoinColumn() + game: Game; + + @Column("text", { nullable: true }) + achievements: string; +} diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index 9cb4f044..7e52577c 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -2,6 +2,8 @@ export * from "./game.entity"; export * from "./repack.entity"; export * from "./user-preferences.entity"; export * from "./game-shop-cache.entity"; +export * from "./game.entity"; +export * from "./game-achievements.entity"; export * from "./download-source.entity"; export * from "./download-queue.entity"; export * from "./user-auth"; diff --git a/src/main/events/achievements/game-achievements-observer.ts b/src/main/events/achievements/game-achievements-observer.ts new file mode 100644 index 00000000..a0cdfaf4 --- /dev/null +++ b/src/main/events/achievements/game-achievements-observer.ts @@ -0,0 +1,89 @@ +import { watch } from "node:fs/promises"; +import { getGameAchievementsToWatch } from "./services/get-game-achievements-to-watch"; +import { checkUnlockedAchievements } from "./util/check-unlocked-achievements"; +import { parseAchievementFile } from "./util/parseAchievementFile"; +import { gameAchievementRepository } from "@main/repository"; + +type GameAchievementObserver = { + [id: number]: AbortController | null; +}; + +const gameAchievementObserver: GameAchievementObserver = {}; + +export const startGameAchievementObserver = async (gameId: number) => { + if ( + gameAchievementObserver[gameId] === null || + gameAchievementObserver[gameId] + ) { + return; + } + + console.log(`Starting: ${gameId}`); + + const achievementsToWatch = await getGameAchievementsToWatch(gameId); + + if (!achievementsToWatch) { + console.log("No achievements to observe"); + gameAchievementObserver[gameId] = null; + return; + } + + const { steamId, checkedAchievements, achievementFiles } = + achievementsToWatch; + + gameAchievementObserver[gameId] = new AbortController(); + + const achievements = checkedAchievements.all; + + for (const file of achievementFiles) { + const signal = gameAchievementObserver[gameId]?.signal; + if (!signal) return; + console.log(`cracker: ${file.type}, steamId: ${steamId}`); + (async () => { + try { + const watcher = watch(file.filePath, { + signal, + }); + for await (const event of watcher) { + if (event.eventType === "change") { + console.log("file modified"); + const localAchievementFile = await parseAchievementFile( + file.filePath + ); + + if (!localAchievementFile) continue; + + const checked = checkUnlockedAchievements( + file.type, + localAchievementFile, + achievements + ); + + if (checked.new) { + console.log(checked.new); + + gameAchievementRepository.update( + { + game: { id: steamId }, + }, + { + achievements: JSON.stringify(checked.all), + } + ); + } + } + } + } catch (err: any) { + console.log(`cracker: ${file.type}, steamId ${steamId}`); + if (err?.name === "AbortError") return; + throw err; + } + })(); + } +}; + +export const stopGameAchievementObserver = (gameId: number) => { + console.log(`Stopping: ${gameId}`); + gameAchievementObserver[gameId]?.abort(); + delete gameAchievementObserver[gameId]; +}; diff --git a/src/main/events/achievements/services/get-game-achievements-to-watch.ts b/src/main/events/achievements/services/get-game-achievements-to-watch.ts new file mode 100644 index 00000000..c44d7b6b --- /dev/null +++ b/src/main/events/achievements/services/get-game-achievements-to-watch.ts @@ -0,0 +1,59 @@ +import { gameRepository, gameAchievementRepository } from "@main/repository"; +import { steamGetAchivement } from "../steam/steam-get-achivement"; +import { steamFindGameAchievementFiles } from "../steam/steam-find-game-achivement-files"; +import { AchievementFile, CheckedAchievements } from "../types"; +import { parseAchievementFile } from "../util/parseAchievementFile"; +import { checkUnlockedAchievements } from "../util/check-unlocked-achievements"; + +export const getGameAchievementsToWatch = async ( + gameId: number +): Promise< + | { + steamId: number; + checkedAchievements: CheckedAchievements; + achievementFiles: AchievementFile[]; + } + | undefined +> => { + const game = await gameRepository.findOne({ where: { id: gameId } }); + + if (!game || game.shop !== "steam") return; + + const steamId = Number(game.objectID); + + const achievements = await steamGetAchivement(game); + console.log(achievements); + if (!achievements || !achievements.length) return; + + const achievementFiles = steamFindGameAchievementFiles(game.objectID)[ + steamId + ]; + console.log(achievementFiles); + if (!achievementFiles || !achievementFiles.length) return; + + const checkedAchievements: CheckedAchievements = { + all: achievements, + new: [], + }; + + for (const achievementFile of achievementFiles) { + const file = await parseAchievementFile(achievementFile.filePath); + + checkedAchievements.new.push( + ...checkUnlockedAchievements(achievementFile.type, file, achievements).new + ); + } + + if (checkedAchievements.new.length) { + await gameAchievementRepository.update( + { + game: { id: gameId }, + }, + { + achievements: JSON.stringify(checkedAchievements.all), + } + ); + } + + return { steamId, checkedAchievements, achievementFiles }; +}; diff --git a/src/main/events/achievements/services/save-all-local-steam-achivements.ts b/src/main/events/achievements/services/save-all-local-steam-achivements.ts new file mode 100644 index 00000000..cb28edbe --- /dev/null +++ b/src/main/events/achievements/services/save-all-local-steam-achivements.ts @@ -0,0 +1,74 @@ +import { gameAchievementRepository, gameRepository } from "@main/repository"; +import { steamFindGameAchievementFiles } from "../steam/steam-find-game-achivement-files"; +import { steamGlobalAchievementPercentages } from "../steam/steam-global-achievement-percentages"; +import { steamAchievementInfo } from "../steam/steam-achievement-info"; +import { steamAchievementMerge } from "../steam/steam-achievement-merge"; +import { parseAchievementFile } from "../util/parseAchievementFile"; +import { checkUnlockedAchievements } from "../util/check-unlocked-achievements"; +import { CheckedAchievements } from "../types"; + +export const saveAllLocalSteamAchivements = async () => { + const gameAchievementFiles = steamFindGameAchievementFiles(); + + for (const key of Object.keys(gameAchievementFiles)) { + const objectId = key; + + const game = await gameRepository.findOne({ + where: { objectID: objectId }, + }); + + if (!game) continue; + + const hasOnDb = await gameAchievementRepository.existsBy({ + game: game, + }); + + if (hasOnDb) continue; + + const achievementPercentage = + await steamGlobalAchievementPercentages(objectId); + + if (!achievementPercentage) { + await gameAchievementRepository.save({ + game, + achievements: "[]", + }); + continue; + } + + const achievementInfo = await steamAchievementInfo(objectId); + + if (!achievementInfo) continue; + + const achievements = steamAchievementMerge( + achievementPercentage, + achievementInfo + ); + + if (!achievements) continue; + + const checkedAchievements: CheckedAchievements = { + all: achievements, + new: [], + }; + + for (const achievementFile of gameAchievementFiles[key]) { + const localAchievementFile = await parseAchievementFile( + achievementFile.filePath + ); + + checkedAchievements.new.push( + ...checkUnlockedAchievements( + achievementFile.type, + localAchievementFile, + achievements + ).new + ); + } + + await gameAchievementRepository.save({ + game, + achievements: JSON.stringify(checkedAchievements.all), + }); + } +}; diff --git a/src/main/events/achievements/steam/steam-achievement-info.ts b/src/main/events/achievements/steam/steam-achievement-info.ts new file mode 100644 index 00000000..1c1a5686 --- /dev/null +++ b/src/main/events/achievements/steam/steam-achievement-info.ts @@ -0,0 +1,54 @@ +import { logger } from "@main/services"; +import { AchievementInfo } from "../types"; +import { JSDOM } from "jsdom"; + +export const steamAchievementInfo = async ( + objectId: string +): Promise => { + const fetchUrl = `https://steamcommunity.com/stats/${objectId}/achievements`; + + const achievementInfosHtmlText = await fetch(fetchUrl, { + method: "GET", + //headers: { "Accept-Language": "" }, + }) + .then((res) => { + if (res.status === 200) return res.text(); + throw new Error(); + }) + .catch((err) => { + logger.error(err, { method: "getSteamGameAchievements" }); + return; + }); + + if (!achievementInfosHtmlText) return; + + const achievementInfos: AchievementInfo[] = []; + + const window = new JSDOM(achievementInfosHtmlText).window; + + const itens = Array.from( + window.document.getElementsByClassName("achieveRow") + ); + + for (const item of itens) { + const imageUrl = item + .getElementsByClassName("achieveImgHolder")?.[0] + .getElementsByTagName("img")?.[0]?.src; + + const achievementName = item + .getElementsByClassName("achieveTxt")?.[0] + .getElementsByTagName("h3")?.[0].innerHTML; + + const achievementDescription = item + .getElementsByClassName("achieveTxt")?.[0] + .getElementsByTagName("h5")?.[0].innerHTML; + + achievementInfos.push({ + imageUrl: imageUrl ?? "", + title: achievementName ?? "", + description: achievementDescription ?? "", + }); + } + + return achievementInfos; +}; diff --git a/src/main/events/achievements/steam/steam-achievement-merge.ts b/src/main/events/achievements/steam/steam-achievement-merge.ts new file mode 100644 index 00000000..163155b8 --- /dev/null +++ b/src/main/events/achievements/steam/steam-achievement-merge.ts @@ -0,0 +1,28 @@ +import { Achievement, AchievementInfo, AchievementPercentage } from "../types"; + +export const steamAchievementMerge = ( + achievementPercentage: AchievementPercentage[], + achievementInfo: AchievementInfo[] +): Achievement[] | undefined => { + if (achievementPercentage.length > achievementInfo.length) return; + + const size = achievementPercentage.length; + + const achievements: Achievement[] = new Array(size); + + for (let i = 0; i < size; i++) { + achievements[i] = { + id: achievementPercentage[i].name, + percent: achievementPercentage[i].percent, + imageUrl: achievementInfo[i].imageUrl, + title: achievementInfo[i].title, + description: achievementInfo[i].description, + achieved: false, + curProgress: 0, + maxProgress: 0, + unlockTime: 0, + }; + } + + return achievements; +}; diff --git a/src/main/events/achievements/steam/steam-find-game-achivement-files.ts b/src/main/events/achievements/steam/steam-find-game-achivement-files.ts new file mode 100644 index 00000000..cc73490a --- /dev/null +++ b/src/main/events/achievements/steam/steam-find-game-achivement-files.ts @@ -0,0 +1,86 @@ +import path from "node:path"; +import { existsSync, readdirSync } from "node:fs"; +import { Cracker, GameAchievementFiles } from "../types"; +import { app } from "electron"; + +const addGame = ( + achievementFiles: GameAchievementFiles, + gamePath: string, + objectId: string, + fileLocation: string[], + type: Cracker +) => { + const filePath = path.join(gamePath, objectId, ...fileLocation); + + if (existsSync(filePath)) { + const achivementFile = { + type, + filePath: filePath, + }; + + achievementFiles[objectId] + ? achievementFiles[objectId].push(achivementFile) + : (achievementFiles[objectId] = [achivementFile]); + } +}; + +export const steamFindGameAchievementFiles = ( + objectId?: string +): GameAchievementFiles => { + //TODO: change to a automatized method + const publicDir = path.join("C:", "Users", "Public", "Documents"); + const appData = app.getPath("appData"); + + const gameAchievementFiles: GameAchievementFiles = {}; + + const crackers: Cracker[] = [ + Cracker.codex, + Cracker.goldberg, + Cracker.rune, + Cracker.onlineFix, + ]; + + for (const cracker of crackers) { + let achievementPath: string; + let fileLocation: string[]; + + if (cracker === Cracker.onlineFix) { + achievementPath = path.join(publicDir, Cracker.onlineFix); + fileLocation = ["Stats", "Achievements.ini"]; + } else if (cracker === Cracker.goldberg) { + achievementPath = path.join(appData, "Goldberg SteamEmu Saves"); + fileLocation = ["achievements.json"]; + } else { + achievementPath = path.join(publicDir, "Steam", cracker); + fileLocation = ["achievements.ini"]; + } + + if (!existsSync(achievementPath)) continue; + + const objectIds = readdirSync(achievementPath); + + if (objectId) { + if (objectIds.includes(objectId)) { + addGame( + gameAchievementFiles, + achievementPath, + objectId, + fileLocation, + cracker + ); + } + } else { + for (const objectId of objectIds) { + addGame( + gameAchievementFiles, + achievementPath, + objectId, + fileLocation, + cracker + ); + } + } + } + + return gameAchievementFiles; +}; diff --git a/src/main/events/achievements/steam/steam-get-achivement.ts b/src/main/events/achievements/steam/steam-get-achivement.ts new file mode 100644 index 00000000..3d556b6f --- /dev/null +++ b/src/main/events/achievements/steam/steam-get-achivement.ts @@ -0,0 +1,48 @@ +import { gameAchievementRepository } from "@main/repository"; +import { steamGlobalAchievementPercentages } from "./steam-global-achievement-percentages"; +import { steamAchievementInfo } from "./steam-achievement-info"; +import { steamAchievementMerge } from "./steam-achievement-merge"; +import { Achievement } from "../types"; +import { Game } from "@main/entity"; + +export const steamGetAchivement = async ( + game: Game +): Promise => { + const gameAchivement = await gameAchievementRepository.findOne({ + where: { game: game }, + }); + + if (!gameAchivement) { + const achievementPercentage = await steamGlobalAchievementPercentages( + game.objectID + ); + console.log(achievementPercentage); + if (!achievementPercentage) { + await gameAchievementRepository.save({ + game, + achievements: "[]", + }); + return []; + } + + const achievementInfo = await steamAchievementInfo(game.objectID); + console.log(achievementInfo); + if (!achievementInfo) return; + + const achievements = steamAchievementMerge( + achievementPercentage, + achievementInfo + ); + + if (!achievements) return; + + await gameAchievementRepository.save({ + game, + achievements: JSON.stringify(achievements), + }); + + return achievements; + } else { + return JSON.parse(gameAchivement.achievements); + } +}; diff --git a/src/main/events/achievements/steam/steam-global-achievement-percentages.ts b/src/main/events/achievements/steam/steam-global-achievement-percentages.ts new file mode 100644 index 00000000..26061e23 --- /dev/null +++ b/src/main/events/achievements/steam/steam-global-achievement-percentages.ts @@ -0,0 +1,33 @@ +import { logger } from "@main/services"; +import { AchievementPercentage } from "../types"; + +interface GlobalAchievementPercentages { + achievementpercentages: { + achievements: Array; + }; +} + +export const steamGlobalAchievementPercentages = async ( + objectId: string +): Promise => { + const fetchUrl = `https://api.steampowered.com/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v0002/?gameid=${objectId}`; + + const achievementPercentages: Array | undefined = ( + await fetch(fetchUrl, { + method: "GET", + }) + .then((res) => { + if (res.status === 200) return res.json(); + return; + }) + .then((data: GlobalAchievementPercentages) => data) + .catch((err) => { + logger.error(err, { method: "getSteamGameAchievements" }); + return; + }) + )?.achievementpercentages.achievements; + + if (!achievementPercentages) return; + + return achievementPercentages; +}; diff --git a/src/main/events/achievements/types/index.ts b/src/main/events/achievements/types/index.ts new file mode 100644 index 00000000..9a04a3a7 --- /dev/null +++ b/src/main/events/achievements/types/index.ts @@ -0,0 +1,52 @@ +export enum Cracker { + codex = "CODEX", + rune = "RUNE", + onlineFix = "OnlineFix", + goldberg = "Goldberg", +} + +export interface CheckedAchievements { + all: Achievement[]; + new: Achievement[]; +} + +export interface Achievement { + id: string; + percent: number; + imageUrl: string; + title: string; + description: string; + achieved: boolean; + curProgress: number; + maxProgress: number; + unlockTime: number; +} + +export interface AchievementInfo { + imageUrl: string; + title: string; + description: string; +} + +export interface AchievementPercentage { + name: string; + percent: number; +} + +export interface CheckedAchievement { + all: Achievement[]; + new: Achievement[]; +} + +export interface AchievementFile { + type: Cracker; + filePath: string; +} + +export type GameAchievementFiles = { + [id: string]: AchievementFile[]; +}; + +export type GameAchievementFile = { + [id: string]: AchievementFile[]; +}; diff --git a/src/main/events/achievements/util/check-unlocked-achievements.ts b/src/main/events/achievements/util/check-unlocked-achievements.ts new file mode 100644 index 00000000..2c9e2b37 --- /dev/null +++ b/src/main/events/achievements/util/check-unlocked-achievements.ts @@ -0,0 +1,102 @@ +import { Achievement, CheckedAchievements, Cracker } from "../types"; + +export const checkUnlockedAchievements = ( + type: Cracker, + unlockedAchievements: any, + achievements: Achievement[] +): CheckedAchievements => { + if (type === Cracker.onlineFix) + return onlineFixMerge(unlockedAchievements, achievements); + if (type === Cracker.goldberg) + return goldbergUnlockedAchievements(unlockedAchievements, achievements); + return defaultMerge(unlockedAchievements, achievements); +}; + +const onlineFixMerge = ( + unlockedAchievements: any, + achievements: Achievement[] +): CheckedAchievements => { + const newUnlockedAchievements: Achievement[] = []; + + for (const achievement of achievements) { + if (achievement.achieved) continue; + + const unlockedAchievement = unlockedAchievements[achievement.id]; + + if (!unlockedAchievement) continue; + + achievement.achieved = Boolean( + unlockedAchievement?.achieved ?? achievement.achieved + ); + + achievement.unlockTime = + unlockedAchievement?.timestamp ?? achievement.unlockTime; + + if (achievement.achieved) { + newUnlockedAchievements.push(achievement); + } + } + + return { all: achievements, new: newUnlockedAchievements }; +}; + +const goldbergUnlockedAchievements = ( + unlockedAchievements: any, + achievements: Achievement[] +): CheckedAchievements => { + const newUnlockedAchievements: Achievement[] = []; + + for (const achievement of achievements) { + if (achievement.achieved) continue; + + const unlockedAchievement = unlockedAchievements[achievement.id]; + + if (!unlockedAchievement) continue; + + achievement.achieved = Boolean( + unlockedAchievement?.earned ?? achievement.achieved + ); + + achievement.unlockTime = + unlockedAchievement?.earned_time ?? achievement.unlockTime; + + if (achievement.achieved) { + newUnlockedAchievements.push(achievement); + } + } + return { all: achievements, new: newUnlockedAchievements }; +}; + +const defaultMerge = ( + unlockedAchievements: any, + achievements: Achievement[] +): CheckedAchievements => { + const newUnlockedAchievements: Achievement[] = []; + console.log("checkUnlockedAchievements"); + for (const achievement of achievements) { + if (achievement.achieved) continue; + + const unlockedAchievement = unlockedAchievements[achievement.id]; + + if (!unlockedAchievement) continue; + + achievement.achieved = Boolean( + unlockedAchievement?.Achieved ?? achievement.achieved + ); + + achievement.curProgress = + unlockedAchievement?.CurProgress ?? achievement.curProgress; + + achievement.maxProgress = + unlockedAchievement?.MaxProgress ?? achievement.maxProgress; + + achievement.unlockTime = + unlockedAchievement?.UnlockTime ?? achievement.unlockTime; + + if (achievement.achieved) { + newUnlockedAchievements.push(achievement); + } + } + console.log("newUnlocked: ", newUnlockedAchievements); + return { all: achievements, new: newUnlockedAchievements }; +}; diff --git a/src/main/events/achievements/util/parseAchievementFile.ts b/src/main/events/achievements/util/parseAchievementFile.ts new file mode 100644 index 00000000..1adf9512 --- /dev/null +++ b/src/main/events/achievements/util/parseAchievementFile.ts @@ -0,0 +1,57 @@ +import { existsSync, createReadStream, readFileSync } from "node:fs"; +import readline from "node:readline"; + +export const parseAchievementFile = async ( + filePath: string +): Promise => { + if (existsSync(filePath)) { + if (filePath.endsWith(".ini")) { + return iniParse(filePath); + } + + if (filePath.endsWith(".json")) { + return jsonParse(filePath); + } + } +}; + +const iniParse = async (filePath: string) => { + try { + const file = createReadStream(filePath); + + const lines = readline.createInterface({ + input: file, + crlfDelay: Infinity, + }); + + let objectName = ""; + const object: any = {}; + + for await (const line of lines) { + if (line.startsWith("###") || !line.length) continue; + + if (line.startsWith("[") && line.endsWith("]")) { + objectName = line.slice(1, -1); + object[objectName] = {}; + } else { + const [name, value] = line.split("="); + + const number = Number(value); + + object[objectName][name] = isNaN(number) ? value : number; + } + } + + return object; + } catch { + return null; + } +}; + +const jsonParse = (filePath: string) => { + try { + return JSON.parse(readFileSync(filePath, "utf-8")); + } catch { + return null; + } +}; diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts index b6ec3e3d..6530e653 100644 --- a/src/main/knex-client.ts +++ b/src/main/knex-client.ts @@ -6,6 +6,7 @@ import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_lang import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris"; import { app } from "electron"; import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns"; +import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement"; export type HydraMigration = Knex.Migration & { name: string }; @@ -17,6 +18,7 @@ class MigrationSource implements Knex.MigrationSource { UpdateUserLanguage, EnsureRepackUris, FixMissingColumns, + CreateGameAchievement, ]); } getMigrationName(migration: HydraMigration): string { diff --git a/src/main/migrations/20240919030940_create_game_achievement.ts b/src/main/migrations/20240919030940_create_game_achievement.ts new file mode 100644 index 00000000..16d90fa7 --- /dev/null +++ b/src/main/migrations/20240919030940_create_game_achievement.ts @@ -0,0 +1,18 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const CreateGameAchievement: HydraMigration = { + name: "CreateGameAchievement", + up: async (knex: Knex) => { + await knex.schema.createTable("game_achievement", (table) => { + table.increments("id").primary(); + table.integer("gameId").notNullable(); + table.text("achievements"); + table.foreign("gameId").references("game.id").onDelete("CASCADE"); + }); + }, + + down: async (knex: Knex) => { + await knex.schema.dropTable("game_achievement"); + }, +}; diff --git a/src/main/repository.ts b/src/main/repository.ts index 4464e775..4e5c115f 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -7,6 +7,7 @@ import { Repack, UserPreferences, UserAuth, + GameAchievement, } from "@main/entity"; export const gameRepository = dataSource.getRepository(Game); @@ -24,3 +25,6 @@ export const downloadSourceRepository = export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); export const userAuthRepository = dataSource.getRepository(UserAuth); + +export const gameAchievementRepository = + dataSource.getRepository(GameAchievement); diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 2a194bf2..50ef42a9 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -5,6 +5,10 @@ import { createGame, updateGamePlaytime } from "./library-sync"; import { GameRunning } from "@types"; import { PythonInstance } from "./download"; import { Game } from "@main/entity"; +import { + startGameAchievementObserver, + stopGameAchievementObserver, +} from "@main/events/achievements/game-achievements-observer"; export const gamesPlaytime = new Map< number, @@ -74,6 +78,8 @@ function onOpenGame(game: Game) { } else { createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {}); } + + startGameAchievementObserver(game.id); } function onTickGame(game: Game) { @@ -125,4 +131,6 @@ const onCloseGame = (game: Game) => { } else { createGame(game).catch(() => {}); } + + stopGameAchievementObserver(game.id); }; From 500cd2a53100696036ac26c6f2e89cc00882ad5d Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:04:46 -0300 Subject: [PATCH 002/163] feat: saving achievements on open launcher --- src/main/entity/game-achievements.entity.ts | 3 + .../get-game-achievements-to-watch.ts | 4 +- .../save-all-local-steam-achivements.ts | 82 +++++++++---------- .../steam/steam-find-game-achivement-files.ts | 16 ++-- .../achievements/util/parseAchievementFile.ts | 2 +- src/main/main.ts | 3 + .../20240919030940_create_game_achievement.ts | 11 +-- 7 files changed, 63 insertions(+), 58 deletions(-) diff --git a/src/main/entity/game-achievements.entity.ts b/src/main/entity/game-achievements.entity.ts index e50d3294..9bad73e5 100644 --- a/src/main/entity/game-achievements.entity.ts +++ b/src/main/entity/game-achievements.entity.ts @@ -16,6 +16,9 @@ export class GameAchievement { @JoinColumn() game: Game; + @Column("text", { nullable: true }) + unlockedAchievements: string; + @Column("text", { nullable: true }) achievements: string; } diff --git a/src/main/events/achievements/services/get-game-achievements-to-watch.ts b/src/main/events/achievements/services/get-game-achievements-to-watch.ts index c44d7b6b..e95ecd02 100644 --- a/src/main/events/achievements/services/get-game-achievements-to-watch.ts +++ b/src/main/events/achievements/services/get-game-achievements-to-watch.ts @@ -22,7 +22,9 @@ export const getGameAchievementsToWatch = async ( const steamId = Number(game.objectID); const achievements = await steamGetAchivement(game); + console.log(achievements); + if (!achievements || !achievements.length) return; const achievementFiles = steamFindGameAchievementFiles(game.objectID)[ @@ -50,7 +52,7 @@ export const getGameAchievementsToWatch = async ( game: { id: gameId }, }, { - achievements: JSON.stringify(checkedAchievements.all), + unlockedAchievements: JSON.stringify(checkedAchievements.all), } ); } diff --git a/src/main/events/achievements/services/save-all-local-steam-achivements.ts b/src/main/events/achievements/services/save-all-local-steam-achivements.ts index cb28edbe..dad2c2b8 100644 --- a/src/main/events/achievements/services/save-all-local-steam-achivements.ts +++ b/src/main/events/achievements/services/save-all-local-steam-achivements.ts @@ -1,11 +1,7 @@ import { gameAchievementRepository, gameRepository } from "@main/repository"; import { steamFindGameAchievementFiles } from "../steam/steam-find-game-achivement-files"; -import { steamGlobalAchievementPercentages } from "../steam/steam-global-achievement-percentages"; -import { steamAchievementInfo } from "../steam/steam-achievement-info"; -import { steamAchievementMerge } from "../steam/steam-achievement-merge"; import { parseAchievementFile } from "../util/parseAchievementFile"; -import { checkUnlockedAchievements } from "../util/check-unlocked-achievements"; -import { CheckedAchievements } from "../types"; +import { HydraApi } from "@main/services"; export const saveAllLocalSteamAchivements = async () => { const gameAchievementFiles = steamFindGameAchievementFiles(); @@ -19,56 +15,56 @@ export const saveAllLocalSteamAchivements = async () => { if (!game) continue; - const hasOnDb = await gameAchievementRepository.existsBy({ + const savedGameAchievements = await gameAchievementRepository.findOneBy({ game: game, }); - if (hasOnDb) continue; - - const achievementPercentage = - await steamGlobalAchievementPercentages(objectId); - - if (!achievementPercentage) { - await gameAchievementRepository.save({ - game, - achievements: "[]", - }); - continue; + if (!savedGameAchievements || !savedGameAchievements.achievements) { + HydraApi.get( + "/games/achievements", + { + shop: "steam", + objectId, + }, + { needsAuth: false } + ) + .then((achievements) => { + return gameAchievementRepository.upsert( + { + game: { id: game.id }, + achievements: JSON.stringify(achievements), + }, + ["game"] + ); + }) + .catch(console.log); } - const achievementInfo = await steamAchievementInfo(objectId); - - if (!achievementInfo) continue; - - const achievements = steamAchievementMerge( - achievementPercentage, - achievementInfo - ); - - if (!achievements) continue; - - const checkedAchievements: CheckedAchievements = { - all: achievements, - new: [], - }; + const unlockedAchievements: { name: string; unlockTime: number }[] = []; for (const achievementFile of gameAchievementFiles[key]) { const localAchievementFile = await parseAchievementFile( achievementFile.filePath ); - checkedAchievements.new.push( - ...checkUnlockedAchievements( - achievementFile.type, - localAchievementFile, - achievements - ).new - ); + console.log(achievementFile.filePath); + console.log(localAchievementFile); + + for (const a of Object.keys(localAchievementFile)) { + // TODO: use checkUnlockedAchievements after refactoring it to be generic + unlockedAchievements.push({ + name: a, + unlockTime: localAchievementFile[a].UnlockTime, + }); + } } - await gameAchievementRepository.save({ - game, - achievements: JSON.stringify(checkedAchievements.all), - }); + await gameAchievementRepository.upsert( + { + game: { id: game.id }, + unlockedAchievements: JSON.stringify(unlockedAchievements), + }, + ["game"] + ); } }; diff --git a/src/main/events/achievements/steam/steam-find-game-achivement-files.ts b/src/main/events/achievements/steam/steam-find-game-achivement-files.ts index cc73490a..631a3a74 100644 --- a/src/main/events/achievements/steam/steam-find-game-achivement-files.ts +++ b/src/main/events/achievements/steam/steam-find-game-achivement-files.ts @@ -1,21 +1,21 @@ import path from "node:path"; -import { existsSync, readdirSync } from "node:fs"; +import fs from "node:fs"; import { Cracker, GameAchievementFiles } from "../types"; import { app } from "electron"; const addGame = ( achievementFiles: GameAchievementFiles, - gamePath: string, + achievementPath: string, objectId: string, fileLocation: string[], type: Cracker ) => { - const filePath = path.join(gamePath, objectId, ...fileLocation); + const filePath = path.join(achievementPath, objectId, ...fileLocation); - if (existsSync(filePath)) { + if (fs.existsSync(filePath)) { const achivementFile = { type, - filePath: filePath, + filePath, }; achievementFiles[objectId] @@ -33,7 +33,7 @@ export const steamFindGameAchievementFiles = ( const gameAchievementFiles: GameAchievementFiles = {}; - const crackers: Cracker[] = [ + const crackers = [ Cracker.codex, Cracker.goldberg, Cracker.rune, @@ -55,9 +55,9 @@ export const steamFindGameAchievementFiles = ( fileLocation = ["achievements.ini"]; } - if (!existsSync(achievementPath)) continue; + if (!fs.existsSync(achievementPath)) continue; - const objectIds = readdirSync(achievementPath); + const objectIds = fs.readdirSync(achievementPath); if (objectId) { if (objectIds.includes(objectId)) { diff --git a/src/main/events/achievements/util/parseAchievementFile.ts b/src/main/events/achievements/util/parseAchievementFile.ts index 1adf9512..64aa120a 100644 --- a/src/main/events/achievements/util/parseAchievementFile.ts +++ b/src/main/events/achievements/util/parseAchievementFile.ts @@ -25,7 +25,7 @@ const iniParse = async (filePath: string) => { }); let objectName = ""; - const object: any = {}; + const object: Record> = {}; for await (const line of lines) { if (line.startsWith("###") || !line.length) continue; diff --git a/src/main/main.ts b/src/main/main.ts index af594e20..abaa93a7 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -16,6 +16,7 @@ import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; +import { saveAllLocalSteamAchivements } from "./events/achievements/services/save-all-local-steam-achivements"; const loadState = async (userPreferences: UserPreferences | null) => { RepacksManager.updateRepacks(); @@ -58,6 +59,8 @@ const loadState = async (userPreferences: UserPreferences | null) => { if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount); }); + + saveAllLocalSteamAchivements(); }; userPreferencesRepository diff --git a/src/main/migrations/20240919030940_create_game_achievement.ts b/src/main/migrations/20240919030940_create_game_achievement.ts index 16d90fa7..a03ea2fc 100644 --- a/src/main/migrations/20240919030940_create_game_achievement.ts +++ b/src/main/migrations/20240919030940_create_game_achievement.ts @@ -3,16 +3,17 @@ import type { Knex } from "knex"; export const CreateGameAchievement: HydraMigration = { name: "CreateGameAchievement", - up: async (knex: Knex) => { - await knex.schema.createTable("game_achievement", (table) => { + up: (knex: Knex) => { + return knex.schema.createTable("game_achievement", (table) => { table.increments("id").primary(); - table.integer("gameId").notNullable(); + table.integer("gameId").notNullable().unique(); table.text("achievements"); + table.text("unlockedAchievements"); table.foreign("gameId").references("game.id").onDelete("CASCADE"); }); }, - down: async (knex: Knex) => { - await knex.schema.dropTable("game_achievement"); + down: (knex: Knex) => { + return knex.schema.dropTable("game_achievement"); }, }; From 5b0cf1e82b2547af0d3b44617168626357a0c278 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:06:32 -0300 Subject: [PATCH 003/163] feat: handle user not logged in error --- src/main/events/profile/sync-friend-requests.ts | 10 +++++++++- src/main/events/user/get-blocked-users.ts | 8 +++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/events/profile/sync-friend-requests.ts b/src/main/events/profile/sync-friend-requests.ts index 4b89701a..c7dfbd81 100644 --- a/src/main/events/profile/sync-friend-requests.ts +++ b/src/main/events/profile/sync-friend-requests.ts @@ -1,9 +1,17 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; +import { UserNotLoggedInError } from "@shared"; import { FriendRequestSync } from "@types"; const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => { - return HydraApi.get(`/profile/friend-requests/sync`); + return HydraApi.get(`/profile/friend-requests/sync`).catch( + (err) => { + if (err instanceof UserNotLoggedInError) { + return { friendRequests: [] }; + } + throw err; + } + ); }; registerEvent("syncFriendRequests", syncFriendRequests); diff --git a/src/main/events/user/get-blocked-users.ts b/src/main/events/user/get-blocked-users.ts index 3d213898..7df6bf9a 100644 --- a/src/main/events/user/get-blocked-users.ts +++ b/src/main/events/user/get-blocked-users.ts @@ -1,5 +1,6 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; +import { UserNotLoggedInError } from "@shared"; import { UserBlocks } from "@types"; export const getBlockedUsers = async ( @@ -7,7 +8,12 @@ export const getBlockedUsers = async ( take: number, skip: number ): Promise => { - return HydraApi.get(`/profile/blocks`, { take, skip }); + return HydraApi.get(`/profile/blocks`, { take, skip }).catch((err) => { + if (err instanceof UserNotLoggedInError) { + return { blocks: [] }; + } + throw err; + }); }; registerEvent("getBlockedUsers", getBlockedUsers); From 7e3cf0a00e74df450de64a1a35b151b608a3793d Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:56:32 -0300 Subject: [PATCH 004/163] feat: starting showing local achievements --- src/main/entity/game-achievements.entity.ts | 4 +- src/main/entity/game.entity.ts | 4 ++ .../events/catalogue/get-game-achievements.ts | 46 +++++++++++++++++++ src/main/events/catalogue/get-game-stats.ts | 9 ++-- src/main/events/index.ts | 1 + src/main/index.ts | 1 + src/main/main.ts | 2 +- .../game-achievements-observer.ts | 0 .../get-game-achievements-to-watch.ts | 0 .../save-all-local-steam-achivements.ts | 0 .../steam/steam-achievement-info.ts | 0 .../steam/steam-achievement-merge.ts | 0 .../steam/steam-find-game-achivement-files.ts | 0 .../steam/steam-get-achivement.ts | 0 .../steam-global-achievement-percentages.ts | 0 .../achievements/types/index.ts | 0 .../util/check-unlocked-achievements.ts | 0 .../achievements/util/parseAchievementFile.ts | 0 src/main/services/process-watcher.ts | 2 +- src/preload/index.ts | 3 ++ .../game-details/game-details.context.tsx | 11 ++++- .../game-details.context.types.ts | 2 + src/renderer/src/declaration.d.ts | 5 ++ .../pages/game-details/sidebar/sidebar.tsx | 14 +++++- src/types/index.ts | 10 ++++ 25 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 src/main/events/catalogue/get-game-achievements.ts rename src/main/{events => services}/achievements/game-achievements-observer.ts (100%) rename src/main/{events => services}/achievements/services/get-game-achievements-to-watch.ts (100%) rename src/main/{events => services}/achievements/services/save-all-local-steam-achivements.ts (100%) rename src/main/{events => services}/achievements/steam/steam-achievement-info.ts (100%) rename src/main/{events => services}/achievements/steam/steam-achievement-merge.ts (100%) rename src/main/{events => services}/achievements/steam/steam-find-game-achivement-files.ts (100%) rename src/main/{events => services}/achievements/steam/steam-get-achivement.ts (100%) rename src/main/{events => services}/achievements/steam/steam-global-achievement-percentages.ts (100%) rename src/main/{events => services}/achievements/types/index.ts (100%) rename src/main/{events => services}/achievements/util/check-unlocked-achievements.ts (100%) rename src/main/{events => services}/achievements/util/parseAchievementFile.ts (100%) diff --git a/src/main/entity/game-achievements.entity.ts b/src/main/entity/game-achievements.entity.ts index 9bad73e5..48ca958b 100644 --- a/src/main/entity/game-achievements.entity.ts +++ b/src/main/entity/game-achievements.entity.ts @@ -5,14 +5,14 @@ import { OneToOne, PrimaryGeneratedColumn, } from "typeorm"; -import { Game } from "./game.entity"; +import type { Game } from "./game.entity"; @Entity("game_achievement") export class GameAchievement { @PrimaryGeneratedColumn() id: number; - @OneToOne(() => Game) + @OneToOne("Game", "achievements") @JoinColumn() game: Game; diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 190e7470..98606167 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -12,6 +12,7 @@ import { Repack } from "./repack.entity"; import type { GameShop, GameStatus } from "@types"; import { Downloader } from "@shared"; import type { DownloadQueue } from "./download-queue.entity"; +import { GameAchievement } from "./game-achievements.entity"; @Entity("game") export class Game { @@ -76,6 +77,9 @@ export class Game { @JoinColumn() repack: Repack; + @OneToOne("GameAchievement", "game") + achievements: GameAchievement; + @OneToOne("DownloadQueue", "game") downloadQueue: DownloadQueue; diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts new file mode 100644 index 00000000..3325b3c0 --- /dev/null +++ b/src/main/events/catalogue/get-game-achievements.ts @@ -0,0 +1,46 @@ +import type { GameShop } from "@types"; + +import { registerEvent } from "../register-event"; +import { HydraApi } from "@main/services"; +import { gameRepository } from "@main/repository"; +import { GameAchievement } from "@main/entity"; + +const getGameAchievements = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop +): Promise => { + const game = await gameRepository.findOne({ + where: { objectID: objectId, shop }, + relations: { + achievements: true, + }, + }); + const gameAchievements = await HydraApi.get( + "/games/achievements", + { objectId, shop }, + { needsAuth: false } + ); + + const unlockedAchievements = JSON.parse( + game?.achievements?.unlockedAchievements || "[]" + ) as { name: string; unlockTime: number }[]; + + return gameAchievements.map((achievement) => { + const unlockedAchiement = unlockedAchievements.find((localAchievement) => { + return localAchievement.name == achievement.name; + }); + + if (unlockedAchiement) { + return { + ...achievement, + unlocked: true, + unlockTime: unlockedAchiement.unlockTime * 1000, + }; + } + + return { ...achievement, unlocked: false, unlockTime: null }; + }); +}; + +registerEvent("getGameAchievements", getGameAchievements); diff --git a/src/main/events/catalogue/get-game-stats.ts b/src/main/events/catalogue/get-game-stats.ts index 87cba054..0d03b42d 100644 --- a/src/main/events/catalogue/get-game-stats.ts +++ b/src/main/events/catalogue/get-game-stats.ts @@ -9,13 +9,10 @@ const getGameStats = async ( objectId: string, shop: GameShop ) => { - const params = new URLSearchParams({ - objectId, - shop, - }); - const response = await HydraApi.get( - `/games/stats?${params.toString()}` + `/games/stats`, + { objectId, shop }, + { needsAuth: false } ); return response; }; diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 0337c9d8..da11ccc3 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -10,6 +10,7 @@ import "./catalogue/search-games"; import "./catalogue/search-game-repacks"; import "./catalogue/get-game-stats"; import "./catalogue/get-trending-games"; +import "./catalogue/get-game-achievements"; import "./hardware/get-disk-free-space"; import "./library/add-game-to-library"; import "./library/create-game-shortcut"; diff --git a/src/main/index.ts b/src/main/index.ts index 00311b46..7281d1fc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -67,6 +67,7 @@ const runMigrations = async () => { ); }); + await knexClient.migrate.down(migrationConfig); await knexClient.migrate.latest(migrationConfig); await knexClient.destroy(); }; diff --git a/src/main/main.ts b/src/main/main.ts index abaa93a7..5e34a593 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -16,7 +16,7 @@ import { publishNewRepacksNotifications } from "./services/notifications"; import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; -import { saveAllLocalSteamAchivements } from "./events/achievements/services/save-all-local-steam-achivements"; +import { saveAllLocalSteamAchivements } from "./services/achievements/services/save-all-local-steam-achivements"; const loadState = async (userPreferences: UserPreferences | null) => { RepacksManager.updateRepacks(); diff --git a/src/main/events/achievements/game-achievements-observer.ts b/src/main/services/achievements/game-achievements-observer.ts similarity index 100% rename from src/main/events/achievements/game-achievements-observer.ts rename to src/main/services/achievements/game-achievements-observer.ts diff --git a/src/main/events/achievements/services/get-game-achievements-to-watch.ts b/src/main/services/achievements/services/get-game-achievements-to-watch.ts similarity index 100% rename from src/main/events/achievements/services/get-game-achievements-to-watch.ts rename to src/main/services/achievements/services/get-game-achievements-to-watch.ts diff --git a/src/main/events/achievements/services/save-all-local-steam-achivements.ts b/src/main/services/achievements/services/save-all-local-steam-achivements.ts similarity index 100% rename from src/main/events/achievements/services/save-all-local-steam-achivements.ts rename to src/main/services/achievements/services/save-all-local-steam-achivements.ts diff --git a/src/main/events/achievements/steam/steam-achievement-info.ts b/src/main/services/achievements/steam/steam-achievement-info.ts similarity index 100% rename from src/main/events/achievements/steam/steam-achievement-info.ts rename to src/main/services/achievements/steam/steam-achievement-info.ts diff --git a/src/main/events/achievements/steam/steam-achievement-merge.ts b/src/main/services/achievements/steam/steam-achievement-merge.ts similarity index 100% rename from src/main/events/achievements/steam/steam-achievement-merge.ts rename to src/main/services/achievements/steam/steam-achievement-merge.ts diff --git a/src/main/events/achievements/steam/steam-find-game-achivement-files.ts b/src/main/services/achievements/steam/steam-find-game-achivement-files.ts similarity index 100% rename from src/main/events/achievements/steam/steam-find-game-achivement-files.ts rename to src/main/services/achievements/steam/steam-find-game-achivement-files.ts diff --git a/src/main/events/achievements/steam/steam-get-achivement.ts b/src/main/services/achievements/steam/steam-get-achivement.ts similarity index 100% rename from src/main/events/achievements/steam/steam-get-achivement.ts rename to src/main/services/achievements/steam/steam-get-achivement.ts diff --git a/src/main/events/achievements/steam/steam-global-achievement-percentages.ts b/src/main/services/achievements/steam/steam-global-achievement-percentages.ts similarity index 100% rename from src/main/events/achievements/steam/steam-global-achievement-percentages.ts rename to src/main/services/achievements/steam/steam-global-achievement-percentages.ts diff --git a/src/main/events/achievements/types/index.ts b/src/main/services/achievements/types/index.ts similarity index 100% rename from src/main/events/achievements/types/index.ts rename to src/main/services/achievements/types/index.ts diff --git a/src/main/events/achievements/util/check-unlocked-achievements.ts b/src/main/services/achievements/util/check-unlocked-achievements.ts similarity index 100% rename from src/main/events/achievements/util/check-unlocked-achievements.ts rename to src/main/services/achievements/util/check-unlocked-achievements.ts diff --git a/src/main/events/achievements/util/parseAchievementFile.ts b/src/main/services/achievements/util/parseAchievementFile.ts similarity index 100% rename from src/main/events/achievements/util/parseAchievementFile.ts rename to src/main/services/achievements/util/parseAchievementFile.ts diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 50ef42a9..e4c72f1d 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -8,7 +8,7 @@ import { Game } from "@main/entity"; import { startGameAchievementObserver, stopGameAchievementObserver, -} from "@main/events/achievements/game-achievements-observer"; +} from "@main/services/achievements/game-achievements-observer"; export const gamesPlaytime = new Map< number, diff --git a/src/preload/index.ts b/src/preload/index.ts index 0f135b99..223d2201 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -13,6 +13,7 @@ import type { UpdateProfileRequest, } from "@types"; import type { CatalogueCategory } from "@shared"; +import { Game } from "@main/entity"; contextBridge.exposeInMainWorld("electron", { /* Torrenting */ @@ -49,6 +50,8 @@ contextBridge.exposeInMainWorld("electron", { getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"), + getGameAchievements: (objectId: string, shop: GameShop) => + ipcRenderer.invoke("getGameAchievements", objectId, shop), /* User preferences */ getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"), 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 e723779f..cd02f7a2 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -7,6 +7,7 @@ import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks"; import type { Game, + GameAchievement, GameRepack, GameShop, GameStats, @@ -53,6 +54,7 @@ export function GameDetailsContextProvider({ const [shopDetails, setShopDetails] = useState(null); const [repacks, setRepacks] = useState([]); + const [achievements, setAchievements] = useState([]); const [game, setGame] = useState(null); const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false); @@ -99,8 +101,9 @@ export function GameDetailsContextProvider({ ), window.electron.searchGameRepacks(gameTitle), window.electron.getGameStats(objectID!, shop as GameShop), + window.electron.getGameAchievements(objectID!, shop as GameShop), ]) - .then(([appDetailsResult, repacksResult, statsResult]) => { + .then(([appDetailsResult, repacksResult, statsResult, achievements]) => { if (appDetailsResult.status === "fulfilled") { setShopDetails(appDetailsResult.value); @@ -117,6 +120,11 @@ export function GameDetailsContextProvider({ setRepacks(repacksResult.value); if (statsResult.status === "fulfilled") setStats(statsResult.value); + + if (achievements.status === "fulfilled") { + console.log(achievements.value); + setAchievements(achievements.value); + } }) .finally(() => { setIsLoading(false); @@ -193,6 +201,7 @@ export function GameDetailsContextProvider({ showGameOptionsModal, showRepacksModal, stats, + achievements, hasNSFWContentBlocked, setHasNSFWContentBlocked, setGameColor, diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts index 7c3bd20b..507f0a9e 100644 --- a/src/renderer/src/context/game-details/game-details.context.types.ts +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -1,5 +1,6 @@ import type { Game, + GameAchievement, GameRepack, GameShop, GameStats, @@ -19,6 +20,7 @@ export interface GameDetailsContext { showRepacksModal: boolean; showGameOptionsModal: boolean; stats: GameStats | null; + achievements: GameAchievement[]; hasNSFWContentBlocked: boolean; setGameColor: React.Dispatch>; selectGameExecutable: () => Promise; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 70b77eec..c81957f4 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -25,6 +25,7 @@ import type { UserStats, UserDetails, FriendRequestSync, + GameAchievement, } from "@types"; import type { DiskSpace } from "check-disk-space"; @@ -65,6 +66,10 @@ declare global { searchGameRepacks: (query: string) => Promise; getGameStats: (objectId: string, shop: GameShop) => Promise; getTrendingGames: () => Promise; + getGameAchievements: ( + objectId: string, + shop: GameShop + ) => Promise; /* Library */ addGameToLibrary: ( diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index e56f0764..624b27c4 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -17,7 +17,8 @@ export function Sidebar() { const [activeRequirement, setActiveRequirement] = useState("minimum"); - const { gameTitle, shopDetails, stats } = useContext(gameDetailsContext); + const { gameTitle, shopDetails, stats, achievements } = + useContext(gameDetailsContext); const { t } = useTranslation("game_details"); @@ -45,6 +46,17 @@ export function Sidebar() { isLoading={howLongToBeat.isLoading} /> */} + {achievements.map((achievement, index) => ( +
+ +

{achievement.displayName}

+ {achievement.unlockTime && + new Date(achievement.unlockTime).toDateString()} +
+ ))} + {stats && ( <>
Date: Tue, 24 Sep 2024 16:32:48 -0300 Subject: [PATCH 005/163] feat: save achievements cache --- .../events/catalogue/get-game-achievements.ts | 63 ++++++++++++++----- .../save-all-local-steam-achivements.ts | 1 - src/renderer/src/hooks/use-date.ts | 7 ++- .../pages/game-details/sidebar/sidebar.tsx | 48 +++++++++++--- 4 files changed, 90 insertions(+), 29 deletions(-) diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts index 3325b3c0..acd7a4a6 100644 --- a/src/main/events/catalogue/get-game-achievements.ts +++ b/src/main/events/catalogue/get-game-achievements.ts @@ -2,7 +2,7 @@ import type { GameShop } from "@types"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import { gameRepository } from "@main/repository"; +import { gameAchievementRepository, gameRepository } from "@main/repository"; import { GameAchievement } from "@main/entity"; const getGameAchievements = async ( @@ -16,31 +16,60 @@ const getGameAchievements = async ( achievements: true, }, }); - const gameAchievements = await HydraApi.get( + + const cachedAchievements = game?.achievements?.achievements; + + const apiAchievement = HydraApi.get( "/games/achievements", { objectId, shop }, { needsAuth: false } - ); + ) + .then((achievements) => { + if (game) { + gameAchievementRepository.upsert( + { + game: { id: game.id }, + achievements: JSON.stringify(achievements), + }, + ["game"] + ); + } + + return achievements; + }) + .catch(() => []); + + const gameAchievements = cachedAchievements + ? JSON.parse(cachedAchievements) + : await apiAchievement; const unlockedAchievements = JSON.parse( game?.achievements?.unlockedAchievements || "[]" ) as { name: string; unlockTime: number }[]; - return gameAchievements.map((achievement) => { - const unlockedAchiement = unlockedAchievements.find((localAchievement) => { - return localAchievement.name == achievement.name; + return gameAchievements + .map((achievement) => { + const unlockedAchiement = unlockedAchievements.find( + (localAchievement) => { + return localAchievement.name == achievement.name; + } + ); + + if (unlockedAchiement) { + return { + ...achievement, + unlocked: true, + unlockTime: unlockedAchiement.unlockTime * 1000, + }; + } + + return { ...achievement, unlocked: false, unlockTime: null }; + }) + .sort((a, b) => { + if (a.unlocked && !b.unlocked) return -1; + if (!a.unlocked && b.unlocked) return 1; + return b.unlockTime - a.unlockTime; }); - - if (unlockedAchiement) { - return { - ...achievement, - unlocked: true, - unlockTime: unlockedAchiement.unlockTime * 1000, - }; - } - - return { ...achievement, unlocked: false, unlockTime: null }; - }); }; registerEvent("getGameAchievements", getGameAchievements); diff --git a/src/main/services/achievements/services/save-all-local-steam-achivements.ts b/src/main/services/achievements/services/save-all-local-steam-achivements.ts index dad2c2b8..6db5f96a 100644 --- a/src/main/services/achievements/services/save-all-local-steam-achivements.ts +++ b/src/main/services/achievements/services/save-all-local-steam-achivements.ts @@ -48,7 +48,6 @@ export const saveAllLocalSteamAchivements = async () => { ); console.log(achievementFile.filePath); - console.log(localAchievementFile); for (const a of Object.keys(localAchievementFile)) { // TODO: use checkUnlockedAchievements after refactoring it to be generic diff --git a/src/renderer/src/hooks/use-date.ts b/src/renderer/src/hooks/use-date.ts index 01f55610..3657a76e 100644 --- a/src/renderer/src/hooks/use-date.ts +++ b/src/renderer/src/hooks/use-date.ts @@ -1,4 +1,4 @@ -import { formatDistance, subMilliseconds } from "date-fns"; +import { format, formatDistance, subMilliseconds } from "date-fns"; import type { FormatDistanceOptions } from "date-fns"; import { ptBR, @@ -67,5 +67,10 @@ export function useDate() { return ""; } }, + + format: (timestamp: number): string => { + const locale = getDateLocale(); + return format(timestamp, locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy - HH:mm"); + }, }; } diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 624b27c4..dee44b1f 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -5,8 +5,9 @@ import { Button } from "@renderer/components"; import * as styles from "./sidebar.css"; import { gameDetailsContext } from "@renderer/context"; -import { useFormat } from "@renderer/hooks"; +import { useDate, useFormat } from "@renderer/hooks"; import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; +import { SPACING_UNIT, vars } from "@renderer/theme.css"; export function Sidebar() { const [_howLongToBeat, _setHowLongToBeat] = useState<{ @@ -21,6 +22,7 @@ export function Sidebar() { useContext(gameDetailsContext); const { t } = useTranslation("game_details"); + const { format } = useDate(); const { numberFormatter } = useFormat(); @@ -46,16 +48,42 @@ export function Sidebar() { isLoading={howLongToBeat.isLoading} /> */} - {achievements.map((achievement, index) => ( -
- -

{achievement.displayName}

- {achievement.unlockTime && - new Date(achievement.unlockTime).toDateString()} + {achievements.length && ( +
+ {achievements.map((achievement, index) => ( +
+ +
+

{achievement.displayName}

+ {achievement.unlockTime && format(achievement.unlockTime)} +
+
+ ))}
- ))} + )} {stats && ( <> From f98432f6c603f8061f23ed36e62985d38f9eb4e8 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:31:49 -0300 Subject: [PATCH 006/163] feat: refactor game achievement table --- src/main/entity/game-achievements.entity.ts | 17 ++++------- src/main/entity/game.entity.ts | 4 --- .../events/catalogue/get-game-achievements.ts | 29 +++++++++---------- .../20240919030940_create_game_achievement.ts | 5 ++-- .../save-all-local-steam-achivements.ts | 29 ++++++++++--------- .../game-details/game-details.context.tsx | 1 + src/renderer/src/hooks/use-date.ts | 5 +++- .../pages/game-details/sidebar/sidebar.tsx | 2 +- 8 files changed, 44 insertions(+), 48 deletions(-) diff --git a/src/main/entity/game-achievements.entity.ts b/src/main/entity/game-achievements.entity.ts index 48ca958b..29cca558 100644 --- a/src/main/entity/game-achievements.entity.ts +++ b/src/main/entity/game-achievements.entity.ts @@ -1,20 +1,15 @@ -import { - Column, - Entity, - JoinColumn, - OneToOne, - PrimaryGeneratedColumn, -} from "typeorm"; -import type { Game } from "./game.entity"; +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; @Entity("game_achievement") export class GameAchievement { @PrimaryGeneratedColumn() id: number; - @OneToOne("Game", "achievements") - @JoinColumn() - game: Game; + @Column("text") + objectId: string; + + @Column("text") + shop: string; @Column("text", { nullable: true }) unlockedAchievements: string; diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 98606167..190e7470 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -12,7 +12,6 @@ import { Repack } from "./repack.entity"; import type { GameShop, GameStatus } from "@types"; import { Downloader } from "@shared"; import type { DownloadQueue } from "./download-queue.entity"; -import { GameAchievement } from "./game-achievements.entity"; @Entity("game") export class Game { @@ -77,9 +76,6 @@ export class Game { @JoinColumn() repack: Repack; - @OneToOne("GameAchievement", "game") - achievements: GameAchievement; - @OneToOne("DownloadQueue", "game") downloadQueue: DownloadQueue; diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts index acd7a4a6..fff05248 100644 --- a/src/main/events/catalogue/get-game-achievements.ts +++ b/src/main/events/catalogue/get-game-achievements.ts @@ -1,23 +1,19 @@ -import type { GameShop } from "@types"; - +import type { GameAchievement, GameShop } from "@types"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { gameAchievementRepository, gameRepository } from "@main/repository"; -import { GameAchievement } from "@main/entity"; const getGameAchievements = async ( _event: Electron.IpcMainInvokeEvent, objectId: string, shop: GameShop ): Promise => { - const game = await gameRepository.findOne({ - where: { objectID: objectId, shop }, - relations: { - achievements: true, - }, - }); - - const cachedAchievements = game?.achievements?.achievements; + const [game, cachedAchievements] = await Promise.all([ + gameRepository.findOne({ + where: { objectID: objectId, shop }, + }), + gameAchievementRepository.findOne({ where: { objectId, shop } }), + ]); const apiAchievement = HydraApi.get( "/games/achievements", @@ -28,10 +24,11 @@ const getGameAchievements = async ( if (game) { gameAchievementRepository.upsert( { - game: { id: game.id }, + objectId, + shop, achievements: JSON.stringify(achievements), }, - ["game"] + ["objectId", "shop"] ); } @@ -39,12 +36,12 @@ const getGameAchievements = async ( }) .catch(() => []); - const gameAchievements = cachedAchievements - ? JSON.parse(cachedAchievements) + const gameAchievements = cachedAchievements?.achievements + ? JSON.parse(cachedAchievements.achievements) : await apiAchievement; const unlockedAchievements = JSON.parse( - game?.achievements?.unlockedAchievements || "[]" + cachedAchievements?.unlockedAchievements || "[]" ) as { name: string; unlockTime: number }[]; return gameAchievements diff --git a/src/main/migrations/20240919030940_create_game_achievement.ts b/src/main/migrations/20240919030940_create_game_achievement.ts index a03ea2fc..791eeb29 100644 --- a/src/main/migrations/20240919030940_create_game_achievement.ts +++ b/src/main/migrations/20240919030940_create_game_achievement.ts @@ -6,10 +6,11 @@ export const CreateGameAchievement: HydraMigration = { up: (knex: Knex) => { return knex.schema.createTable("game_achievement", (table) => { table.increments("id").primary(); - table.integer("gameId").notNullable().unique(); + table.text("objectId").notNullable(); + table.text("shop").notNullable(); table.text("achievements"); table.text("unlockedAchievements"); - table.foreign("gameId").references("game.id").onDelete("CASCADE"); + table.unique(["objectId", "shop"]); }); }, diff --git a/src/main/services/achievements/services/save-all-local-steam-achivements.ts b/src/main/services/achievements/services/save-all-local-steam-achivements.ts index 6db5f96a..fde1ba19 100644 --- a/src/main/services/achievements/services/save-all-local-steam-achivements.ts +++ b/src/main/services/achievements/services/save-all-local-steam-achivements.ts @@ -9,17 +9,18 @@ export const saveAllLocalSteamAchivements = async () => { for (const key of Object.keys(gameAchievementFiles)) { const objectId = key; - const game = await gameRepository.findOne({ - where: { objectID: objectId }, - }); + const [game, localAchievements] = await Promise.all([ + gameRepository.findOne({ + where: { objectID: objectId, shop: "steam", isDeleted: false }, + }), + gameAchievementRepository.findOne({ + where: { objectId, shop: "steam" }, + }), + ]); if (!game) continue; - const savedGameAchievements = await gameAchievementRepository.findOneBy({ - game: game, - }); - - if (!savedGameAchievements || !savedGameAchievements.achievements) { + if (!localAchievements || !localAchievements.achievements) { HydraApi.get( "/games/achievements", { @@ -31,10 +32,11 @@ export const saveAllLocalSteamAchivements = async () => { .then((achievements) => { return gameAchievementRepository.upsert( { - game: { id: game.id }, + objectId, + shop: "steam", achievements: JSON.stringify(achievements), }, - ["game"] + ["objectId", "shop"] ); }) .catch(console.log); @@ -58,12 +60,13 @@ export const saveAllLocalSteamAchivements = async () => { } } - await gameAchievementRepository.upsert( + gameAchievementRepository.upsert( { - game: { id: game.id }, + objectId, + shop: "steam", unlockedAchievements: JSON.stringify(unlockedAchievements), }, - ["game"] + ["objectId", "shop"] ); } }; 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 cd02f7a2..c8ac4814 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -31,6 +31,7 @@ export const gameDetailsContext = createContext({ showRepacksModal: false, showGameOptionsModal: false, stats: null, + achievements: [], hasNSFWContentBlocked: false, setGameColor: () => {}, selectGameExecutable: async () => null, diff --git a/src/renderer/src/hooks/use-date.ts b/src/renderer/src/hooks/use-date.ts index 3657a76e..a0cfdb9f 100644 --- a/src/renderer/src/hooks/use-date.ts +++ b/src/renderer/src/hooks/use-date.ts @@ -70,7 +70,10 @@ export function useDate() { format: (timestamp: number): string => { const locale = getDateLocale(); - return format(timestamp, locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy - HH:mm"); + return format( + timestamp, + locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy - HH:mm" + ); }, }; } diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index dee44b1f..0ef16194 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -7,7 +7,7 @@ import * as styles from "./sidebar.css"; import { gameDetailsContext } from "@renderer/context"; import { useDate, useFormat } from "@renderer/hooks"; import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT } from "@renderer/theme.css"; export function Sidebar() { const [_howLongToBeat, _setHowLongToBeat] = useState<{ From e64a414309c3d3ff43dffaf411c7bca546111a13 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 25 Sep 2024 19:37:28 +0100 Subject: [PATCH 007/163] feat: adding cloud sync --- .gitignore | 5 +- electron-builder.yml | 1 + package.json | 7 +- postinstall.cjs | 44 +++ src/main/constants.ts | 2 + .../check-game-cloud-sync-support.ts | 14 + .../events/cloud-sync/delete-game-artifact.ts | 9 + .../cloud-sync/download-game-artifact.ts | 56 ++++ .../events/cloud-sync/get-game-artifacts.ts | 18 ++ .../cloud-sync/get-game-backup-preview.ts | 17 ++ .../events/cloud-sync/upload-save-game.ts | 101 +++++++ src/main/events/index.ts | 6 + src/main/events/profile/update-profile.ts | 3 + src/main/services/index.ts | 1 + src/main/services/ludusavi.ts | 63 ++++ src/main/services/steam-grid.ts | 3 +- src/main/workers/ludusavi.worker.ts | 61 ++++ src/preload/index.ts | 36 +++ src/renderer/src/assets/lottie/cloud.json | 1 + .../context/cloud-sync/cloud-sync.context.tsx | 187 ++++++++++++ .../game-details/game-details.context.tsx | 27 +- src/renderer/src/context/index.ts | 1 + src/renderer/src/declaration.d.ts | 33 +++ src/renderer/src/dexie.ts | 12 +- .../cloud-sync-modal/cloud-sync-modal.css.ts | 26 ++ .../cloud-sync-modal/cloud-sync-modal.tsx | 178 ++++++++++++ .../game-details/game-details-content.tsx | 35 ++- .../pages/game-details/game-details.css.ts | 36 ++- .../src/pages/game-details/game-details.tsx | 134 +++++---- src/types/index.ts | 10 + src/types/ludusavi.types.ts | 23 ++ torrent-client/torrent_downloader.py | 11 +- yarn.lock | 275 +++++++++++++++++- 33 files changed, 1352 insertions(+), 84 deletions(-) create mode 100644 postinstall.cjs create mode 100644 src/main/events/cloud-sync/check-game-cloud-sync-support.ts create mode 100644 src/main/events/cloud-sync/delete-game-artifact.ts create mode 100644 src/main/events/cloud-sync/download-game-artifact.ts create mode 100644 src/main/events/cloud-sync/get-game-artifacts.ts create mode 100644 src/main/events/cloud-sync/get-game-backup-preview.ts create mode 100644 src/main/events/cloud-sync/upload-save-game.ts create mode 100644 src/main/services/ludusavi.ts create mode 100644 src/main/workers/ludusavi.worker.ts create mode 100644 src/renderer/src/assets/lottie/cloud.json create mode 100644 src/renderer/src/context/cloud-sync/cloud-sync.context.tsx create mode 100644 src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts create mode 100644 src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx create mode 100644 src/types/ludusavi.types.ts diff --git a/.gitignore b/.gitignore index fb4badd7..b9dcfecb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -.vscode -node_modules +.vscode/ +node_modules/ hydra-download-manager/ fastlist.exe __pycache__ @@ -10,3 +10,4 @@ out .env .vite sentry.properties +ludusavi/ \ No newline at end of file diff --git a/electron-builder.yml b/electron-builder.yml index a085b1e9..46f4a872 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,6 +3,7 @@ productName: Hydra directories: buildResources: build extraResources: + - ludusavi - hydra-download-manager - seeds - from: node_modules/create-desktop-shortcuts/src/windows.vbs diff --git a/package.json b/package.json index 6fd3f905..08f096f7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", - "postinstall": "electron-builder install-app-deps", + "postinstall": "electron-builder install-app-deps && node ./postinstall.cjs", "build:unpack": "npm run build && electron-builder --dir", "build:win": "electron-vite build && electron-builder --win", "build:mac": "electron-vite build && electron-builder --mac", @@ -42,6 +42,8 @@ "@vanilla-extract/css": "^1.14.2", "@vanilla-extract/dynamic": "^2.1.1", "@vanilla-extract/recipes": "^0.5.2", + "adm-zip": "^0.5.16", + "archiver": "^7.0.1", "auto-launch": "^5.0.6", "axios": "^1.7.7", "better-sqlite3": "^11.2.1", @@ -86,8 +88,11 @@ "@electron-toolkit/tsconfig": "^1.0.1", "@sentry/vite-plugin": "^2.20.1", "@swc/core": "^1.4.16", + "@types/adm-zip": "^0.5.5", + "@types/archiver": "^6.0.2", "@types/auto-launch": "^5.0.5", "@types/color": "^3.0.6", + "@types/folder-hash": "^4.0.4", "@types/jsdom": "^21.1.6", "@types/jsonwebtoken": "^9.0.6", "@types/lodash-es": "^4.17.12", diff --git a/postinstall.cjs b/postinstall.cjs new file mode 100644 index 00000000..ce9c5909 --- /dev/null +++ b/postinstall.cjs @@ -0,0 +1,44 @@ +const { default: axios } = require("axios"); +const util = require("node:util"); +const fs = require("node:fs"); +const path = require("node:path"); + +const exec = util.promisify(require("node:child_process").exec); + +const fileName = { + win32: "ludusavi-v0.25.0-win64.zip", + linux: "ludusavi-v0.25.0-linux.zip", + darwin: "ludusavi-v0.25.0-mac.zip", +}; + +const downloadLudusavi = async () => { + if (fs.existsSync("ludusavi")) { + console.log("Ludusavi already exists, skipping download..."); + return; + } + + const file = fileName[process.platform]; + const downloadUrl = `https://github.com/mtkennerly/ludusavi/releases/download/v0.25.0/${file}`; + + console.log(`Downloading ${file}...`); + + const response = await axios.get(downloadUrl, { responseType: "stream" }); + + const stream = response.data.pipe(fs.createWriteStream(file)); + + stream.on("finish", async () => { + console.log(`Downloaded ${file}, extracting...`); + + const pwd = process.cwd(); + const targetPath = path.join(pwd, "ludusavi"); + + await exec(`npx extract-zip ${file} ${targetPath}`); + fs.chmodSync(path.join(targetPath, "ludusavi"), 0o755); + console.log("Extracted. Renaming folder..."); + + console.log(`Extracted ${file}, removing compressed downloaded file...`); + fs.rmSync(file); + }); +}; + +downloadLudusavi(); diff --git a/src/main/constants.ts b/src/main/constants.ts index 92973118..8af17a44 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -12,4 +12,6 @@ export const seedsPath = app.isPackaged ? path.join(process.resourcesPath, "seeds") : path.join(__dirname, "..", "..", "seeds"); +export const backupsPath = path.join(app.getPath("userData"), "Backups"); + export const appVersion = app.getVersion(); diff --git a/src/main/events/cloud-sync/check-game-cloud-sync-support.ts b/src/main/events/cloud-sync/check-game-cloud-sync-support.ts new file mode 100644 index 00000000..4054d430 --- /dev/null +++ b/src/main/events/cloud-sync/check-game-cloud-sync-support.ts @@ -0,0 +1,14 @@ +import { registerEvent } from "../register-event"; +import { GameShop } from "@types"; +import { Ludusavi } from "@main/services"; + +const checkGameCloudSyncSupport = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop +) => { + const games = await Ludusavi.findGames(shop, objectId); + return games.length === 1; +}; + +registerEvent("checkGameCloudSyncSupport", checkGameCloudSyncSupport); diff --git a/src/main/events/cloud-sync/delete-game-artifact.ts b/src/main/events/cloud-sync/delete-game-artifact.ts new file mode 100644 index 00000000..fa869896 --- /dev/null +++ b/src/main/events/cloud-sync/delete-game-artifact.ts @@ -0,0 +1,9 @@ +import { HydraApi } from "@main/services"; +import { registerEvent } from "../register-event"; + +const deleteGameArtifact = async ( + _event: Electron.IpcMainInvokeEvent, + gameArtifactId: string +) => HydraApi.delete<{ ok: boolean }>(`/games/artifacts/${gameArtifactId}`); + +registerEvent("deleteGameArtifact", deleteGameArtifact); diff --git a/src/main/events/cloud-sync/download-game-artifact.ts b/src/main/events/cloud-sync/download-game-artifact.ts new file mode 100644 index 00000000..a1254dc3 --- /dev/null +++ b/src/main/events/cloud-sync/download-game-artifact.ts @@ -0,0 +1,56 @@ +import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services"; +import fs from "node:fs"; +import AdmZip from "adm-zip"; +import { registerEvent } from "../register-event"; +import axios from "axios"; +import { app } from "electron"; +import path from "node:path"; +import { backupsPath } from "@main/constants"; +import type { GameShop } from "@types"; + +const downloadGameArtifact = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop, + gameArtifactId: string +) => { + const { downloadUrl, objectKey } = await HydraApi.post<{ + downloadUrl: string; + objectKey: string; + }>(`/games/artifacts/${gameArtifactId}/download`); + + const response = await axios.get(downloadUrl, { + responseType: "stream", + }); + + const zipLocation = path.join(app.getPath("userData"), objectKey); + const backupPath = path.join(backupsPath, `${shop}-${objectId}`); + + const writer = fs.createWriteStream(zipLocation); + + response.data.pipe(writer); + + writer.on("error", (err) => { + logger.error("Failed to write zip", err); + throw err; + }); + + writer.on("close", () => { + const zip = new AdmZip(zipLocation); + zip.extractAllToAsync(backupPath, true, true, (err) => { + if (err) { + logger.error("Failed to extract zip", err); + throw err; + } + + Ludusavi.restoreBackup(backupPath).then(() => { + WindowManager.mainWindow?.webContents.send( + `on-download-complete-${objectId}-${shop}`, + true + ); + }); + }); + }); +}; + +registerEvent("downloadGameArtifact", downloadGameArtifact); diff --git a/src/main/events/cloud-sync/get-game-artifacts.ts b/src/main/events/cloud-sync/get-game-artifacts.ts new file mode 100644 index 00000000..b32dfd79 --- /dev/null +++ b/src/main/events/cloud-sync/get-game-artifacts.ts @@ -0,0 +1,18 @@ +import { HydraApi } from "@main/services"; +import { registerEvent } from "../register-event"; +import type { GameArtifact, GameShop } from "@types"; + +const getGameArtifacts = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop +) => { + const params = new URLSearchParams({ + objectId, + shop, + }); + + return HydraApi.get(`/games/artifacts?${params.toString()}`); +}; + +registerEvent("getGameArtifacts", getGameArtifacts); diff --git a/src/main/events/cloud-sync/get-game-backup-preview.ts b/src/main/events/cloud-sync/get-game-backup-preview.ts new file mode 100644 index 00000000..433fccc4 --- /dev/null +++ b/src/main/events/cloud-sync/get-game-backup-preview.ts @@ -0,0 +1,17 @@ +import { registerEvent } from "../register-event"; +import { GameShop } from "@types"; +import { Ludusavi } from "@main/services"; +import path from "node:path"; +import { backupsPath } from "@main/constants"; + +const getGameBackupPreview = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop +) => { + const backupPath = path.join(backupsPath, `${shop}-${objectId}`); + + return Ludusavi.getBackupPreview(shop, objectId, backupPath); +}; + +registerEvent("getGameBackupPreview", getGameBackupPreview); diff --git a/src/main/events/cloud-sync/upload-save-game.ts b/src/main/events/cloud-sync/upload-save-game.ts new file mode 100644 index 00000000..0c9a4fbd --- /dev/null +++ b/src/main/events/cloud-sync/upload-save-game.ts @@ -0,0 +1,101 @@ +import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services"; +import { registerEvent } from "../register-event"; +import fs from "node:fs"; +import path from "node:path"; +import archiver from "archiver"; +import crypto from "node:crypto"; +import { GameShop } from "@types"; +import axios from "axios"; +import os from "node:os"; +import { app } from "electron"; +import { backupsPath } from "@main/constants"; + +const compressBackupToArtifact = async ( + shop: GameShop, + objectId: string, + cb: (zipLocation: string) => void +) => { + const backupPath = path.join(backupsPath, `${shop}-${objectId}`); + + await Ludusavi.backupGame(shop, objectId, backupPath); + + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + const zipLocation = path.join( + app.getPath("userData"), + `${crypto.randomUUID()}.zip` + ); + + const output = fs.createWriteStream(zipLocation); + + output.on("close", () => { + cb(zipLocation); + }); + + output.on("error", (err) => { + logger.error("Failed to compress folder", err); + throw err; + }); + + archive.pipe(output); + + archive.directory(backupPath, false); + archive.finalize(); +}; + +const uploadSaveGame = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop +) => { + compressBackupToArtifact(shop, objectId, (zipLocation) => { + fs.stat(zipLocation, async (err, stat) => { + if (err) { + logger.error("Failed to get zip file stats", err); + throw err; + } + + const { uploadUrl } = await HydraApi.post<{ + id: string; + uploadUrl: string; + }>("/games/artifacts", { + artifactLengthInBytes: stat.size, + shop, + objectId, + hostname: os.hostname(), + }); + + fs.readFile(zipLocation, async (err, fileBuffer) => { + if (err) { + logger.error("Failed to read zip file", err); + throw err; + } + + axios.put(uploadUrl, fileBuffer, { + headers: { + "Content-Type": "application/zip", + }, + onUploadProgress: (progressEvent) => { + if (progressEvent.progress === 1) { + fs.rm(zipLocation, (err) => { + if (err) { + logger.error("Failed to remove zip file", err); + throw err; + } + + WindowManager.mainWindow?.webContents.send( + `on-upload-complete-${objectId}-${shop}`, + true + ); + }); + } + }, + }); + }); + }); + }); +}; + +registerEvent("uploadSaveGame", uploadSaveGame); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 73bf38f4..4caa577c 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -58,6 +58,12 @@ import "./profile/update-profile"; import "./profile/process-profile-image"; import "./profile/send-friend-request"; import "./profile/sync-friend-requests"; +import "./cloud-sync/download-game-artifact"; +import "./cloud-sync//get-game-artifacts"; +import "./cloud-sync/get-game-backup-preview"; +import "./cloud-sync/upload-save-game"; +import "./cloud-sync/check-game-cloud-sync-support"; +import "./cloud-sync/delete-game-artifact"; import { isPortableVersion } from "@main/helpers"; ipcMain.handle("ping", () => "pong"); diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index eb80bc47..4135aae5 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -33,6 +33,9 @@ const getNewProfileImageUrl = async (localImageUrl: string) => { headers: { "Content-Type": mimeType, }, + onUploadProgress: (progressEvent) => { + console.log(progressEvent); + }, }); return profileImageUrl; diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 255b3871..8c6e6cda 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -9,3 +9,4 @@ export * from "./process-watcher"; export * from "./main-loop"; export * from "./repacks-manager"; export * from "./hydra-api"; +export * from "./ludusavi"; diff --git a/src/main/services/ludusavi.ts b/src/main/services/ludusavi.ts new file mode 100644 index 00000000..838b5f9b --- /dev/null +++ b/src/main/services/ludusavi.ts @@ -0,0 +1,63 @@ +import { GameShop, LudusaviBackup } from "@types"; +import Piscina from "piscina"; + +import { app } from "electron"; +import path from "node:path"; + +import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath"; + +const binaryPath = app.isPackaged + ? path.join(process.resourcesPath, "ludusavi", "ludusavi") + : path.join(__dirname, "..", "..", "ludusavi", "ludusavi"); + +export class Ludusavi { + private static worker = new Piscina({ + filename: ludusaviWorkerPath, + workerData: { + binaryPath, + }, + }); + + static async findGames(shop: GameShop, objectId: string): Promise { + const games = await this.worker.run( + { objectId, shop }, + { name: "findGames" } + ); + + return games; + } + + static async backupGame( + shop: GameShop, + objectId: string, + backupPath: string + ): Promise { + const games = await this.findGames(shop, objectId); + if (!games.length) throw new Error("Game not found"); + + return this.worker.run( + { title: games[0], backupPath }, + { name: "backupGame" } + ); + } + + static async getBackupPreview( + shop: GameShop, + objectId: string, + backupPath: string + ): Promise { + const games = await this.findGames(shop, objectId); + if (!games.length) return null; + + const backupData = await this.worker.run( + { title: games[0], backupPath, preview: true }, + { name: "backupGame" } + ); + + return backupData; + } + + static async restoreBackup(backupPath: string) { + return this.worker.run(backupPath, { name: "restoreBackup" }); + } +} diff --git a/src/main/services/steam-grid.ts b/src/main/services/steam-grid.ts index c762eaf6..2bdee28d 100644 --- a/src/main/services/steam-grid.ts +++ b/src/main/services/steam-grid.ts @@ -1,3 +1,4 @@ +import type { GameShop } from "@types"; import axios from "axios"; export interface SteamGridResponse { @@ -22,7 +23,7 @@ export interface SteamGridGameResponse { export const getSteamGridData = async ( objectID: string, path: string, - shop: string, + shop: GameShop, params: Record = {} ): Promise => { const searchParams = new URLSearchParams(params); diff --git a/src/main/workers/ludusavi.worker.ts b/src/main/workers/ludusavi.worker.ts new file mode 100644 index 00000000..2a1d266c --- /dev/null +++ b/src/main/workers/ludusavi.worker.ts @@ -0,0 +1,61 @@ +import type { GameShop, LudusaviBackup, LudusaviFindResult } from "@types"; +import cp from "node:child_process"; + +import { workerData } from "node:worker_threads"; + +const { binaryPath } = workerData; + +export const findGames = ({ + shop, + objectId, +}: { + shop: GameShop; + objectId: string; +}) => { + const args = ["find", "--api"]; + + if (shop === "steam") { + args.push("--steam-id", objectId); + } + + const result = cp.execFileSync(binaryPath, args); + + const games = JSON.parse(result.toString("utf-8")) as LudusaviFindResult; + return Object.keys(games.games); +}; + +export const backupGame = ({ + title, + backupPath, + preview = false, +}: { + title: string; + backupPath: string; + preview?: boolean; +}) => { + const args = ["backup", title, "--api", "--force"]; + + if (preview) { + args.push("--preview"); + } + + if (backupPath) { + args.push("--path", backupPath); + } + + const result = cp.execFileSync(binaryPath, args); + + return JSON.parse(result.toString("utf-8")) as LudusaviBackup; +}; + +export const restoreBackup = (backupPath: string) => { + const result = cp.execFileSync(binaryPath, [ + "restore", + "--path", + backupPath, + "--api", + "--force", + ]); + + return JSON.parse(result.toString("utf-8")) as LudusaviBackup; +}; diff --git a/src/preload/index.ts b/src/preload/index.ts index 4d7b7183..32a747a5 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -115,6 +115,42 @@ contextBridge.exposeInMainWorld("electron", { getDiskFreeSpace: (path: string) => ipcRenderer.invoke("getDiskFreeSpace", path), + /* Cloud sync */ + uploadSaveGame: (objectId: string, shop: GameShop) => + ipcRenderer.invoke("uploadSaveGame", objectId, shop), + downloadGameArtifact: ( + objectId: string, + shop: GameShop, + gameArtifactId: string + ) => + ipcRenderer.invoke("downloadGameArtifact", objectId, shop, gameArtifactId), + getGameArtifacts: (objectId: string, shop: GameShop) => + ipcRenderer.invoke("getGameArtifacts", objectId, shop), + getGameBackupPreview: (objectId: string, shop: GameShop) => + ipcRenderer.invoke("getGameBackupPreview", objectId, shop), + checkGameCloudSyncSupport: (objectId: string, shop: GameShop) => + ipcRenderer.invoke("checkGameCloudSyncSupport", objectId, shop), + deleteGameArtifact: (gameArtifactId: string) => + ipcRenderer.invoke("deleteGameArtifact", gameArtifactId), + onUploadComplete: (objectId: string, shop: GameShop, cb: () => void) => { + const listener = (_event: Electron.IpcRendererEvent) => cb(); + ipcRenderer.on(`on-upload-complete-${objectId}-${shop}`, listener); + return () => + ipcRenderer.removeListener( + `on-upload-complete-${objectId}-${shop}`, + listener + ); + }, + onDownloadComplete: (objectId: string, shop: GameShop, cb: () => void) => { + const listener = (_event: Electron.IpcRendererEvent) => cb(); + ipcRenderer.on(`on-download-complete-${objectId}-${shop}`, listener); + return () => + ipcRenderer.removeListener( + `on-download-complete-${objectId}-${shop}`, + listener + ); + }, + /* Misc */ ping: () => ipcRenderer.invoke("ping"), getVersion: () => ipcRenderer.invoke("getVersion"), diff --git a/src/renderer/src/assets/lottie/cloud.json b/src/renderer/src/assets/lottie/cloud.json new file mode 100644 index 00000000..9df1e119 --- /dev/null +++ b/src/renderer/src/assets/lottie/cloud.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":30,"ip":0,"op":60,"w":400,"h":400,"nm":"Cloud","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[322.789,202.565,0],"to":[-1.5,-0.167,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[313.789,201.565,0],"to":[0,0,0],"ti":[-1.5,-0.167,0]},{"t":60,"s":[322.789,202.565,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-38.564],[38.564,0],[0,38.564],[-38.564,0]],"o":[[0,38.564],[-38.564,0],[0,-38.564],[38.564,0]],"v":[[69.827,0],[0,69.827],[-69.827,0],[0,-69.827]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[243.704,202.565,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[233.704,202.565,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"t":60,"s":[243.704,202.565,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-38.564],[38.564,0],[0,38.564],[-38.564,0]],"o":[[0,38.564],[-38.564,0],[0,-38.564],[38.564,0]],"v":[[69.827,0],[0,69.827],[-69.827,0],[0,-69.827]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[260.681,151.053,0],"to":[1.333,-1.333,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[268.681,143.053,0],"to":[0,0,0],"ti":[1.333,-1.333,0]},{"t":60,"s":[260.681,151.053,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-38.564],[38.564,0],[0,38.564],[-38.564,0]],"o":[[0,38.564],[-38.564,0],[0,-38.564],[38.564,0]],"v":[[69.827,0],[0,69.827],[-69.827,0],[0,-69.827]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[162.135,206.563,0],"to":[-0.833,-0.167,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[157.135,205.563,0],"to":[0,0,0],"ti":[-0.833,-0.167,0]},{"t":60,"s":[162.135,206.563,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-36.66],[36.66,0],[0,36.66],[-36.66,0]],"o":[[0,36.66],[-36.66,0],[0,-36.66],[36.66,0]],"v":[[66.378,0],[0,66.378],[-66.378,0],[0,-66.378]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[180.178,132.225,0],"to":[-0.5,-2.333,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[177.178,118.225,0],"to":[0,0,0],"ti":[-0.5,-2.333,0]},{"t":60,"s":[180.178,132.225,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-50.068],[50.068,0],[0,50.068],[-50.068,0]],"o":[[0,50.068],[-50.068,0],[0,-50.068],[50.068,0]],"v":[[90.655,0],[0,90.655],[-90.655,0],[0,-90.655]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[95.756,208.288,0],"to":[-1.167,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[88.756,208.288,0],"to":[0,0,0],"ti":[-1.167,0,0]},{"t":60,"s":[95.756,208.288,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-35.403],[35.403,0],[0,35.403],[-35.403,0]],"o":[[0,35.403],[-35.403,0],[0,-35.403],[35.403,0]],"v":[[64.103,0],[0,64.103],[-64.103,0],[0,-64.103]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":3,"nm":"Null 1","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[19.822,67.775,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":270,"st":0,"bm":0}],"markers":[],"props":{}} \ No newline at end of file diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx new file mode 100644 index 00000000..38bbdb40 --- /dev/null +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -0,0 +1,187 @@ +import { gameBackupsTable } from "@renderer/dexie"; +import { useToast } from "@renderer/hooks"; +import type { LudusaviBackup, GameArtifact, GameShop } from "@types"; +import React, { + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; + +export enum CloudSyncState { + New, + Different, + Same, + Unknown, +} + +export interface CloudSyncContext { + backupPreview: LudusaviBackup | null; + artifacts: GameArtifact[]; + showCloudSyncModal: boolean; + supportsCloudSync: boolean | null; + backupState: CloudSyncState; + setShowCloudSyncModal: React.Dispatch>; + downloadGameArtifact: (gameArtifactId: string) => Promise; + uploadSaveGame: () => Promise; + deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>; + restoringBackup: boolean; + uploadingBackup: boolean; +} + +export const cloudSyncContext = createContext({ + backupPreview: null, + showCloudSyncModal: false, + supportsCloudSync: null, + backupState: CloudSyncState.Unknown, + setShowCloudSyncModal: () => {}, + downloadGameArtifact: async () => {}, + uploadSaveGame: async () => {}, + artifacts: [], + deleteGameArtifact: async () => ({ ok: false }), + restoringBackup: false, + uploadingBackup: false, +}); + +const { Provider } = cloudSyncContext; +export const { Consumer: CloudSyncContextConsumer } = cloudSyncContext; + +export interface CloudSyncContextProviderProps { + children: React.ReactNode; + objectId: string; + shop: GameShop; +} + +export function CloudSyncContextProvider({ + children, + objectId, + shop, +}: CloudSyncContextProviderProps) { + const [supportsCloudSync, setSupportsCloudSync] = useState( + null + ); + const [artifacts, setArtifacts] = useState([]); + const [showCloudSyncModal, setShowCloudSyncModal] = useState(false); + const [backupPreview, setBackupPreview] = useState( + null + ); + const [restoringBackup, setRestoringBackup] = useState(false); + const [uploadingBackup, setUploadingBackup] = useState(false); + + const { showSuccessToast } = useToast(); + + const downloadGameArtifact = useCallback( + async (gameArtifactId: string) => { + setRestoringBackup(true); + window.electron.downloadGameArtifact(objectId, shop, gameArtifactId); + }, + [objectId, shop] + ); + + const getGameBackupPreview = useCallback(async () => { + window.electron.getGameArtifacts(objectId, shop).then((results) => { + setArtifacts(results); + }); + + window.electron.getGameBackupPreview(objectId, shop).then((preview) => { + if (preview && Object.keys(preview.games).length) { + setBackupPreview(preview); + } + }); + }, [objectId, shop]); + + const uploadSaveGame = useCallback(async () => { + setUploadingBackup(true); + window.electron.uploadSaveGame(objectId, shop); + }, [objectId, shop]); + + useEffect(() => { + const removeUploadCompleteListener = window.electron.onUploadComplete( + objectId, + shop, + () => { + showSuccessToast("backup_uploaded"); + + setUploadingBackup(false); + gameBackupsTable.add({ + objectId, + shop, + createdAt: new Date(), + }); + + getGameBackupPreview(); + } + ); + + const removeDownloadCompleteListener = window.electron.onDownloadComplete( + objectId, + shop, + () => { + showSuccessToast("backup_restored"); + + setRestoringBackup(false); + getGameBackupPreview(); + } + ); + + return () => { + removeUploadCompleteListener(); + removeDownloadCompleteListener(); + }; + }, [objectId, shop, showSuccessToast, getGameBackupPreview]); + + const deleteGameArtifact = useCallback( + async (gameArtifactId: string) => { + return window.electron.deleteGameArtifact(gameArtifactId).then(() => { + getGameBackupPreview(); + return { ok: true }; + }); + }, + [getGameBackupPreview] + ); + + useEffect(() => { + getGameBackupPreview(); + + window.electron.checkGameCloudSyncSupport(objectId, shop).then((result) => { + setSupportsCloudSync(result); + }); + }, [objectId, shop, getGameBackupPreview]); + + useEffect(() => { + if (showCloudSyncModal) { + getGameBackupPreview(); + } + }, [getGameBackupPreview, showCloudSyncModal]); + + const backupState = useMemo(() => { + if (!backupPreview) return CloudSyncState.Unknown; + if (backupPreview.overall.changedGames.new) return CloudSyncState.New; + if (backupPreview.overall.changedGames.different) + return CloudSyncState.Different; + if (backupPreview.overall.changedGames.same) return CloudSyncState.Same; + + return CloudSyncState.Unknown; + }, [backupPreview]); + + return ( + + {children} + + ); +} 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 120728b1..82984d9a 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -5,7 +5,6 @@ import { useEffect, useState, } from "react"; -import { useParams, useSearchParams } from "react-router-dom"; import { setHeaderTitle } from "@renderer/features"; import { getSteamLanguage } from "@renderer/helpers"; @@ -51,13 +50,17 @@ export const { Consumer: GameDetailsContextConsumer } = gameDetailsContext; export interface GameDetailsContextProps { children: React.ReactNode; + objectId: string; + gameTitle: string; + shop: GameShop; } export function GameDetailsContextProvider({ children, + objectId, + gameTitle, + shop, }: GameDetailsContextProps) { - const { objectID, shop } = useParams(); - const [shopDetails, setShopDetails] = useState(null); const [game, setGame] = useState(null); const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false); @@ -72,10 +75,6 @@ export function GameDetailsContextProvider({ const [repacks, setRepacks] = useState([]); - const [searchParams] = useSearchParams(); - - const gameTitle = searchParams.get("title")!; - const { searchRepacks, isIndexingRepacks } = useContext(repacksContext); useEffect(() => { @@ -98,9 +97,9 @@ export function GameDetailsContextProvider({ const updateGame = useCallback(async () => { return window.electron - .getGameByObjectID(objectID!) + .getGameByObjectID(objectId!) .then((result) => setGame(result)); - }, [setGame, objectID]); + }, [setGame, objectId]); const isGameDownloading = lastPacket?.game.id === game?.id; @@ -111,7 +110,7 @@ export function GameDetailsContextProvider({ useEffect(() => { window.electron .getGameShopDetails( - objectID!, + objectId!, shop as GameShop, getSteamLanguage(i18n.language) ) @@ -130,12 +129,12 @@ export function GameDetailsContextProvider({ setIsLoading(false); }); - window.electron.getGameStats(objectID!, shop as GameShop).then((result) => { + window.electron.getGameStats(objectId!, shop as GameShop).then((result) => { setStats(result); }); updateGame(); - }, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]); + }, [updateGame, dispatch, gameTitle, objectId, shop, i18n.language]); useEffect(() => { setShopDetails(null); @@ -143,7 +142,7 @@ export function GameDetailsContextProvider({ setIsLoading(true); setisGameRunning(false); dispatch(setHeaderTitle(gameTitle)); - }, [objectID, gameTitle, dispatch]); + }, [objectId, gameTitle, dispatch]); useEffect(() => { const unsubscribe = window.electron.onGamesRunning((gamesIds) => { @@ -200,7 +199,7 @@ export function GameDetailsContextProvider({ gameTitle, isGameRunning, isLoading, - objectID, + objectID: objectId, gameColor, showGameOptionsModal, showRepacksModal, diff --git a/src/renderer/src/context/index.ts b/src/renderer/src/context/index.ts index 8d8b9223..948b90b2 100644 --- a/src/renderer/src/context/index.ts +++ b/src/renderer/src/context/index.ts @@ -2,3 +2,4 @@ export * from "./game-details/game-details.context"; export * from "./settings/settings.context"; export * from "./user-profile/user-profile.context"; export * from "./repacks/repacks.context"; +export * from "./cloud-sync/cloud-sync.context"; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 28c5caf7..6fab054a 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -26,6 +26,8 @@ import type { UserDetails, FriendRequestSync, DownloadSourceValidationResult, + GameArtifact, + LudusaviBackup, } from "@types"; import type { DiskSpace } from "check-disk-space"; @@ -113,6 +115,37 @@ declare global { /* Hardware */ getDiskFreeSpace: (path: string) => Promise; + /* Cloud sync */ + uploadSaveGame: (objectId: string, shop: GameShop) => Promise; + downloadGameArtifact: ( + objectId: string, + shop: GameShop, + gameArtifactId: string + ) => Promise; + getGameArtifacts: ( + objectId: string, + shop: GameShop + ) => Promise; + getGameBackupPreview: ( + objectId: string, + shop: GameShop + ) => Promise; + checkGameCloudSyncSupport: ( + objectId: string, + shop: GameShop + ) => Promise; + deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>; + onDownloadComplete: ( + objectId: string, + shop: GameShop, + cb: () => void + ) => () => Electron.IpcRenderer; + onUploadComplete: ( + objectId: string, + shop: GameShop, + cb: () => void + ) => () => Electron.IpcRenderer; + /* Misc */ openExternal: (src: string) => Promise; getVersion: () => Promise; diff --git a/src/renderer/src/dexie.ts b/src/renderer/src/dexie.ts index 23f0bf83..75dc6079 100644 --- a/src/renderer/src/dexie.ts +++ b/src/renderer/src/dexie.ts @@ -1,13 +1,23 @@ +import { GameShop } from "@types"; import { Dexie } from "dexie"; +export interface GameBackup { + id?: number; + shop: GameShop; + objectId: string; + createdAt: Date; +} + export const db = new Dexie("Hydra"); -db.version(1).stores({ +db.version(3).stores({ repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`, downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`, + gameBackups: `++id, [shop+objectId], createdAt`, }); export const downloadSourcesTable = db.table("downloadSources"); export const repacksTable = db.table("repacks"); +export const gameBackupsTable = db.table("gameBackups"); db.open(); diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts new file mode 100644 index 00000000..bb3335fa --- /dev/null +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts @@ -0,0 +1,26 @@ +import { style } from "@vanilla-extract/css"; + +import { SPACING_UNIT, vars } from "../../../theme.css"; + +export const artifacts = style({ + display: "flex", + gap: `${SPACING_UNIT}px`, + flexDirection: "column", + listStyle: "none", + margin: "0", + padding: "0", +}); + +export const artifactButton = style({ + display: "flex", + textAlign: "left", + flexDirection: "row", + alignItems: "center", + gap: `${SPACING_UNIT}px`, + color: vars.color.body, + padding: `${SPACING_UNIT * 2}px`, + backgroundColor: vars.color.darkBackground, + border: `1px solid ${vars.color.border}`, + borderRadius: "4px", + justifyContent: "space-between", +}); diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx new file mode 100644 index 00000000..fd38eb76 --- /dev/null +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -0,0 +1,178 @@ +import { Button, Modal, ModalProps } from "@renderer/components"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; + +import * as styles from "./cloud-sync-modal.css"; +import { formatBytes } from "@shared"; +import { format } from "date-fns"; +import { + CheckCircleFillIcon, + ClockIcon, + DeviceDesktopIcon, + DownloadIcon, + SyncIcon, + TrashIcon, + UploadIcon, +} from "@primer/octicons-react"; +import { useToast } from "@renderer/hooks"; +import { GameBackup, gameBackupsTable } from "@renderer/dexie"; + +export interface CloudSyncModalProps + extends Omit {} + +export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { + const [deletingArtifact, setDeletingArtifact] = useState(false); + const [lastBackup, setLastBackup] = useState(null); + + const { + artifacts, + backupPreview, + uploadingBackup, + restoringBackup, + uploadSaveGame, + downloadGameArtifact, + deleteGameArtifact, + } = useContext(cloudSyncContext); + + const { objectID, shop, gameTitle } = useContext(gameDetailsContext); + + const { showSuccessToast, showErrorToast } = useToast(); + + const handleDeleteArtifactClick = async (gameArtifactId: string) => { + setDeletingArtifact(true); + + try { + await deleteGameArtifact(gameArtifactId); + + showSuccessToast("backup_successfully_deleted"); + } catch (err) { + showErrorToast("backup_deletion_failed"); + } finally { + setDeletingArtifact(false); + } + }; + + useEffect(() => { + gameBackupsTable + .where({ shop: shop, objectId: objectID }) + .last() + .then((lastBackup) => setLastBackup(lastBackup || null)); + }, [backupPreview, objectID, shop]); + + const backupStateLabel = useMemo(() => { + if (uploadingBackup) { + return ( + + + creating_backup + + ); + } + + if (restoringBackup) { + return ( + + + restoring_backup + + ); + } + + if (lastBackup) { + return ( +

+ + Último backup em {format(lastBackup.createdAt, "dd/MM/yyyy HH:mm")} +

+ ); + } + + return "no_backups"; + }, [uploadingBackup, lastBackup, restoringBackup]); + + const disableActions = uploadingBackup || restoringBackup || deletingArtifact; + + return ( + +
+
+

{gameTitle}

+ {backupStateLabel} +
+ + +
+ +

backups

+ +
    + {artifacts.map((artifact) => ( +
  • +
    +
    +

    Backup do dia {format(artifact.createdAt, "dd/MM")}

    + {formatBytes(artifact.artifactLengthInBytes)} +
    + + + + {artifact.hostname} + + + + + {format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")} + +
    + +
    + + +
    +
  • + ))} +
+
+ ); +} 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 2ba19246..80974a14 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -9,8 +9,11 @@ import { Sidebar } from "./sidebar/sidebar"; import * as styles from "./game-details.css"; import { useTranslation } from "react-i18next"; -import { gameDetailsContext } from "@renderer/context"; +import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; import { steamUrlBuilder } from "@shared"; +import Lottie from "lottie-react"; + +import downloadingAnimation from "@renderer/assets/lottie/cloud.json"; const HERO_ANIMATION_THRESHOLD = 25; @@ -30,6 +33,9 @@ export function GameDetailsContent() { hasNSFWContentBlocked, } = useContext(gameDetailsContext); + const { supportsCloudSync, setShowCloudSyncModal } = + useContext(cloudSyncContext); + const [backdropOpactiy, setBackdropOpacity] = useState(1); const handleHeroLoad = async () => { @@ -102,6 +108,33 @@ export function GameDetailsContent() { className={styles.gameLogo} alt={game?.title} /> + + {supportsCloudSync && ( + + )}
diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts index 2de28f1f..228b2aeb 100644 --- a/src/renderer/src/pages/game-details/game-details.css.ts +++ b/src/renderer/src/pages/game-details/game-details.css.ts @@ -6,8 +6,8 @@ import { recipe } from "@vanilla-extract/recipes"; export const HERO_HEIGHT = 300; export const slideIn = keyframes({ - "0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)` }, - "100%": { transform: "translateY(0)" }, + "0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)`, opacity: "0px" }, + "100%": { transform: "translateY(0)", opacity: "1" }, }); export const wrapper = recipe({ @@ -49,6 +49,8 @@ export const heroContent = style({ height: "100%", width: "100%", display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", }); export const heroLogoBackdrop = style({ @@ -200,3 +202,33 @@ globalStyle(`${description} img`, { globalStyle(`${description} a`, { color: vars.color.body, }); + +export const cloudSyncButton = style({ + padding: `${SPACING_UNIT * 1.5}px ${SPACING_UNIT * 2}px`, + backgroundColor: "rgba(0, 0, 0, 0.6)", + backdropFilter: "blur(20px)", + borderRadius: "8px", + transition: "all ease 0.2s", + cursor: "pointer", + minHeight: "40px", + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: `${SPACING_UNIT}px`, + color: vars.color.muted, + fontSize: "14px", + border: `solid 1px ${vars.color.border}`, + boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.8)", + animation: `${slideIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`, + animationDuration: "0.3s", + ":active": { + opacity: "0.9", + }, + ":disabled": { + opacity: vars.opacity.disabled, + cursor: "not-allowed", + }, + ":hover": { + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, +}); diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index 54fe75ac..fbd59488 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -18,21 +18,25 @@ import { vars } from "@renderer/theme.css"; import { GameDetailsContent } from "./game-details-content"; import { + CloudSyncContextConsumer, + CloudSyncContextProvider, GameDetailsContextConsumer, GameDetailsContextProvider, } from "@renderer/context"; import { useDownload } from "@renderer/hooks"; import { GameOptionsModal, RepacksModal } from "./modals"; import { Downloader, getDownloadersForUri } from "@shared"; +import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal"; export function GameDetails() { const [randomGame, setRandomGame] = useState(null); const [randomizerLocked, setRandomizerLocked] = useState(false); - const { objectID } = useParams(); + const { objectID, shop } = useParams(); const [searchParams] = useSearchParams(); const fromRandomizer = searchParams.get("fromRandomizer"); + const gameTitle = searchParams.get("title"); const { startDownload } = useDownload(); @@ -74,7 +78,11 @@ export function GameDetails() { repack.uris.find((uri) => getDownloadersForUri(uri).includes(downloader))!; return ( - + {({ isLoading, @@ -115,64 +123,80 @@ export function GameDetails() { }; return ( - - {isLoading ? : } + + {({ showCloudSyncModal, setShowCloudSyncModal }) => ( + setShowCloudSyncModal(false)} + visible={showCloudSyncModal} + /> + )} + - setShowRepacksModal(false)} - /> + + {isLoading ? : } - setHasNSFWContentBlocked(false)} - clickOutsideToClose={false} - /> - - {game && ( - { - setShowGameOptionsModal(false); - }} + setShowRepacksModal(false)} /> - )} - {fromRandomizer && ( - - )} - + setHasNSFWContentBlocked(false)} + clickOutsideToClose={false} + /> + + {game && ( + { + setShowGameOptionsModal(false); + }} + /> + )} + + {fromRandomizer && ( + + )} + + ); }} diff --git a/src/types/index.ts b/src/types/index.ts index 9e6f7def..caf9bdd0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -266,5 +266,15 @@ export interface UserStats { friendsCount: number; } +export interface GameArtifact { + id: string; + artifactLengthInBytes: number; + createdAt: string; + updatedAt: string; + hostname: string; + downloadCount: number; +} + export * from "./steam.types"; export * from "./real-debrid.types"; +export * from "./ludusavi.types"; diff --git a/src/types/ludusavi.types.ts b/src/types/ludusavi.types.ts new file mode 100644 index 00000000..a2adebf9 --- /dev/null +++ b/src/types/ludusavi.types.ts @@ -0,0 +1,23 @@ +export interface LudusaviScanChange { + change: "New" | "Different" | "Removed" | "Same" | "Unknown"; + decision: "Processed" | "Cancelled" | "Ignore"; +} + +export interface LudusaviBackup { + overall: { + totalGames: number; + totalBytes: number; + processedGames: number; + processedBytes: number; + changedGames: { + new: number; + different: number; + same: number; + }; + }; + games: Record; +} + +export interface LudusaviFindResult { + games: Record; +} diff --git a/torrent-client/torrent_downloader.py b/torrent-client/torrent_downloader.py index d59cd28b..b5280260 100644 --- a/torrent-client/torrent_downloader.py +++ b/torrent-client/torrent_downloader.py @@ -144,8 +144,8 @@ class TorrentDownloader: status = torrent_handle.status() info = torrent_handle.get_torrent_info() - - return { + + response = { 'folderName': info.name() if info else "", 'fileSize': info.total_size() if info else 0, 'gameId': self.downloading_game_id, @@ -156,3 +156,10 @@ class TorrentDownloader: 'status': status.state, 'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download, } + + if status.progress == 1: + torrent_handle.pause() + self.session.remove_torrent(torrent_handle) + self.downloading_game_id = -1 + + return response diff --git a/yarn.lock b/yarn.lock index 14651b4b..75b9a7d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1923,6 +1923,20 @@ dependencies: "@types/node" "*" +"@types/adm-zip@^0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.5.5.tgz#4588042726aa5f351d7ea88232e4a952f60e7c1a" + integrity sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw== + dependencies: + "@types/node" "*" + +"@types/archiver@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-6.0.2.tgz#0daf8c83359cbde69de1e4b33dcade6a48a929e2" + integrity sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw== + dependencies: + "@types/readdir-glob" "*" + "@types/auto-launch@^5.0.5": version "5.0.5" resolved "https://registry.npmjs.org/@types/auto-launch/-/auto-launch-5.0.5.tgz" @@ -2066,6 +2080,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/folder-hash@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/folder-hash/-/folder-hash-4.0.4.tgz#c3262d58a01b756ee2aae3694707fad1ef676a9f" + integrity sha512-c+PwHm51Dw3fXM8SDK+93PO3oXdk4XNouCCvV67lj4aijRkZz5g67myk+9wqWWnyv3go6q96hT6ywcd3XtoZiQ== + "@types/fs-extra@9.0.13", "@types/fs-extra@^9.0.11": version "9.0.13" resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz" @@ -2292,6 +2311,13 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/readdir-glob@*": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/readdir-glob/-/readdir-glob-1.1.5.tgz#21a4a98898fc606cb568ad815f2a0eedc24d412a" + integrity sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg== + dependencies: + "@types/node" "*" + "@types/responselike@^1.0.0": version "1.0.3" resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz" @@ -2583,6 +2609,11 @@ acorn@^8.8.1, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== +adm-zip@^0.5.16: + version "0.5.16" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" + integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== + agent-base@6: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" @@ -2712,6 +2743,32 @@ applescript@^1.0.0: resolved "https://registry.npmjs.org/applescript/-/applescript-1.0.0.tgz" integrity sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ== +archiver-utils@^5.0.0, archiver-utils@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" + integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA== + dependencies: + glob "^10.0.0" + graceful-fs "^4.2.0" + is-stream "^2.0.1" + lazystream "^1.0.0" + lodash "^4.17.15" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + +archiver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" + integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ== + dependencies: + archiver-utils "^5.0.2" + async "^3.2.4" + buffer-crc32 "^1.0.0" + readable-stream "^4.0.0" + readdir-glob "^1.1.2" + tar-stream "^3.0.0" + zip-stream "^6.0.1" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -2851,6 +2908,11 @@ async@^3.2.3: resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== +async@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -2900,11 +2962,21 @@ axobject-query@^3.2.1: dependencies: dequal "^2.0.3" +b4a@^1.6.4: + version "1.6.6" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.6.tgz#a4cc349a3851987c3c4ac2d7785c18744f6da9ba" + integrity sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bare-events@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc" + integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A== + base64-arraybuffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz" @@ -3017,6 +3089,11 @@ browserslist@^4.22.2: node-releases "^2.0.14" update-browserslist-db "^1.0.13" +buffer-crc32@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" + integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" @@ -3337,6 +3414,17 @@ compare-version@^0.1.2: resolved "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz" integrity sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A== +compress-commons@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" + integrity sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg== + dependencies: + crc-32 "^1.2.0" + crc32-stream "^6.0.0" + is-stream "^2.0.1" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -3389,6 +3477,11 @@ core-util-is@1.0.2: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cosmiconfig-typescript-loader@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz" @@ -3416,6 +3509,19 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" + integrity sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g== + dependencies: + crc-32 "^1.2.0" + readable-stream "^4.0.0" + crc@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" @@ -4239,6 +4345,11 @@ event-target-shim@^5.0.0: resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + execa@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz" @@ -4285,6 +4396,11 @@ fast-diff@^1.1.2: resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^3.2.9: version "3.3.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" @@ -4605,6 +4721,18 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob@^10.0.0: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^10.3.10: version "10.3.15" resolved "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz" @@ -4973,7 +5101,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5170,6 +5298,11 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" +is-stream@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-stream@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz" @@ -5228,6 +5361,11 @@ isarray@^2.0.5: resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz" @@ -5263,6 +5401,15 @@ jackspeak@^2.3.6: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jake@^10.8.5: version "10.9.1" resolved "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz" @@ -5475,6 +5622,13 @@ lazy-val@^1.0.4, lazy-val@^1.0.5: resolved "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz" integrity sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q== +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + levn@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" @@ -5763,7 +5917,7 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@^5.1.1: +minimatch@^5.0.1, minimatch@^5.1.0, minimatch@^5.1.1: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -5784,6 +5938,13 @@ minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" @@ -5811,6 +5972,11 @@ minipass@^5.0.0: resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz" integrity sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA== +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" @@ -6116,6 +6282,11 @@ p-locate@^6.0.0: dependencies: p-limit "^4.0.0" +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -6199,7 +6370,7 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.11.0, path-scurry@^1.6.1: +path-scurry@^1.11.0, path-scurry@^1.11.1, path-scurry@^1.6.1: version "1.11.1" resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== @@ -6409,6 +6580,16 @@ prettier@^3.2.4: resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz" integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + progress@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" @@ -6469,6 +6650,11 @@ queue-microtask@^1.2.2, queue-microtask@^1.2.3: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" @@ -6562,6 +6748,19 @@ read-config-file@6.3.2: json5 "^2.2.0" lazy-val "^1.0.4" +readable-stream@^2.0.5: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" @@ -6571,6 +6770,17 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-web-to-node-stream@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz" @@ -6578,6 +6788,13 @@ readable-web-to-node-stream@^3.0.2: dependencies: readable-stream "^3.6.0" +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -6795,6 +7012,11 @@ safe-buffer@^5.0.1, safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-regex-test@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz" @@ -7012,6 +7234,17 @@ stat-mode@^1.0.0: resolved "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz" integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== +streamx@^2.15.0: + version "2.20.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.1.tgz#471c4f8b860f7b696feb83d5b125caab2fdbb93c" + integrity sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -7085,13 +7318,20 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -7206,6 +7446,15 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar-stream@^3.0.0: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + tar@^6.1.12: version "6.2.1" resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz" @@ -7231,6 +7480,13 @@ temp-file@^3.4.0: async-exit-hook "^2.0.1" fs-extra "^10.0.0" +text-decoder@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.0.tgz#85f19d4d5088e0b45cd841bdfaeac458dbffeefc" + integrity sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg== + dependencies: + b4a "^1.6.4" + text-extensions@^2.0.0: version "2.4.0" resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz" @@ -7581,7 +7837,7 @@ utf8-byte-length@^1.0.1: resolved "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz" integrity sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA== -util-deprecate@^1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -7914,6 +8170,15 @@ yup@^1.4.0: toposort "^2.0.2" type-fest "^2.19.0" +zip-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb" + integrity sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA== + dependencies: + archiver-utils "^5.0.0" + compress-commons "^6.0.2" + readable-stream "^4.0.0" + zod@^3.23.8: version "3.23.8" resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz" From b87aade2a3c660a936ab091624301cbd911b781c Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 25 Sep 2024 19:48:32 +0100 Subject: [PATCH 008/163] ci: pointing build to staging --- .github/workflows/build.yml | 8 +- .github/workflows/release.yml | 16 + src/renderer/src/assets/lottie/cloud.json | 726 +++++++++++++++++++++- 3 files changed, 745 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5cc4aa4d..1a01d550 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,8 +40,8 @@ jobs: sudo apt-get install -y libarchive-tools yarn build:linux env: - MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} - MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} + MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }} + MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -50,8 +50,8 @@ jobs: if: matrix.os == 'windows-latest' run: yarn build:win env: - MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} - MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} + MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }} + MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96b6a08d..9fc71ec1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,6 +58,22 @@ jobs: MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create artifact + uses: actions/upload-artifact@v4 + with: + name: Build-${{ matrix.os }} + path: | + dist/win-unpacked/** + dist/*-portable.exe + dist/*.zip + dist/*.dmg + dist/*.deb + dist/*.rpm + dist/*.tar.gz + dist/*.yml + dist/*.blockmap + dist/*.pacman + - name: Release uses: softprops/action-gh-release@v1 with: diff --git a/src/renderer/src/assets/lottie/cloud.json b/src/renderer/src/assets/lottie/cloud.json index 9df1e119..c8e4bce7 100644 --- a/src/renderer/src/assets/lottie/cloud.json +++ b/src/renderer/src/assets/lottie/cloud.json @@ -1 +1,725 @@ -{"v":"5.12.1","fr":30,"ip":0,"op":60,"w":400,"h":400,"nm":"Cloud","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[322.789,202.565,0],"to":[-1.5,-0.167,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[313.789,201.565,0],"to":[0,0,0],"ti":[-1.5,-0.167,0]},{"t":60,"s":[322.789,202.565,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-38.564],[38.564,0],[0,38.564],[-38.564,0]],"o":[[0,38.564],[-38.564,0],[0,-38.564],[38.564,0]],"v":[[69.827,0],[0,69.827],[-69.827,0],[0,-69.827]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[243.704,202.565,0],"to":[-1.667,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[233.704,202.565,0],"to":[0,0,0],"ti":[-1.667,0,0]},{"t":60,"s":[243.704,202.565,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-38.564],[38.564,0],[0,38.564],[-38.564,0]],"o":[[0,38.564],[-38.564,0],[0,-38.564],[38.564,0]],"v":[[69.827,0],[0,69.827],[-69.827,0],[0,-69.827]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[260.681,151.053,0],"to":[1.333,-1.333,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[268.681,143.053,0],"to":[0,0,0],"ti":[1.333,-1.333,0]},{"t":60,"s":[260.681,151.053,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-38.564],[38.564,0],[0,38.564],[-38.564,0]],"o":[[0,38.564],[-38.564,0],[0,-38.564],[38.564,0]],"v":[[69.827,0],[0,69.827],[-69.827,0],[0,-69.827]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[162.135,206.563,0],"to":[-0.833,-0.167,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[157.135,205.563,0],"to":[0,0,0],"ti":[-0.833,-0.167,0]},{"t":60,"s":[162.135,206.563,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-36.66],[36.66,0],[0,36.66],[-36.66,0]],"o":[[0,36.66],[-36.66,0],[0,-36.66],[36.66,0]],"v":[[66.378,0],[0,66.378],[-66.378,0],[0,-66.378]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[180.178,132.225,0],"to":[-0.5,-2.333,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[177.178,118.225,0],"to":[0,0,0],"ti":[-0.5,-2.333,0]},{"t":60,"s":[180.178,132.225,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-50.068],[50.068,0],[0,50.068],[-50.068,0]],"o":[[0,50.068],[-50.068,0],[0,-50.068],[50.068,0]],"v":[[90.655,0],[0,90.655],[-90.655,0],[0,-90.655]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[95.756,208.288,0],"to":[-1.167,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[88.756,208.288,0],"to":[0,0,0],"ti":[-1.167,0,0]},{"t":60,"s":[95.756,208.288,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-35.403],[35.403,0],[0,35.403],[-35.403,0]],"o":[[0,35.403],[-35.403,0],[0,-35.403],[35.403,0]],"v":[[64.103,0],[0,64.103],[-64.103,0],[0,-64.103]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.839215686275,0.854901960784,0.933333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":3,"nm":"Null 1","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[19.822,67.775,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":270,"st":0,"bm":0}],"markers":[],"props":{}} \ No newline at end of file +{ + "v": "5.12.1", + "fr": 30, + "ip": 0, + "op": 60, + "w": 400, + "h": 400, + "nm": "Cloud", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Layer 6", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [322.789, 202.565, 0], + "to": [-1.5, -0.167, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 30, + "s": [313.789, 201.565, 0], + "to": [0, 0, 0], + "ti": [-1.5, -0.167, 0] + }, + { "t": 60, "s": [322.789, 202.565, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, -38.564], + [38.564, 0], + [0, 38.564], + [-38.564, 0] + ], + "o": [ + [0, 38.564], + [-38.564, 0], + [0, -38.564], + [38.564, 0] + ], + "v": [ + [69.827, 0], + [0, 69.827], + [-69.827, 0], + [0, -69.827] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.839215686275, 0.854901960784, 0.933333333333, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 270, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "Layer 5", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [243.704, 202.565, 0], + "to": [-1.667, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 30, + "s": [233.704, 202.565, 0], + "to": [0, 0, 0], + "ti": [-1.667, 0, 0] + }, + { "t": 60, "s": [243.704, 202.565, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, -38.564], + [38.564, 0], + [0, 38.564], + [-38.564, 0] + ], + "o": [ + [0, 38.564], + [-38.564, 0], + [0, -38.564], + [38.564, 0] + ], + "v": [ + [69.827, 0], + [0, 69.827], + [-69.827, 0], + [0, -69.827] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.839215686275, 0.854901960784, 0.933333333333, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 270, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "Layer 4", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [260.681, 151.053, 0], + "to": [1.333, -1.333, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 30, + "s": [268.681, 143.053, 0], + "to": [0, 0, 0], + "ti": [1.333, -1.333, 0] + }, + { "t": 60, "s": [260.681, 151.053, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, -38.564], + [38.564, 0], + [0, 38.564], + [-38.564, 0] + ], + "o": [ + [0, 38.564], + [-38.564, 0], + [0, -38.564], + [38.564, 0] + ], + "v": [ + [69.827, 0], + [0, 69.827], + [-69.827, 0], + [0, -69.827] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.839215686275, 0.854901960784, 0.933333333333, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 270, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "Layer 3", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [162.135, 206.563, 0], + "to": [-0.833, -0.167, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 30, + "s": [157.135, 205.563, 0], + "to": [0, 0, 0], + "ti": [-0.833, -0.167, 0] + }, + { "t": 60, "s": [162.135, 206.563, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, -36.66], + [36.66, 0], + [0, 36.66], + [-36.66, 0] + ], + "o": [ + [0, 36.66], + [-36.66, 0], + [0, -36.66], + [36.66, 0] + ], + "v": [ + [66.378, 0], + [0, 66.378], + [-66.378, 0], + [0, -66.378] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.839215686275, 0.854901960784, 0.933333333333, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 270, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "Layer 2", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [180.178, 132.225, 0], + "to": [-0.5, -2.333, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 30, + "s": [177.178, 118.225, 0], + "to": [0, 0, 0], + "ti": [-0.5, -2.333, 0] + }, + { "t": 60, "s": [180.178, 132.225, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, -50.068], + [50.068, 0], + [0, 50.068], + [-50.068, 0] + ], + "o": [ + [0, 50.068], + [-50.068, 0], + [0, -50.068], + [50.068, 0] + ], + "v": [ + [90.655, 0], + [0, 90.655], + [-90.655, 0], + [0, -90.655] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.839215686275, 0.854901960784, 0.933333333333, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 270, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 4, + "nm": "Layer 1", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [95.756, 208.288, 0], + "to": [-1.167, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 30, + "s": [88.756, 208.288, 0], + "to": [0, 0, 0], + "ti": [-1.167, 0, 0] + }, + { "t": 60, "s": [95.756, 208.288, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, -35.403], + [35.403, 0], + [0, 35.403], + [-35.403, 0] + ], + "o": [ + [0, 35.403], + [-35.403, 0], + [0, -35.403], + [35.403, 0] + ], + "v": [ + [64.103, 0], + [0, 64.103], + [-64.103, 0], + [0, -64.103] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.839215686275, 0.854901960784, 0.933333333333, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 270, + "st": 0, + "ct": 1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 8, + "ty": 3, + "nm": "Null 1", + "parent": 6, + "sr": 1, + "ks": { + "o": { "a": 0, "k": 0, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [19.822, 67.775, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "ip": 0, + "op": 270, + "st": 0, + "bm": 0 + } + ], + "markers": [], + "props": {} +} From 0ea7329aa3e928812ba112f50ede31d2876a2c5d Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 25 Sep 2024 19:53:37 +0100 Subject: [PATCH 009/163] fix: fixing chmod for windows on postinstall --- postinstall.cjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/postinstall.cjs b/postinstall.cjs index ce9c5909..25d27c0a 100644 --- a/postinstall.cjs +++ b/postinstall.cjs @@ -30,10 +30,15 @@ const downloadLudusavi = async () => { console.log(`Downloaded ${file}, extracting...`); const pwd = process.cwd(); + const targetPath = path.join(pwd, "ludusavi"); await exec(`npx extract-zip ${file} ${targetPath}`); - fs.chmodSync(path.join(targetPath, "ludusavi"), 0o755); + + if (process.platform !== "win32") { + fs.chmodSync(path.join(targetPath, "ludusavi"), 0o755); + } + console.log("Extracted. Renaming folder..."); console.log(`Extracted ${file}, removing compressed downloaded file...`); From 89b830fe9abb91d265af58e1a439bf234b7357ed Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 25 Sep 2024 20:44:56 +0100 Subject: [PATCH 010/163] feat: clearing backup history on sign out --- src/renderer/src/hooks/use-user-details.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 50e2fad9..ed2c3b14 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -14,6 +14,7 @@ import type { UserDetails, } from "@types"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; +import { gameBackupsTable } from "@renderer/dexie"; export function useUserDetails() { const dispatch = useAppDispatch(); @@ -32,6 +33,7 @@ export function useUserDetails() { dispatch(setUserDetails(null)); dispatch(setProfileBackground(null)); + await gameBackupsTable.clear(); window.localStorage.removeItem("userDetails"); }, [dispatch]); From 3e165e05fbe665a94bf25892611a399be0fdf33f Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 25 Sep 2024 21:07:41 +0100 Subject: [PATCH 011/163] fix: adding no backup preview condition --- src/main/events/profile/update-profile.ts | 3 --- .../context/cloud-sync/cloud-sync.context.tsx | 16 +++++++---- src/renderer/src/hooks/use-user-details.ts | 27 ++----------------- .../cloud-sync-modal/cloud-sync-modal.tsx | 10 ++++--- 4 files changed, 20 insertions(+), 36 deletions(-) diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index 4135aae5..eb80bc47 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -33,9 +33,6 @@ const getNewProfileImageUrl = async (localImageUrl: string) => { headers: { "Content-Type": mimeType, }, - onUploadProgress: (progressEvent) => { - console.log(progressEvent); - }, }); return profileImageUrl; diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx index 38bbdb40..2f0addaf 100644 --- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -25,7 +25,7 @@ export interface CloudSyncContext { setShowCloudSyncModal: React.Dispatch>; downloadGameArtifact: (gameArtifactId: string) => Promise; uploadSaveGame: () => Promise; - deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>; + deleteGameArtifact: (gameArtifactId: string) => Promise; restoringBackup: boolean; uploadingBackup: boolean; } @@ -39,7 +39,7 @@ export const cloudSyncContext = createContext({ downloadGameArtifact: async () => {}, uploadSaveGame: async () => {}, artifacts: [], - deleteGameArtifact: async () => ({ ok: false }), + deleteGameArtifact: async () => {}, restoringBackup: false, uploadingBackup: false, }); @@ -135,20 +135,26 @@ export function CloudSyncContextProvider({ async (gameArtifactId: string) => { return window.electron.deleteGameArtifact(gameArtifactId).then(() => { getGameBackupPreview(); - return { ok: true }; }); }, [getGameBackupPreview] ); useEffect(() => { - getGameBackupPreview(); - window.electron.checkGameCloudSyncSupport(objectId, shop).then((result) => { setSupportsCloudSync(result); }); }, [objectId, shop, getGameBackupPreview]); + useEffect(() => { + setBackupPreview(null); + setArtifacts([]); + setSupportsCloudSync(null); + setShowCloudSyncModal(false); + setRestoringBackup(false); + setUploadingBackup(false); + }, [objectId, shop]); + useEffect(() => { if (showCloudSyncModal) { getGameBackupPreview(); diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index ed2c3b14..7e08144d 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -46,32 +46,9 @@ export function useUserDetails() { const updateUserDetails = useCallback( async (userDetails: UserDetails) => { dispatch(setUserDetails(userDetails)); - - if (userDetails.profileImageUrl) { - // TODO: Decide if we want to use this - // const profileBackground = await profileBackgroundFromProfileImage( - // userDetails.profileImageUrl - // ).catch((err) => { - // logger.error("profileBackgroundFromProfileImage", err); - // return `#151515B3`; - // }); - // dispatch(setProfileBackground(profileBackground)); - - window.localStorage.setItem( - "userDetails", - JSON.stringify({ ...userDetails, profileBackground }) - ); - } else { - const profileBackground = `#151515B3`; - dispatch(setProfileBackground(profileBackground)); - - window.localStorage.setItem( - "userDetails", - JSON.stringify({ ...userDetails, profileBackground }) - ); - } + window.localStorage.setItem("userDetails", JSON.stringify(userDetails)); }, - [dispatch, profileBackground] + [dispatch] ); const fetchUserDetails = useCallback(async () => { diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index fd38eb76..7461c4b2 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -87,8 +87,12 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { ); } - return "no_backups"; - }, [uploadingBackup, lastBackup, restoringBackup]); + if (!backupPreview) { + return "no_backup_preview"; + } + + return "no_artifacts"; + }, [uploadingBackup, lastBackup, backupPreview, restoringBackup]); const disableActions = uploadingBackup || restoringBackup || deletingArtifact; @@ -116,7 +120,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { -

backups

+

{t("backups")}

)} diff --git a/src/types/index.ts b/src/types/index.ts index 303d47ae..7c6028be 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -28,6 +28,15 @@ export interface GameRepack { updatedAt: Date; } +export interface AchievementData { + name: string; + displayName: string; + description?: string; + icon: string; + icongray: string; + hidden: boolean; +} + export interface GameAchievement { name: string; displayName: string; @@ -36,6 +45,7 @@ export interface GameAchievement { unlockTime: number | null; icon: string; icongray: string; + hidden: boolean; } export type ShopDetails = SteamAppDetails & { From 03413a9e6b899c2c52121c3f6aada1b1ec350c0e Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:28:48 -0300 Subject: [PATCH 089/163] fix: open game installer when download is zip --- src/main/events/library/open-game-installer.ts | 3 ++- src/renderer/src/pages/game-details/sidebar/sidebar.tsx | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/events/library/open-game-installer.ts b/src/main/events/library/open-game-installer.ts index 8532dae4..b21a6f16 100644 --- a/src/main/events/library/open-game-installer.ts +++ b/src/main/events/library/open-game-installer.ts @@ -50,7 +50,8 @@ const openGameInstaller = async ( } if (fs.lstatSync(gamePath).isFile()) { - return executeGameInstaller(gamePath); + shell.showItemInFolder(gamePath); + return true; } const setupPath = path.join(gamePath, "setup.exe"); diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index b2c07885..fd39deeb 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -10,7 +10,6 @@ import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; import { HowLongToBeatSection } from "./how-long-to-beat-section"; import { howLongToBeatEntriesTable } from "@renderer/dexie"; import { SidebarSection } from "../sidebar-section/sidebar-section"; -import { useNavigate } from "react-router-dom"; export function Sidebar() { const [howLongToBeat, setHowLongToBeat] = useState<{ From e3f61bbaa86f2bd03efed73062614cca49897c82 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Thu, 10 Oct 2024 00:33:16 +0100 Subject: [PATCH 090/163] fix: fixing games with : --- src/main/events/cloud-save/download-game-artifact.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/events/cloud-save/download-game-artifact.ts b/src/main/events/cloud-save/download-game-artifact.ts index 00fae6aa..ef00629b 100644 --- a/src/main/events/cloud-save/download-game-artifact.ts +++ b/src/main/events/cloud-save/download-game-artifact.ts @@ -125,7 +125,11 @@ const downloadGameArtifact = async ( const [game] = await Ludusavi.findGames(shop, objectId); if (!game) throw new Error("Game not found in Ludusavi manifest"); - replaceLudusaviBackupWithCurrentUser(backupPath, game, homeDir); + replaceLudusaviBackupWithCurrentUser( + backupPath, + game.replaceAll(":", "_"), + normalizePath(homeDir) + ); Ludusavi.restoreBackup(backupPath).then(() => { WindowManager.mainWindow?.webContents.send( From fa026f82a6be5b6333cf1aa601ee8f1de0308de0 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:33:33 -0300 Subject: [PATCH 091/163] feat: update achievements page --- src/locales/en/translation.json | 4 +- src/locales/pt-BR/translation.json | 4 +- .../events/catalogue/get-game-achievements.ts | 4 +- .../achievements/find-achivement-files.ts | 2 +- src/renderer/src/helpers.ts | 15 +++ .../src/pages/achievement/achievements.css.ts | 82 ++++++++++++ .../src/pages/achievement/achievements.tsx | 126 ++++++++++++++---- .../pages/game-details/sidebar/sidebar.tsx | 23 ++-- .../profile-content/profile-content.tsx | 20 ++- 9 files changed, 228 insertions(+), 52 deletions(-) create mode 100644 src/renderer/src/pages/achievement/achievements.css.ts diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b80f8bd6..6d2ea93b 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -333,6 +333,8 @@ "your_friend_code": "Your friend code:" }, "achievement": { - "achievement_unlocked": "Achievement unlocked" + "achievement_unlocked": "Achievement unlocked", + "user_achievements": "{{displayName}}'s Achievements", + "your_achievements": "Your Achievements" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index c5a15f6b..f5d91d46 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -335,6 +335,8 @@ "your_friend_code": "Seu código de amigo:" }, "achievement": { - "achievement_unlocked": "Conquista desbloqueada" + "achievement_unlocked": "Conquista desbloqueada", + "your_achievements": "Suas Conquistas", + "user_achievements": "Conquistas de {{displayName}}" } } diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts index 07bc2e91..8b040874 100644 --- a/src/main/events/catalogue/get-game-achievements.ts +++ b/src/main/events/catalogue/get-game-achievements.ts @@ -83,9 +83,9 @@ export const getGameAchievements = async ( unlocked: false, unlockTime: null, icongray, - }; + } as GameAchievement; }) - .sort((a: GameAchievement, b: GameAchievement) => { + .sort((a, b) => { if (a.unlocked && !b.unlocked) return -1; if (!a.unlocked && b.unlocked) return 1; if (a.unlocked && b.unlocked) { diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 917e5351..dc43f827 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -53,7 +53,7 @@ const getPathFromCracker = (cracker: Cracker) => { if (cracker === Cracker.onlineFix) { return [ { - folderPath: path.join(publicDocuments, Cracker.onlineFix), + folderPath: path.join(publicDocuments, "OnlineFix"), fileLocation: ["Stats", "Achievements.ini"], }, ]; diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index a9fc3cdd..2eb83df6 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -34,5 +34,20 @@ export const buildGameDetailsPath = ( return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`; }; +export const buildGameAchievementPath = ( + game: { shop: GameShop; objectId: string; title: string }, + user?: { userId: string; displayName: string } +) => { + const searchParams = new URLSearchParams({ + title: game.title, + shop: game.shop, + objectId: game.objectId, + userId: user?.userId || "", + displayName: user?.displayName || "", + }); + + return `/achievements/?${searchParams.toString()}`; +}; + export const darkenColor = (color: string, amount: number, alpha: number = 1) => new Color(color).darken(amount).alpha(alpha).toString(); diff --git a/src/renderer/src/pages/achievement/achievements.css.ts b/src/renderer/src/pages/achievement/achievements.css.ts new file mode 100644 index 00000000..f5f548e6 --- /dev/null +++ b/src/renderer/src/pages/achievement/achievements.css.ts @@ -0,0 +1,82 @@ +import { SPACING_UNIT, vars } from "../../theme.css"; +import { style } from "@vanilla-extract/css"; +import { recipe } from "@vanilla-extract/recipes"; + +export const container = style({ + width: "100%", + padding: `${SPACING_UNIT * 2}px`, + display: "flex", + flexDirection: "column", + gap: `${SPACING_UNIT * 2}px`, +}); + +export const header = style({ + display: "flex", + gap: `${SPACING_UNIT}px`, + width: "50%", +}); + +export const headerImage = style({ + borderRadius: "4px", + objectFit: "cover", + cursor: "pointer", + height: "160px", + transition: "all ease 0.2s", + ":hover": { + transform: "scale(1.05)", + }, +}); + +export const list = style({ + listStyle: "none", + margin: "0", + display: "flex", + flexDirection: "column", + gap: `${SPACING_UNIT * 2}px`, + padding: 0, +}); + +export const listItem = style({ + display: "flex", + transition: "all ease 0.1s", + color: vars.color.muted, + width: "100%", + overflow: "hidden", + borderRadius: "4px", + padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`, + gap: `${SPACING_UNIT * 2}px`, + alignItems: "center", + textAlign: "left", + ":hover": { + backgroundColor: "rgba(255, 255, 255, 0.15)", + textDecoration: "none", + }, +}); + +export const listItemImage = recipe({ + base: { + width: "54px", + height: "54px", + borderRadius: "4px", + objectFit: "cover", + }, + variants: { + unlocked: { + false: { + filter: "grayscale(100%)", + }, + }, + }, +}); + +export const achievementsProgressBar = style({ + width: "100%", + height: "8px", + transition: "all ease 0.2s", + "::-webkit-progress-bar": { + backgroundColor: "rgba(255, 255, 255, 0.15)", + }, + "::-webkit-progress-value": { + backgroundColor: vars.color.muted, + }, +}); diff --git a/src/renderer/src/pages/achievement/achievements.tsx b/src/renderer/src/pages/achievement/achievements.tsx index dd50c0ab..19152f9c 100644 --- a/src/renderer/src/pages/achievement/achievements.tsx +++ b/src/renderer/src/pages/achievement/achievements.tsx @@ -1,9 +1,17 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useDate } from "@renderer/hooks"; -import { SPACING_UNIT } from "@renderer/theme.css"; -import { GameAchievement, GameShop } from "@types"; +import { steamUrlBuilder } from "@shared"; +import type { GameAchievement, GameShop } from "@types"; import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import * as styles from "./achievements.css"; +import { + buildGameDetailsPath, + formatDownloadProgress, +} from "@renderer/helpers"; +import { TrophyIcon } from "@primer/octicons-react"; +import { vars } from "@renderer/theme.css"; export function Achievement() { const [searchParams] = useSearchParams(); @@ -11,8 +19,12 @@ export function Achievement() { const shop = searchParams.get("shop"); const title = searchParams.get("title"); const userId = searchParams.get("userId"); + const displayName = searchParams.get("displayName"); + + const { t } = useTranslation("achievement"); const { format } = useDate(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); @@ -30,53 +42,109 @@ export function Achievement() { useEffect(() => { if (title) { - dispatch(setHeaderTitle(title + " Achievements")); + dispatch(setHeaderTitle(title)); } }, [dispatch, title]); - return ( -
-

Achievement

+ if (!objectId || !shop || !title) return null; -
- {achievements.map((achievement, index) => ( + const unlockedAchievementCount = achievements.filter( + (achievement) => achievement.unlocked + ).length; + + const totalAchievementCount = achievements.length; + + const handleClickGame = () => { + navigate( + buildGameDetailsPath({ + shop: shop as GameShop, + objectId, + title, + }) + ); + }; + + return ( +
+
+ +
+

+ {displayName + ? t("user_achievements", { + displayName, + }) + : t("your_achievements")} +

- + + + {unlockedAchievementCount} / {totalAchievementCount} + +
+ + + {formatDownloadProgress( + unlockedAchievementCount / totalAchievementCount + )} + +
+ +
+
+ +
    + {achievements.map((achievement, index) => ( +
  • + {achievement.displayName}

    {achievement.displayName}

    {achievement.description}

    - {achievement.unlockTime && format(achievement.unlockTime)} + + {achievement.unlockTime && format(achievement.unlockTime)} +
    -
+ ))} -
+ ); } diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index fd39deeb..e05a9187 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -10,6 +10,7 @@ import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; import { HowLongToBeatSection } from "./how-long-to-beat-section"; import { howLongToBeatEntriesTable } from "@renderer/dexie"; import { SidebarSection } from "../sidebar-section/sidebar-section"; +import { buildGameAchievementPath } from "@renderer/helpers"; export function Sidebar() { const [howLongToBeat, setHowLongToBeat] = useState<{ @@ -28,16 +29,6 @@ export function Sidebar() { const { numberFormatter } = useFormat(); - const buildGameAchievementPath = () => { - const urlParams = new URLSearchParams({ - objectId: objectId!, - shop, - title: gameTitle, - }); - - return `/achievements?${urlParams.toString()}`; - }; - useEffect(() => { if (objectId) { setHowLongToBeat({ isLoading: true, data: null }); @@ -88,7 +79,11 @@ export function Sidebar() { {achievements.slice(0, 4).map((achievement, index) => (
  • @@ -116,7 +111,11 @@ export function Sidebar() { {t("see_all_achievements")} diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index b7c955f9..312ea963 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -16,7 +16,7 @@ import { FriendsBox } from "./friends-box"; import { RecentGamesBox } from "./recent-games-box"; import { UserGame } from "@types"; import { - buildGameDetailsPath, + buildGameAchievementPath, formatDownloadProgress, } from "@renderer/helpers"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; @@ -44,11 +44,19 @@ export function ProfileContent() { return userProfile?.relation?.status === "ACCEPTED"; }, [userProfile]); - const buildUserGameDetailsPath = (game: UserGame) => - buildGameDetailsPath({ - ...game, - objectId: game.objectId, - }); + const buildUserGameDetailsPath = (game: UserGame) => { + // TODO: check if user has hydra cloud + // buildGameDetailsPath({ + // ...game, + // objectId: game.objectId, + // }); + + const userParams = userProfile + ? { userId: userProfile.id, displayName: userProfile.displayName } + : undefined; + + return buildGameAchievementPath({ ...game }, userParams); + }; const formatPlayTime = useCallback( (playTimeInSeconds = 0) => { From c8022896a6651559e0f9db6a496c2a44f7f870d3 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Wed, 9 Oct 2024 23:11:28 -0300 Subject: [PATCH 092/163] feat: refactor --- .../events/catalogue/get-game-achievements.ts | 115 ++++++++++++------ .../game-details/game-details.context.tsx | 4 +- .../game-details.context.types.ts | 4 +- src/renderer/src/declaration.d.ts | 3 +- .../src/pages/achievement/achievements.tsx | 8 +- .../pages/game-details/sidebar/sidebar.tsx | 6 +- src/types/index.ts | 21 +++- 7 files changed, 111 insertions(+), 50 deletions(-) diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts index 8b040874..511e255f 100644 --- a/src/main/events/catalogue/get-game-achievements.ts +++ b/src/main/events/catalogue/get-game-achievements.ts @@ -1,23 +1,81 @@ import type { AchievementData, - GameAchievement, GameShop, + RemoteUnlockedAchievement, UnlockedAchievement, + UserAchievement, } from "@types"; import { registerEvent } from "../register-event"; import { gameAchievementRepository, - userAuthRepository, + userPreferencesRepository, } from "@main/repository"; import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; -import { HydraApi } from "@main/services"; +import { HydraApi, logger } from "@main/services"; -const getAchievements = async ( +const getAchievementLocalUser = async (shop: string, objectId: string) => { + const cachedAchievements = await gameAchievementRepository.findOne({ + where: { objectId, shop }, + }); + + const achievementsData: AchievementData[] = cachedAchievements?.achievements + ? JSON.parse(cachedAchievements.achievements) + : await getGameAchievementData(objectId, shop); + + const unlockedAchievements = JSON.parse( + cachedAchievements?.unlockedAchievements || "[]" + ) as UnlockedAchievement[]; + + return achievementsData + .map((achievementData) => { + logger.info("unclockedAchievements", unlockedAchievements); + + const unlockedAchiementData = unlockedAchievements.find( + (localAchievement) => { + return ( + localAchievement.name.toUpperCase() == + achievementData.name.toUpperCase() + ); + } + ); + + const icongray = achievementData.icongray.endsWith("/") + ? achievementData.icon + : achievementData.icongray; + + if (unlockedAchiementData) { + return { + ...achievementData, + unlocked: true, + unlockTime: unlockedAchiementData.unlockTime, + }; + } + + return { + ...achievementData, + unlocked: false, + unlockTime: null, + icon: icongray, + } as UserAchievement; + }) + .sort((a, b) => { + if (a.unlocked && !b.unlocked) return -1; + if (!a.unlocked && b.unlocked) return 1; + if (a.unlocked && b.unlocked) { + return b.unlockTime! - a.unlockTime!; + } + return Number(a.hidden) - Number(b.hidden); + }); +}; + +const getAchievementsRemoteUser = async ( shop: string, objectId: string, - userId?: string + userId: string ) => { - const userAuth = await userAuthRepository.findOne({ where: { userId } }); + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); const cachedAchievements = await gameAchievementRepository.findOne({ where: { objectId, shop }, @@ -27,31 +85,9 @@ const getAchievements = async ( ? JSON.parse(cachedAchievements.achievements) : await getGameAchievementData(objectId, shop); - if (!userId || userAuth) { - const unlockedAchievements = JSON.parse( - cachedAchievements?.unlockedAchievements || "[]" - ) as UnlockedAchievement[]; - - return { achievementsData, unlockedAchievements }; - } - - const unlockedAchievements = await HydraApi.get( + const unlockedAchievements = await HydraApi.get( `/users/${userId}/games/achievements`, - { shop, objectId, language: "en" } - ); - - return { achievementsData, unlockedAchievements }; -}; - -export const getGameAchievements = async ( - objectId: string, - shop: GameShop, - userId?: string -): Promise => { - const { achievementsData, unlockedAchievements } = await getAchievements( - shop, - objectId, - userId + { shop, objectId, language: userPreferences?.language || "en" } ); return achievementsData @@ -74,7 +110,6 @@ export const getGameAchievements = async ( ...achievementData, unlocked: true, unlockTime: unlockedAchiementData.unlockTime, - icongray, }; } @@ -82,8 +117,8 @@ export const getGameAchievements = async ( ...achievementData, unlocked: false, unlockTime: null, - icongray, - } as GameAchievement; + icon: icongray, + } as UserAchievement; }) .sort((a, b) => { if (a.unlocked && !b.unlocked) return -1; @@ -95,12 +130,24 @@ export const getGameAchievements = async ( }); }; +export const getGameAchievements = async ( + objectId: string, + shop: GameShop, + userId?: string +): Promise => { + if (!userId) { + return getAchievementLocalUser(shop, objectId); + } + + return getAchievementsRemoteUser(shop, objectId, userId); +}; + const getGameAchievementsEvent = async ( _event: Electron.IpcMainInvokeEvent, objectId: string, shop: GameShop, userId?: string -): Promise => { +): Promise => { return getGameAchievements(objectId, shop, userId); }; 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 7ff8bb09..a08f4a55 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -12,11 +12,11 @@ import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks"; import type { Game, - GameAchievement, GameRepack, GameShop, GameStats, ShopDetails, + UserAchievement, } from "@types"; import { useTranslation } from "react-i18next"; @@ -64,7 +64,7 @@ export function GameDetailsContextProvider({ shop, }: GameDetailsContextProps) { const [shopDetails, setShopDetails] = useState(null); - const [achievements, setAchievements] = useState([]); + const [achievements, setAchievements] = useState([]); const [game, setGame] = useState(null); const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false); diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts index 956d5c68..d410334b 100644 --- a/src/renderer/src/context/game-details/game-details.context.types.ts +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -1,10 +1,10 @@ import type { Game, - GameAchievement, GameRepack, GameShop, GameStats, ShopDetails, + UserAchievement, } from "@types"; export interface GameDetailsContext { @@ -20,7 +20,7 @@ export interface GameDetailsContext { showRepacksModal: boolean; showGameOptionsModal: boolean; stats: GameStats | null; - achievements: GameAchievement[]; + achievements: UserAchievement[]; hasNSFWContentBlocked: boolean; setGameColor: React.Dispatch>; selectGameExecutable: () => Promise; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 677c3ee2..f0342a43 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -28,6 +28,7 @@ import type { GameAchievement, GameArtifact, LudusaviBackup, + UserAchievement, } from "@types"; import type { AxiosProgressEvent } from "axios"; import type { DiskSpace } from "check-disk-space"; @@ -68,7 +69,7 @@ declare global { objectId: string, shop: GameShop, userId?: string - ) => Promise; + ) => Promise; onAchievementUnlocked: ( cb: ( objectId: string, diff --git a/src/renderer/src/pages/achievement/achievements.tsx b/src/renderer/src/pages/achievement/achievements.tsx index 272473dd..943cd64d 100644 --- a/src/renderer/src/pages/achievement/achievements.tsx +++ b/src/renderer/src/pages/achievement/achievements.tsx @@ -1,7 +1,7 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useDate } from "@renderer/hooks"; import { steamUrlBuilder } from "@shared"; -import type { GameAchievement, GameShop } from "@types"; +import type { GameShop, UserAchievement } from "@types"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; @@ -28,7 +28,7 @@ export function Achievement() { const dispatch = useAppDispatch(); - const [achievements, setAchievements] = useState([]); + const [achievements, setAchievements] = useState([]); useEffect(() => { if (objectId && shop) { @@ -130,9 +130,7 @@ export function Achievement() { className={styles.listItemImage({ unlocked: achievement.unlocked, })} - src={ - achievement.unlocked ? achievement.icon : achievement.icongray - } + src={achievement.icon} alt={achievement.displayName} loading="lazy" /> diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index e05a9187..d04afbc6 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -91,11 +91,7 @@ export function Sidebar() { className={styles.listItemImage({ unlocked: achievement.unlocked, })} - src={ - achievement.unlocked - ? achievement.icon - : achievement.icongray - } + src={achievement.icon} alt={achievement.displayName} loading="lazy" /> diff --git a/src/types/index.ts b/src/types/index.ts index 7c6028be..7ef8073a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -37,15 +37,34 @@ export interface AchievementData { hidden: boolean; } +export interface UserAchievement { + name: string; + hidden: boolean; + displayName: string; + description?: string; + unlocked: boolean; + unlockTime: number | null; + icon: string; +} + +export interface RemoteUnlockedAchievement { + name: string; + hidden: boolean; + icon: string; + displayName: string; + description?: string; + unlockTime: number; +} + export interface GameAchievement { name: string; + hidden: boolean; displayName: string; description?: string; unlocked: boolean; unlockTime: number | null; icon: string; icongray: string; - hidden: boolean; } export type ShopDetails = SteamAppDetails & { From 0241d8752bf27008f84f9ea21af99aedead8d3dd Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Thu, 10 Oct 2024 13:29:32 +0100 Subject: [PATCH 093/163] feat: adding border to profile hero --- .../profile/profile-hero/profile-hero.css.ts | 38 ++++++++++++++++++- .../profile/profile-hero/profile-hero.tsx | 1 + 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts index 2334d605..46e32556 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts @@ -1,5 +1,17 @@ import { SPACING_UNIT, vars } from "../../../theme.css"; -import { style } from "@vanilla-extract/css"; +import { keyframes, style } from "@vanilla-extract/css"; + +const animateBackground = keyframes({ + "0%": { + backgroundPosition: "0% 50%", + }, + "50%": { + backgroundPosition: "100% 50%", + }, + "100%": { + backgroundPosition: "0% 50%", + }, +}); export const profileContentBox = style({ display: "flex", @@ -16,12 +28,12 @@ export const profileAvatarButton = style({ justifyContent: "center", alignItems: "center", backgroundColor: vars.color.background, - overflow: "hidden", border: `solid 1px ${vars.color.border}`, boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)", cursor: "pointer", transition: "all ease 0.3s", color: vars.color.muted, + position: "relative", ":hover": { boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.7)", }, @@ -87,3 +99,25 @@ export const currentGameDetails = style({ gap: `${SPACING_UNIT}px`, alignItems: "center", }); + +export const xdTotal = style({ + background: `linear-gradient( + 60deg, + #f79533, + #f37055, + #ef4e7b, + #a166ab, + #5073b8, + #1098ad, + #07b39b, + #6fba82 + )`, + width: "102px", + minWidth: "102px", + height: "102px", + animation: `${animateBackground} 4s ease alternate infinite`, + backgroundSize: "300% 300%", + zIndex: -1, + borderRadius: "4px", + position: "absolute", +}); diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index f81761ea..8b70945c 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -330,6 +330,7 @@ export function ProfileHero() { className={styles.profileAvatarButton} onClick={handleAvatarClick} > +
    {userProfile?.profileImageUrl ? ( Date: Fri, 11 Oct 2024 13:09:26 -0300 Subject: [PATCH 094/163] feat: temp remove fetch from local cache --- .github/workflows/build.yml | 13 +++++++++++++ .github/workflows/lint.yml | 13 +++++++++++++ .../events/catalogue/get-game-achievements.ts | 19 ++++++------------- .../achievements/get-game-achievement-data.ts | 17 +++++++++++++---- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a01d550..fa8027d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,19 @@ jobs: - name: Check out Git repository uses: actions/checkout@v4 + - name: Cache node modules + id: cache-npm + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Install Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cf74c4e8..8574b668 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,6 +10,19 @@ jobs: - name: Check out Git repository uses: actions/checkout@v4 + - name: Cache node modules + id: cache-npm + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Install Node.js uses: actions/setup-node@v4 with: diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts index 511e255f..93e89441 100644 --- a/src/main/events/catalogue/get-game-achievements.ts +++ b/src/main/events/catalogue/get-game-achievements.ts @@ -11,16 +11,14 @@ import { userPreferencesRepository, } from "@main/repository"; import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; -import { HydraApi, logger } from "@main/services"; +import { HydraApi } from "@main/services"; const getAchievementLocalUser = async (shop: string, objectId: string) => { const cachedAchievements = await gameAchievementRepository.findOne({ where: { objectId, shop }, }); - const achievementsData: AchievementData[] = cachedAchievements?.achievements - ? JSON.parse(cachedAchievements.achievements) - : await getGameAchievementData(objectId, shop); + const achievementsData = await getGameAchievementData(objectId, shop); const unlockedAchievements = JSON.parse( cachedAchievements?.unlockedAchievements || "[]" @@ -28,8 +26,6 @@ const getAchievementLocalUser = async (shop: string, objectId: string) => { return achievementsData .map((achievementData) => { - logger.info("unclockedAchievements", unlockedAchievements); - const unlockedAchiementData = unlockedAchievements.find( (localAchievement) => { return ( @@ -77,13 +73,10 @@ const getAchievementsRemoteUser = async ( where: { id: 1 }, }); - const cachedAchievements = await gameAchievementRepository.findOne({ - where: { objectId, shop }, - }); - - const achievementsData: AchievementData[] = cachedAchievements?.achievements - ? JSON.parse(cachedAchievements.achievements) - : await getGameAchievementData(objectId, shop); + const achievementsData: AchievementData[] = await getGameAchievementData( + objectId, + shop + ); const unlockedAchievements = await HydraApi.get( `/users/${userId}/games/achievements`, diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index 7f1e6b5a..0cbadb54 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -3,6 +3,7 @@ import { userPreferencesRepository, } from "@main/repository"; import { HydraApi } from "../hydra-api"; +import { AchievementData } from "@types"; export const getGameAchievementData = async ( objectId: string, @@ -12,13 +13,13 @@ export const getGameAchievementData = async ( where: { id: 1 }, }); - return HydraApi.get("/games/achievements", { + return HydraApi.get("/games/achievements", { shop, objectId, language: userPreferences?.language || "en", }) - .then(async (achievements) => { - await gameAchievementRepository.upsert( + .then((achievements) => { + gameAchievementRepository.upsert( { objectId, shop, @@ -29,5 +30,13 @@ export const getGameAchievementData = async ( return achievements; }) - .catch(() => []); + .catch(() => { + return gameAchievementRepository + .findOne({ + where: { objectId, shop }, + }) + .then((gameAchievements) => { + return JSON.parse(gameAchievements?.achievements || "[]"); + }); + }); }; From 887ec3f8ebc971e7b0acfb054ae7e3f15fe59efb Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:24:43 -0300 Subject: [PATCH 095/163] feat: update achievements page --- .../src/pages/achievement/achievements.css.ts | 10 +++----- .../src/pages/achievement/achievements.tsx | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/renderer/src/pages/achievement/achievements.css.ts b/src/renderer/src/pages/achievement/achievements.css.ts index f5f548e6..b4de2aed 100644 --- a/src/renderer/src/pages/achievement/achievements.css.ts +++ b/src/renderer/src/pages/achievement/achievements.css.ts @@ -4,7 +4,6 @@ import { recipe } from "@vanilla-extract/recipes"; export const container = style({ width: "100%", - padding: `${SPACING_UNIT * 2}px`, display: "flex", flexDirection: "column", gap: `${SPACING_UNIT * 2}px`, @@ -13,18 +12,15 @@ export const container = style({ export const header = style({ display: "flex", gap: `${SPACING_UNIT}px`, - width: "50%", + flexDirection: "column", }); export const headerImage = style({ borderRadius: "4px", objectFit: "cover", cursor: "pointer", - height: "160px", + width: "100%", transition: "all ease 0.2s", - ":hover": { - transform: "scale(1.05)", - }, }); export const list = style({ @@ -33,7 +29,7 @@ export const list = style({ display: "flex", flexDirection: "column", gap: `${SPACING_UNIT * 2}px`, - padding: 0, + padding: `${SPACING_UNIT * 2}px`, }); export const listItem = style({ diff --git a/src/renderer/src/pages/achievement/achievements.tsx b/src/renderer/src/pages/achievement/achievements.tsx index 943cd64d..7625ad60 100644 --- a/src/renderer/src/pages/achievement/achievements.tsx +++ b/src/renderer/src/pages/achievement/achievements.tsx @@ -11,7 +11,7 @@ import { formatDownloadProgress, } from "@renderer/helpers"; import { TrophyIcon } from "@primer/octicons-react"; -import { vars } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "@renderer/theme.css"; export function Achievement() { const [searchParams] = useSearchParams(); @@ -42,7 +42,15 @@ export function Achievement() { useEffect(() => { if (title) { - dispatch(setHeaderTitle(title)); + dispatch( + setHeaderTitle( + displayName + ? t("user_achievements", { + displayName, + }) + : t("your_achievements") + ) + ); } }, [dispatch, title]); @@ -69,7 +77,7 @@ export function Achievement() {
    -
    -

    {title}

    + + {title} +
    + +
    +

    + {displayName + ? t("user_achievements", { + displayName, + }) + : t("your_achievements")} +

    @@ -124,29 +147,29 @@ export function Achievement() { className={styles.achievementsProgressBar} />
    -
    -
      - {achievements.map((achievement, index) => ( -
    • - {achievement.displayName} -
      -

      {achievement.displayName}

      -

      {achievement.description}

      - - {achievement.unlockTime && format(achievement.unlockTime)} - -
      -
    • - ))} -
    +
      + {achievements.map((achievement, index) => ( +
    • + {achievement.displayName} +
      +

      {achievement.displayName}

      +

      {achievement.description}

      + + {achievement.unlockTime && format(achievement.unlockTime)} + +
      +
    • + ))} +
    +
    ); } From a064958d4c3613078a5c2ad2ff1b947ef8c047a8 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:03:54 -0300 Subject: [PATCH 102/163] feat: update achievements processors --- src/main/services/achievements/parse-achievement-file.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index 47c8e805..b2031989 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -112,7 +112,7 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => { for (const achievement of Object.keys(unlockedAchievements)) { const unlockedAchievement = unlockedAchievements[achievement]; - if (unlockedAchievement?.achieved) { + if (unlockedAchievement?.achieved == "true") { parsedUnlockedAchievements.push({ name: achievement, unlockTime: unlockedAchievement.timestamp * 1000, @@ -129,7 +129,7 @@ const processCreamAPI = (unlockedAchievements: any): UnlockedAchievement[] => { for (const achievement of Object.keys(unlockedAchievements)) { const unlockedAchievement = unlockedAchievements[achievement]; - if (unlockedAchievement?.achieved) { + if (unlockedAchievement?.achieved == "true") { const unlockTime = unlockedAchievement.unlocktime; parsedUnlockedAchievements.push({ name: achievement, @@ -207,7 +207,7 @@ const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => { for (const achievement of Object.keys(unlockedAchievements)) { const unlockedAchievement = unlockedAchievements[achievement]; - if (unlockedAchievement?.Achieved) { + if (unlockedAchievement?.Achieved == "1") { newUnlockedAchievements.push({ name: achievement, unlockTime: unlockedAchievement.UnlockTime * 1000, From a4475d2145b6c56cd1e29674fa516ad4df78d0bb Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 13 Oct 2024 21:14:06 -0300 Subject: [PATCH 103/163] feat: achievement section for user not logged in --- src/locales/en/translation.json | 6 +- src/locales/pt-BR/translation.json | 6 +- src/locales/pt-PT/translation.json | 3 +- .../achievements/get-game-achievement-data.ts | 7 +- .../game-details/game-details.context.tsx | 37 ++++++-- .../sidebar-section/sidebar-section.tsx | 1 + .../pages/game-details/sidebar/sidebar.tsx | 90 +++++++++++++++++-- 7 files changed, 131 insertions(+), 19 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 6d2ea93b..53760b42 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -131,7 +131,8 @@ "executable_path_in_use": "Executable already in use by \"{{game}}\"", "warning": "Warning:", "hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util its conclusion. In case Hydra closes before the conclusion, you will lose your progress.", - "achievements": "Achievements {{unlockedCount}}/{{achievementsCount}}", + "achievements": "Achievements", + "achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}", "cloud_save": "Cloud save", "cloud_save_description": "Save your progress in the cloud and continue playing on any device", "backups": "Backups", @@ -146,7 +147,8 @@ "backup_uploaded": "Backup uploaded", "backup_deleted": "Backup deleted", "backup_restored": "Backup restored", - "see_all_achievements": "See all achievements" + "see_all_achievements": "See all achievements", + "sign_in_to_see_achievements": "Sign in to see achievements" }, "activation": { "title": "Activate Hydra", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index f5d91d46..72a941bc 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -127,7 +127,8 @@ "executable_path_in_use": "Executável em uso por \"{{game}}\"", "warning": "Aviso:", "hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.", - "achievements": "Conquistas ({{unlockedCount}}/{{achievementsCount}})", + "achievements": "Conquistas", + "achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})", "cloud_save": "Salvamento em nuvem", "cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo", "backups": "Backups", @@ -142,7 +143,8 @@ "backup_uploaded": "Backup criado", "backup_deleted": "Backup apagado", "backup_restored": "Backup restaurado", - "see_all_achievements": "Ver todas as conquistas" + "see_all_achievements": "Ver todas as conquistas", + "sign_in_to_see_achievements": "Faça login para ver as conquistas" }, "activation": { "title": "Ativação", diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index a65ace2f..01fa3140 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -116,7 +116,8 @@ "executable_path_in_use": "Executável em uso por \"{{game}}\"", "warning": "Aviso:", "hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.", - "achievements": "Conquistas ({{unlockedCount}}/{{achievementsCount}})", + "achievements": "Conquistas", + "achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})", "see_all_achievements": "Ver todas as conquistas" }, "activation": { diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index 0cbadb54..b94cbe7f 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -4,6 +4,7 @@ import { } from "@main/repository"; import { HydraApi } from "../hydra-api"; import { AchievementData } from "@types"; +import { UserNotLoggedInError } from "@shared"; export const getGameAchievementData = async ( objectId: string, @@ -30,7 +31,11 @@ export const getGameAchievementData = async ( return achievements; }) - .catch(() => { + .catch((err) => { + if (err instanceof UserNotLoggedInError) { + throw err; + } + return gameAchievementRepository .findOne({ where: { objectId, shop }, 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 a08f4a55..5c5a0014 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -3,12 +3,18 @@ import { useCallback, useContext, useEffect, + useRef, useState, } from "react"; import { setHeaderTitle } from "@renderer/features"; import { getSteamLanguage } from "@renderer/helpers"; -import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks"; +import { + useAppDispatch, + useAppSelector, + useDownload, + useUserDetails, +} from "@renderer/hooks"; import type { Game, @@ -67,6 +73,7 @@ export function GameDetailsContextProvider({ const [achievements, setAchievements] = useState([]); const [game, setGame] = useState(null); const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false); + const abortControllerRef = useRef(null); const [stats, setStats] = useState(null); @@ -93,6 +100,7 @@ export function GameDetailsContextProvider({ const dispatch = useAppDispatch(); const { lastPacket } = useDownload(); + const { userDetails } = useUserDetails(); const userPreferences = useAppSelector( (state) => state.userPreferences.value @@ -111,6 +119,10 @@ export function GameDetailsContextProvider({ }, [updateGame, isGameDownloading, lastPacket?.game.status]); useEffect(() => { + if (abortControllerRef.current) abortControllerRef.current.abort(); + const abortController = new AbortController(); + abortControllerRef.current = abortController; + window.electron .getGameShopDetails( objectId!, @@ -118,6 +130,8 @@ export function GameDetailsContextProvider({ getSteamLanguage(i18n.language) ) .then((result) => { + if (abortController.signal.aborted) return; + setShopDetails(result); if ( @@ -133,21 +147,29 @@ export function GameDetailsContextProvider({ }); window.electron.getGameStats(objectId, shop as GameShop).then((result) => { + if (abortController.signal.aborted) return; setStats(result); }); window.electron .getGameAchievements(objectId, shop as GameShop) .then((achievements) => { - // TODO: race condition + if (abortController.signal.aborted) return; + if (!userDetails) return; setAchievements(achievements); }) - .catch(() => { - // TODO: handle user not logged in error - }); + .catch(() => {}); updateGame(); - }, [updateGame, dispatch, gameTitle, objectId, shop, i18n.language]); + }, [ + updateGame, + dispatch, + gameTitle, + objectId, + shop, + i18n.language, + userDetails, + ]); useEffect(() => { setShopDetails(null); @@ -180,6 +202,7 @@ export function GameDetailsContextProvider({ objectId, shop, (achievements) => { + if (!userDetails) return; setAchievements(achievements); } ); @@ -187,7 +210,7 @@ export function GameDetailsContextProvider({ return () => { unsubscribe(); }; - }, [objectId, shop]); + }, [objectId, shop, userDetails]); const getDownloadsPath = async () => { if (userPreferences?.downloadsPath) return userPreferences.downloadsPath; diff --git a/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.tsx b/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.tsx index 9ed48c9b..da9d078f 100644 --- a/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.tsx +++ b/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.tsx @@ -29,6 +29,7 @@ export function SidebarSection({ title, children }: SidebarSectionProps) { maxHeight: isOpen ? `${content.current?.scrollHeight}px` : "0", overflow: "hidden", transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)", + position: "relative", }} > {children} diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index d04afbc6..f72fcadc 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -1,16 +1,49 @@ import { useContext, useEffect, useState } from "react"; -import type { HowLongToBeatCategory, SteamAppDetails } from "@types"; +import type { + HowLongToBeatCategory, + SteamAppDetails, + UserAchievement, +} from "@types"; import { useTranslation } from "react-i18next"; import { Button, Link } from "@renderer/components"; import * as styles from "./sidebar.css"; import { gameDetailsContext } from "@renderer/context"; -import { useDate, useFormat } from "@renderer/hooks"; -import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; +import { useDate, useFormat, useUserDetails } from "@renderer/hooks"; +import { DownloadIcon, LockIcon, PeopleIcon } from "@primer/octicons-react"; import { HowLongToBeatSection } from "./how-long-to-beat-section"; import { howLongToBeatEntriesTable } from "@renderer/dexie"; import { SidebarSection } from "../sidebar-section/sidebar-section"; import { buildGameAchievementPath } from "@renderer/helpers"; +import { SPACING_UNIT } from "@renderer/theme.css"; + +const fakeAchievements: UserAchievement[] = [ + { + displayName: "Timber!!", + name: "", + hidden: false, + description: "Chop down your first tree.", + icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg", + unlocked: true, + unlockTime: Date.now(), + }, + { + displayName: "Supreme Helper Minion!", + name: "", + hidden: false, + icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg", + unlocked: false, + unlockTime: null, + }, + { + displayName: "Feast of Midas", + name: "", + hidden: false, + icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg", + unlocked: false, + unlockTime: null, + }, +]; export function Sidebar() { const [howLongToBeat, setHowLongToBeat] = useState<{ @@ -18,6 +51,8 @@ export function Sidebar() { data: HowLongToBeatCategory[] | null; }>({ isLoading: true, data: null }); + const { userDetails } = useUserDetails(); + const [activeRequirement, setActiveRequirement] = useState("minimum"); @@ -68,9 +103,53 @@ export function Sidebar() { return (
  • 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 de94bcff..635c7f99 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -7,11 +7,11 @@ import type { GameRepack } from "@types"; import * as styles from "./repacks-modal.css"; import { SPACING_UNIT } from "@renderer/theme.css"; -import { format } from "date-fns"; import { DownloadSettingsModal } from "./download-settings-modal"; import { gameDetailsContext } from "@renderer/context"; import { Downloader } from "@shared"; import { orderBy } from "lodash-es"; +import { useDate } from "@renderer/hooks"; export interface RepacksModalProps { visible: boolean; @@ -36,6 +36,8 @@ export function RepacksModal({ const { t } = useTranslation("game_details"); + const { formatDate } = useDate(); + const sortedRepacks = useMemo(() => { return orderBy(repacks, (repack) => repack.uploadDate, "desc"); }, [repacks]); @@ -109,9 +111,7 @@ export function RepacksModal({

    {repack.fileSize} - {repack.repacker} -{" "} - {repack.uploadDate - ? format(repack.uploadDate, "dd/MM/yyyy") - : ""} + {repack.uploadDate ? formatDate(repack.uploadDate!) : ""}

    ); diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index f72fcadc..66874aff 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -60,7 +60,7 @@ export function Sidebar() { useContext(gameDetailsContext); const { t } = useTranslation("game_details"); - const { format } = useDate(); + const { formatDateTime } = useDate(); const { numberFormatter } = useFormat(); @@ -138,7 +138,8 @@ export function Sidebar() {

    {achievement.displayName}

    - {achievement.unlockTime && format(achievement.unlockTime)} + {achievement.unlockTime && + formatDateTime(achievement.unlockTime)}
    @@ -176,7 +177,8 @@ export function Sidebar() {

    {achievement.displayName}

    - {achievement.unlockTime && format(achievement.unlockTime)} + {achievement.unlockTime && + formatDateTime(achievement.unlockTime)}
    From 694e0cd12c8f789458cb9269e5deb5b00d9be60c Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:07:16 -0300 Subject: [PATCH 105/163] feat: achievements ui --- .../achievement/achievements-content.tsx | 207 ++++++++++++++++++ .../achievement/achievements-skeleton.tsx | 13 ++ .../src/pages/achievement/achievements.css.ts | 51 ++++- .../src/pages/achievement/achievements.tsx | 182 +++------------ 4 files changed, 301 insertions(+), 152 deletions(-) create mode 100644 src/renderer/src/pages/achievement/achievements-content.tsx create mode 100644 src/renderer/src/pages/achievement/achievements-skeleton.tsx diff --git a/src/renderer/src/pages/achievement/achievements-content.tsx b/src/renderer/src/pages/achievement/achievements-content.tsx new file mode 100644 index 00000000..3520ffcc --- /dev/null +++ b/src/renderer/src/pages/achievement/achievements-content.tsx @@ -0,0 +1,207 @@ +import { setHeaderTitle } from "@renderer/features"; +import { useAppDispatch, useDate } from "@renderer/hooks"; +import { steamUrlBuilder } from "@shared"; +import { useContext, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import * as styles from "./achievements.css"; +import { formatDownloadProgress } from "@renderer/helpers"; +import { TrophyIcon } from "@primer/octicons-react"; +import { vars } from "@renderer/theme.css"; +import { gameDetailsContext } from "@renderer/context"; +import { GameShop, UserAchievement } from "@types"; +import { average } from "color.js"; +import Color from "color"; + +const HERO_ANIMATION_THRESHOLD = 25; + +interface AchievementsContentProps { + userId: string | null; + displayName: string | null; +} + +export function AchievementsContent({ + userId, + displayName, +}: AchievementsContentProps) { + const heroRef = useRef(null); + const containerRef = useRef(null); + const [isHeaderStuck, setIsHeaderStuck] = useState(false); + const [backdropOpactiy, setBackdropOpacity] = useState(1); + const [pageAchievements, setPageAchievements] = useState( + [] + ); + + const { t } = useTranslation("achievement"); + + const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } = + useContext(gameDetailsContext); + + const { formatDateTime } = useDate(); + + const dispatch = useAppDispatch(); + + useEffect(() => { + if (gameTitle) { + dispatch(setHeaderTitle(gameTitle)); + } + }, [dispatch, gameTitle]); + + const handleHeroLoad = async () => { + const output = await average(steamUrlBuilder.libraryHero(objectId!), { + amount: 1, + format: "hex", + }); + + const backgroundColor = output + ? (new Color(output).darken(0.7).toString() as string) + : ""; + + setGameColor(backgroundColor); + }; + + useEffect(() => { + if (objectId && shop && userId) { + window.electron + .getGameAchievements(objectId, shop as GameShop, userId) + .then((achievements) => { + setPageAchievements(achievements); + }); + } + }, [objectId, shop, userId]); + + const onScroll: React.UIEventHandler = (event) => { + const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT; + + const scrollY = (event.target as HTMLDivElement).scrollTop; + const opacity = Math.max( + 0, + 1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD) + ); + + if (scrollY >= heroHeight && !isHeaderStuck) { + setIsHeaderStuck(true); + } + + if (scrollY <= heroHeight && isHeaderStuck) { + setIsHeaderStuck(false); + } + + setBackdropOpacity(opacity); + }; + + if (!objectId || !shop || !gameTitle) return null; + + const userAchievements = userId ? pageAchievements : achievements; + + const unlockedAchievementCount = userAchievements.filter( + (achievement) => achievement.unlocked + ).length; + + const totalAchievementCount = userAchievements.length; + + return ( +
    + {gameTitle} + +
    +
    +
    + +
    +
    + {gameTitle} +
    +
    +
    + +
    +

    + {displayName + ? t("user_achievements", { + displayName, + }) + : t("your_achievements")} +

    +
    +
    + + + {unlockedAchievementCount} / {totalAchievementCount} + +
    + + + {formatDownloadProgress( + unlockedAchievementCount / totalAchievementCount + )} + +
    + +
    + +
      + {userAchievements.map((achievement, index) => ( +
    • + {achievement.displayName} +
      +

      {achievement.displayName}

      +

      {achievement.description}

      + + {achievement.unlockTime && + formatDateTime(achievement.unlockTime)} + +
      +
    • + ))} +
    +
    +
    + ); +} diff --git a/src/renderer/src/pages/achievement/achievements-skeleton.tsx b/src/renderer/src/pages/achievement/achievements-skeleton.tsx new file mode 100644 index 00000000..f9ae81ac --- /dev/null +++ b/src/renderer/src/pages/achievement/achievements-skeleton.tsx @@ -0,0 +1,13 @@ +import Skeleton from "react-loading-skeleton"; +import * as styles from "./achievements.css"; + +export function AchievementsSkeleton() { + return ( +
    +
    + +
    +
    +
    + ); +} diff --git a/src/renderer/src/pages/achievement/achievements.css.ts b/src/renderer/src/pages/achievement/achievements.css.ts index 792548ae..9dc6ac00 100644 --- a/src/renderer/src/pages/achievement/achievements.css.ts +++ b/src/renderer/src/pages/achievement/achievements.css.ts @@ -29,7 +29,7 @@ export const header = style({ }, }); -export const headerImage = style({ +export const hero = style({ position: "absolute", inset: "0", borderRadius: "4px", @@ -39,9 +39,17 @@ export const headerImage = style({ transition: "all ease 0.2s", }); -export const gameLogo = style({ +export const heroContent = style({ padding: `${SPACING_UNIT * 2}px`, - width: "300px", + height: "100%", + width: "100%", + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", +}); + +export const gameLogo = style({ + width: 300, }); export const container = style({ @@ -132,3 +140,40 @@ export const achievementsProgressBar = style({ backgroundColor: vars.color.muted, }, }); + +export const heroLogoBackdrop = style({ + width: "100%", + height: "100%", + background: "linear-gradient(0deg, rgba(0, 0, 0, 0.3) 60%, transparent 100%)", + position: "absolute", + display: "flex", + flexDirection: "column", + justifyContent: "flex-end", +}); + +export const heroImageSkeleton = style({ + height: "300px", + "@media": { + "(min-width: 1250px)": { + height: "350px", + }, + }, +}); + +export const heroPanelSkeleton = style({ + width: "100%", + padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`, + display: "flex", + alignItems: "center", + backgroundColor: vars.color.background, + height: "72px", + borderBottom: `solid 1px ${vars.color.border}`, +}); + +export const listItemSkeleton = style({ + width: "100%", + overflow: "hidden", + borderRadius: "4px", + padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`, + gap: `${SPACING_UNIT * 2}px`, +}); diff --git a/src/renderer/src/pages/achievement/achievements.tsx b/src/renderer/src/pages/achievement/achievements.tsx index fdffddd8..e5f5de31 100644 --- a/src/renderer/src/pages/achievement/achievements.tsx +++ b/src/renderer/src/pages/achievement/achievements.tsx @@ -1,16 +1,16 @@ import { setHeaderTitle } from "@renderer/features"; -import { useAppDispatch, useDate } from "@renderer/hooks"; -import { steamUrlBuilder } from "@shared"; -import type { GameShop, UserAchievement } from "@types"; -import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { useAppDispatch } from "@renderer/hooks"; +import type { GameShop } from "@types"; +import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; -import * as styles from "./achievements.css"; -import { formatDownloadProgress } from "@renderer/helpers"; -import { TrophyIcon } from "@primer/octicons-react"; import { vars } from "@renderer/theme.css"; - -const HERO_ANIMATION_THRESHOLD = 25; +import { + GameDetailsContextConsumer, + GameDetailsContextProvider, +} from "@renderer/context"; +import { SkeletonTheme } from "react-loading-skeleton"; +import { AchievementsSkeleton } from "./achievements-skeleton"; +import { AchievementsContent } from "./achievements-content"; export function Achievement() { const [searchParams] = useSearchParams(); @@ -20,157 +20,41 @@ export function Achievement() { const userId = searchParams.get("userId"); const displayName = searchParams.get("displayName"); - const heroRef = useRef(null); - const containerRef = useRef(null); - const [isHeaderStuck, setIsHeaderStuck] = useState(false); - const [backdropOpactiy, setBackdropOpacity] = useState(1); - - const { t } = useTranslation("achievement"); - - const { formatDateTime } = useDate(); - const dispatch = useAppDispatch(); - const [achievements, setAchievements] = useState([]); - - useEffect(() => { - if (objectId && shop) { - window.electron - .getGameAchievements(objectId, shop as GameShop, userId || undefined) - .then((achievements) => { - setAchievements(achievements); - }); - } - }, [objectId, shop, userId]); - useEffect(() => { if (title) { dispatch(setHeaderTitle(title)); } }, [dispatch, title]); - const onScroll: React.UIEventHandler = (event) => { - const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT; - - const scrollY = (event.target as HTMLDivElement).scrollTop; - const opacity = Math.max( - 0, - 1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD) - ); - - if (scrollY >= heroHeight && !isHeaderStuck) { - setIsHeaderStuck(true); - } - - if (scrollY <= heroHeight && isHeaderStuck) { - setIsHeaderStuck(false); - } - - setBackdropOpacity(opacity); - }; - if (!objectId || !shop || !title) return null; - const unlockedAchievementCount = achievements.filter( - (achievement) => achievement.unlocked - ).length; - - const totalAchievementCount = achievements.length; - return ( -
    - {title} - -
    -
    -
    - - {title} -
    - -
    -

    - {displayName - ? t("user_achievements", { - displayName, - }) - : t("your_achievements")} -

    -
    -
    + + {({ isLoading }) => { + return ( + - - - {unlockedAchievementCount} / {totalAchievementCount} - -
    - - - {formatDownloadProgress( - unlockedAchievementCount / totalAchievementCount + {isLoading ? ( + + ) : ( + )} - -
    - -
    - -
      - {achievements.map((achievement, index) => ( -
    • - {achievement.displayName} -
      -

      {achievement.displayName}

      -

      {achievement.description}

      - - {achievement.unlockTime && - formatDateTime(achievement.unlockTime)} - -
      -
    • - ))} -
    -
    -
    + + ); + }} + +
    ); } From c24f6be1b7b99ace92838b8033da66d6cefbd4ac Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:22:36 -0300 Subject: [PATCH 106/163] feat: comparing achievements --- .../achievement/achievements-content.tsx | 191 +++++++++++------- .../src/pages/achievement/achievements.css.ts | 4 +- 2 files changed, 121 insertions(+), 74 deletions(-) diff --git a/src/renderer/src/pages/achievement/achievements-content.tsx b/src/renderer/src/pages/achievement/achievements-content.tsx index 3520ffcc..51dda100 100644 --- a/src/renderer/src/pages/achievement/achievements-content.tsx +++ b/src/renderer/src/pages/achievement/achievements-content.tsx @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next"; import * as styles from "./achievements.css"; import { formatDownloadProgress } from "@renderer/helpers"; import { TrophyIcon } from "@primer/octicons-react"; -import { vars } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; import { GameShop, UserAchievement } from "@types"; import { average } from "color.js"; @@ -19,6 +19,107 @@ interface AchievementsContentProps { displayName: string | null; } +interface AchievementListProps { + achievements: UserAchievement[]; +} + +interface AchievementPanelProps { + achievements: UserAchievement[]; + displayName: string | null; +} + +function AchievementPanel({ + achievements, + displayName, +}: AchievementPanelProps) { + const { t } = useTranslation("achievement"); + + const unlockedAchievementCount = achievements.filter( + (achievement) => achievement.unlocked + ).length; + + const totalAchievementCount = achievements.length; + return ( +
    +

    + {displayName + ? t("user_achievements", { + displayName, + }) + : t("your_achievements")} +

    +
    +
    + + + {unlockedAchievementCount} / {totalAchievementCount} + +
    + + + {formatDownloadProgress( + unlockedAchievementCount / totalAchievementCount + )} + +
    + +
    + ); +} + +function AchievementList({ achievements }: AchievementListProps) { + const { formatDateTime } = useDate(); + + return ( +
      + {achievements.map((achievement, index) => ( +
    • + {achievement.displayName} +
      +

      {achievement.displayName}

      +

      {achievement.description}

      + + {achievement.unlockTime && formatDateTime(achievement.unlockTime)} + +
      +
    • + ))} +
    + ); +} + export function AchievementsContent({ userId, displayName, @@ -31,13 +132,9 @@ export function AchievementsContent({ [] ); - const { t } = useTranslation("achievement"); - const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } = useContext(gameDetailsContext); - const { formatDateTime } = useDate(); - const dispatch = useAppDispatch(); useEffect(() => { @@ -93,12 +190,6 @@ export function AchievementsContent({ const userAchievements = userId ? pageAchievements : achievements; - const unlockedAchievementCount = userAchievements.filter( - (achievement) => achievement.unlocked - ).length; - - const totalAchievementCount = userAchievements.length; - return (
    -

    - {displayName - ? t("user_achievements", { - displayName, - }) - : t("your_achievements")} -

    -
    -
    - - - {unlockedAchievementCount} / {totalAchievementCount} - -
    - - - {formatDownloadProgress( - unlockedAchievementCount / totalAchievementCount - )} - -
    - + {pageAchievements.length > 0 && ( + + )}
    +
    + {pageAchievements.length > 0 && ( + + )} -
      - {userAchievements.map((achievement, index) => ( -
    • - {achievement.displayName} -
      -

      {achievement.displayName}

      -

      {achievement.description}

      - - {achievement.unlockTime && - formatDateTime(achievement.unlockTime)} - -
      -
    • - ))} -
    + +
    ); diff --git a/src/renderer/src/pages/achievement/achievements.css.ts b/src/renderer/src/pages/achievement/achievements.css.ts index 9dc6ac00..8c6683b6 100644 --- a/src/renderer/src/pages/achievement/achievements.css.ts +++ b/src/renderer/src/pages/achievement/achievements.css.ts @@ -66,10 +66,10 @@ export const panel = recipe({ width: "100%", height: "100px", minHeight: "100px", - padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`, + padding: `${SPACING_UNIT * 2}px 0`, backgroundColor: vars.color.darkBackground, display: "flex", - flexDirection: "column", + flexDirection: "row", transition: "all ease 0.2s", borderBottom: `solid 1px ${vars.color.border}`, position: "sticky", From 1d29bc3620387964406b2f2668c8a634463ea4f9 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:37:36 -0300 Subject: [PATCH 107/163] feat: add achievement notifications setting --- src/locales/en/translation.json | 3 ++- src/locales/pt-BR/translation.json | 3 ++- src/main/entity/user-preferences.entity.ts | 3 +++ src/main/knex-client.ts | 2 ++ ...add_achievement_notification_preference.ts | 17 ++++++++++++ .../achievements/merge-achievements.ts | 27 +++++++++++++------ .../src/pages/settings/settings-general.tsx | 14 ++++++++++ src/types/index.ts | 1 + 8 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 src/main/migrations/20241013012900_add_achievement_notification_preference.ts diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 53760b42..3d730e39 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -235,7 +235,8 @@ "source_already_exists": "This source has been already added", "must_be_valid_url": "The source must be a valid URL", "blocked_users": "Blocked users", - "user_unblocked": "User has been unblocked" + "user_unblocked": "User has been unblocked", + "enable_achievement_notifications": "When an achievement in unlocked" }, "notifications": { "download_complete": "Download complete", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 72a941bc..c658ce78 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -234,7 +234,8 @@ "source_already_exists": "Essa fonte já foi adicionada", "must_be_valid_url": "A fonte deve ser uma URL válida", "blocked_users": "Usuários bloqueados", - "user_unblocked": "Usuário desbloqueado" + "user_unblocked": "Usuário desbloqueado", + "enable_achievement_notifications": "Quando uma conquista é desbloqueada" }, "notifications": { "download_complete": "Download concluído", diff --git a/src/main/entity/user-preferences.entity.ts b/src/main/entity/user-preferences.entity.ts index 92db958d..dc6d465d 100644 --- a/src/main/entity/user-preferences.entity.ts +++ b/src/main/entity/user-preferences.entity.ts @@ -26,6 +26,9 @@ export class UserPreferences { @Column("boolean", { default: false }) repackUpdatesNotificationsEnabled: boolean; + @Column("boolean", { default: true }) + achievementNotificationsEnabled: boolean; + @Column("boolean", { default: false }) preferQuitInsteadOfHiding: boolean; diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts index 6530e653..c289eebe 100644 --- a/src/main/knex-client.ts +++ b/src/main/knex-client.ts @@ -7,6 +7,7 @@ import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris import { app } from "electron"; import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns"; import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement"; +import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference"; export type HydraMigration = Knex.Migration & { name: string }; @@ -19,6 +20,7 @@ class MigrationSource implements Knex.MigrationSource { EnsureRepackUris, FixMissingColumns, CreateGameAchievement, + AddAchievementNotificationPreference, ]); } getMigrationName(migration: HydraMigration): string { diff --git a/src/main/migrations/20241013012900_add_achievement_notification_preference.ts b/src/main/migrations/20241013012900_add_achievement_notification_preference.ts new file mode 100644 index 00000000..a4f48265 --- /dev/null +++ b/src/main/migrations/20241013012900_add_achievement_notification_preference.ts @@ -0,0 +1,17 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const AddAchievementNotificationPreference: HydraMigration = { + name: "AddAchievementNotificationPreference", + up: (knex: Knex) => { + return knex.schema.alterTable("user_preferences", (table) => { + return table.boolean("achievementNotificationsEnabled").defaultTo(true); + }); + }, + + down: (knex: Knex) => { + return knex.schema.alterTable("user_preferences", (table) => { + return table.dropColumn("achievementNotificationsEnabled"); + }); + }, +}; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 6438a413..bf2ee461 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -1,4 +1,8 @@ -import { gameAchievementRepository, gameRepository } from "@main/repository"; +import { + gameAchievementRepository, + gameRepository, + userPreferencesRepository, +} from "@main/repository"; import type { GameShop, UnlockedAchievement } from "@types"; import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; @@ -38,12 +42,15 @@ export const mergeAchievements = async ( if (!game) return; - const localGameAchievement = await gameAchievementRepository.findOne({ - where: { - objectId, - shop, - }, - }); + const [localGameAchievement, userPreferences] = await Promise.all([ + gameAchievementRepository.findOne({ + where: { + objectId, + shop, + }, + }), + userPreferencesRepository.findOne({ where: { id: 1 } }), + ]); const unlockedAchievements = JSON.parse( localGameAchievement?.unlockedAchievements || "[]" @@ -64,7 +71,11 @@ export const mergeAchievements = async ( }; }); - if (newAchievements.length && publishNotification) { + if ( + newAchievements.length && + publishNotification && + userPreferences?.achievementNotificationsEnabled + ) { const achievementsInfo = newAchievements .sort((a, b) => { return a.unlockTime - b.unlockTime; diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index a363f55d..6737c4b7 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -30,6 +30,7 @@ export function SettingsGeneral() { downloadsPath: "", downloadNotificationsEnabled: false, repackUpdatesNotificationsEnabled: false, + achievementNotificationsEnabled: false, language: "", }); @@ -103,6 +104,8 @@ export function SettingsGeneral() { userPreferences.downloadNotificationsEnabled, repackUpdatesNotificationsEnabled: userPreferences.repackUpdatesNotificationsEnabled, + achievementNotificationsEnabled: + userPreferences.achievementNotificationsEnabled, language: language ?? "en", })); } @@ -155,6 +158,17 @@ export function SettingsGeneral() { }) } /> + + + handleChange({ + achievementNotificationsEnabled: + !form.achievementNotificationsEnabled, + }) + } + /> ); } diff --git a/src/types/index.ts b/src/types/index.ts index 7ef8073a..987e9f32 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -154,6 +154,7 @@ export interface UserPreferences { language: string; downloadNotificationsEnabled: boolean; repackUpdatesNotificationsEnabled: boolean; + achievementNotificationsEnabled: boolean; realDebridApiToken: string | null; preferQuitInsteadOfHiding: boolean; runAtStartup: boolean; From 034e88e2865516a3376e80e4cc83d3415178498f Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:46:25 -0300 Subject: [PATCH 108/163] feat: adjusting achievements page --- src/locales/en/translation.json | 3 +- src/locales/pt-BR/translation.json | 3 +- src/locales/pt-PT/translation.json | 3 +- .../achievements/merge-achievements.ts | 26 +-- .../achievement/achievements-content.tsx | 167 ++++++++++++++---- .../src/pages/achievement/achievements.css.ts | 3 +- 6 files changed, 153 insertions(+), 52 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 3d730e39..6e749b3e 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -338,6 +338,7 @@ "achievement": { "achievement_unlocked": "Achievement unlocked", "user_achievements": "{{displayName}}'s Achievements", - "your_achievements": "Your Achievements" + "your_achievements": "Your Achievements", + "unlocked_at": "Unlocked at:" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index c658ce78..ba572424 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -340,6 +340,7 @@ "achievement": { "achievement_unlocked": "Conquista desbloqueada", "your_achievements": "Suas Conquistas", - "user_achievements": "Conquistas de {{displayName}}" + "user_achievements": "Conquistas de {{displayName}}", + "unlocked_at": "Desbloqueado em:" } } diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index 01fa3140..b59db09f 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -282,6 +282,7 @@ "your_friend_code": "Seu código de amigo:" }, "achievement": { - "achievement_unlocked": "Conquista desbloqueada" + "achievement_unlocked": "Conquista desbloqueada", + "unlocked_at": "Desbloqueado em:" } } diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index bf2ee461..b7c50d37 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -3,7 +3,7 @@ import { gameRepository, userPreferencesRepository, } from "@main/repository"; -import type { GameShop, UnlockedAchievement } from "@types"; +import type { AchievementData, GameShop, UnlockedAchievement } from "@types"; import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; import { getGameAchievements } from "@main/events/catalogue/get-game-achievements"; @@ -52,9 +52,13 @@ export const mergeAchievements = async ( userPreferencesRepository.findOne({ where: { id: 1 } }), ]); + const achievementsData = JSON.parse( + localGameAchievement?.achievements || "[]" + ) as AchievementData[]; + const unlockedAchievements = JSON.parse( localGameAchievement?.unlockedAchievements || "[]" - ).filter((achievement) => achievement.name); + ).filter((achievement) => achievement.name) as UnlockedAchievement[]; const newAchievements = achievements .filter((achievement) => { @@ -81,20 +85,18 @@ export const mergeAchievements = async ( return a.unlockTime - b.unlockTime; }) .map((achievement) => { - return JSON.parse(localGameAchievement?.achievements || "[]").find( - (steamAchievement) => { - return ( - achievement.name.toUpperCase() === - steamAchievement.name.toUpperCase() - ); - } - ); + return achievementsData.find((steamAchievement) => { + return ( + achievement.name.toUpperCase() === + steamAchievement.name.toUpperCase() + ); + }); }) .filter((achievement) => achievement) .map((achievement) => { return { - displayName: achievement.displayName, - iconUrl: achievement.icon, + displayName: achievement!.displayName, + iconUrl: achievement!.icon, }; }); diff --git a/src/renderer/src/pages/achievement/achievements-content.tsx b/src/renderer/src/pages/achievement/achievements-content.tsx index 51dda100..25b04afd 100644 --- a/src/renderer/src/pages/achievement/achievements-content.tsx +++ b/src/renderer/src/pages/achievement/achievements-content.tsx @@ -1,7 +1,7 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useDate } from "@renderer/hooks"; import { steamUrlBuilder } from "@shared"; -import { useContext, useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./achievements.css"; import { formatDownloadProgress } from "@renderer/helpers"; @@ -21,6 +21,7 @@ interface AchievementsContentProps { interface AchievementListProps { achievements: UserAchievement[]; + otherUserAchievements: UserAchievement[]; } interface AchievementPanelProps { @@ -92,27 +93,106 @@ function AchievementPanel({ ); } -function AchievementList({ achievements }: AchievementListProps) { +function AchievementList({ + achievements, + otherUserAchievements, +}: AchievementListProps) { + const { t } = useTranslation("achievement"); const { formatDateTime } = useDate(); + if (otherUserAchievements.length === 0) { + return ( +
      + {achievements.map((achievement, index) => ( +
    • + {achievement.displayName} +
      +

      {achievement.displayName}

      +

      {achievement.description}

      +
      + {achievement.unlockTime && ( +
      + {t("unlocked_at")} +

      {formatDateTime(achievement.unlockTime)}

      +
      + )} +
    • + ))} +
    + ); + } + return (
      - {achievements.map((achievement, index) => ( -
    • - {achievement.displayName} -
      -

      {achievement.displayName}

      -

      {achievement.description}

      - - {achievement.unlockTime && formatDateTime(achievement.unlockTime)} - + {otherUserAchievements.map((otherUserAchievement, index) => ( +
    • +
      + {otherUserAchievement.displayName} + {otherUserAchievement.unlockTime && ( +
      + {t("unlocked_at")} +

      {formatDateTime(otherUserAchievement.unlockTime)}

      +
      + )} +
      + +
      +

      {otherUserAchievement.displayName}

      +

      {otherUserAchievement.description}

      +
      + +
      + {achievements[index].displayName} + {achievements[index].unlockTime && ( +
      + {t("unlocked_at")} +

      {formatDateTime(achievements[index].unlockTime)}

      +
      + )}
    • ))} @@ -128,13 +208,27 @@ export function AchievementsContent({ const containerRef = useRef(null); const [isHeaderStuck, setIsHeaderStuck] = useState(false); const [backdropOpactiy, setBackdropOpacity] = useState(1); - const [pageAchievements, setPageAchievements] = useState( - [] - ); + const [otherUserAchievements, setOtherUserAchievements] = useState< + UserAchievement[] + >([]); const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } = useContext(gameDetailsContext); + const sortedAchievements = useMemo(() => { + if (otherUserAchievements.length === 0) return achievements; + + return achievements.sort((a, b) => { + const indexA = otherUserAchievements.findIndex( + (achievement) => achievement.name === a.name + ); + const indexB = otherUserAchievements.findIndex( + (achievement) => achievement.name === b.name + ); + return indexA - indexB; + }); + }, [achievements, otherUserAchievements]); + const dispatch = useAppDispatch(); useEffect(() => { @@ -161,7 +255,7 @@ export function AchievementsContent({ window.electron .getGameAchievements(objectId, shop as GameShop, userId) .then((achievements) => { - setPageAchievements(achievements); + setOtherUserAchievements(achievements); }); } }, [objectId, shop, userId]); @@ -188,8 +282,6 @@ export function AchievementsContent({ if (!objectId || !shop || !gameTitle) return null; - const userAchievements = userId ? pageAchievements : achievements; - return (
      - - {pageAchievements.length > 0 && ( - + {userId && ( + )} + +
      - {pageAchievements.length > 0 && ( - - )} - - +
      diff --git a/src/renderer/src/pages/achievement/achievements.css.ts b/src/renderer/src/pages/achievement/achievements.css.ts index 8c6683b6..0c877586 100644 --- a/src/renderer/src/pages/achievement/achievements.css.ts +++ b/src/renderer/src/pages/achievement/achievements.css.ts @@ -93,11 +93,10 @@ export const list = style({ flexDirection: "column", gap: `${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 2}px`, - backgroundColor: vars.color.background, + width: "100%", }); export const listItem = style({ - display: "flex", transition: "all ease 0.1s", color: vars.color.muted, width: "100%", From 359733fa4034b5ff77bdc84a640bc178b8fd0242 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:42:16 -0300 Subject: [PATCH 109/163] fix: seeing own profile --- .../src/pages/achievement/achievements-content.tsx | 8 ++++---- src/renderer/src/pages/achievement/achievements.tsx | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/pages/achievement/achievements-content.tsx b/src/renderer/src/pages/achievement/achievements-content.tsx index 25b04afd..24fdf672 100644 --- a/src/renderer/src/pages/achievement/achievements-content.tsx +++ b/src/renderer/src/pages/achievement/achievements-content.tsx @@ -15,8 +15,8 @@ import Color from "color"; const HERO_ANIMATION_THRESHOLD = 25; interface AchievementsContentProps { - userId: string | null; - displayName: string | null; + otherUserId: string | null; + otherUserDisplayName: string | null; } interface AchievementListProps { @@ -201,8 +201,8 @@ function AchievementList({ } export function AchievementsContent({ - userId, - displayName, + otherUserId: userId, + otherUserDisplayName: displayName, }: AchievementsContentProps) { const heroRef = useRef(null); const containerRef = useRef(null); diff --git a/src/renderer/src/pages/achievement/achievements.tsx b/src/renderer/src/pages/achievement/achievements.tsx index e5f5de31..3dcf3fcf 100644 --- a/src/renderer/src/pages/achievement/achievements.tsx +++ b/src/renderer/src/pages/achievement/achievements.tsx @@ -1,5 +1,5 @@ import { setHeaderTitle } from "@renderer/features"; -import { useAppDispatch } from "@renderer/hooks"; +import { useAppDispatch, useUserDetails } from "@renderer/hooks"; import type { GameShop } from "@types"; import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; @@ -20,6 +20,8 @@ export function Achievement() { const userId = searchParams.get("userId"); const displayName = searchParams.get("displayName"); + const { userDetails } = useUserDetails(); + const dispatch = useAppDispatch(); useEffect(() => { @@ -30,6 +32,8 @@ export function Achievement() { if (!objectId || !shop || !title) return null; + const otherUserId = userDetails?.id == userId ? null : userId; + return ( ) : ( )} From e7a4888f54cd4ec25adb3955c87e50796c8351b1 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:03:50 -0300 Subject: [PATCH 110/163] feat: skeleton --- .../game-details/game-details.context.tsx | 8 +-- .../game-details.context.types.ts | 2 +- .../achievement/achievements-content.tsx | 49 +++++++------------ .../src/pages/achievement/achievements.tsx | 44 ++++++++++++++--- .../pages/game-details/sidebar/sidebar.tsx | 2 +- 5 files changed, 61 insertions(+), 44 deletions(-) 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 5c5a0014..6aaf112a 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -43,7 +43,7 @@ export const gameDetailsContext = createContext({ showRepacksModal: false, showGameOptionsModal: false, stats: null, - achievements: [], + achievements: null, hasNSFWContentBlocked: false, setGameColor: () => {}, selectGameExecutable: async () => null, @@ -70,7 +70,9 @@ export function GameDetailsContextProvider({ shop, }: GameDetailsContextProps) { const [shopDetails, setShopDetails] = useState(null); - const [achievements, setAchievements] = useState([]); + const [achievements, setAchievements] = useState( + null + ); const [game, setGame] = useState(null); const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false); const abortControllerRef = useRef(null); @@ -176,7 +178,7 @@ export function GameDetailsContextProvider({ setGame(null); setIsLoading(true); setisGameRunning(false); - setAchievements([]); + setAchievements(null); dispatch(setHeaderTitle(gameTitle)); }, [objectId, gameTitle, dispatch]); diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts index d410334b..ad5c4de7 100644 --- a/src/renderer/src/context/game-details/game-details.context.types.ts +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -20,7 +20,7 @@ export interface GameDetailsContext { showRepacksModal: boolean; showGameOptionsModal: boolean; stats: GameStats | null; - achievements: UserAchievement[]; + achievements: UserAchievement[] | null; hasNSFWContentBlocked: boolean; setGameColor: React.Dispatch>; selectGameExecutable: () => Promise; diff --git a/src/renderer/src/pages/achievement/achievements-content.tsx b/src/renderer/src/pages/achievement/achievements-content.tsx index 24fdf672..5e0af51f 100644 --- a/src/renderer/src/pages/achievement/achievements-content.tsx +++ b/src/renderer/src/pages/achievement/achievements-content.tsx @@ -8,20 +8,23 @@ import { formatDownloadProgress } from "@renderer/helpers"; import { TrophyIcon } from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; -import { GameShop, UserAchievement } from "@types"; +import { UserAchievement } from "@types"; import { average } from "color.js"; import Color from "color"; const HERO_ANIMATION_THRESHOLD = 25; interface AchievementsContentProps { - otherUserId: string | null; - otherUserDisplayName: string | null; + otherUser: { + userId: string; + displayName: string; + achievements: UserAchievement[]; + } | null; } interface AchievementListProps { achievements: UserAchievement[]; - otherUserAchievements: UserAchievement[]; + otherUserAchievements?: UserAchievement[]; } interface AchievementPanelProps { @@ -100,7 +103,7 @@ function AchievementList({ const { t } = useTranslation("achievement"); const { formatDateTime } = useDate(); - if (otherUserAchievements.length === 0) { + if (!otherUserAchievements || otherUserAchievements.length === 0) { return (
        {achievements.map((achievement, index) => ( @@ -200,34 +203,28 @@ function AchievementList({ ); } -export function AchievementsContent({ - otherUserId: userId, - otherUserDisplayName: displayName, -}: AchievementsContentProps) { +export function AchievementsContent({ otherUser }: AchievementsContentProps) { const heroRef = useRef(null); const containerRef = useRef(null); const [isHeaderStuck, setIsHeaderStuck] = useState(false); const [backdropOpactiy, setBackdropOpacity] = useState(1); - const [otherUserAchievements, setOtherUserAchievements] = useState< - UserAchievement[] - >([]); const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } = useContext(gameDetailsContext); const sortedAchievements = useMemo(() => { - if (otherUserAchievements.length === 0) return achievements; + if (!otherUser || otherUser.achievements.length === 0) return achievements!; - return achievements.sort((a, b) => { - const indexA = otherUserAchievements.findIndex( + return achievements!.sort((a, b) => { + const indexA = otherUser.achievements.findIndex( (achievement) => achievement.name === a.name ); - const indexB = otherUserAchievements.findIndex( + const indexB = otherUser.achievements.findIndex( (achievement) => achievement.name === b.name ); return indexA - indexB; }); - }, [achievements, otherUserAchievements]); + }, [achievements, otherUser]); const dispatch = useAppDispatch(); @@ -250,16 +247,6 @@ export function AchievementsContent({ setGameColor(backgroundColor); }; - useEffect(() => { - if (objectId && shop && userId) { - window.electron - .getGameAchievements(objectId, shop as GameShop, userId) - .then((achievements) => { - setOtherUserAchievements(achievements); - }); - } - }, [objectId, shop, userId]); - const onScroll: React.UIEventHandler = (event) => { const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT; @@ -320,10 +307,10 @@ export function AchievementsContent({
        - {userId && ( + {otherUser && ( )} @@ -342,7 +329,7 @@ export function AchievementsContent({ >
        diff --git a/src/renderer/src/pages/achievement/achievements.tsx b/src/renderer/src/pages/achievement/achievements.tsx index 3dcf3fcf..7110ac02 100644 --- a/src/renderer/src/pages/achievement/achievements.tsx +++ b/src/renderer/src/pages/achievement/achievements.tsx @@ -1,7 +1,7 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useUserDetails } from "@renderer/hooks"; -import type { GameShop } from "@types"; -import { useEffect } from "react"; +import type { GameShop, UserAchievement } from "@types"; +import { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { vars } from "@renderer/theme.css"; import { @@ -22,6 +22,10 @@ export function Achievement() { const { userDetails } = useUserDetails(); + const [otherUserAchievements, setOtherUserAchievements] = useState< + UserAchievement[] | null + >(null); + const dispatch = useAppDispatch(); useEffect(() => { @@ -30,10 +34,35 @@ export function Achievement() { } }, [dispatch, title]); + useEffect(() => { + setOtherUserAchievements(null); + if (userDetails?.id == userId) { + setOtherUserAchievements([]); + return; + } + + if (objectId && shop && userId) { + window.electron + .getGameAchievements(objectId, shop as GameShop, userId) + .then((achievements) => { + setOtherUserAchievements(achievements); + }); + } + }, [objectId, shop, userId]); + if (!objectId || !shop || !title) return null; const otherUserId = userDetails?.id == userId ? null : userId; + const otherUser = + otherUserId != null + ? { + userId: otherUserId, + displayName: displayName || "", + achievements: otherUserAchievements || [], + } + : null; + return ( - {({ isLoading }) => { + {({ isLoading, achievements }) => { return ( - {isLoading ? ( + {isLoading || + achievements === null || + otherUserAchievements === null ? ( ) : ( - + )} ); diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 66874aff..8d1fe8d6 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -148,7 +148,7 @@ export function Sidebar() {
      )} - {userDetails && achievements.length > 0 && ( + {userDetails && achievements && achievements.length > 0 && ( a.unlocked).length, From e9186e0a3cb600cc90d219f8536078bbb4b68c6a Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:46:52 -0300 Subject: [PATCH 111/163] fix: bug --- .../src/pages/achievement/achievements.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/pages/achievement/achievements.tsx b/src/renderer/src/pages/achievement/achievements.tsx index 7110ac02..d145027b 100644 --- a/src/renderer/src/pages/achievement/achievements.tsx +++ b/src/renderer/src/pages/achievement/achievements.tsx @@ -52,16 +52,15 @@ export function Achievement() { if (!objectId || !shop || !title) return null; - const otherUserId = userDetails?.id == userId ? null : userId; + const otherUserId = userDetails?.id === userId ? null : userId; - const otherUser = - otherUserId != null - ? { - userId: otherUserId, - displayName: displayName || "", - achievements: otherUserAchievements || [], - } - : null; + const otherUser = otherUserId + ? { + userId: otherUserId, + displayName: displayName || "", + achievements: otherUserAchievements || [], + } + : null; return ( {isLoading || achievements === null || - otherUserAchievements === null ? ( + (otherUserId && otherUserAchievements === null) ? ( ) : ( From 64c16e82b46daed9198424035279bad0de135f03 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:48:41 -0300 Subject: [PATCH 112/163] fix: ini files with BOM and new online fix location --- .../achievements/find-achivement-files.ts | 4 ++++ .../achievements/parse-achievement-file.ts | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index dc43f827..9a2c70ff 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -56,6 +56,10 @@ const getPathFromCracker = (cracker: Cracker) => { folderPath: path.join(publicDocuments, "OnlineFix"), fileLocation: ["Stats", "Achievements.ini"], }, + { + folderPath: path.join(publicDocuments, "OnlineFix"), + fileLocation: ["Achievements.ini"], + }, ]; } diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index b2031989..9c875a3f 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -73,7 +73,12 @@ export const parseAchievementFile = ( const iniParse = (filePath: string) => { try { - const lines = readFileSync(filePath, "utf-8").split(/[\r\n]+/); + const fileContent = readFileSync(filePath, "utf-8"); + + const lines = + fileContent.charCodeAt(0) === 0xfeff + ? fileContent.slice(1).split(/[\r\n]+/) + : fileContent.split(/[\r\n]+/); let objectName = ""; const object: Record> = {}; @@ -117,6 +122,16 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => { name: achievement, unlockTime: unlockedAchievement.timestamp * 1000, }); + } else if (unlockedAchievement?.Achieved == "true") { + const unlockTime = unlockedAchievement.TimeUnlocked; + + parsedUnlockedAchievements.push({ + name: achievement, + unlockTime: + unlockTime.length === 7 + ? unlockTime * 1000 * 1000 + : unlockTime * 1000, + }); } } From c5764a49e128739eb51d5225f5995c7776673e59 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:56:53 -0300 Subject: [PATCH 113/163] chore: update dependencies --- package.json | 9 ++-- requirements.txt | 1 - yarn.lock | 119 +++++++++++++++++++---------------------------- 3 files changed, 53 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 601b399f..f869f42f 100644 --- a/package.json +++ b/package.json @@ -53,18 +53,17 @@ "date-fns": "^3.6.0", "dexie": "^4.0.8", "electron-log": "^5.2.0", - "electron-updater": "^6.3.4", - "fetch-cookie": "^3.0.1", + "electron-updater": "^6.3.9", "flexsearch": "^0.7.43", "i18next": "^23.11.2", "i18next-browser-languagedetector": "^7.2.1", - "icojs": "^0.19.3", + "icojs": "^0.19.4", "jsdom": "^24.0.0", "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", "lodash-es": "^4.17.21", "lottie-react": "^2.4.0", - "parse-torrent": "^11.0.16", + "parse-torrent": "^11.0.17", "piscina": "^4.5.1", "react-hook-form": "^7.53.0", "react-i18next": "^14.1.0", @@ -101,7 +100,7 @@ "@vanilla-extract/vite-plugin": "^4.0.7", "@vitejs/plugin-react": "^4.2.1", "electron": "^30.3.0", - "electron-builder": "^25.1.6", + "electron-builder": "^25.1.8", "electron-vite": "^2.0.0", "eslint": "^8.56.0", "eslint-plugin-jsx-a11y": "^6.8.0", diff --git a/requirements.txt b/requirements.txt index cdd5371d..a379f371 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,3 @@ cx_Logging; sys_platform == 'win32' pywin32; sys_platform == 'win32' psutil Pillow -requests diff --git a/yarn.lock b/yarn.lock index 2289c292..d98e97dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -789,10 +789,10 @@ minimist "^1.2.6" plist "^3.0.5" -"@electron/rebuild@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.6.0.tgz#60211375a5f8541a71eb07dd2f97354ad0b2b96f" - integrity sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw== +"@electron/rebuild@3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.6.1.tgz#59e8e36c3f6e6b94a699425dfb61f0394c3dd4df" + integrity sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w== dependencies: "@malept/cross-spawn-promise" "^2.0.0" chalk "^4.0.0" @@ -2991,29 +2991,29 @@ app-builder-bin@5.0.0-alpha.10: resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz#cf12e593b6b847fb9d04027fa755c6c6610d778b" integrity sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw== -app-builder-lib@25.1.6: - version "25.1.6" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-25.1.6.tgz#a9618e186a5fa7b38cfa2d51ebba36d176ac31b6" - integrity sha512-iui5q65skaawkTBcsaf2wCaegCWO+JoK5VaPnaNdIWXm2bq8a/g53W88FHjR535L9viLeGbuBp/iU1XZSe2DQQ== +app-builder-lib@25.1.8: + version "25.1.8" + resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-25.1.8.tgz#ae376039c5f269c7d562af494a087e5bc6310f1b" + integrity sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg== dependencies: "@develar/schema-utils" "~2.6.5" "@electron/notarize" "2.5.0" "@electron/osx-sign" "1.3.1" - "@electron/rebuild" "3.6.0" + "@electron/rebuild" "3.6.1" "@electron/universal" "2.0.1" "@malept/flatpak-bundler" "^0.4.0" "@types/fs-extra" "9.0.13" async-exit-hook "^2.0.1" bluebird-lst "^1.0.9" - builder-util "25.1.6" - builder-util-runtime "9.2.9" + builder-util "25.1.7" + builder-util-runtime "9.2.10" chromium-pickle-js "^0.2.0" config-file-ts "0.2.8-rc1" debug "^4.3.4" dotenv "^16.4.5" dotenv-expand "^11.0.6" ejs "^3.1.8" - electron-publish "25.1.6" + electron-publish "25.1.7" form-data "^4.0.0" fs-extra "^10.1.0" hosted-git-info "^4.1.0" @@ -3369,32 +3369,24 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -builder-util-runtime@9.2.5: - version "9.2.5" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz#0afdffa0adb5c84c14926c7dd2cf3c6e96e9be83" - integrity sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg== +builder-util-runtime@9.2.10: + version "9.2.10" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz#a0f7d9e214158402e78b74a745c8d9f870c604bc" + integrity sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw== dependencies: debug "^4.3.4" sax "^1.2.4" -builder-util-runtime@9.2.9: - version "9.2.9" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.9.tgz#e7d5dcb3a7caa9575406edea28940f6088d7cbb3" - integrity sha512-DWeHdrRFVvNnVyD4+vMztRpXegOGaQHodsAjyhstTbUNBIjebxM1ahxokQL+T1v8vpW8SY7aJ5is/zILH82lAw== - dependencies: - debug "^4.3.4" - sax "^1.2.4" - -builder-util@25.1.6: - version "25.1.6" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-25.1.6.tgz#8fb5cadd67afd10e8bb6a5d29583e52cebce4fbd" - integrity sha512-BOMVCO/CLYaIwK2uh7BKJUmRy9fYhn674c4Cauxc/lSZ7CyLLNkUMZJUFCPd3OqD1FIQO06MZ/u7akKtVyXlJw== +builder-util@25.1.7: + version "25.1.7" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-25.1.7.tgz#a07b404f0cb1a635aa165902be65297d58932ff8" + integrity sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww== dependencies: "7zip-bin" "~5.2.0" "@types/debug" "^4.1.6" app-builder-bin "5.0.0-alpha.10" bluebird-lst "^1.0.9" - builder-util-runtime "9.2.9" + builder-util-runtime "9.2.10" chalk "^4.1.2" cross-spawn "^7.0.3" debug "^4.3.4" @@ -4078,14 +4070,14 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dmg-builder@25.1.6: - version "25.1.6" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.6.tgz#ac9c9c35e09727610f342f400256f329cc7ba064" - integrity sha512-TeKjLNKBu6ODewl36Q1FH0+Bv/Rnb76x1vzPJ0Xw1T/YlAByYl1G+1PaKoEpVEDisrLRSrM9a0k3vDj53xQi9w== +dmg-builder@25.1.8: + version "25.1.8" + resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.8.tgz#41f3b725edd896156e891016a44129e1bd580430" + integrity sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ== dependencies: - app-builder-lib "25.1.6" - builder-util "25.1.6" - builder-util-runtime "9.2.9" + app-builder-lib "25.1.8" + builder-util "25.1.7" + builder-util-runtime "9.2.10" fs-extra "^10.1.0" iconv-lite "^0.6.2" js-yaml "^4.1.0" @@ -4166,16 +4158,16 @@ ejs@^3.1.8: dependencies: jake "^10.8.5" -electron-builder@^25.1.6: - version "25.1.6" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-25.1.6.tgz#e2a8a4b4a7b6db615405a9b9b18fb3dcf7c84d8c" - integrity sha512-a+w0leNGcr57u9fiQBsVvVuXtTsg/6VpMOps1Q1TPAceHtK4kWtX2wc7X2cnJQrIME8bWbdm/YR9Swr/xF1/ng== +electron-builder@^25.1.8: + version "25.1.8" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-25.1.8.tgz#b0e310f1600787610bb84c3f39bc7aadb2548486" + integrity sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig== dependencies: - app-builder-lib "25.1.6" - builder-util "25.1.6" - builder-util-runtime "9.2.9" + app-builder-lib "25.1.8" + builder-util "25.1.7" + builder-util-runtime "9.2.10" chalk "^4.1.2" - dmg-builder "25.1.6" + dmg-builder "25.1.8" fs-extra "^10.1.0" is-ci "^3.0.0" lazy-val "^1.0.5" @@ -4187,14 +4179,14 @@ electron-log@^5.2.0: resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.2.0.tgz#505716926dfcf9cb3e74f42b1003be6d865bcb88" integrity sha512-VjLkvaLmbP3AOGOh5Fob9M8bFU0mmeSAb5G2EoTBx+kQLf2XA/0byzjsVGBTHhikbT+m1AB27NEQUv9wX9nM8w== -electron-publish@25.1.6: - version "25.1.6" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-25.1.6.tgz#09cd851b3af6b5ca334ee4dd35581251efe0323e" - integrity sha512-Hiy/tVyu57cbkyxS0GrzEnxm7+B/9IeFcBTia2QQBCTg10zvHURdAj7Sk7XHKys8kKLr1tVNThuG165uTnNJdQ== +electron-publish@25.1.7: + version "25.1.7" + resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-25.1.7.tgz#14e50c2a3fafdc1c454eadbbc47ead89a48bb554" + integrity sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg== dependencies: "@types/fs-extra" "^9.0.11" - builder-util "25.1.6" - builder-util-runtime "9.2.9" + builder-util "25.1.7" + builder-util-runtime "9.2.10" chalk "^4.1.2" fs-extra "^10.1.0" lazy-val "^1.0.5" @@ -4205,12 +4197,12 @@ electron-to-chromium@^1.5.28: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.30.tgz#5b264b489cfe0c3dd71097c164d795444834e7c7" integrity sha512-sXI35EBN4lYxzc/pIGorlymYNzDBOqkSlVRe6MkgBsW/hW1tpC/HDJ2fjG7XnjakzfLEuvdmux0Mjs6jHq4UOA== -electron-updater@^6.3.4: - version "6.3.4" - resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.4.tgz#3934bc89875bb524c2cbbd11041114e97c0c2496" - integrity sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg== +electron-updater@^6.3.9: + version "6.3.9" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.9.tgz#e1e7f155624c58e6f3760f376c3a584028165ec4" + integrity sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw== dependencies: - builder-util-runtime "9.2.5" + builder-util-runtime "9.2.10" fs-extra "^10.1.0" js-yaml "^4.1.0" lazy-val "^1.0.5" @@ -4766,14 +4758,6 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4: node-domexception "^1.0.0" web-streams-polyfill "^3.0.3" -fetch-cookie@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-3.0.1.tgz#6a77f7495e1a639ae019db916a234db8c85d5963" - integrity sha512-ZGXe8Y5Z/1FWqQ9q/CrJhkUD73DyBU9VF0hBQmEO/wPHe4A9PKTjplFDLeFX8aOsYypZUcX5Ji/eByn3VCVO3Q== - dependencies: - set-cookie-parser "^2.4.8" - tough-cookie "^4.0.0" - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -6851,7 +6835,7 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-torrent@^11.0.16: +parse-torrent@^11.0.17: version "11.0.17" resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-11.0.17.tgz#60614845b28e24b869a60adce492d37c2b1a3133" integrity sha512-bkfEtrqIMT4+bSWs+m7+Ktd7LSJsDefA9qfJQ3UFwOeBqipiQ+347guu79zX++nRwMnrdvRecLmgaRcdiYjE4w== @@ -7600,11 +7584,6 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -set-cookie-parser@^2.4.8: - version "2.7.0" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz#ef5552b56dc01baae102acb5fc9fb8cd060c30f9" - integrity sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ== - set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -8129,7 +8108,7 @@ toposort@^2.0.2: resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== -tough-cookie@^4.0.0, tough-cookie@^4.1.4: +tough-cookie@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== From 958a7d037fbc779ebe98c66de863d6a81b59a7db Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:36:24 -0300 Subject: [PATCH 114/163] feat: add profileImage to achievements page --- src/renderer/src/helpers.ts | 3 +- .../achievement/achievements-content.tsx | 98 ++++++++++++------- .../src/pages/achievement/achievements.css.ts | 12 +++ .../src/pages/achievement/achievements.tsx | 2 + .../profile-content/profile-content.tsx | 6 +- 5 files changed, 86 insertions(+), 35 deletions(-) diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index 2eb83df6..d4563330 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -36,7 +36,7 @@ export const buildGameDetailsPath = ( export const buildGameAchievementPath = ( game: { shop: GameShop; objectId: string; title: string }, - user?: { userId: string; displayName: string } + user?: { userId: string; displayName: string; profileImageUrl: string | null } ) => { const searchParams = new URLSearchParams({ title: game.title, @@ -44,6 +44,7 @@ export const buildGameAchievementPath = ( objectId: game.objectId, userId: user?.userId || "", displayName: user?.displayName || "", + profileImageUrl: user?.profileImageUrl || "", }); return `/achievements/?${searchParams.toString()}`; diff --git a/src/renderer/src/pages/achievement/achievements-content.tsx b/src/renderer/src/pages/achievement/achievements-content.tsx index 5e0af51f..1a3fbd14 100644 --- a/src/renderer/src/pages/achievement/achievements-content.tsx +++ b/src/renderer/src/pages/achievement/achievements-content.tsx @@ -1,11 +1,11 @@ import { setHeaderTitle } from "@renderer/features"; -import { useAppDispatch, useDate } from "@renderer/hooks"; +import { useAppDispatch, useDate, useUserDetails } from "@renderer/hooks"; import { steamUrlBuilder } from "@shared"; import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./achievements.css"; import { formatDownloadProgress } from "@renderer/helpers"; -import { TrophyIcon } from "@primer/octicons-react"; +import { PersonIcon, TrophyIcon } from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; import { UserAchievement } from "@types"; @@ -14,12 +14,15 @@ import Color from "color"; const HERO_ANIMATION_THRESHOLD = 25; +interface UserInfo { + userId: string; + displayName: string; + achievements: UserAchievement[]; + profileImageUrl: string; +} + interface AchievementsContentProps { - otherUser: { - userId: string; - displayName: string; - achievements: UserAchievement[]; - } | null; + otherUser: UserInfo | null; } interface AchievementListProps { @@ -30,11 +33,13 @@ interface AchievementListProps { interface AchievementPanelProps { achievements: UserAchievement[]; displayName: string | null; + profileImageUrl: string | null; } function AchievementPanel({ achievements, displayName, + profileImageUrl, }: AchievementPanelProps) { const { t } = useTranslation("achievement"); @@ -42,56 +47,81 @@ function AchievementPanel({ (achievement) => achievement.unlocked ).length; + const { userDetails } = useUserDetails(); + + const getProfileImage = () => { + const imageUrl = profileImageUrl || userDetails?.profileImageUrl; + return ( +
      + {imageUrl ? ( + {"teste"} + ) : ( + + )} +
      + ); + }; + const totalAchievementCount = achievements.length; return (
      -

      - {displayName - ? t("user_achievements", { - displayName, - }) - : t("your_achievements")} -

      + {getProfileImage()}
      +

      + {displayName + ? t("user_achievements", { + displayName, + }) + : t("your_achievements")} +

      - +
      + + + {unlockedAchievementCount} / {totalAchievementCount} + +
      + - {unlockedAchievementCount} / {totalAchievementCount} + {formatDownloadProgress( + unlockedAchievementCount / totalAchievementCount + )}
      - - - {formatDownloadProgress( - unlockedAchievementCount / totalAchievementCount - )} - +
      -
      ); } @@ -311,12 +341,14 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { )}
      Date: Tue, 15 Oct 2024 16:16:38 -0300 Subject: [PATCH 115/163] feat: refactor --- .../achievement/achievements-content.tsx | 249 +++++++++++++----- 1 file changed, 181 insertions(+), 68 deletions(-) diff --git a/src/renderer/src/pages/achievement/achievements-content.tsx b/src/renderer/src/pages/achievement/achievements-content.tsx index 1a3fbd14..6b2bd1e1 100644 --- a/src/renderer/src/pages/achievement/achievements-content.tsx +++ b/src/renderer/src/pages/achievement/achievements-content.tsx @@ -18,7 +18,7 @@ interface UserInfo { userId: string; displayName: string; achievements: UserAchievement[]; - profileImageUrl: string; + profileImageUrl: string | null; } interface AchievementsContentProps { @@ -31,26 +31,14 @@ interface AchievementListProps { } interface AchievementPanelProps { - achievements: UserAchievement[]; - displayName: string | null; - profileImageUrl: string | null; + user: UserInfo; + otherUser: UserInfo | null; } -function AchievementPanel({ - achievements, - displayName, - profileImageUrl, -}: AchievementPanelProps) { +function AchievementPanel({ user, otherUser }: AchievementPanelProps) { const { t } = useTranslation("achievement"); - const unlockedAchievementCount = achievements.filter( - (achievement) => achievement.unlocked - ).length; - - const { userDetails } = useUserDetails(); - - const getProfileImage = () => { - const imageUrl = profileImageUrl || userDetails?.profileImageUrl; + const getProfileImage = (imageUrl: string | null | undefined) => { return (
      {imageUrl ? ( @@ -62,67 +50,195 @@ function AchievementPanel({ ); }; - const totalAchievementCount = achievements.length; - return ( -
      - {getProfileImage()} + const userTotalAchievementCount = user.achievements.length; + const userUnlockedAchievementCount = user.achievements.filter( + (achievement) => achievement.unlocked + ).length; + + if (!otherUser) { + return (
      -

      - {displayName - ? t("user_achievements", { - displayName, - }) - : t("your_achievements")} -

      + {getProfileImage(user.profileImageUrl)}
      +

      + {t("your_achievements")} +

      - +
      + + + {userUnlockedAchievementCount} / {userTotalAchievementCount} + +
      + - {unlockedAchievementCount} / {totalAchievementCount} + {formatDownloadProgress( + userUnlockedAchievementCount / userTotalAchievementCount + )}
      - - - {formatDownloadProgress( - unlockedAchievementCount / totalAchievementCount - )} - +
      -
      -
      + ); + } + + const otherUserUnlockedAchievementCount = otherUser.achievements.filter( + (achievement) => achievement.unlocked + ).length; + const otherUserTotalAchievementCount = otherUser.achievements.length; + + return ( + <> +
      + {getProfileImage(otherUser.profileImageUrl)} +
      +

      + {t("user_achievements", { + displayName: otherUser.displayName, + })} +

      +
      +
      + + + {otherUserUnlockedAchievementCount} /{" "} + {otherUserTotalAchievementCount} + +
      + + + {formatDownloadProgress( + otherUserUnlockedAchievementCount / + otherUserTotalAchievementCount + )} + +
      + +
      +
      +
      + {getProfileImage(user.profileImageUrl)} +
      +

      + {t("your_achievements")} +

      +
      +
      + + + {userUnlockedAchievementCount} / {userTotalAchievementCount} + +
      + + + {formatDownloadProgress( + userUnlockedAchievementCount / userTotalAchievementCount + )} + +
      + +
      +
      + ); } @@ -258,6 +374,8 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { const dispatch = useAppDispatch(); + const { userDetails } = useUserDetails(); + useEffect(() => { if (gameTitle) { dispatch(setHeaderTitle(gameTitle)); @@ -297,7 +415,7 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { setBackdropOpacity(opacity); }; - if (!objectId || !shop || !gameTitle) return null; + if (!objectId || !shop || !gameTitle || !userDetails) return null; return (
      @@ -337,18 +455,13 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
      - {otherUser && ( - - )} -
      Date: Tue, 15 Oct 2024 22:40:47 -0300 Subject: [PATCH 116/163] feat: create UserSubscription --- src/main/data-source.ts | 4 +- src/main/entity/index.ts | 3 +- .../{user-auth.ts => user-auth.entity.ts} | 5 +++ src/main/entity/user-subscription.entity.ts | 42 +++++++++++++++++++ src/main/events/profile/get-me.ts | 39 +++++++++++++++-- src/main/knex-client.ts | 2 + ...20241015235142_create_user_subscription.ts | 27 ++++++++++++ src/main/migrations/migration.stub | 4 +- src/main/repository.ts | 4 ++ .../achievements/achievement-watcher.ts | 4 +- .../achievements/get-game-achievement-data.ts | 3 +- .../achievements/merge-achievements.ts | 14 ++++--- src/renderer/src/hooks/use-user-details.ts | 1 + src/types/index.ts | 10 +++++ yarn.lock | 2 +- 15 files changed, 147 insertions(+), 17 deletions(-) rename src/main/entity/{user-auth.ts => user-auth.entity.ts} (80%) create mode 100644 src/main/entity/user-subscription.entity.ts create mode 100644 src/main/migrations/20241015235142_create_user_subscription.ts diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 9745abd8..80a40f47 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -8,6 +8,7 @@ import { UserPreferences, UserAuth, GameAchievement, + UserSubscription, } from "@main/entity"; import { databasePath } from "./constants"; @@ -17,11 +18,12 @@ export const dataSource = new DataSource({ entities: [ Game, Repack, + UserAuth, UserPreferences, + UserSubscription, GameShopCache, DownloadSource, DownloadQueue, - UserAuth, GameAchievement, ], synchronize: false, diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index 7e52577c..5829e6a2 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,9 +1,10 @@ export * from "./game.entity"; export * from "./repack.entity"; +export * from "./user-auth.entity"; export * from "./user-preferences.entity"; +export * from "./user-subscription.entity"; export * from "./game-shop-cache.entity"; export * from "./game.entity"; export * from "./game-achievements.entity"; export * from "./download-source.entity"; export * from "./download-queue.entity"; -export * from "./user-auth"; diff --git a/src/main/entity/user-auth.ts b/src/main/entity/user-auth.entity.ts similarity index 80% rename from src/main/entity/user-auth.ts rename to src/main/entity/user-auth.entity.ts index 6bfd6ad7..f63b19d0 100644 --- a/src/main/entity/user-auth.ts +++ b/src/main/entity/user-auth.entity.ts @@ -4,7 +4,9 @@ import { Column, CreateDateColumn, UpdateDateColumn, + OneToOne, } from "typeorm"; +import { UserSubscription } from "./user-subscription.entity"; @Entity("user_auth") export class UserAuth { @@ -29,6 +31,9 @@ export class UserAuth { @Column("int", { default: 0 }) tokenExpirationTimestamp: number; + @OneToOne("UserSubscription", "user") + subscription: UserSubscription | null; + @CreateDateColumn() createdAt: Date; diff --git a/src/main/entity/user-subscription.entity.ts b/src/main/entity/user-subscription.entity.ts new file mode 100644 index 00000000..e74ada48 --- /dev/null +++ b/src/main/entity/user-subscription.entity.ts @@ -0,0 +1,42 @@ +import type { SubscriptionStatus } from "@types"; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToOne, + JoinColumn, +} from "typeorm"; +import { UserAuth } from "./user-auth.entity"; + +@Entity("user_subscription") +export class UserSubscription { + @PrimaryGeneratedColumn() + id: number; + + @Column("text", { default: "" }) + subscriptionId: string; + + @OneToOne("UserAuth", "subscription") + @JoinColumn() + user: UserAuth; + + @Column("text", { default: "" }) + status: SubscriptionStatus; + + @Column("text", { default: "" }) + planId: string; + + @Column("text", { default: "" }) + planName: string; + + @Column("datetime", { nullable: true }) + expiresAt: Date | null; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts index 1eeecbf9..81c74f22 100644 --- a/src/main/events/profile/get-me.ts +++ b/src/main/events/profile/get-me.ts @@ -1,15 +1,18 @@ import { registerEvent } from "../register-event"; import * as Sentry from "@sentry/electron/main"; -import { HydraApi } from "@main/services"; +import { HydraApi, logger } from "@main/services"; import { ProfileVisibility, UserDetails } from "@types"; -import { userAuthRepository } from "@main/repository"; +import { + userAuthRepository, + userSubscriptionRepository, +} from "@main/repository"; import { UserNotLoggedInError } from "@shared"; const getMe = async ( _event: Electron.IpcMainInvokeEvent ): Promise => { return HydraApi.get(`/profile/me`) - .then(async (me) => { + .then((me) => { userAuthRepository.upsert( { id: 1, @@ -20,6 +23,23 @@ const getMe = async ( ["id"] ); + if (me.subscription) { + userSubscriptionRepository.upsert( + { + id: 1, + subscriptionId: me.subscription?.id || "", + status: me.subscription?.status || "", + planId: me.subscription?.plan.id || "", + planName: me.subscription?.plan.name || "", + expiresAt: me.subscription?.expiresAt || null, + user: { id: 1 }, + }, + ["id"] + ); + } else { + userSubscriptionRepository.delete({ id: 1 }); + } + Sentry.setUser({ id: me.id, username: me.username }); return me; @@ -28,7 +48,7 @@ const getMe = async ( if (err instanceof UserNotLoggedInError) { return null; } - + logger.error("Failed to get logged user", err); const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } }); if (loggedUser) { @@ -38,6 +58,17 @@ const getMe = async ( username: "", bio: "", profileVisibility: "PUBLIC" as ProfileVisibility, + subscription: loggedUser.subscription + ? { + id: loggedUser.subscription.subscriptionId, + status: loggedUser.subscription.status, + plan: { + id: loggedUser.subscription.planId, + name: loggedUser.subscription.planName, + }, + expiresAt: loggedUser.subscription.expiresAt, + } + : null, }; } diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts index c289eebe..c9be5437 100644 --- a/src/main/knex-client.ts +++ b/src/main/knex-client.ts @@ -8,6 +8,7 @@ import { app } from "electron"; import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns"; import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement"; import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference"; +import { CreateUserSubscription } from "./migrations/20241015235142_create_user_subscription"; export type HydraMigration = Knex.Migration & { name: string }; @@ -21,6 +22,7 @@ class MigrationSource implements Knex.MigrationSource { FixMissingColumns, CreateGameAchievement, AddAchievementNotificationPreference, + CreateUserSubscription, ]); } getMigrationName(migration: HydraMigration): string { diff --git a/src/main/migrations/20241015235142_create_user_subscription.ts b/src/main/migrations/20241015235142_create_user_subscription.ts new file mode 100644 index 00000000..5f9ecab1 --- /dev/null +++ b/src/main/migrations/20241015235142_create_user_subscription.ts @@ -0,0 +1,27 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const CreateUserSubscription: HydraMigration = { + name: "CreateUserSubscription", + up: async (knex: Knex) => { + return knex.schema.createTable("user_subscription", (table) => { + table.increments("id").primary(); + table.string("subscriptionId").defaultTo(""); + table + .text("userId") + .notNullable() + .references("user_auth.id") + .onDelete("CASCADE"); + table.string("status").defaultTo(""); + table.string("planId").defaultTo(""); + table.string("planName").defaultTo(""); + table.dateTime("expiresAt").nullable(); + table.dateTime("createdAt").defaultTo(knex.fn.now()); + table.dateTime("updatedAt").defaultTo(knex.fn.now()); + }); + }, + + down: async (knex: Knex) => { + return knex.schema.dropTable("user_subscription"); + }, +}; diff --git a/src/main/migrations/migration.stub b/src/main/migrations/migration.stub index 9cb0cbab..299b3fc2 100644 --- a/src/main/migrations/migration.stub +++ b/src/main/migrations/migration.stub @@ -3,8 +3,8 @@ import type { Knex } from "knex"; export const MigrationName: HydraMigration = { name: "MigrationName", - up: async (knex: Knex) => { - await knex.schema.createTable("table_name", (table) => {}); + up: (knex: Knex) => { + return knex.schema.createTable("table_name", async (table) => {}); }, down: async (knex: Knex) => {}, diff --git a/src/main/repository.ts b/src/main/repository.ts index 4e5c115f..cf3ab143 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -8,6 +8,7 @@ import { UserPreferences, UserAuth, GameAchievement, + UserSubscription, } from "@main/entity"; export const gameRepository = dataSource.getRepository(Game); @@ -26,5 +27,8 @@ export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); export const userAuthRepository = dataSource.getRepository(UserAuth); +export const userSubscriptionRepository = + dataSource.getRepository(UserSubscription); + export const gameAchievementRepository = dataSource.getRepository(GameAchievement); diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index 166e6cff..ac078468 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -113,8 +113,8 @@ const compareFile = async (game: Game, file: AchievementFile) => { logger.log( "Detected change in file", file.filePath, - currentStat.mtimeMs, - fileStats.get(file.filePath) + previousStat, + currentStat.mtimeMs ); await processAchievementFileDiff(game, file); } catch (err) { diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index b94cbe7f..443af59a 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -5,6 +5,7 @@ import { import { HydraApi } from "../hydra-api"; import { AchievementData } from "@types"; import { UserNotLoggedInError } from "@shared"; +import { logger } from "../logger"; export const getGameAchievementData = async ( objectId: string, @@ -35,7 +36,7 @@ export const getGameAchievementData = async ( if (err instanceof UserNotLoggedInError) { throw err; } - + logger.error("Failed to get game achievements", err); return gameAchievementRepository .findOne({ where: { objectId, shop }, diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index b7c50d37..ae2ba3ed 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -22,11 +22,15 @@ const saveAchievementsOnLocal = async ( }, ["objectId", "shop"] ) - .then(async () => { - WindowManager.mainWindow?.webContents.send( - `on-update-achievements-${objectId}-${shop}`, - await getGameAchievements(objectId, shop as GameShop) - ); + .then(() => { + return getGameAchievements(objectId, shop as GameShop) + .then((achievements) => { + WindowManager.mainWindow?.webContents.send( + `on-update-achievements-${objectId}-${shop}`, + achievements + ); + }) + .catch(() => {}); }); }; diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 7e08144d..50689aeb 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -67,6 +67,7 @@ export function useUserDetails() { return updateUserDetails({ ...response, username: userDetails?.username || "", + subscription: userDetails?.subscription || null, }); }, [updateUserDetails, userDetails?.username] diff --git a/src/types/index.ts b/src/types/index.ts index 987e9f32..5408cbc3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -230,6 +230,15 @@ export interface UserProfileCurrentGame extends Omit { export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS"; +export type SubscriptionStatus = "active" | "pending" | "cancelled"; + +export interface Subscription { + id: string; + status: SubscriptionStatus; + plan: { id: string; name: string }; + expiresAt: Date | null; +} + export interface UserDetails { id: string; username: string; @@ -237,6 +246,7 @@ export interface UserDetails { profileImageUrl: string | null; profileVisibility: ProfileVisibility; bio: string; + subscription: Subscription | null; } export interface UserProfile { diff --git a/yarn.lock b/yarn.lock index d98e97dd..51391923 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5347,7 +5347,7 @@ i18next@^23.11.2: dependencies: "@babel/runtime" "^7.23.2" -icojs@^0.19.3: +icojs@^0.19.4: version "0.19.4" resolved "https://registry.yarnpkg.com/icojs/-/icojs-0.19.4.tgz#fdbc9e61a0945ed1d331beb358d67f72cf7d78dc" integrity sha512-86oNepPk2jAmbb96BPeucZI7HoSBobFlXDhhjIbwRb3wkQpvdBO5HO9KtMUNzMFT3qqQZsjLsfW+L0/9Rl9VqA== From 5c4ddd9b7abe33991884c67c2a89f3f7e5801889 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Wed, 16 Oct 2024 01:08:10 -0300 Subject: [PATCH 117/163] feat: profile active subscription --- .../profile-content/profile-content.tsx | 76 ++++++++++--------- src/types/index.ts | 1 + 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 3509584e..99e09889 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -17,6 +17,7 @@ import { RecentGamesBox } from "./recent-games-box"; import { UserGame } from "@types"; import { buildGameAchievementPath, + buildGameDetailsPath, formatDownloadProgress, } from "@renderer/helpers"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; @@ -45,11 +46,12 @@ export function ProfileContent() { }, [userProfile]); const buildUserGameDetailsPath = (game: UserGame) => { - // TODO: check if user has hydra cloud - // buildGameDetailsPath({ - // ...game, - // objectId: game.objectId, - // }); + if (!userProfile?.hasActiveSubscription) { + return buildGameDetailsPath({ + ...game, + objectId: game.objectId, + }); + } const userParams = userProfile ? { @@ -172,53 +174,55 @@ export function ProfileContent() { {formatPlayTime(game.playTimeInSeconds)} -
      + {userProfile.hasActiveSubscription && (
      - +
      + + + {game.unlockedAchievementCount} /{" "} + {game.achievementCount} + +
      + - {game.unlockedAchievementCount} /{" "} - {game.achievementCount} + {formatDownloadProgress( + game.unlockedAchievementCount / + game.achievementCount + )}
      - - {formatDownloadProgress( + + game.achievementCount + } + className={styles.achievementsProgressBar} + />
      - - -
      + )}
      Date: Wed, 16 Oct 2024 01:36:36 -0300 Subject: [PATCH 118/163] feat: WIP validate cloud subscription --- .../achievements/merge-achievements.ts | 12 ++-- src/main/services/hydra-api.ts | 67 ++++++++++++------- src/shared/index.ts | 7 ++ 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index ae2ba3ed..c97e8f56 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -115,10 +115,14 @@ export const mergeAchievements = async ( const mergedLocalAchievements = unlockedAchievements.concat(newAchievements); if (game?.remoteId) { - return HydraApi.put("/profile/games/achievements", { - id: game.remoteId, - achievements: mergedLocalAchievements, - }) + return HydraApi.put( + "/profile/games/achievements", + { + id: game.remoteId, + achievements: mergedLocalAchievements, + }, + { needsCloud: true } + ) .then((response) => { return saveAchievementsOnLocal( response.objectId, diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index d62bafcb..c8f3795e 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -1,16 +1,23 @@ -import { userAuthRepository } from "@main/repository"; +import { + userAuthRepository, + userSubscriptionRepository, +} from "@main/repository"; import axios, { AxiosError, AxiosInstance } from "axios"; import { WindowManager } from "./window-manager"; import url from "url"; import { uploadGamesBatch } from "./library-sync"; import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id"; import { logger } from "./logger"; -import { UserNotLoggedInError } from "@shared"; +import { + UserNotLoggedInError, + UserWithoutCloudSubscriptionError, +} from "@shared"; import { omit } from "lodash-es"; import { appVersion } from "@main/constants"; interface HydraApiOptions { - needsAuth: boolean; + needsAuth?: boolean; + needsCloud?: boolean; } export class HydraApi { @@ -31,6 +38,19 @@ export class HydraApi { return this.userAuth.authToken !== ""; } + private static async hasCloudSubscription() { + // TODO change this later, this is just a quick test + return userSubscriptionRepository + .findOne({ where: { id: 1 } }) + .then((userSubscription) => { + if (userSubscription?.status !== "active") return false; + return ( + !userSubscription.expiresAt || + userSubscription!.expiresAt > new Date() + ); + }); + } + static async handleExternalAuth(uri: string) { const { payload } = url.parse(uri, true).query; @@ -234,15 +254,28 @@ export class HydraApi { throw err; }; + private static async validateOptions(options?: HydraApiOptions) { + const needsAuth = options?.needsAuth == undefined || options.needsAuth; + const needsCloud = options?.needsCloud === true; + + if (needsAuth) { + if (!this.isLoggedIn()) throw new UserNotLoggedInError(); + await this.revalidateAccessTokenIfExpired(); + } + + if (needsCloud) { + if (!(await this.hasCloudSubscription())) { + throw new UserWithoutCloudSubscriptionError(); + } + } + } + static async get( url: string, params?: any, options?: HydraApiOptions ) { - if (!options || options.needsAuth) { - if (!this.isLoggedIn()) throw new UserNotLoggedInError(); - await this.revalidateAccessTokenIfExpired(); - } + await this.validateOptions(options); return this.instance .get(url, { params, ...this.getAxiosConfig() }) @@ -255,10 +288,7 @@ export class HydraApi { data?: any, options?: HydraApiOptions ) { - if (!options || options.needsAuth) { - if (!this.isLoggedIn()) throw new UserNotLoggedInError(); - await this.revalidateAccessTokenIfExpired(); - } + await this.validateOptions(options); return this.instance .post(url, data, this.getAxiosConfig()) @@ -271,10 +301,7 @@ export class HydraApi { data?: any, options?: HydraApiOptions ) { - if (!options || options.needsAuth) { - if (!this.isLoggedIn()) throw new UserNotLoggedInError(); - await this.revalidateAccessTokenIfExpired(); - } + await this.validateOptions(options); return this.instance .put(url, data, this.getAxiosConfig()) @@ -287,10 +314,7 @@ export class HydraApi { data?: any, options?: HydraApiOptions ) { - if (!options || options.needsAuth) { - if (!this.isLoggedIn()) throw new UserNotLoggedInError(); - await this.revalidateAccessTokenIfExpired(); - } + await this.validateOptions(options); return this.instance .patch(url, data, this.getAxiosConfig()) @@ -299,10 +323,7 @@ export class HydraApi { } static async delete(url: string, options?: HydraApiOptions) { - if (!options || options.needsAuth) { - if (!this.isLoggedIn()) throw new UserNotLoggedInError(); - await this.revalidateAccessTokenIfExpired(); - } + await this.validateOptions(options); return this.instance .delete(url, this.getAxiosConfig()) diff --git a/src/shared/index.ts b/src/shared/index.ts index 5d216183..c2a98c8a 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -10,6 +10,13 @@ export class UserNotLoggedInError extends Error { } } +export class UserWithoutCloudSubscriptionError extends Error { + constructor() { + super("user does not have hydra cloud subscription"); + this.name = "UserWithoutCloudSubscriptionError"; + } +} + const FORMAT = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; export const formatBytes = (bytes: number): string => { From 05625e75948bcfe998345af279bf925457e1ae21 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 16 Oct 2024 10:46:17 +0100 Subject: [PATCH 119/163] feat: enabling gif upload --- .../check-game-cloud-sync-support.ts | 14 --- src/main/events/index.ts | 1 - src/main/events/profile/update-profile.ts | 67 +++++++---- src/preload/index.ts | 2 - .../src/components/avatar/avatar.css.ts | 23 ++++ src/renderer/src/components/avatar/avatar.tsx | 32 ++++++ .../src/components/game-card/game-card.tsx | 7 +- src/renderer/src/components/hero/hero.tsx | 7 +- src/renderer/src/components/index.ts | 2 + .../components/sidebar/sidebar-profile.css.ts | 13 --- .../components/sidebar/sidebar-profile.tsx | 19 ++-- .../src/components/sidebar/sidebar.tsx | 1 + .../suspense-wrapper/suspense-wrapper.tsx | 13 +++ .../context/cloud-sync/cloud-sync.context.tsx | 63 ++++------- .../user-profile/user-profile.context.tsx | 20 +++- src/renderer/src/declaration.d.ts | 4 - src/renderer/src/main.tsx | 52 ++++++--- .../src/pages/catalogue/catalogue.tsx | 2 +- .../src/pages/downloads/downloads.tsx | 2 +- .../game-details/game-details-content.tsx | 79 ++++++++----- .../src/pages/game-details/game-details.tsx | 2 +- src/renderer/src/pages/home/home.tsx | 2 +- .../src/pages/home/search-results.tsx | 2 +- src/renderer/src/pages/index.ts | 7 -- .../edit-profile-modal.css.ts | 16 +-- .../edit-profile-modal/edit-profile-modal.tsx | 31 +++-- .../profile/profile-content/friends-box.tsx | 19 +--- .../profile/profile-hero/profile-hero.css.ts | 38 +------ .../profile/profile-hero/profile-hero.tsx | 107 ++++++------------ src/renderer/src/pages/profile/profile.tsx | 2 +- .../upload-background-image-button.css.ts | 12 ++ .../upload-background-image-button.tsx | 58 ++++++++++ src/renderer/src/pages/settings/settings.tsx | 2 +- .../user-friend-modal/user-friend-item.tsx | 32 +----- .../user-friend-modal.css.ts | 20 ---- src/types/index.ts | 3 + 36 files changed, 403 insertions(+), 373 deletions(-) delete mode 100644 src/main/events/cloud-save/check-game-cloud-sync-support.ts create mode 100644 src/renderer/src/components/avatar/avatar.css.ts create mode 100644 src/renderer/src/components/avatar/avatar.tsx create mode 100644 src/renderer/src/components/suspense-wrapper/suspense-wrapper.tsx delete mode 100644 src/renderer/src/pages/index.ts create mode 100644 src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.css.ts create mode 100644 src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx diff --git a/src/main/events/cloud-save/check-game-cloud-sync-support.ts b/src/main/events/cloud-save/check-game-cloud-sync-support.ts deleted file mode 100644 index 4054d430..00000000 --- a/src/main/events/cloud-save/check-game-cloud-sync-support.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { registerEvent } from "../register-event"; -import { GameShop } from "@types"; -import { Ludusavi } from "@main/services"; - -const checkGameCloudSyncSupport = async ( - _event: Electron.IpcMainInvokeEvent, - objectId: string, - shop: GameShop -) => { - const games = await Ludusavi.findGames(shop, objectId); - return games.length === 1; -}; - -registerEvent("checkGameCloudSyncSupport", checkGameCloudSyncSupport); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 37e28447..1b621a30 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -61,7 +61,6 @@ import "./cloud-save/download-game-artifact"; import "./cloud-save/get-game-artifacts"; import "./cloud-save/get-game-backup-preview"; import "./cloud-save/upload-save-game"; -import "./cloud-save/check-game-cloud-sync-support"; import "./cloud-save/delete-game-artifact"; import "./notifications/publish-new-repacks-notification"; import { isPortableVersion } from "@main/helpers"; diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts index eb80bc47..7b90e483 100644 --- a/src/main/events/profile/update-profile.ts +++ b/src/main/events/profile/update-profile.ts @@ -1,56 +1,75 @@ import { registerEvent } from "../register-event"; -import { HydraApi, PythonInstance } from "@main/services"; +import { HydraApi } from "@main/services"; import fs from "node:fs"; import path from "node:path"; import type { UpdateProfileRequest, UserProfile } from "@types"; import { omit } from "lodash-es"; import axios from "axios"; - -interface PresignedResponse { - presignedUrl: string; - profileImageUrl: string; -} +import { fileTypeFromFile } from "file-type"; const patchUserProfile = async (updateProfile: UpdateProfileRequest) => { return HydraApi.patch("/profile", updateProfile); }; -const getNewProfileImageUrl = async (localImageUrl: string) => { - const { imagePath, mimeType } = - await PythonInstance.processProfileImage(localImageUrl); - - const stats = fs.statSync(imagePath); +const uploadImage = async ( + type: "profile-image" | "background-image", + imagePath: string +) => { + const stat = fs.statSync(imagePath); const fileBuffer = fs.readFileSync(imagePath); - const fileSizeInBytes = stats.size; + const fileSizeInBytes = stat.size; - const { presignedUrl, profileImageUrl } = - await HydraApi.post(`/presigned-urls/profile-image`, { + const response = await HydraApi.post<{ presignedUrl: string }>( + `/presigned-urls/${type}`, + { imageExt: path.extname(imagePath).slice(1), imageLength: fileSizeInBytes, - }); + } + ); - await axios.put(presignedUrl, fileBuffer, { + const mimeType = await fileTypeFromFile(imagePath); + + await axios.put(response.presignedUrl, fileBuffer, { headers: { - "Content-Type": mimeType, + "Content-Type": mimeType?.mime, }, }); - return profileImageUrl; + if (type === "background-image") { + return response["backgroundImageUrl"]; + } + + return response["profileImageUrl"]; }; const updateProfile = async ( _event: Electron.IpcMainInvokeEvent, updateProfile: UpdateProfileRequest ) => { - if (!updateProfile.profileImageUrl) { - return patchUserProfile(omit(updateProfile, "profileImageUrl")); + const payload = omit(updateProfile, [ + "profileImageUrl", + "backgroundImageUrl", + ]); + + if (updateProfile.profileImageUrl) { + const profileImageUrl = await uploadImage( + "profile-image", + updateProfile.profileImageUrl + ).catch(() => undefined); + + payload["profileImageUrl"] = profileImageUrl; } - const profileImageUrl = await getNewProfileImageUrl( - updateProfile.profileImageUrl - ).catch(() => undefined); + if (updateProfile.backgroundImageUrl) { + const backgroundImageUrl = await uploadImage( + "background-image", + updateProfile.backgroundImageUrl + ).catch(() => undefined); - return patchUserProfile({ ...updateProfile, profileImageUrl }); + payload["backgroundImageUrl"] = backgroundImageUrl; + } + + return patchUserProfile(payload); }; registerEvent("updateProfile", updateProfile); diff --git a/src/preload/index.ts b/src/preload/index.ts index 40a6c101..11086199 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -160,8 +160,6 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("getGameArtifacts", objectId, shop), getGameBackupPreview: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameBackupPreview", objectId, shop), - checkGameCloudSyncSupport: (objectId: string, shop: GameShop) => - ipcRenderer.invoke("checkGameCloudSyncSupport", objectId, shop), deleteGameArtifact: (gameArtifactId: string) => ipcRenderer.invoke("deleteGameArtifact", gameArtifactId), onUploadComplete: (objectId: string, shop: GameShop, cb: () => void) => { diff --git a/src/renderer/src/components/avatar/avatar.css.ts b/src/renderer/src/components/avatar/avatar.css.ts new file mode 100644 index 00000000..34249860 --- /dev/null +++ b/src/renderer/src/components/avatar/avatar.css.ts @@ -0,0 +1,23 @@ +import { style } from "@vanilla-extract/css"; + +import { vars } from "../../theme.css"; + +export const profileAvatar = style({ + borderRadius: "4px", + display: "flex", + justifyContent: "center", + alignItems: "center", + backgroundColor: vars.color.background, + border: `solid 1px ${vars.color.border}`, + cursor: "pointer", + color: vars.color.muted, + position: "relative", +}); + +export const profileAvatarImage = style({ + height: "100%", + width: "100%", + objectFit: "cover", + overflow: "hidden", + borderRadius: "4px", +}); diff --git a/src/renderer/src/components/avatar/avatar.tsx b/src/renderer/src/components/avatar/avatar.tsx new file mode 100644 index 00000000..1a355872 --- /dev/null +++ b/src/renderer/src/components/avatar/avatar.tsx @@ -0,0 +1,32 @@ +import { PersonIcon } from "@primer/octicons-react"; + +import * as styles from "./avatar.css"; + +export interface AvatarProps + extends Omit< + React.DetailedHTMLProps< + React.ImgHTMLAttributes, + HTMLImageElement + >, + "src" + > { + size: number; + src?: string | null; +} + +export function Avatar({ size, alt, src, ...props }: AvatarProps) { + return ( +
      + {src ? ( + {alt} + ) : ( + + )} +
      + ); +} diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index c548be52..869cb2d6 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -60,7 +60,12 @@ export function GameCard({ game, ...props }: GameCardProps) { onMouseEnter={handleHover} >
      - {game.title} + {game.title}
      diff --git a/src/renderer/src/components/hero/hero.tsx b/src/renderer/src/components/hero/hero.tsx index 0e5a7849..9986a7d8 100644 --- a/src/renderer/src/components/hero/hero.tsx +++ b/src/renderer/src/components/hero/hero.tsx @@ -49,7 +49,12 @@ export function Hero() {
      {game.logo && ( - {game.description} + {game.description} )}

      {game.description}

      diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index 52124238..65d07440 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -1,3 +1,4 @@ +export * from "./avatar/avatar"; export * from "./bottom-panel/bottom-panel"; export * from "./button/button"; export * from "./game-card/game-card"; @@ -12,3 +13,4 @@ export * from "./select-field/select-field"; export * from "./toast/toast"; export * from "./badge/badge"; export * from "./confirmation-modal/confirmation-modal"; +export * from "./suspense-wrapper/suspense-wrapper"; diff --git a/src/renderer/src/components/sidebar/sidebar-profile.css.ts b/src/renderer/src/components/sidebar/sidebar-profile.css.ts index f8e0e969..bed9ac93 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.css.ts +++ b/src/renderer/src/components/sidebar/sidebar-profile.css.ts @@ -31,19 +31,6 @@ export const profileButtonContent = style({ width: "100%", }); -export const profileAvatar = style({ - width: "35px", - height: "35px", - borderRadius: "4px", - display: "flex", - justifyContent: "center", - alignItems: "center", - backgroundColor: vars.color.background, - border: `solid 1px ${vars.color.border}`, - position: "relative", - objectFit: "cover", -}); - export const profileButtonInformation = style({ display: "flex", flexDirection: "column", diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index b241ef0e..79acf414 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -1,11 +1,12 @@ import { useNavigate } from "react-router-dom"; -import { PeopleIcon, PersonIcon } from "@primer/octicons-react"; +import { PeopleIcon } from "@primer/octicons-react"; import * as styles from "./sidebar-profile.css"; import { useAppSelector, useUserDetails } from "@renderer/hooks"; import { useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; +import { Avatar } from "../avatar/avatar"; const LONG_POLLING_INTERVAL = 60_000; @@ -94,17 +95,11 @@ export function SidebarProfile() { onClick={handleProfileClick} >
      -
      - {userDetails?.profileImageUrl ? ( - {userDetails.displayName} - ) : ( - - )} -
      +

      diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index c509e489..206fdc81 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -225,6 +225,7 @@ export function Sidebar() { className={styles.gameIcon} src={game.iconUrl} alt={game.title} + loading="lazy" /> ) : ( diff --git a/src/renderer/src/components/suspense-wrapper/suspense-wrapper.tsx b/src/renderer/src/components/suspense-wrapper/suspense-wrapper.tsx new file mode 100644 index 00000000..d5888d33 --- /dev/null +++ b/src/renderer/src/components/suspense-wrapper/suspense-wrapper.tsx @@ -0,0 +1,13 @@ +import { Suspense } from "react"; + +export interface SuspenseWrapperProps { + Component: React.LazyExoticComponent<() => JSX.Element>; +} + +export function SuspenseWrapper({ Component }: SuspenseWrapperProps) { + return ( + + + + ); +} diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx index 086a8c94..5a0a66f0 100644 --- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -23,13 +23,13 @@ export interface CloudSyncContext { artifacts: GameArtifact[]; showCloudSyncModal: boolean; showCloudSyncFilesModal: boolean; - supportsCloudSync: boolean | null; backupState: CloudSyncState; setShowCloudSyncModal: React.Dispatch>; downloadGameArtifact: (gameArtifactId: string) => Promise; uploadSaveGame: () => Promise; deleteGameArtifact: (gameArtifactId: string) => Promise; setShowCloudSyncFilesModal: React.Dispatch>; + getGameBackupPreview: () => Promise; restoringBackup: boolean; uploadingBackup: boolean; } @@ -37,7 +37,6 @@ export interface CloudSyncContext { export const cloudSyncContext = createContext({ backupPreview: null, showCloudSyncModal: false, - supportsCloudSync: null, backupState: CloudSyncState.Unknown, setShowCloudSyncModal: () => {}, downloadGameArtifact: async () => {}, @@ -46,6 +45,7 @@ export const cloudSyncContext = createContext({ deleteGameArtifact: async () => {}, showCloudSyncFilesModal: false, setShowCloudSyncFilesModal: () => {}, + getGameBackupPreview: async () => {}, restoringBackup: false, uploadingBackup: false, }); @@ -66,9 +66,6 @@ export function CloudSyncContextProvider({ }: CloudSyncContextProviderProps) { const { t } = useTranslation("game_details"); - const [supportsCloudSync, setSupportsCloudSync] = useState( - null - ); const [artifacts, setArtifacts] = useState([]); const [showCloudSyncModal, setShowCloudSyncModal] = useState(false); const [backupPreview, setBackupPreview] = useState( @@ -89,21 +86,26 @@ export function CloudSyncContextProvider({ ); const getGameBackupPreview = useCallback(async () => { - window.electron.getGameArtifacts(objectId, shop).then((results) => { - setArtifacts(results); - }); - - window.electron - .getGameBackupPreview(objectId, shop) - .then((preview) => { - logger.info("Game backup preview", objectId, shop, preview); - if (preview && Object.keys(preview.games).length) { - setBackupPreview(preview); - } - }) - .catch((err) => { - logger.error("Failed to get game backup preview", objectId, shop, err); - }); + await Promise.allSettled([ + window.electron.getGameArtifacts(objectId, shop).then((results) => { + setArtifacts(results); + }), + window.electron + .getGameBackupPreview(objectId, shop) + .then((preview) => { + if (preview && Object.keys(preview.games).length) { + setBackupPreview(preview); + } + }) + .catch((err) => { + logger.error( + "Failed to get game backup preview", + objectId, + shop, + err + ); + }), + ]); }, [objectId, shop]); const uploadSaveGame = useCallback(async () => { @@ -152,33 +154,14 @@ export function CloudSyncContextProvider({ [getGameBackupPreview] ); - useEffect(() => { - window.electron - .checkGameCloudSyncSupport(objectId, shop) - .then((result) => { - logger.info("Cloud sync support", objectId, shop, result); - setSupportsCloudSync(result); - }) - .catch((err) => { - logger.error("Failed to check cloud sync support", err); - }); - }, [objectId, shop, getGameBackupPreview]); - useEffect(() => { setBackupPreview(null); setArtifacts([]); - setSupportsCloudSync(null); setShowCloudSyncModal(false); setRestoringBackup(false); setUploadingBackup(false); }, [objectId, shop]); - useEffect(() => { - if (showCloudSyncModal) { - getGameBackupPreview(); - } - }, [getGameBackupPreview, showCloudSyncModal]); - const backupState = useMemo(() => { if (!backupPreview) return CloudSyncState.Unknown; if (backupPreview.overall.changedGames.new) return CloudSyncState.New; @@ -192,7 +175,6 @@ export function CloudSyncContextProvider({ return ( {children} diff --git a/src/renderer/src/context/user-profile/user-profile.context.tsx b/src/renderer/src/context/user-profile/user-profile.context.tsx index 1eb0c47c..98a25a77 100644 --- a/src/renderer/src/context/user-profile/user-profile.context.tsx +++ b/src/renderer/src/context/user-profile/user-profile.context.tsx @@ -13,8 +13,9 @@ export interface UserProfileContext { /* Indicates if the current user is viewing their own profile */ isMe: boolean; userStats: UserStats | null; - getUserProfile: () => Promise; + setSelectedBackgroundImage: React.Dispatch>; + backgroundImage: string; } export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3"; @@ -25,6 +26,8 @@ export const userProfileContext = createContext({ isMe: false, userStats: null, getUserProfile: async () => {}, + setSelectedBackgroundImage: () => {}, + backgroundImage: "", }); const { Provider } = userProfileContext; @@ -47,6 +50,9 @@ export function UserProfileContextProvider({ const [heroBackground, setHeroBackground] = useState( DEFAULT_USER_PROFILE_BACKGROUND ); + const [selectedBackgroundImage, setSelectedBackgroundImage] = useState(""); + + const isMe = userDetails?.id === userProfile?.id; const getHeroBackgroundFromImageUrl = async (imageUrl: string) => { const output = await average(imageUrl, { @@ -57,6 +63,14 @@ export function UserProfileContextProvider({ return `linear-gradient(135deg, ${darkenColor(output as string, 0.5)}, ${darkenColor(output as string, 0.6, 0.5)})`; }; + const getBackgroundImageUrl = () => { + if (selectedBackgroundImage && isMe) + return `local:${selectedBackgroundImage}`; + if (userProfile?.backgroundImageUrl) return userProfile.backgroundImageUrl; + + return ""; + }; + const { t } = useTranslation("user_profile"); const { showErrorToast } = useToast(); @@ -99,8 +113,10 @@ export function UserProfileContextProvider({ value={{ userProfile, heroBackground, - isMe: userDetails?.id === userProfile?.id, + isMe, getUserProfile, + setSelectedBackgroundImage, + backgroundImage: getBackgroundImageUrl(), userStats, }} > diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 677c3ee2..c9d4e5e8 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -138,10 +138,6 @@ declare global { objectId: string, shop: GameShop ) => Promise; - checkGameCloudSyncSupport: ( - objectId: string, - shop: GameShop - ) => Promise; deleteGameArtifact: (gameArtifactId: string) => Promise<{ ok: boolean }>; onBackupDownloadComplete: ( objectId: string, diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 5f91b424..bc3522ae 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -15,15 +15,6 @@ import "@fontsource/noto-sans/700.css"; import "react-loading-skeleton/dist/skeleton.css"; import { App } from "./app"; -import { - Home, - Downloads, - GameDetails, - SearchResults, - Settings, - Catalogue, - Profile, -} from "@renderer/pages"; import { store } from "./store"; @@ -33,6 +24,17 @@ import { AchievementNotification } from "./pages/achievement/notification/achiev import "./workers"; import { RepacksContextProvider } from "./context"; import { Achievement } from "./pages/achievement/achievements"; +import { SuspenseWrapper } from "./components"; + +const Home = React.lazy(() => import("./pages/home/home")); +const GameDetails = React.lazy( + () => import("./pages/game-details/game-details") +); +const Downloads = React.lazy(() => import("./pages/downloads/downloads")); +const SearchResults = React.lazy(() => import("./pages/home/search-results")); +const Settings = React.lazy(() => import("./pages/settings/settings")); +const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue")); +const Profile = React.lazy(() => import("./pages/profile/profile")); Sentry.init({}); @@ -63,13 +65,31 @@ ReactDOM.createRoot(document.getElementById("root")!).render( }> - - - - - - - + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> { + const aboutTheGame = shopDetails?.about_the_game; + if (aboutTheGame) { + const document = new DOMParser().parseFromString( + aboutTheGame, + "text/html" + ); + + const $images = Array.from(document.querySelectorAll("img")); + $images.forEach(($image) => { + $image.loading = "lazy"; + }); + + return document.body.outerHTML; + } + + return t("no_shop_details"); + }, [shopDetails, t]); + const [backdropOpactiy, setBackdropOpacity] = useState(1); const handleHeroLoad = async () => { @@ -87,6 +106,10 @@ export function GameDetailsContent() { setShowCloudSyncModal(true); }; + useEffect(() => { + getGameBackupPreview(); + }, [getGameBackupPreview]); + return (

      {game?.title} - {supportsCloudSync && ( - - )} + +
      + {t("cloud_save")} +
      @@ -160,7 +181,7 @@ export function GameDetailsContent() {
      diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index f2b928b5..bab9452f 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -29,7 +29,7 @@ import { Downloader, getDownloadersForUri } from "@shared"; import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal"; import { CloudSyncFilesModal } from "./cloud-sync-files-modal/cloud-sync-files-modal"; -export function GameDetails() { +export default function GameDetails() { const [randomGame, setRandomGame] = useState(null); const [randomizerLocked, setRandomizerLocked] = useState(false); diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index e70a58fd..ad306726 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -16,7 +16,7 @@ import Lottie, { type LottieRefCurrentProps } from "lottie-react"; import { buildGameDetailsPath } from "@renderer/helpers"; import { CatalogueCategory } from "@shared"; -export function Home() { +export default function Home() { const { t } = useTranslation("home"); const navigate = useNavigate(); diff --git a/src/renderer/src/pages/home/search-results.tsx b/src/renderer/src/pages/home/search-results.tsx index 32c4ad89..d86a362a 100644 --- a/src/renderer/src/pages/home/search-results.tsx +++ b/src/renderer/src/pages/home/search-results.tsx @@ -17,7 +17,7 @@ import { buildGameDetailsPath } from "@renderer/helpers"; import { vars } from "@renderer/theme.css"; -export function SearchResults() { +export default function SearchResults() { const dispatch = useAppDispatch(); const { t } = useTranslation("home"); diff --git a/src/renderer/src/pages/index.ts b/src/renderer/src/pages/index.ts deleted file mode 100644 index 5dc22c2d..00000000 --- a/src/renderer/src/pages/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./home/home"; -export * from "./game-details/game-details"; -export * from "./downloads/downloads"; -export * from "./home/search-results"; -export * from "./settings/settings"; -export * from "./catalogue/catalogue"; -export * from "./profile/profile"; diff --git a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts index bd873a8a..b4232096 100644 --- a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts +++ b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts @@ -3,28 +3,18 @@ import { globalStyle, style } from "@vanilla-extract/css"; export const profileAvatarEditContainer = style({ alignSelf: "center", - width: "128px", - height: "128px", + // width: "132px", + // height: "132px", display: "flex", - borderRadius: "4px", + // borderRadius: "4px", color: vars.color.body, justifyContent: "center", alignItems: "center", backgroundColor: vars.color.background, position: "relative", - border: `solid 1px ${vars.color.border}`, - boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)", cursor: "pointer", }); -export const profileAvatar = style({ - height: "100%", - width: "100%", - objectFit: "cover", - borderRadius: "4px", - overflow: "hidden", -}); - export const profileAvatarEditOverlay = style({ position: "absolute", width: "100%", diff --git a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx index cd43641a..cad4ed9e 100644 --- a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx +++ b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx @@ -2,8 +2,9 @@ import { useContext, useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { Trans, useTranslation } from "react-i18next"; -import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react"; +import { DeviceCameraIcon } from "@primer/octicons-react"; import { + Avatar, Button, Link, Modal, @@ -111,14 +112,14 @@ export function EditProfileModal( if (filePaths && filePaths.length > 0) { const path = filePaths[0]; - const { imagePath } = await window.electron - .processProfileImage(path) - .catch(() => { - showErrorToast(t("image_process_failure")); - return { imagePath: null }; - }); + // const { imagePath } = await window.electron + // .processProfileImage(path) + // .catch(() => { + // showErrorToast(t("image_process_failure")); + // return { imagePath: null }; + // }); - onChange(imagePath); + onChange(path); } }; @@ -138,15 +139,11 @@ export function EditProfileModal( className={styles.profileAvatarEditContainer} onClick={handleChangeProfileAvatar} > - {imageUrl ? ( - {userDetails?.displayName} - ) : ( - - )} +
      diff --git a/src/renderer/src/pages/profile/profile-content/friends-box.tsx b/src/renderer/src/pages/profile/profile-content/friends-box.tsx index 151f9c80..82d4ff9d 100644 --- a/src/renderer/src/pages/profile/profile-content/friends-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/friends-box.tsx @@ -4,8 +4,7 @@ import { useContext } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./profile-content.css"; -import { Link } from "@renderer/components"; -import { PersonIcon } from "@primer/octicons-react"; +import { Avatar, Link } from "@renderer/components"; export function FriendsBox() { const { userProfile, userStats } = useContext(userProfileContext); @@ -30,17 +29,11 @@ export function FriendsBox() { {userProfile?.friends.map((friend) => (
    • - {friend.profileImageUrl ? ( - {friend.displayName} - ) : ( -
      - -
      - )} + {friend.displayName} diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts index 46e32556..cdebc5df 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts @@ -1,17 +1,5 @@ import { SPACING_UNIT, vars } from "../../../theme.css"; -import { keyframes, style } from "@vanilla-extract/css"; - -const animateBackground = keyframes({ - "0%": { - backgroundPosition: "0% 50%", - }, - "50%": { - backgroundPosition: "100% 50%", - }, - "100%": { - backgroundPosition: "0% 50%", - }, -}); +import { style } from "@vanilla-extract/css"; export const profileContentBox = style({ display: "flex", @@ -74,7 +62,7 @@ export const heroPanel = style({ display: "flex", gap: `${SPACING_UNIT}px`, justifyContent: "space-between", - backdropFilter: `blur(10px)`, + backdropFilter: `blur(15px)`, borderTop: `solid 1px rgba(255, 255, 255, 0.1)`, boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)", backgroundColor: "rgba(0, 0, 0, 0.3)", @@ -99,25 +87,3 @@ export const currentGameDetails = style({ gap: `${SPACING_UNIT}px`, alignItems: "center", }); - -export const xdTotal = style({ - background: `linear-gradient( - 60deg, - #f79533, - #f37055, - #ef4e7b, - #a166ab, - #5073b8, - #1098ad, - #07b39b, - #6fba82 - )`, - width: "102px", - minWidth: "102px", - height: "102px", - animation: `${animateBackground} 4s ease alternate infinite`, - backgroundSize: "300% 300%", - zIndex: -1, - borderRadius: "4px", - position: "absolute", -}); diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index 8b70945c..b8528810 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -8,13 +8,11 @@ import { CheckCircleFillIcon, PencilIcon, PersonAddIcon, - PersonIcon, SignOutIcon, - UploadIcon, XCircleFillIcon, } from "@primer/octicons-react"; import { buildGameDetailsPath } from "@renderer/helpers"; -import { Button, Link } from "@renderer/components"; +import { Avatar, Button, Link } from "@renderer/components"; import { useTranslation } from "react-i18next"; import { useAppSelector, @@ -28,16 +26,21 @@ import { useNavigate } from "react-router-dom"; import type { FriendRequestAction } from "@types"; import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal"; import Skeleton from "react-loading-skeleton"; +import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button"; type FriendAction = | FriendRequestAction | ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND"); +const backgroundImageLayer = + "linear-gradient(135deg, rgb(0 0 0 / 50%), rgb(0 0 0 / 60%))"; + export function ProfileHero() { const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false); - const { isMe, getUserProfile, userProfile } = useContext(userProfileContext); + const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } = + useContext(userProfileContext); const { signOut, updateFriendRequestState, @@ -48,8 +51,6 @@ export function ProfileHero() { const { gameRunning } = useAppSelector((state) => state.gameRunning); - const [hero, setHero] = useState(""); - const { t } = useTranslation("user_profile"); const { formatDistance } = useDate(); @@ -186,6 +187,7 @@ export function ProfileHero() { handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP") } disabled={isPerformingAction} + style={{ borderColor: vars.color.body }} > {t("undo_friendship")} @@ -260,35 +262,6 @@ export function ProfileHero() { return userProfile?.currentGame; }, [isMe, userProfile, gameRunning]); - const handleChangeCoverClick = async () => { - const { filePaths } = await window.electron.showOpenDialog({ - properties: ["openFile"], - filters: [ - { - name: "Image", - extensions: ["jpg", "jpeg", "png", "gif", "webp"], - }, - ], - }); - - if (filePaths && filePaths.length > 0) { - const path = filePaths[0]; - - setHero(path); - - // onChange(imagePath); - } - }; - - const getImageUrl = () => { - if (hero) return `local:${hero}`; - // if (userDetails?.profileImageUrl) return userDetails.profileImageUrl; - - return ""; - }; - - // const imageUrl = getImageUrl(); - return ( <> {/* setShowEditProfileModal(false)} /> -
      - +
      + {backgroundImage && ( + + )} +
      -
      - {userProfile?.profileImageUrl ? ( - {userProfile?.displayName} - ) : ( - - )} +
      @@ -379,28 +352,14 @@ export function ProfileHero() { )}
      - +
      { + try { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: "Image", + extensions: ["jpg", "jpeg", "png", "gif", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + const path = filePaths[0]; + + setSelectedBackgroundImage(path); + setIsUploadingBackgorundImage(true); + + await patchUser({ backgroundImageUrl: path }); + + showSuccessToast("Background image updated"); + } + } finally { + setIsUploadingBackgorundImage(false); + } + }; + + if (!isMe) return null; + + return ( + + ); +} diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index be7e9597..dffdfbae 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -15,7 +15,7 @@ import { SettingsPrivacy } from "./settings-privacy"; import { useUserDetails } from "@renderer/hooks"; import { useMemo } from "react"; -export function Settings() { +export default function Settings() { const { t } = useTranslation("settings"); const { userDetails } = useUserDetails(); diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx index 3ca837fa..38f0dd25 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx @@ -1,11 +1,8 @@ -import { - CheckCircleIcon, - PersonIcon, - XCircleIcon, -} from "@primer/octicons-react"; +import { CheckCircleIcon, XCircleIcon } from "@primer/octicons-react"; import * as styles from "./user-friend-modal.css"; import { SPACING_UNIT } from "@renderer/theme.css"; import { useTranslation } from "react-i18next"; +import { Avatar } from "@renderer/components"; export type UserFriendItemProps = { userId: string; @@ -109,17 +106,8 @@ export const UserFriendItem = (props: UserFriendItemProps) => { return (
      -
      - {profileImageUrl ? ( - {displayName} - ) : ( - - )} -
      + +
      { className={styles.friendListButton} onClick={() => props.onClickItem(userId)} > -
      - {profileImageUrl ? ( - {displayName} - ) : ( - - )} -
      +
      Date: Wed, 16 Oct 2024 10:59:57 +0100 Subject: [PATCH 120/163] feat: adding background image upload --- src/renderer/src/hooks/use-user-details.ts | 9 +++---- src/renderer/src/main.tsx | 11 ++++++--- .../achievements-content.tsx | 0 .../achievements-skeleton.tsx | 0 .../achievements.css.ts | 0 .../achievements.tsx | 8 +++---- .../achievement-notification.css.ts | 0 .../notification/achievement-notification.tsx | 0 .../edit-profile-modal/edit-profile-modal.tsx | 24 +++++++++++-------- .../upload-background-image-button.tsx | 3 ++- 10 files changed, 32 insertions(+), 23 deletions(-) rename src/renderer/src/pages/{achievement => achievements}/achievements-content.tsx (100%) rename src/renderer/src/pages/{achievement => achievements}/achievements-skeleton.tsx (100%) rename src/renderer/src/pages/{achievement => achievements}/achievements.css.ts (100%) rename src/renderer/src/pages/{achievement => achievements}/achievements.tsx (95%) rename src/renderer/src/pages/{achievement => achievements}/notification/achievement-notification.css.ts (100%) rename src/renderer/src/pages/{achievement => achievements}/notification/achievement-notification.tsx (100%) diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 50689aeb..649d24a4 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -70,7 +70,7 @@ export function useUserDetails() { subscription: userDetails?.subscription || null, }); }, - [updateUserDetails, userDetails?.username] + [updateUserDetails, userDetails?.username, userDetails?.subscription] ); const syncFriendRequests = useCallback(async () => { @@ -127,9 +127,9 @@ export function useUserDetails() { const blockUser = (userId: string) => window.electron.blockUser(userId); - const unblockUser = (userId: string) => { - return window.electron.unblockUser(userId); - }; + const unblockUser = (userId: string) => window.electron.unblockUser(userId); + + const hasActiveSubscription = userDetails?.subscription?.status === "active"; return { userDetails, @@ -139,6 +139,7 @@ export function useUserDetails() { friendRequetsModalTab, isFriendsModalVisible, friendModalUserId, + hasActiveSubscription, showFriendsModal, hideFriendsModal, fetchUserDetails, diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index bc3522ae..11003295 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -19,11 +19,10 @@ import { App } from "./app"; import { store } from "./store"; import resources from "@locales"; -import { AchievementNotification } from "./pages/achievement/notification/achievement-notification"; +import { AchievementNotification } from "./pages/achievements/notification/achievement-notification"; import "./workers"; import { RepacksContextProvider } from "./context"; -import { Achievement } from "./pages/achievement/achievements"; import { SuspenseWrapper } from "./components"; const Home = React.lazy(() => import("./pages/home/home")); @@ -35,6 +34,9 @@ const SearchResults = React.lazy(() => import("./pages/home/search-results")); const Settings = React.lazy(() => import("./pages/settings/settings")); const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue")); const Profile = React.lazy(() => import("./pages/profile/profile")); +const Achievements = React.lazy( + () => import("./pages/achievements/achievements") +); Sentry.init({}); @@ -90,7 +92,10 @@ ReactDOM.createRoot(document.getElementById("root")!).render( path="/profile/:userId" element={} /> - + } + /> {({ isLoading, achievements }) => { diff --git a/src/renderer/src/pages/achievement/notification/achievement-notification.css.ts b/src/renderer/src/pages/achievements/notification/achievement-notification.css.ts similarity index 100% rename from src/renderer/src/pages/achievement/notification/achievement-notification.css.ts rename to src/renderer/src/pages/achievements/notification/achievement-notification.css.ts diff --git a/src/renderer/src/pages/achievement/notification/achievement-notification.tsx b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx similarity index 100% rename from src/renderer/src/pages/achievement/notification/achievement-notification.tsx rename to src/renderer/src/pages/achievements/notification/achievement-notification.tsx diff --git a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx index cad4ed9e..cc3ed69f 100644 --- a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx +++ b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.tsx @@ -11,7 +11,7 @@ import { ModalProps, TextField, } from "@renderer/components"; -import { useAppSelector, useToast, useUserDetails } from "@renderer/hooks"; +import { useToast, useUserDetails } from "@renderer/hooks"; import { SPACING_UNIT } from "@renderer/theme.css"; import { yupResolver } from "@hookform/resolvers/yup"; @@ -51,8 +51,8 @@ export function EditProfileModal( const { getUserProfile } = useContext(userProfileContext); - const { userDetails } = useAppSelector((state) => state.userDetails); - const { fetchUserDetails } = useUserDetails(); + const { userDetails, fetchUserDetails, hasActiveSubscription } = + useUserDetails(); useEffect(() => { if (userDetails) { @@ -112,14 +112,18 @@ export function EditProfileModal( if (filePaths && filePaths.length > 0) { const path = filePaths[0]; - // const { imagePath } = await window.electron - // .processProfileImage(path) - // .catch(() => { - // showErrorToast(t("image_process_failure")); - // return { imagePath: null }; - // }); + if (!hasActiveSubscription) { + const { imagePath } = await window.electron + .processProfileImage(path) + .catch(() => { + showErrorToast(t("image_process_failure")); + return { imagePath: null }; + }); - onChange(path); + onChange(imagePath); + } else { + onChange(path); + } } }; diff --git a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx index c777380c..ebb52df3 100644 --- a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx +++ b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx @@ -9,6 +9,7 @@ import { useToast, useUserDetails } from "@renderer/hooks"; export function UploadBackgroundImageButton() { const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] = useState(false); + const { hasActiveSubscription } = useUserDetails(); const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext); const { patchUser } = useUserDetails(); @@ -42,7 +43,7 @@ export function UploadBackgroundImageButton() { } }; - if (!isMe) return null; + if (!isMe || !hasActiveSubscription) return null; return (
      diff --git a/src/renderer/src/pages/achievements/achievements.css.ts b/src/renderer/src/pages/achievements/achievements.css.ts index 986f809f..0a95e889 100644 --- a/src/renderer/src/pages/achievements/achievements.css.ts +++ b/src/renderer/src/pages/achievements/achievements.css.ts @@ -72,8 +72,8 @@ export const container = style({ export const panel = recipe({ base: { width: "100%", - height: "180px", - minHeight: "180px", + height: "150px", + minHeight: "150px", padding: `${SPACING_UNIT * 2}px`, backgroundColor: vars.color.darkBackground, transition: "all ease 0.2s", @@ -184,8 +184,8 @@ export const listItemSkeleton = style({ }); export const profileAvatar = style({ - height: "65px", - width: "65px", + height: "32px", + width: "32px", borderRadius: "4px", display: "flex", justifyContent: "center", From ab27fd21d78dbec96c19f2a8fec04468c67ed5a4 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:52:43 -0300 Subject: [PATCH 125/163] feat: redoing page --- src/renderer/src/hooks/use-user-details.ts | 13 +- .../achievements/achievements-content.tsx | 337 ++++++++---------- .../pages/achievements/achievements.css.ts | 25 +- 3 files changed, 170 insertions(+), 205 deletions(-) diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 649d24a4..1872c95d 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { useAppDispatch, useAppSelector } from "./redux"; import { setProfileBackground, @@ -129,7 +129,16 @@ export function useUserDetails() { const unblockUser = (userId: string) => window.electron.unblockUser(userId); - const hasActiveSubscription = userDetails?.subscription?.status === "active"; + const hasActiveSubscription = useMemo(() => { + if (!userDetails?.subscription) { + return false; + } + + return ( + userDetails.subscription.expiresAt == null || + new Date(userDetails.subscription.expiresAt) > new Date() + ); + }, [userDetails]); return { userDetails, diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index fbf9da9f..874368a8 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -5,15 +5,19 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./achievements.css"; import { formatDownloadProgress } from "@renderer/helpers"; -import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react"; +import { + CheckCircleIcon, + LockIcon, + PersonIcon, + TrophyIcon, + UnlockIcon, +} from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; import { UserAchievement } from "@types"; import { average } from "color.js"; import Color from "color"; -const HERO_ANIMATION_THRESHOLD = 25; - interface UserInfo { userId: string; displayName: string; @@ -32,180 +36,85 @@ interface AchievementListProps { interface AchievementPanelProps { user: UserInfo; - otherUser: UserInfo | null; } -function AchievementPanel({ user, otherUser }: AchievementPanelProps) { - const { t } = useTranslation("achievement"); - const { userDetails } = useUserDetails(); +function AchievementPanel({ user }: AchievementPanelProps) { + const { userDetails, hasActiveSubscription } = useUserDetails(); const userTotalAchievementCount = user.achievements.length; const userUnlockedAchievementCount = user.achievements.filter( (achievement) => achievement.unlocked ).length; - if (!otherUser) { + const getProfileImage = (user: UserInfo) => { return ( -
      -
      -

      - {t("your_achievements")} -

      -
      -
      - - - {userUnlockedAchievementCount} / {userTotalAchievementCount} - -
      - - - {formatDownloadProgress( - userUnlockedAchievementCount / userTotalAchievementCount - )} - -
      - + {user.profileImageUrl ? ( + {user.displayName} -
      + ) : ( + + )}
      ); - } + }; - const otherUserUnlockedAchievementCount = otherUser.achievements.filter( - (achievement) => achievement.unlocked - ).length; - const otherUserTotalAchievementCount = otherUser.achievements.length; + if (userDetails?.id == user.userId && !hasActiveSubscription) { + return <>; + } return (
      -
      + {getProfileImage(user)} +
      +

      {user.displayName}

      -

      - {otherUser.displayName} -

      -
      - - - {otherUserUnlockedAchievementCount} /{" "} - {otherUserTotalAchievementCount} - -
      - + - {formatDownloadProgress( - otherUserUnlockedAchievementCount / - otherUserTotalAchievementCount - )} + {userUnlockedAchievementCount} / {userTotalAchievementCount}
      - -
      -
      -

      - {userDetails?.displayName} -

      -
      -
      - - - {userUnlockedAchievementCount} / {userTotalAchievementCount} - -
      - - {formatDownloadProgress( - userUnlockedAchievementCount / userTotalAchievementCount - )} - -
      - + + {formatDownloadProgress( + userUnlockedAchievementCount / userTotalAchievementCount + )} +
      +
      ); @@ -220,18 +129,6 @@ function AchievementList({ user, otherUser }: AchievementListProps) { const { userDetails } = useUserDetails(); - const getProfileImage = (imageUrl: string | null | undefined) => { - return ( -
      - {imageUrl ? ( - {"teste"} - ) : ( - - )} -
      - ); - }; - if (!otherUserAchievements || otherUserAchievements.length === 0) { return (
        @@ -271,7 +168,7 @@ function AchievementList({ user, otherUser }: AchievementListProps) {
      • -
        +
        {otherUserAchievement.unlockTime ? ( -
        - {getProfileImage(otherUser.profileImageUrl)} - {t("unlocked_at")} -

        {formatDateTime(otherUserAchievement.unlockTime)}

        +
        + + {formatDateTime(otherUserAchievement.unlockTime)}
        ) : (
        -

        Não desbloqueada

        )}
        -
        +
        {userDetails?.subscription && achievements[index].unlockTime ? (
        - {getProfileImage(user.profileImageUrl)} - {t("unlocked_at")} +

        {formatDateTime(achievements[index].unlockTime)}

        ) : (
        -

        Não desbloqueada

        )}
        @@ -334,7 +245,6 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { const heroRef = useRef(null); const containerRef = useRef(null); const [isHeaderStuck, setIsHeaderStuck] = useState(false); - const [backdropOpactiy, setBackdropOpacity] = useState(1); const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } = useContext(gameDetailsContext); @@ -380,11 +290,6 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT; const scrollY = (event.target as HTMLDivElement).scrollTop; - const opacity = Math.max( - 0, - 1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD) - ); - if (scrollY >= heroHeight && !isHeaderStuck) { setIsHeaderStuck(true); } @@ -392,8 +297,22 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { if (scrollY <= heroHeight && isHeaderStuck) { setIsHeaderStuck(false); } + }; - setBackdropOpacity(opacity); + const getProfileImage = (user: UserInfo) => { + return ( +
        + {user.profileImageUrl ? ( + {user.displayName} + ) : ( + + )} +
        + ); }; if (!objectId || !shop || !gameTitle || !userDetails) return null; @@ -402,6 +321,7 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
        {gameTitle} -
        -
        - -
        +
        +
        + +
        + + + {otherUser && } +
        -
        - -
        + {otherUser && ( +
        +
        +
        +
        {getProfileImage(otherUser)}
        +
        + {getProfileImage({ + ...userDetails, + userId: userDetails.id, + achievements: sortedAchievements, + })} +
        +
        +
        + )} +
        Date: Fri, 18 Oct 2024 12:31:16 -0300 Subject: [PATCH 126/163] feat: adjusting ui --- src/renderer/src/hooks/use-date.ts | 2 +- .../achievements/achievements-content.tsx | 102 ++++++++++-------- .../pages/achievements/achievements.css.ts | 5 +- 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/src/renderer/src/hooks/use-date.ts b/src/renderer/src/hooks/use-date.ts index 21ed1b34..9486400c 100644 --- a/src/renderer/src/hooks/use-date.ts +++ b/src/renderer/src/hooks/use-date.ts @@ -72,7 +72,7 @@ export function useDate() { const locale = getDateLocale(); return format( date, - locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy - HH:mm" + locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy HH:mm" ); }, diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index 874368a8..c41f376a 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -10,7 +10,6 @@ import { LockIcon, PersonIcon, TrophyIcon, - UnlockIcon, } from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; @@ -192,49 +191,55 @@ function AchievementList({ user, otherUser }: AchievementListProps) {
        -
        - {otherUserAchievement.unlockTime ? ( -
        - - {formatDateTime(otherUserAchievement.unlockTime)} -
        - ) : ( -
        - -
        - )} -
        + {otherUserAchievement.unlocked ? ( +
        + + {formatDateTime(otherUserAchievement.unlockTime!)} +
        + ) : ( +
        + +
        + )} -
        - {userDetails?.subscription && achievements[index].unlockTime ? ( -
        - -

        {formatDateTime(achievements[index].unlockTime)}

        -
        - ) : ( -
        - -
        - )} -
        + {userDetails?.subscription && achievements[index].unlocked ? ( +
        + + {formatDateTime(achievements[index].unlockTime!)} +
        + ) : ( +
        + +
        + )}
      • ))}
      @@ -371,17 +376,20 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
      {otherUser && ( -
      +
      -
      {getProfileImage(otherUser)}
      -
      +
      + {getProfileImage(otherUser)} +
      +
      {getProfileImage({ ...userDetails, userId: userDetails.id, diff --git a/src/renderer/src/pages/achievements/achievements.css.ts b/src/renderer/src/pages/achievements/achievements.css.ts index 77013ba8..84daa165 100644 --- a/src/renderer/src/pages/achievements/achievements.css.ts +++ b/src/renderer/src/pages/achievements/achievements.css.ts @@ -4,6 +4,7 @@ import { recipe } from "@vanilla-extract/recipes"; export const HERO_HEIGHT = 150; export const LOGO_HEIGHT = 100; +export const LOGO_MAX_WIDTH = 200; export const wrapper = style({ display: "flex", @@ -59,7 +60,9 @@ export const heroContent = style({ }); export const gameLogo = style({ + width: LOGO_MAX_WIDTH, height: LOGO_HEIGHT, + objectFit: "contain", }); export const container = style({ @@ -71,7 +74,7 @@ export const container = style({ zIndex: "1", }); -export const panel = recipe({ +export const tableHeader = recipe({ base: { width: "100%", backgroundColor: vars.color.darkBackground, From 584f725edaa6080be1d36b799d8d3d3ce97040d5 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 19 Oct 2024 01:01:14 -0300 Subject: [PATCH 127/163] feat: adjustments --- .../achievements/achievements-content.tsx | 149 ++++++++++++------ .../pages/achievements/achievements.css.ts | 1 - 2 files changed, 105 insertions(+), 45 deletions(-) diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index c41f376a..d876ff9e 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -33,11 +33,12 @@ interface AchievementListProps { otherUser: UserInfo | null; } -interface AchievementPanelProps { +interface AchievementSummaryProps { user: UserInfo; + isComparison?: boolean; } -function AchievementPanel({ user }: AchievementPanelProps) { +function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { const { userDetails, hasActiveSubscription } = useUserDetails(); const userTotalAchievementCount = user.achievements.length; @@ -61,8 +62,55 @@ function AchievementPanel({ user }: AchievementPanelProps) { ); }; - if (userDetails?.id == user.userId && !hasActiveSubscription) { - return <>; + if ( + isComparison && + userDetails?.id == user.userId && + !hasActiveSubscription + ) { + return ( +
      +
      + +

      Assine o HYDRA CLOUD para comparar suas conquistas!!!!

      +
      +
      + {getProfileImage(user)} +

      {user.displayName}

      +
      +
      + ); } return ( @@ -71,6 +119,7 @@ function AchievementPanel({ user }: AchievementPanelProps) { display: "flex", gap: `${SPACING_UNIT * 2}px`, alignItems: "center", + padding: `${SPACING_UNIT}px`, }} > {getProfileImage(user)} @@ -126,7 +175,7 @@ function AchievementList({ user, otherUser }: AchievementListProps) { const { t } = useTranslation("achievement"); const { formatDateTime } = useDate(); - const { userDetails } = useUserDetails(); + const { hasActiveSubscription } = useUserDetails(); if (!otherUserAchievements || otherUserAchievements.length === 0) { return ( @@ -167,7 +216,12 @@ function AchievementList({ user, otherUser }: AchievementListProps) {
    • + {hasActiveSubscription ? ( + achievements[index].unlocked ? ( +
      + + {formatDateTime(achievements[index].unlockTime!)} +
      + ) : ( +
      + +
      + ) + ) : null} + {otherUserAchievement.unlocked ? (
      )} - - {userDetails?.subscription && achievements[index].unlocked ? ( -
      - - {formatDateTime(achievements[index].unlockTime!)} -
      - ) : ( -
      - -
      - )}
    • ))}
    @@ -270,7 +326,7 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { const dispatch = useAppDispatch(); - const { userDetails } = useUserDetails(); + const { userDetails, hasActiveSubscription } = useUserDetails(); useEffect(() => { if (gameTitle) { @@ -359,19 +415,20 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { display: "flex", flexDirection: "column", width: "100%", - gap: `${SPACING_UNIT * 2}px`, - padding: `${SPACING_UNIT * 2}px`, + gap: `${SPACING_UNIT}px`, + padding: `${SPACING_UNIT}px`, }} > - - {otherUser && } + {otherUser && } @@ -380,22 +437,26 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
    + {hasActiveSubscription && ( +
    + {getProfileImage({ + ...userDetails, + userId: userDetails.id, + achievements: sortedAchievements, + })} +
    + )}
    {getProfileImage(otherUser)}
    -
    - {getProfileImage({ - ...userDetails, - userId: userDetails.id, - achievements: sortedAchievements, - })} -
    )} diff --git a/src/renderer/src/pages/achievements/achievements.css.ts b/src/renderer/src/pages/achievements/achievements.css.ts index 84daa165..ebd63aab 100644 --- a/src/renderer/src/pages/achievements/achievements.css.ts +++ b/src/renderer/src/pages/achievements/achievements.css.ts @@ -52,7 +52,6 @@ export const heroImage = style({ export const heroContent = style({ padding: `${SPACING_UNIT * 2}px`, - height: "100%", width: "100%", display: "flex", justifyContent: "space-between", From 0e5d37a3a0325b11a72eac312839211dc9a33bd7 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sat, 19 Oct 2024 15:48:41 +0100 Subject: [PATCH 128/163] feat: adding wine prefix --- src/main/entity/game.entity.ts | 3 + .../events/cloud-save/upload-save-game.ts | 26 +- src/main/events/index.ts | 1 + .../events/library/select-game-wine-prefix.ts | 13 + src/main/knex-client.ts | 2 + src/main/main.ts | 9 +- .../20241019081648_add_wine_prefix_to_game.ts | 17 ++ src/main/services/how-long-to-beat.ts | 2 +- src/main/services/ludusavi.ts | 63 ++++- src/main/workers/ludusavi.worker.ts | 6 +- src/preload/index.ts | 12 +- .../context/cloud-sync/cloud-sync.context.tsx | 13 +- .../game-details/game-details.context.tsx | 16 ++ .../game-details.context.types.ts | 1 + src/renderer/src/declaration.d.ts | 7 +- .../pages/achievements/achievements.css.ts | 2 + .../cloud-sync-files-modal.tsx | 44 +++- .../cloud-sync-modal/cloud-sync-modal.css.ts | 11 + .../cloud-sync-modal/cloud-sync-modal.tsx | 232 ++++++++++-------- .../modals/game-options-modal.tsx | 47 ++++ .../profile-content/profile-content.css.ts | 2 + .../profile/profile-hero/profile-hero.css.ts | 2 +- .../profile/profile-hero/profile-hero.tsx | 2 +- .../upload-background-image-button.tsx | 3 +- src/types/index.ts | 2 + src/types/ludusavi.types.ts | 15 ++ 26 files changed, 423 insertions(+), 130 deletions(-) create mode 100644 src/main/events/library/select-game-wine-prefix.ts create mode 100644 src/main/migrations/20241019081648_add_wine_prefix_to_game.ts diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts index 190e7470..19905c32 100644 --- a/src/main/entity/game.entity.ts +++ b/src/main/entity/game.entity.ts @@ -39,6 +39,9 @@ export class Game { @Column("text", { nullable: true }) executablePath: string | null; + @Column("text", { nullable: true }) + winePrefixPath: string | null; + @Column("int", { default: 0 }) playTimeInMilliseconds: number; diff --git a/src/main/events/cloud-save/upload-save-game.ts b/src/main/events/cloud-save/upload-save-game.ts index b66fd081..6cf68596 100644 --- a/src/main/events/cloud-save/upload-save-game.ts +++ b/src/main/events/cloud-save/upload-save-game.ts @@ -10,8 +10,13 @@ import os from "node:os"; import { backupsPath } from "@main/constants"; import { app } from "electron"; import { normalizePath } from "@main/helpers"; +import { gameRepository } from "@main/repository"; -const bundleBackup = async (shop: GameShop, objectId: string) => { +const bundleBackup = async ( + shop: GameShop, + objectId: string, + winePrefix: string | null +) => { const backupPath = path.join(backupsPath, `${shop}-${objectId}`); // Remove existing backup @@ -19,7 +24,7 @@ const bundleBackup = async (shop: GameShop, objectId: string) => { fs.rmSync(backupPath, { recursive: true }); } - await Ludusavi.backupGame(shop, objectId, backupPath); + await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix); const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`); @@ -38,9 +43,21 @@ const bundleBackup = async (shop: GameShop, objectId: string) => { const uploadSaveGame = async ( _event: Electron.IpcMainInvokeEvent, objectId: string, - shop: GameShop + shop: GameShop, + downloadOptionTitle: string | null ) => { - const bundleLocation = await bundleBackup(shop, objectId); + const game = await gameRepository.findOne({ + where: { + objectID: objectId, + shop, + }, + }); + + const bundleLocation = await bundleBackup( + shop, + objectId, + game?.winePrefixPath ?? null + ); fs.stat(bundleLocation, async (err, stat) => { if (err) { @@ -57,6 +74,7 @@ const uploadSaveGame = async ( objectId, hostname: os.hostname(), homeDir: normalizePath(app.getPath("home")), + downloadOptionTitle, platform: os.platform(), }); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 1b621a30..80363e4e 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -25,6 +25,7 @@ import "./library/update-executable-path"; import "./library/verify-executable-path"; import "./library/remove-game"; import "./library/remove-game-from-library"; +import "./library/select-game-wine-prefix"; import "./misc/open-external"; import "./misc/show-open-dialog"; import "./torrenting/cancel-game-download"; diff --git a/src/main/events/library/select-game-wine-prefix.ts b/src/main/events/library/select-game-wine-prefix.ts new file mode 100644 index 00000000..a75a3cb0 --- /dev/null +++ b/src/main/events/library/select-game-wine-prefix.ts @@ -0,0 +1,13 @@ +import { gameRepository } from "@main/repository"; + +import { registerEvent } from "../register-event"; + +const selectGameWinePrefix = async ( + _event: Electron.IpcMainInvokeEvent, + id: number, + winePrefixPath: string +) => { + return gameRepository.update({ id }, { winePrefixPath }); +}; + +registerEvent("selectGameWinePrefix", selectGameWinePrefix); diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts index 48561670..5f81ffbc 100644 --- a/src/main/knex-client.ts +++ b/src/main/knex-client.ts @@ -10,6 +10,7 @@ import { CreateGameAchievement } from "./migrations/20240919030940_create_game_a import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference"; import { CreateUserSubscription } from "./migrations/20241015235142_create_user_subscription"; import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url"; +import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game"; export type HydraMigration = Knex.Migration & { name: string }; @@ -25,6 +26,7 @@ class MigrationSource implements Knex.MigrationSource { AddAchievementNotificationPreference, CreateUserSubscription, AddBackgroundImageUrl, + AddWinePrefixToGame, ]); } getMigrationName(migration: HydraMigration): string { diff --git a/src/main/main.ts b/src/main/main.ts index 7f3d6370..69bc62e0 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,4 +1,9 @@ -import { DownloadManager, PythonInstance, startMainLoop } from "./services"; +import { + DownloadManager, + Ludusavi, + PythonInstance, + startMainLoop, +} from "./services"; import { downloadQueueRepository, userPreferencesRepository, @@ -15,6 +20,8 @@ const loadState = async (userPreferences: UserPreferences | null) => { RealDebridClient.authorize(userPreferences?.realDebridApiToken); } + Ludusavi.addManifestToLudusaviConfig(); + HydraApi.setupApi().then(() => { uploadGamesBatch(); }); diff --git a/src/main/migrations/20241019081648_add_wine_prefix_to_game.ts b/src/main/migrations/20241019081648_add_wine_prefix_to_game.ts new file mode 100644 index 00000000..517f6fb5 --- /dev/null +++ b/src/main/migrations/20241019081648_add_wine_prefix_to_game.ts @@ -0,0 +1,17 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const AddWinePrefixToGame: HydraMigration = { + name: "AddWinePrefixToGame", + up: (knex: Knex) => { + return knex.schema.alterTable("game", (table) => { + return table.text("winePrefixPath").nullable(); + }); + }, + + down: async (knex: Knex) => { + return knex.schema.alterTable("game", (table) => { + return table.dropColumn("winePrefixPath"); + }); + }, +}; diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts index 5a82d8e7..1e5f3279 100644 --- a/src/main/services/how-long-to-beat.ts +++ b/src/main/services/how-long-to-beat.ts @@ -50,7 +50,7 @@ export const searchHowLongToBeat = async (gameName: string) => { const response = await axios .post( - "https://howlongtobeat.com/api/search/8fbd64723a8204dd", + `https://howlongtobeat.com/api/search/${state.apiKey}`, { searchType: "games", searchTerms: formatName(gameName).split(" "), diff --git a/src/main/services/ludusavi.ts b/src/main/services/ludusavi.ts index 838b5f9b..91633e36 100644 --- a/src/main/services/ludusavi.ts +++ b/src/main/services/ludusavi.ts @@ -1,20 +1,27 @@ -import { GameShop, LudusaviBackup } from "@types"; +import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types"; import Piscina from "piscina"; import { app } from "electron"; +import fs from "node:fs"; import path from "node:path"; +import YAML from "yaml"; import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath"; -const binaryPath = app.isPackaged - ? path.join(process.resourcesPath, "ludusavi", "ludusavi") - : path.join(__dirname, "..", "..", "ludusavi", "ludusavi"); - export class Ludusavi { + private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi"); + private static ludusaviConfigPath = path.join( + this.ludusaviPath, + "config.yaml" + ); + private static binaryPath = app.isPackaged + ? path.join(process.resourcesPath, "ludusavi", "ludusavi") + : path.join(__dirname, "..", "..", "ludusavi", "ludusavi"); + private static worker = new Piscina({ filename: ludusaviWorkerPath, workerData: { - binaryPath, + binaryPath: this.binaryPath, }, }); @@ -27,16 +34,29 @@ export class Ludusavi { return games; } + static async getConfig() { + if (!fs.existsSync(this.ludusaviConfigPath)) { + await this.worker.run(undefined, { name: "generateConfig" }); + } + + const config = YAML.parse( + fs.readFileSync(this.ludusaviConfigPath, "utf-8") + ) as LudusaviConfig; + + return config; + } + static async backupGame( shop: GameShop, objectId: string, - backupPath: string + backupPath: string, + winePrefix?: string | null ): Promise { const games = await this.findGames(shop, objectId); if (!games.length) throw new Error("Game not found"); return this.worker.run( - { title: games[0], backupPath }, + { title: games[0], backupPath, winePrefix }, { name: "backupGame" } ); } @@ -60,4 +80,31 @@ export class Ludusavi { static async restoreBackup(backupPath: string) { return this.worker.run(backupPath, { name: "restoreBackup" }); } + + static async addManifestToLudusaviConfig() { + const config = await this.getConfig(); + + config.manifest.enable = false; + config.manifest.secondary = [ + { url: "https://cdn.losbroxas.org/manifest.yaml", enable: true }, + ]; + + fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config)); + } + + static async addCustomGame(title: string, savePath: string) { + const config = await this.getConfig(); + const filteredGames = config.customGames.filter( + (game) => game.name !== title + ); + + filteredGames.push({ + name: title, + files: [savePath], + registry: [], + }); + + config.customGames = filteredGames; + fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config)); + } } diff --git a/src/main/workers/ludusavi.worker.ts b/src/main/workers/ludusavi.worker.ts index e6ccdaad..469ab721 100644 --- a/src/main/workers/ludusavi.worker.ts +++ b/src/main/workers/ludusavi.worker.ts @@ -58,4 +58,8 @@ export const restoreBackup = (backupPath: string) => { return JSON.parse(result.toString("utf-8")) as LudusaviBackup; }; -// --wine-prefix +export const generateConfig = () => { + const result = cp.execFileSync(binaryPath, ["schema", "config"]); + + return JSON.parse(result.toString("utf-8")) as LudusaviBackup; +}; diff --git a/src/preload/index.ts b/src/preload/index.ts index 11086199..fc2e5a91 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -107,6 +107,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("createGameShortcut", id), updateExecutablePath: (id: number, executablePath: string) => ipcRenderer.invoke("updateExecutablePath", id, executablePath), + selectGameWinePrefix: (id: number, winePrefixPath: string) => + ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath), verifyExecutablePathInUse: (executablePath: string) => ipcRenderer.invoke("verifyExecutablePathInUse", executablePath), getLibrary: () => ipcRenderer.invoke("getLibrary"), @@ -148,8 +150,12 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("getDiskFreeSpace", path), /* Cloud save */ - uploadSaveGame: (objectId: string, shop: GameShop) => - ipcRenderer.invoke("uploadSaveGame", objectId, shop), + uploadSaveGame: ( + objectId: string, + shop: GameShop, + downloadOptionTitle: string | null + ) => + ipcRenderer.invoke("uploadSaveGame", objectId, shop, downloadOptionTitle), downloadGameArtifact: ( objectId: string, shop: GameShop, @@ -183,7 +189,7 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.on(`on-backup-download-progress-${objectId}-${shop}`, listener); return () => ipcRenderer.removeListener( - `on-backup-download-complete-${objectId}-${shop}`, + `on-backup-download-progress-${objectId}-${shop}`, listener ); }, diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx index 5a0a66f0..7b102918 100644 --- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -26,7 +26,7 @@ export interface CloudSyncContext { backupState: CloudSyncState; setShowCloudSyncModal: React.Dispatch>; downloadGameArtifact: (gameArtifactId: string) => Promise; - uploadSaveGame: () => Promise; + uploadSaveGame: (downloadOptionTitle: string | null) => Promise; deleteGameArtifact: (gameArtifactId: string) => Promise; setShowCloudSyncFilesModal: React.Dispatch>; getGameBackupPreview: () => Promise; @@ -108,10 +108,13 @@ export function CloudSyncContextProvider({ ]); }, [objectId, shop]); - const uploadSaveGame = useCallback(async () => { - setUploadingBackup(true); - window.electron.uploadSaveGame(objectId, shop); - }, [objectId, shop]); + const uploadSaveGame = useCallback( + async (downloadOptionTitle: string | null) => { + setUploadingBackup(true); + window.electron.uploadSaveGame(objectId, shop, downloadOptionTitle); + }, + [objectId, shop] + ); useEffect(() => { const removeUploadCompleteListener = window.electron.onUploadComplete( 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 6aaf112a..c308ab2f 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -3,6 +3,7 @@ import { useCallback, useContext, useEffect, + useMemo, useRef, useState, } from "react"; @@ -45,6 +46,7 @@ export const gameDetailsContext = createContext({ stats: null, achievements: null, hasNSFWContentBlocked: false, + lastDownloadedOption: null, setGameColor: () => {}, selectGameExecutable: async () => null, updateGame: async () => {}, @@ -199,6 +201,19 @@ export function GameDetailsContextProvider({ }; }, [game?.id, isGameRunning, updateGame]); + const lastDownloadedOption = useMemo(() => { + if (game?.uri) { + const repack = repacks.find((repack) => + repack.uris.some((uri) => uri.includes(game.uri!)) + ); + + if (!repack) return null; + return repack; + } + + return null; + }, [game?.uri, repacks]); + useEffect(() => { const unsubscribe = window.electron.onUpdateAchievements( objectId, @@ -259,6 +274,7 @@ export function GameDetailsContextProvider({ stats, achievements, hasNSFWContentBlocked, + lastDownloadedOption, setHasNSFWContentBlocked, setGameColor, selectGameExecutable, diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts index ad5c4de7..49718430 100644 --- a/src/renderer/src/context/game-details/game-details.context.types.ts +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -22,6 +22,7 @@ export interface GameDetailsContext { stats: GameStats | null; achievements: UserAchievement[] | null; hasNSFWContentBlocked: boolean; + lastDownloadedOption: GameRepack | null; setGameColor: React.Dispatch>; selectGameExecutable: () => Promise; updateGame: () => Promise; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 75ed137a..91bed316 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -91,6 +91,7 @@ declare global { ) => Promise; createGameShortcut: (id: number) => Promise; updateExecutablePath: (id: number, executablePath: string) => Promise; + selectGameWinePrefix: (id: number, winePrefixPath: string) => Promise; verifyExecutablePathInUse: (executablePath: string) => Promise; getLibrary: () => Promise; openGameInstaller: (gameId: number) => Promise; @@ -125,7 +126,11 @@ declare global { getDiskFreeSpace: (path: string) => Promise; /* Cloud save */ - uploadSaveGame: (objectId: string, shop: GameShop) => Promise; + uploadSaveGame: ( + objectId: string, + shop: GameShop, + downloadOptionTitle: string | null + ) => Promise; downloadGameArtifact: ( objectId: string, shop: GameShop, diff --git a/src/renderer/src/pages/achievements/achievements.css.ts b/src/renderer/src/pages/achievements/achievements.css.ts index 682fd2e5..24f1d507 100644 --- a/src/renderer/src/pages/achievements/achievements.css.ts +++ b/src/renderer/src/pages/achievements/achievements.css.ts @@ -134,9 +134,11 @@ export const achievementsProgressBar = style({ transition: "all ease 0.2s", "::-webkit-progress-bar": { backgroundColor: "rgba(255, 255, 255, 0.15)", + borderRadius: "4px", }, "::-webkit-progress-value": { backgroundColor: vars.color.muted, + borderRadius: "4px", }, }); diff --git a/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx index 9c57c6d2..900e96c5 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx @@ -1,6 +1,7 @@ -import { Modal, ModalProps } from "@renderer/components"; +import { Button, Modal, ModalProps, TextField } from "@renderer/components"; import { useContext, useMemo } from "react"; import { cloudSyncContext } from "@renderer/context"; +import { useTranslation } from "react-i18next"; export interface CloudSyncFilesModalProps extends Omit {} @@ -11,6 +12,8 @@ export function CloudSyncFilesModal({ }: CloudSyncFilesModalProps) { const { backupPreview } = useContext(cloudSyncContext); + const { t } = useTranslation("game_details"); + const files = useMemo(() => { if (!backupPreview) { return []; @@ -24,6 +27,24 @@ export function CloudSyncFilesModal({ }); }, [backupPreview]); + const handleChangeExecutableLocation = async () => { + const path = await selectGameExecutable(); + + if (path) { + const gameUsingPath = + await window.electron.verifyExecutablePathInUse(path); + + if (gameUsingPath) { + showErrorToast( + t("executable_path_in_use", { game: gameUsingPath.title }) + ); + return; + } + + window.electron.updateExecutablePath(game.id, path).then(updateGame); + } + }; + return ( - {/*
    + {/*
    {["AUTOMATIC", "CUSTOM"].map((downloader) => ( + } + /> + diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts index 916b7a1f..77231403 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts @@ -38,3 +38,14 @@ export const syncIcon = style({ animationIterationCount: "infinite", animationTimingFunction: "linear", }); + +export const progress = style({ + width: "100%", + height: "5px", + "::-webkit-progress-bar": { + backgroundColor: vars.color.darkBackground, + }, + "::-webkit-progress-value": { + backgroundColor: vars.color.muted, + }, +}); diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index 27ac7d80..f636d9d2 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -1,4 +1,9 @@ -import { Button, Modal, ModalProps } from "@renderer/components"; +import { + Button, + ConfirmationModal, + Modal, + ModalProps, +} from "@renderer/components"; import { useContext, useEffect, useMemo, useState } from "react"; import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; @@ -10,6 +15,7 @@ import { ClockIcon, DeviceDesktopIcon, HistoryIcon, + InfoIcon, SyncIcon, TrashIcon, UploadIcon, @@ -43,7 +49,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { setShowCloudSyncFilesModal, } = useContext(cloudSyncContext); - const { objectId, shop, gameTitle } = useContext(gameDetailsContext); + const { objectId, shop, gameTitle, lastDownloadedOption } = + useContext(gameDetailsContext); const { showSuccessToast, showErrorToast } = useToast(); @@ -140,112 +147,137 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { const disableActions = uploadingBackup || restoringBackup || deletingArtifact; return ( - -
    -
    -

    {gameTitle}

    -

    {backupStateLabel}

    + <> + {/* {}} + onClose={() => {}} + visible + /> */} - +
    + + + + {t("create_backup")} +
    - - +
    +
    +

    {t("backups")}

    + {artifacts.length} / 2 +
    -
    -

    {t("backups")}

    - {artifacts.length} / 2 -
    +
    + Espaço usado + +
    +
    -
      - {artifacts.map((artifact) => ( -
    • -
      -
      -

      Backup do dia {format(artifact.createdAt, "dd/MM")}

      - {formatBytes(artifact.artifactLengthInBytes)} +
        + {artifacts.map((artifact) => ( +
      • +
        +
        +

        Backup do dia {format(artifact.createdAt, "dd/MM")}

        + {formatBytes(artifact.artifactLengthInBytes)} +
        + + + + {artifact.hostname} + + + + + {artifact.downloadOptionTitle ?? t("no_download_option_info")} + + + + + {format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")} +
        - - - {artifact.hostname} - - - - - {format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")} - -
      - -
      - - -
      -
    • - ))} -
    -
    +
    + + +
    + + ))} + + + ); } diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index d1253318..afbcfd92 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -7,6 +7,7 @@ import { gameDetailsContext } from "@renderer/context"; import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal"; import { useDownload, useToast } from "@renderer/hooks"; import { RemoveGameFromLibraryModal } from "./remove-from-library-modal"; +import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react"; export interface GameOptionsModalProps { visible: boolean; @@ -94,6 +95,20 @@ export function GameOptionsModal({ await window.electron.openGameExecutablePath(game.id); }; + const handleChangeWinePrefixPath = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openDirectory"], + }); + + if (filePaths && filePaths.length > 0) { + await window.electron.selectGameWinePrefix(game.id, filePaths[0]); + await updateGame(); + } + }; + + const shouldShowWinePrefixConfiguration = + window.electron.platform === "darwin"; + return ( <> + {t("select_executable")} } @@ -155,12 +171,41 @@ export function GameOptionsModal({ )} + {shouldShowWinePrefixConfiguration && ( +
    +
    +

    {t("wine_prefix")}

    +

    + {t("wine_prefix_description")} +

    +
    + + + {t("select_executable")} + + } + /> +
    + )} +

    {t("downloads_secion_title")}

    {t("downloads_section_description")}

    +
    )}
    +

    {t("danger_zone_section_title")}

    {t("danger_zone_section_description")}

    +
    Date: Sat, 19 Oct 2024 13:29:14 -0300 Subject: [PATCH 130/163] fix: headers for auth window --- src/main/services/window-manager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index eaa05f7a..ecdf64f5 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -80,6 +80,10 @@ export class WindowManager { this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders( (details, callback) => { + if (details.webContentsId !== this.mainWindow?.webContents.id) { + return callback(details); + } + const userAgent = new UserAgent(); callback({ From 89bb099caaf0d94692463202296f1282241967b0 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 19 Oct 2024 13:45:21 -0300 Subject: [PATCH 131/163] feat: add link to game in achievements page --- .github/workflows/build.yml | 2 -- .../achievements/achievements-content.tsx | 24 +++++++++++++------ .../pages/achievements/achievements.css.ts | 4 ++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 908f6c80..05066c1c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20.11.1 - cache: "yarn" - name: Install dependencies run: yarn @@ -27,7 +26,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.9 - cache: "pip" - name: Install dependencies run: pip install -r requirements.txt diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index e61cf91d..09b3d311 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -4,7 +4,10 @@ import { steamUrlBuilder } from "@shared"; import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./achievements.css"; -import { formatDownloadProgress } from "@renderer/helpers"; +import { + buildGameDetailsPath, + formatDownloadProgress, +} from "@renderer/helpers"; import { CheckCircleIcon, LockIcon, @@ -16,6 +19,7 @@ import { gameDetailsContext } from "@renderer/context"; import { UserAchievement } from "@types"; import { average } from "color.js"; import Color from "color"; +import { Link } from "@renderer/components"; interface UserInfo { userId: string; @@ -95,7 +99,9 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { }} > -

    {t("subscription_needed")}

    +

    + {t("subscription_needed")} +

    - {gameTitle} + + {gameTitle} +
    diff --git a/src/renderer/src/pages/achievements/achievements.css.ts b/src/renderer/src/pages/achievements/achievements.css.ts index b6f42ce3..c4b66384 100644 --- a/src/renderer/src/pages/achievements/achievements.css.ts +++ b/src/renderer/src/pages/achievements/achievements.css.ts @@ -62,6 +62,10 @@ export const gameLogo = style({ width: LOGO_MAX_WIDTH, height: LOGO_HEIGHT, objectFit: "contain", + transition: "all ease 0.2s", + ":hover": { + transform: "scale(1.05)", + }, }); export const container = style({ From f0a2bf2f485e32b873791e67b2e1430a81c11d95 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 19 Oct 2024 17:23:26 -0300 Subject: [PATCH 132/163] feat: use new endpoint to get compared achievements --- src/main/events/index.ts | 1 + .../get-compared-unlocked-achievements.ts | 44 ++++ src/preload/index.ts | 11 + src/renderer/src/declaration.d.ts | 6 + src/renderer/src/helpers.ts | 4 +- .../achievements/achievements-content.tsx | 233 +++++------------- .../src/pages/achievements/achievements.tsx | 56 +++-- .../compared-achievement-list.tsx | 110 +++++++++ .../profile-content/profile-content.tsx | 2 - src/types/index.ts | 27 ++ 10 files changed, 291 insertions(+), 203 deletions(-) create mode 100644 src/main/events/user/get-compared-unlocked-achievements.ts create mode 100644 src/renderer/src/pages/achievements/compared-achievement-list.tsx diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 80363e4e..ffdfc354 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -50,6 +50,7 @@ import "./user/unblock-user"; import "./user/get-user-friends"; import "./user/get-user-stats"; import "./user/report-user"; +import "./user/get-compared-unlocked-achievements"; import "./profile/get-friend-requests"; import "./profile/get-me"; import "./profile/undo-friendship"; diff --git a/src/main/events/user/get-compared-unlocked-achievements.ts b/src/main/events/user/get-compared-unlocked-achievements.ts new file mode 100644 index 00000000..8c5c8779 --- /dev/null +++ b/src/main/events/user/get-compared-unlocked-achievements.ts @@ -0,0 +1,44 @@ +import type { ComparedAchievements, GameShop } from "@types"; +import { registerEvent } from "../register-event"; +import { userPreferencesRepository } from "@main/repository"; +import { HydraApi } from "@main/services"; + +const getComparedUnlockedAchievements = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop, + userId: string +) => { + const userPreferences = await userPreferencesRepository.findOne({ + where: { id: 1 }, + }); + + return HydraApi.get( + `/users/${userId}/games/achievements/compare`, + { + shop, + objectId, + language: userPreferences?.language || "en", + } + ).then((achievements) => { + const sortedAchievements = achievements.achievements.sort((a, b) => { + if (a.otherUserStat.unlocked && !b.otherUserStat.unlocked) return -1; + if (!a.otherUserStat.unlocked && b.otherUserStat.unlocked) return 1; + if (a.otherUserStat.unlocked && b.otherUserStat.unlocked) { + return b.otherUserStat.unlockTime! - a.otherUserStat.unlockTime!; + } + + return Number(a.hidden) - Number(b.hidden); + }); + + return { + ...achievements, + achievements: sortedAchievements, + } as ComparedAchievements; + }); +}; + +registerEvent( + "getComparedUnlockedAchievements", + getComparedUnlockedAchievements +); diff --git a/src/preload/index.ts b/src/preload/index.ts index fc2e5a91..0eadd409 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -259,6 +259,17 @@ contextBridge.exposeInMainWorld("electron", { getUserStats: (userId: string) => ipcRenderer.invoke("getUserStats", userId), reportUser: (userId: string, reason: string, description: string) => ipcRenderer.invoke("reportUser", userId, reason, description), + getComparedUnlockedAchievements: ( + objectId: string, + shop: GameShop, + userId: string + ) => + ipcRenderer.invoke( + "getComparedUnlockedAchievements", + objectId, + shop, + userId + ), /* Auth */ signOut: () => ipcRenderer.invoke("signOut"), diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 91bed316..e6a47959 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -29,6 +29,7 @@ import type { GameArtifact, LudusaviBackup, UserAchievement, + ComparedAchievements, } from "@types"; import type { AxiosProgressEvent } from "axios"; import type { DiskSpace } from "check-disk-space"; @@ -202,6 +203,11 @@ declare global { reason: string, description: string ) => Promise; + getComparedUnlockedAchievements: ( + objectId: string, + shop: GameShop, + userId: string + ) => Promise; /* Profile */ getMe: () => Promise; diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index d4563330..a241bf47 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -36,15 +36,13 @@ export const buildGameDetailsPath = ( export const buildGameAchievementPath = ( game: { shop: GameShop; objectId: string; title: string }, - user?: { userId: string; displayName: string; profileImageUrl: string | null } + user?: { userId: string } ) => { const searchParams = new URLSearchParams({ title: game.title, shop: game.shop, objectId: game.objectId, userId: user?.userId || "", - displayName: user?.displayName || "", - profileImageUrl: user?.profileImageUrl || "", }); return `/achievements/?${searchParams.toString()}`; diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index 09b3d311..ca60be70 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -1,40 +1,37 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useDate, useUserDetails } from "@renderer/hooks"; import { steamUrlBuilder } from "@shared"; -import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import * as styles from "./achievements.css"; import { buildGameDetailsPath, formatDownloadProgress, } from "@renderer/helpers"; -import { - CheckCircleIcon, - LockIcon, - PersonIcon, - TrophyIcon, -} from "@primer/octicons-react"; +import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; -import { UserAchievement } from "@types"; +import { ComparedAchievements, UserAchievement } from "@types"; import { average } from "color.js"; import Color from "color"; import { Link } from "@renderer/components"; +import { ComparedAchievementList } from "./compared-achievement-list"; interface UserInfo { userId: string; displayName: string; - achievements: UserAchievement[]; profileImageUrl: string | null; + totalAchievementCount: number; + unlockedAchievementCount: number; } interface AchievementsContentProps { otherUser: UserInfo | null; + comparedAchievements: ComparedAchievements | null; } interface AchievementListProps { - user: UserInfo; - otherUser: UserInfo | null; + achievements: UserAchievement[]; } interface AchievementSummaryProps { @@ -46,11 +43,6 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { const { t } = useTranslation("achievement"); const { userDetails, hasActiveSubscription } = useUserDetails(); - const userTotalAchievementCount = user.achievements.length; - const userUnlockedAchievementCount = user.achievements.filter( - (achievement) => achievement.unlocked - ).length; - const getProfileImage = (user: UserInfo) => { return (
    @@ -155,19 +147,19 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { > - {userUnlockedAchievementCount} / {userTotalAchievementCount} + {user.unlockedAchievementCount} / {user.totalAchievementCount}
    {formatDownloadProgress( - userUnlockedAchievementCount / userTotalAchievementCount + user.unlockedAchievementCount / user.totalAchievementCount )}
    @@ -175,132 +167,30 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { ); } -function AchievementList({ user, otherUser }: AchievementListProps) { - const achievements = user.achievements; - const otherUserAchievements = otherUser?.achievements; - +function AchievementList({ achievements }: AchievementListProps) { const { t } = useTranslation("achievement"); const { formatDateTime } = useDate(); - const { hasActiveSubscription } = useUserDetails(); - - if (!otherUserAchievements || otherUserAchievements.length === 0) { - return ( -
      - {achievements.map((achievement, index) => ( -
    • - {achievement.displayName} -
      -

      {achievement.displayName}

      -

      {achievement.description}

      -
      - {achievement.unlockTime && ( -
      - {t("unlocked_at")} -

      {formatDateTime(achievement.unlockTime)}

      -
      - )} -
    • - ))} -
    - ); - } - return (
      - {otherUserAchievements.map((otherUserAchievement, index) => ( -
    • -
      - {otherUserAchievement.displayName} -
      -

      {otherUserAchievement.displayName}

      -

      {otherUserAchievement.description}

      -
      + {achievements.map((achievement, index) => ( +
    • + {achievement.displayName} +
      +

      {achievement.displayName}

      +

      {achievement.description}

      - - {hasActiveSubscription ? ( - achievements[index].unlocked ? ( -
      - - {formatDateTime(achievements[index].unlockTime!)} -
      - ) : ( -
      - -
      - ) - ) : null} - - {otherUserAchievement.unlocked ? ( -
      - - {formatDateTime(otherUserAchievement.unlockTime!)} -
      - ) : ( -
      - + {achievement.unlockTime && ( +
      + {t("unlocked_at")} +

      {formatDateTime(achievement.unlockTime)}

      )}
    • @@ -309,7 +199,10 @@ function AchievementList({ user, otherUser }: AchievementListProps) { ); } -export function AchievementsContent({ otherUser }: AchievementsContentProps) { +export function AchievementsContent({ + otherUser, + comparedAchievements, +}: AchievementsContentProps) { const heroRef = useRef(null); const containerRef = useRef(null); const [isHeaderStuck, setIsHeaderStuck] = useState(false); @@ -317,20 +210,6 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } = useContext(gameDetailsContext); - const sortedAchievements = useMemo(() => { - if (!otherUser || otherUser.achievements.length === 0) return achievements!; - - return achievements!.sort((a, b) => { - const indexA = otherUser.achievements.findIndex( - (achievement) => achievement.name === a.name - ); - const indexB = otherUser.achievements.findIndex( - (achievement) => achievement.name === b.name - ); - return indexA - indexB; - }); - }, [achievements, otherUser]); - const dispatch = useAppDispatch(); const { userDetails, hasActiveSubscription } = useUserDetails(); @@ -367,14 +246,17 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { } }; - const getProfileImage = (user: UserInfo) => { + const getProfileImage = ( + profileImageUrl: string | null, + displayName: string + ) => { return (
      - {user.profileImageUrl ? ( + {profileImageUrl ? ( {user.displayName} ) : ( @@ -434,7 +316,13 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { user={{ ...userDetails, userId: userDetails.id, - achievements: sortedAchievements, + totalAchievementCount: comparedAchievements + ? comparedAchievements.ownerUser.totalAchievementCount + : achievements!.length, + unlockedAchievementCount: comparedAchievements + ? comparedAchievements.ownerUser.unlockedAchievementCount + : achievements!.filter((achievement) => achievement.unlocked) + .length, }} isComparison={otherUser !== null} /> @@ -458,15 +346,17 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) {
      {hasActiveSubscription && (
      - {getProfileImage({ - ...userDetails, - userId: userDetails.id, - achievements: sortedAchievements, - })} + {getProfileImage( + userDetails.profileImageUrl, + userDetails.displayName + )}
      )}
      - {getProfileImage(otherUser)} + {getProfileImage( + otherUser.profileImageUrl, + otherUser.displayName + )}
      @@ -480,14 +370,11 @@ export function AchievementsContent({ otherUser }: AchievementsContentProps) { backgroundColor: vars.color.background, }} > - + {otherUser ? ( + + ) : ( + + )} diff --git a/src/renderer/src/pages/achievements/achievements.tsx b/src/renderer/src/pages/achievements/achievements.tsx index a53eaba8..d15ba0b9 100644 --- a/src/renderer/src/pages/achievements/achievements.tsx +++ b/src/renderer/src/pages/achievements/achievements.tsx @@ -1,7 +1,7 @@ import { setHeaderTitle } from "@renderer/features"; import { useAppDispatch, useUserDetails } from "@renderer/hooks"; -import type { GameShop, UserAchievement } from "@types"; -import { useEffect, useState } from "react"; +import type { ComparedAchievements, GameShop } from "@types"; +import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { vars } from "@renderer/theme.css"; import { @@ -18,14 +18,11 @@ export default function Achievements() { const shop = searchParams.get("shop"); const title = searchParams.get("title"); const userId = searchParams.get("userId"); - const displayName = searchParams.get("displayName"); - const profileImageUrl = searchParams.get("profileImageUrl"); const { userDetails } = useUserDetails(); - const [otherUserAchievements, setOtherUserAchievements] = useState< - UserAchievement[] | null - >(null); + const [comparedAchievements, setComparedAchievements] = + useState(null); const dispatch = useAppDispatch(); @@ -36,31 +33,34 @@ export default function Achievements() { }, [dispatch, title]); useEffect(() => { - setOtherUserAchievements(null); + setComparedAchievements(null); + if (userDetails?.id == userId) { - setOtherUserAchievements([]); return; } if (objectId && shop && userId) { window.electron - .getGameAchievements(objectId, shop as GameShop, userId) - .then((achievements) => { - setOtherUserAchievements(achievements); - }); + .getComparedUnlockedAchievements(objectId, shop as GameShop, userId) + .then(setComparedAchievements); } }, [objectId, shop, userId]); const otherUserId = userDetails?.id === userId ? null : userId; - const otherUser = otherUserId - ? { - userId: otherUserId, - displayName: displayName || "", - achievements: otherUserAchievements || [], - profileImageUrl: profileImageUrl || "", - } - : null; + const otherUser = useMemo(() => { + if (!otherUserId || !comparedAchievements) return null; + + return { + userId: otherUserId, + displayName: comparedAchievements.otherUser.displayName, + profileImageUrl: comparedAchievements.otherUser.profileImageUrl, + totalAchievementCount: + comparedAchievements.otherUser.totalAchievementCount, + unlockedAchievementCount: + comparedAchievements.otherUser.unlockedAchievementCount, + }; + }, [otherUserId, comparedAchievements]); return ( {({ isLoading, achievements }) => { + const showSkeleton = + isLoading || + achievements === null || + (otherUserId && comparedAchievements === null); + return ( - {isLoading || - achievements === null || - (otherUserId && otherUserAchievements === null) ? ( + {showSkeleton ? ( ) : ( - + )} ); diff --git a/src/renderer/src/pages/achievements/compared-achievement-list.tsx b/src/renderer/src/pages/achievements/compared-achievement-list.tsx new file mode 100644 index 00000000..6c0484c3 --- /dev/null +++ b/src/renderer/src/pages/achievements/compared-achievement-list.tsx @@ -0,0 +1,110 @@ +import type { ComparedAchievements } from "@types"; +import * as styles from "./achievements.css"; +import { CheckCircleIcon, LockIcon } from "@primer/octicons-react"; +import { useDate } from "@renderer/hooks"; +import { SPACING_UNIT } from "@renderer/theme.css"; + +export interface ComparedAchievementListProps { + achievements: ComparedAchievements; +} + +export function ComparedAchievementList({ + achievements, +}: ComparedAchievementListProps) { + const { formatDateTime } = useDate(); + + return ( +
        + {achievements.achievements.map((achievement, index) => ( +
      • +
        + {achievement.displayName} +
        +

        {achievement.displayName}

        +

        {achievement.description}

        +
        +
        + + {achievement.onwerUserStat ? ( + achievement.onwerUserStat.unlocked ? ( +
        + + + {formatDateTime(achievement.onwerUserStat.unlockTime!)} + +
        + ) : ( +
        + +
        + ) + ) : null} + + {achievement.otherUserStat.unlocked ? ( +
        + + + {formatDateTime(achievement.otherUserStat.unlockTime!)} + +
        + ) : ( +
        + +
        + )} +
      • + ))} +
      + ); +} diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index de22313e..a989b81d 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -56,8 +56,6 @@ export function ProfileContent() { const userParams = userProfile ? { userId: userProfile.id, - displayName: userProfile.displayName, - profileImageUrl: userProfile.profileImageUrl, } : undefined; diff --git a/src/types/index.ts b/src/types/index.ts index 6da26914..c3a91053 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -342,6 +342,33 @@ export interface GameArtifact { downloadCount: number; } +export interface ComparedAchievements { + ownerUser: { + totalAchievementCount: number; + unlockedAchievementCount: number; + }; + otherUser: { + displayName: string; + profileImageUrl: string; + totalAchievementCount: number; + unlockedAchievementCount: number; + }; + achievements: { + hidden: boolean; + icon: string; + displayName: string; + description: string; + onwerUserStat?: { + unlocked: boolean; + unlockTime: number; + }; + otherUserStat: { + unlocked: boolean; + unlockTime: number; + }; + }[]; +} + export * from "./steam.types"; export * from "./real-debrid.types"; export * from "./ludusavi.types"; From b7c9b5ec549e647c632145d8b43e093c824ef354 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 20 Oct 2024 08:09:15 +0100 Subject: [PATCH 133/163] fix: fixing multiple calls for backup --- .../cloud-save/get-game-backup-preview.ts | 1 + .../achievements/achievement-watcher.ts | 10 +- .../update-local-unlocked-achivements.ts | 2 +- src/main/services/hydra-api.ts | 113 +++++++++--------- src/main/services/ludusavi.ts | 3 + .../context/cloud-sync/cloud-sync.context.tsx | 47 +++++--- .../cloud-sync-files-modal.tsx | 28 +---- .../cloud-sync-modal/cloud-sync-modal.tsx | 7 +- .../modals/game-options-modal.tsx | 2 +- .../profile/profile-hero/profile-hero.css.ts | 2 + 10 files changed, 101 insertions(+), 114 deletions(-) diff --git a/src/main/events/cloud-save/get-game-backup-preview.ts b/src/main/events/cloud-save/get-game-backup-preview.ts index 433fccc4..ee95059f 100644 --- a/src/main/events/cloud-save/get-game-backup-preview.ts +++ b/src/main/events/cloud-save/get-game-backup-preview.ts @@ -11,6 +11,7 @@ const getGameBackupPreview = async ( ) => { const backupPath = path.join(backupsPath, `${shop}-${objectId}`); + console.log("preview invoked>>"); return Ludusavi.getBackupPreview(shop, objectId, backupPath); }; diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index ac078468..d625a927 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -36,11 +36,11 @@ export const watchAchievements = async () => { if (!gameAchievementFiles.length) continue; - console.log( - "Achievements files to observe for:", - game.title, - gameAchievementFiles - ); + // console.log( + // "Achievements files to observe for:", + // game.title, + // gameAchievementFiles + // ); for (const file of gameAchievementFiles) { compareFile(game, file); diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index 13f33fcd..a11c3487 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -72,7 +72,7 @@ export const updateLocalUnlockedAchivements = async (game: Game) => { gameAchievementFiles.push(...achievementFileInsideDirectory); - console.log("Achievements files for", game.title, gameAchievementFiles); + // console.log("Achievements files for", game.title, gameAchievementFiles); const unlockedAchievements: UnlockedAchievement[] = []; diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index c8f3795e..4ae6a32e 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -12,7 +12,7 @@ import { UserNotLoggedInError, UserWithoutCloudSubscriptionError, } from "@shared"; -import { omit } from "lodash-es"; +// import { omit } from "lodash-es"; import { appVersion } from "@main/constants"; interface HydraApiOptions { @@ -109,64 +109,59 @@ export class HydraApi { }); if (this.ADD_LOG_INTERCEPTOR) { - this.instance.interceptors.request.use( - (request) => { - logger.log(" ---- REQUEST -----"); - const data = Array.isArray(request.data) - ? request.data - : omit(request.data, ["refreshToken"]); - logger.log(request.method, request.url, request.params, data); - return request; - }, - (error) => { - logger.error("request error", error); - return Promise.reject(error); - } - ); - - this.instance.interceptors.response.use( - (response) => { - logger.log(" ---- RESPONSE -----"); - const data = Array.isArray(response.data) - ? response.data - : omit(response.data, ["username", "accessToken", "refreshToken"]); - logger.log( - response.status, - response.config.method, - response.config.url, - data - ); - return response; - }, - (error) => { - logger.error(" ---- RESPONSE ERROR -----"); - - const { config } = error; - - logger.error( - config.method, - config.baseURL, - config.url, - config.headers, - config.data - ); - - if (error.response) { - logger.error( - "Response", - error.response.status, - error.response.data - ); - } else if (error.request) { - logger.error("Request", error.request); - } else { - logger.error("Error", error.message); - } - - logger.error(" ----- END RESPONSE ERROR -------"); - return Promise.reject(error); - } - ); + // this.instance.interceptors.request.use( + // (request) => { + // logger.log(" ---- REQUEST -----"); + // const data = Array.isArray(request.data) + // ? request.data + // : omit(request.data, ["refreshToken"]); + // logger.log(request.method, request.url, request.params, data); + // return request; + // }, + // (error) => { + // logger.error("request error", error); + // return Promise.reject(error); + // } + // ); + // this.instance.interceptors.response.use( + // (response) => { + // logger.log(" ---- RESPONSE -----"); + // const data = Array.isArray(response.data) + // ? response.data + // : omit(response.data, ["username", "accessToken", "refreshToken"]); + // logger.log( + // response.status, + // response.config.method, + // response.config.url, + // data + // ); + // return response; + // }, + // (error) => { + // logger.error(" ---- RESPONSE ERROR -----"); + // const { config } = error; + // logger.error( + // config.method, + // config.baseURL, + // config.url, + // config.headers, + // config.data + // ); + // if (error.response) { + // logger.error( + // "Response", + // error.response.status, + // error.response.data + // ); + // } else if (error.request) { + // logger.error("Request", error.request); + // } else { + // logger.error("Error", error.message); + // } + // logger.error(" ----- END RESPONSE ERROR -------"); + // return Promise.reject(error); + // } + // ); } const userAuth = await userAuthRepository.findOne({ diff --git a/src/main/services/ludusavi.ts b/src/main/services/ludusavi.ts index 91633e36..04f16875 100644 --- a/src/main/services/ludusavi.ts +++ b/src/main/services/ludusavi.ts @@ -66,13 +66,16 @@ export class Ludusavi { objectId: string, backupPath: string ): Promise { + console.log("a"); const games = await this.findGames(shop, objectId); if (!games.length) return null; + console.log("b"); const backupData = await this.worker.run( { title: games[0], backupPath, preview: true }, { name: "backupGame" } ); + console.log("c"); return backupData; } diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx index 7b102918..6db61332 100644 --- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useMemo, + useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; @@ -66,6 +67,8 @@ export function CloudSyncContextProvider({ }: CloudSyncContextProviderProps) { const { t } = useTranslation("game_details"); + const backupPreviewLock = useRef(""); + const [artifacts, setArtifacts] = useState([]); const [showCloudSyncModal, setShowCloudSyncModal] = useState(false); const [backupPreview, setBackupPreview] = useState( @@ -86,26 +89,32 @@ export function CloudSyncContextProvider({ ); const getGameBackupPreview = useCallback(async () => { - await Promise.allSettled([ - window.electron.getGameArtifacts(objectId, shop).then((results) => { - setArtifacts(results); - }), - window.electron - .getGameBackupPreview(objectId, shop) - .then((preview) => { - if (preview && Object.keys(preview.games).length) { - setBackupPreview(preview); - } - }) - .catch((err) => { - logger.error( - "Failed to get game backup preview", - objectId, - shop, - err - ); + const backupPreviewLockKey = `${objectId}-${shop}`; + + if (backupPreviewLock.current !== backupPreviewLockKey) { + backupPreviewLock.current = backupPreviewLockKey; + await Promise.allSettled([ + window.electron.getGameArtifacts(objectId, shop).then((results) => { + setArtifacts(results); }), - ]); + window.electron + .getGameBackupPreview(objectId, shop) + .then((preview) => { + backupPreviewLock.current = ""; + if (preview && Object.keys(preview.games).length) { + setBackupPreview(preview); + } + }) + .catch((err) => { + logger.error( + "Failed to get game backup preview", + objectId, + shop, + err + ); + }), + ]); + } }, [objectId, shop]); const uploadSaveGame = useCallback( diff --git a/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx index 900e96c5..ab416773 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx @@ -1,7 +1,7 @@ -import { Button, Modal, ModalProps, TextField } from "@renderer/components"; +import { Modal, ModalProps } from "@renderer/components"; import { useContext, useMemo } from "react"; import { cloudSyncContext } from "@renderer/context"; -import { useTranslation } from "react-i18next"; +// import { useTranslation } from "react-i18next"; export interface CloudSyncFilesModalProps extends Omit {} @@ -12,7 +12,7 @@ export function CloudSyncFilesModal({ }: CloudSyncFilesModalProps) { const { backupPreview } = useContext(cloudSyncContext); - const { t } = useTranslation("game_details"); + // const { t } = useTranslation("game_details"); const files = useMemo(() => { if (!backupPreview) { @@ -27,24 +27,6 @@ export function CloudSyncFilesModal({ }); }, [backupPreview]); - const handleChangeExecutableLocation = async () => { - const path = await selectGameExecutable(); - - if (path) { - const gameUsingPath = - await window.electron.verifyExecutablePathInUse(path); - - if (gameUsingPath) { - showErrorToast( - t("executable_path_in_use", { game: gameUsingPath.title }) - ); - return; - } - - window.electron.updateExecutablePath(game.id, path).then(updateGame); - } - }; - return ( */} - } - /> + /> */}
    diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index f636d9d2..87260f59 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -1,9 +1,4 @@ -import { - Button, - ConfirmationModal, - Modal, - ModalProps, -} from "@renderer/components"; +import { Button, Modal, ModalProps } from "@renderer/components"; import { useContext, useEffect, useMemo, useState } from "react"; import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index afbcfd92..64346d52 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -107,7 +107,7 @@ export function GameOptionsModal({ }; const shouldShowWinePrefixConfiguration = - window.electron.platform === "darwin"; + window.electron.platform === "linux"; return ( <> diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts index 88857c08..fd02d11f 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts @@ -52,6 +52,8 @@ export const profileDisplayName = style({ display: "flex", alignItems: "center", position: "relative", + textShadow: + "0 0 40px rgb(0 0 0), 0 0 20px rgb(0 0 0 / 50%), 0 0 10px rgb(0 0 0 / 20%)", }); export const heroPanel = style({ From fbae552b1b883717780021c618f1ffb0466d7df0 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 20 Oct 2024 08:55:41 +0100 Subject: [PATCH 134/163] feat: adding file parser --- src/main/services/ludusavi.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/services/ludusavi.ts b/src/main/services/ludusavi.ts index 04f16875..6c338a48 100644 --- a/src/main/services/ludusavi.ts +++ b/src/main/services/ludusavi.ts @@ -7,6 +7,9 @@ import path from "node:path"; import YAML from "yaml"; import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath"; +import axios from "axios"; + +let a = null; export class Ludusavi { private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi"); @@ -66,16 +69,27 @@ export class Ludusavi { objectId: string, backupPath: string ): Promise { - console.log("a"); - const games = await this.findGames(shop, objectId); - if (!games.length) return null; - console.log("b"); + if (!a) { + await axios + .get( + "https://gist.githubusercontent.com/thegrannychaseroperation/b23d53e654e3ea060066a5c01b0cacc8/raw/57bf254a1c99dab9315136f660ff7b3d547de215/keys.json" + ) + .then((response) => { + a = response.data; + return response.data; + }); + } + + const game = a[objectId]; + + // if (!games.length) return null; + + // const [game] = games; const backupData = await this.worker.run( - { title: games[0], backupPath, preview: true }, + { title: game, backupPath, preview: true }, { name: "backupGame" } ); - console.log("c"); return backupData; } From ded56c518da45c56aba20311ee8c849cd38c34fe Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 20 Oct 2024 08:56:52 +0100 Subject: [PATCH 135/163] feat: adding file parser --- src/main/services/ludusavi.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/services/ludusavi.ts b/src/main/services/ludusavi.ts index 6c338a48..8d0b2ee9 100644 --- a/src/main/services/ludusavi.ts +++ b/src/main/services/ludusavi.ts @@ -9,7 +9,7 @@ import YAML from "yaml"; import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath"; import axios from "axios"; -let a = null; +let a: Record | null = null; export class Ludusavi { private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi"); @@ -65,7 +65,7 @@ export class Ludusavi { } static async getBackupPreview( - shop: GameShop, + _shop: GameShop, objectId: string, backupPath: string ): Promise { @@ -80,7 +80,7 @@ export class Ludusavi { }); } - const game = a[objectId]; + const game = a?.[objectId]; // if (!games.length) return null; From 993b35cf3bdc3433963c6800b695959f055247c4 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:48:04 -0300 Subject: [PATCH 136/163] feat: adjust rld achievement --- .../achievements/merge-achievements.ts | 2 +- .../achievements/parse-achievement-file.ts | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 7b03723f..367a5550 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -120,7 +120,7 @@ export const mergeAchievements = async ( const mergedLocalAchievements = unlockedAchievements.concat(newAchievements); - if (game?.remoteId) { + if (game.remoteId) { return HydraApi.put( "/profile/games/achievements", { diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index 9c875a3f..eb6321a3 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -242,15 +242,23 @@ const processRld = (unlockedAchievements: any): UnlockedAchievement[] => { const unlockedAchievement = unlockedAchievements[achievement]; if (unlockedAchievement?.State) { - newUnlockedAchievements.push({ - name: achievement, - unlockTime: - new DataView( - new Uint8Array( - Buffer.from(unlockedAchievement.Time.toString(), "hex") - ).buffer - ).getUint32(0, true) * 1000, - }); + const unlocked = new DataView( + new Uint8Array( + Buffer.from(unlockedAchievement.State.toString(), "hex") + ).buffer + ).getUint32(0, true); + + if (unlocked === 1) { + newUnlockedAchievements.push({ + name: achievement, + unlockTime: + new DataView( + new Uint8Array( + Buffer.from(unlockedAchievement.Time.toString(), "hex") + ).buffer + ).getUint32(0, true) * 1000, + }); + } } } From 1d7858438d1dfe768422a8cf4ed958adba26ccae Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:55:08 -0300 Subject: [PATCH 137/163] feat: update logs for achievements --- .../services/achievements/achievement-watcher.ts | 15 ++++++++++----- .../update-local-unlocked-achivements.ts | 14 +++++++------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index d625a927..269e85e9 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -9,7 +9,7 @@ import { getAlternativeObjectIds, } from "./find-achivement-files"; import type { AchievementFile } from "@types"; -import { achievementsLogger, logger } from "../logger"; +import { achievementsLogger } from "../logger"; import { Cracker } from "@shared"; const fileStats: Map = new Map(); @@ -55,8 +55,6 @@ const processAchievementFileDiff = async ( ) => { const unlockedAchievements = parseAchievementFile(file.filePath, file.type); - logger.log("Achievements from file", file.filePath, unlockedAchievements); - if (unlockedAchievements.length) { return mergeAchievements( game.objectID, @@ -80,7 +78,7 @@ const compareFltFolder = async (game: Game, file: AchievementFile) => { return; } - logger.log("Detected change in FLT folder", file.filePath); + achievementsLogger.log("Detected change in FLT folder", file.filePath); await processAchievementFileDiff(game, file); } catch (err) { achievementsLogger.error(err); @@ -101,6 +99,13 @@ const compareFile = async (game: Game, file: AchievementFile) => { if (!previousStat) { if (currentStat.mtimeMs) { + achievementsLogger.log( + "First change in file", + file.filePath, + previousStat, + currentStat.mtimeMs + ); + await processAchievementFileDiff(game, file); return; } @@ -110,7 +115,7 @@ const compareFile = async (game: Game, file: AchievementFile) => { return; } - logger.log( + achievementsLogger.log( "Detected change in file", file.filePath, previousStat, diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index a11c3487..38c1fa2d 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -49,14 +49,14 @@ export const updateAllLocalUnlockedAchievements = async () => { if (parsedAchievements.length) { unlockedAchievements.push(...parsedAchievements); - } - achievementsLogger.log( - "Achievement file for", - game.title, - achievementFile.filePath, - parsedAchievements - ); + achievementsLogger.log( + "Achievement file for", + game.title, + achievementFile.filePath, + parsedAchievements + ); + } } mergeAchievements(game.objectID, "steam", unlockedAchievements, false); From 735b540af42295f5409fdc2420495d89c1486112 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 20 Oct 2024 18:11:13 +0100 Subject: [PATCH 138/163] feat: adding console.log --- .../cloud-save/get-game-backup-preview.ts | 1 - .../achievements/achievement-watcher.ts | 10 +- .../update-local-unlocked-achivements.ts | 2 +- src/main/services/hydra-api.ts | 108 +++++++++--------- .../components/bottom-panel/bottom-panel.tsx | 4 +- .../upload-background-image-button.tsx | 2 +- 6 files changed, 63 insertions(+), 64 deletions(-) diff --git a/src/main/events/cloud-save/get-game-backup-preview.ts b/src/main/events/cloud-save/get-game-backup-preview.ts index ee95059f..433fccc4 100644 --- a/src/main/events/cloud-save/get-game-backup-preview.ts +++ b/src/main/events/cloud-save/get-game-backup-preview.ts @@ -11,7 +11,6 @@ const getGameBackupPreview = async ( ) => { const backupPath = path.join(backupsPath, `${shop}-${objectId}`); - console.log("preview invoked>>"); return Ludusavi.getBackupPreview(shop, objectId, backupPath); }; diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index d625a927..ac078468 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -36,11 +36,11 @@ export const watchAchievements = async () => { if (!gameAchievementFiles.length) continue; - // console.log( - // "Achievements files to observe for:", - // game.title, - // gameAchievementFiles - // ); + console.log( + "Achievements files to observe for:", + game.title, + gameAchievementFiles + ); for (const file of gameAchievementFiles) { compareFile(game, file); diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index a11c3487..13f33fcd 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -72,7 +72,7 @@ export const updateLocalUnlockedAchivements = async (game: Game) => { gameAchievementFiles.push(...achievementFileInsideDirectory); - // console.log("Achievements files for", game.title, gameAchievementFiles); + console.log("Achievements files for", game.title, gameAchievementFiles); const unlockedAchievements: UnlockedAchievement[] = []; diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 4ae6a32e..f5c241fc 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -12,7 +12,7 @@ import { UserNotLoggedInError, UserWithoutCloudSubscriptionError, } from "@shared"; -// import { omit } from "lodash-es"; +import { omit } from "lodash-es"; import { appVersion } from "@main/constants"; interface HydraApiOptions { @@ -109,59 +109,59 @@ export class HydraApi { }); if (this.ADD_LOG_INTERCEPTOR) { - // this.instance.interceptors.request.use( - // (request) => { - // logger.log(" ---- REQUEST -----"); - // const data = Array.isArray(request.data) - // ? request.data - // : omit(request.data, ["refreshToken"]); - // logger.log(request.method, request.url, request.params, data); - // return request; - // }, - // (error) => { - // logger.error("request error", error); - // return Promise.reject(error); - // } - // ); - // this.instance.interceptors.response.use( - // (response) => { - // logger.log(" ---- RESPONSE -----"); - // const data = Array.isArray(response.data) - // ? response.data - // : omit(response.data, ["username", "accessToken", "refreshToken"]); - // logger.log( - // response.status, - // response.config.method, - // response.config.url, - // data - // ); - // return response; - // }, - // (error) => { - // logger.error(" ---- RESPONSE ERROR -----"); - // const { config } = error; - // logger.error( - // config.method, - // config.baseURL, - // config.url, - // config.headers, - // config.data - // ); - // if (error.response) { - // logger.error( - // "Response", - // error.response.status, - // error.response.data - // ); - // } else if (error.request) { - // logger.error("Request", error.request); - // } else { - // logger.error("Error", error.message); - // } - // logger.error(" ----- END RESPONSE ERROR -------"); - // return Promise.reject(error); - // } - // ); + this.instance.interceptors.request.use( + (request) => { + logger.log(" ---- REQUEST -----"); + const data = Array.isArray(request.data) + ? request.data + : omit(request.data, ["refreshToken"]); + logger.log(request.method, request.url, request.params, data); + return request; + }, + (error) => { + logger.error("request error", error); + return Promise.reject(error); + } + ); + this.instance.interceptors.response.use( + (response) => { + logger.log(" ---- RESPONSE -----"); + const data = Array.isArray(response.data) + ? response.data + : omit(response.data, ["username", "accessToken", "refreshToken"]); + logger.log( + response.status, + response.config.method, + response.config.url, + data + ); + return response; + }, + (error) => { + logger.error(" ---- RESPONSE ERROR -----"); + const { config } = error; + logger.error( + config.method, + config.baseURL, + config.url, + config.headers, + config.data + ); + if (error.response) { + logger.error( + "Response", + error.response.status, + error.response.data + ); + } else if (error.request) { + logger.error("Request", error.request); + } else { + logger.error("Error", error.message); + } + logger.error(" ----- END RESPONSE ERROR -------"); + return Promise.reject(error); + } + ); } const userAuth = await userAuthRepository.findOne({ diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index 0d28a26e..045158a5 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -81,10 +81,10 @@ export function BottomPanel() { {status} - + {/* {sessionHash ? `${sessionHash} -` : ""} v{version} " {VERSION_CODENAME}" - + */} ); } diff --git a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx index d0b1e013..a11c9069 100644 --- a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx +++ b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx @@ -54,7 +54,7 @@ export function UploadBackgroundImageButton() { disabled={isUploadingBackgroundImage} > - {isUploadingBackgroundImage ? "Uploading..." : "Upload background"} + {isUploadingBackgroundImage ? "Uploading..." : "Atualizar banner"} ); } From 33e91e20072dd29b98ca037e842035db3a279b88 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:37:30 -0300 Subject: [PATCH 139/163] feat: update compare achievement response --- .../get-compared-unlocked-achievements.ts | 8 ++--- .../achievements/achievement-watcher.ts | 31 ++++++++++++++----- .../achievements/achievements-content.tsx | 4 +-- .../src/pages/achievements/achievements.tsx | 9 +++--- .../compared-achievement-list.tsx | 12 +++---- src/types/index.ts | 8 ++--- 6 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/main/events/user/get-compared-unlocked-achievements.ts b/src/main/events/user/get-compared-unlocked-achievements.ts index 8c5c8779..0c117140 100644 --- a/src/main/events/user/get-compared-unlocked-achievements.ts +++ b/src/main/events/user/get-compared-unlocked-achievements.ts @@ -22,10 +22,10 @@ const getComparedUnlockedAchievements = async ( } ).then((achievements) => { const sortedAchievements = achievements.achievements.sort((a, b) => { - if (a.otherUserStat.unlocked && !b.otherUserStat.unlocked) return -1; - if (!a.otherUserStat.unlocked && b.otherUserStat.unlocked) return 1; - if (a.otherUserStat.unlocked && b.otherUserStat.unlocked) { - return b.otherUserStat.unlockTime! - a.otherUserStat.unlockTime!; + if (a.targetStat.unlocked && !b.targetStat.unlocked) return -1; + if (!a.targetStat.unlocked && b.targetStat.unlocked) return 1; + if (a.targetStat.unlocked && b.targetStat.unlocked) { + return b.targetStat.unlockTime! - a.targetStat.unlockTime!; } return Number(a.hidden) - Number(b.hidden); diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index 269e85e9..7e0009eb 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -11,11 +11,12 @@ import { import type { AchievementFile } from "@types"; import { achievementsLogger } from "../logger"; import { Cracker } from "@shared"; +import { IsNull, Not } from "typeorm"; const fileStats: Map = new Map(); const fltFiles: Map> = new Map(); -export const watchAchievements = async () => { +const watchAchiievementsWindows = async () => { const games = await gameRepository.find({ where: { isDeleted: false, @@ -23,7 +24,6 @@ export const watchAchievements = async () => { }); if (games.length === 0) return; - const achievementFiles = findAllAchievementFiles(); for (const game of games) { @@ -36,12 +36,6 @@ export const watchAchievements = async () => { if (!gameAchievementFiles.length) continue; - // console.log( - // "Achievements files to observe for:", - // game.title, - // gameAchievementFiles - // ); - for (const file of gameAchievementFiles) { compareFile(game, file); } @@ -49,6 +43,27 @@ export const watchAchievements = async () => { } }; +const watchAchievementsWithWine = async () => { + const games = await gameRepository.find({ + where: { + isDeleted: false, + winePrefixPath: Not(IsNull()), + }, + }); + + if (games.length === 0) return; + + // TODO: watch achievements with wine +}; + +export const watchAchievements = async () => { + if (process.platform === "win32") { + return watchAchiievementsWindows(); + } + + watchAchievementsWithWine(); +}; + const processAchievementFileDiff = async ( game: Game, file: AchievementFile diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index ca60be70..7a37358b 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -317,10 +317,10 @@ export function AchievementsContent({ ...userDetails, userId: userDetails.id, totalAchievementCount: comparedAchievements - ? comparedAchievements.ownerUser.totalAchievementCount + ? comparedAchievements.owner.totalAchievementCount : achievements!.length, unlockedAchievementCount: comparedAchievements - ? comparedAchievements.ownerUser.unlockedAchievementCount + ? comparedAchievements.owner.unlockedAchievementCount : achievements!.filter((achievement) => achievement.unlocked) .length, }} diff --git a/src/renderer/src/pages/achievements/achievements.tsx b/src/renderer/src/pages/achievements/achievements.tsx index d15ba0b9..c831dd0e 100644 --- a/src/renderer/src/pages/achievements/achievements.tsx +++ b/src/renderer/src/pages/achievements/achievements.tsx @@ -53,12 +53,11 @@ export default function Achievements() { return { userId: otherUserId, - displayName: comparedAchievements.otherUser.displayName, - profileImageUrl: comparedAchievements.otherUser.profileImageUrl, - totalAchievementCount: - comparedAchievements.otherUser.totalAchievementCount, + displayName: comparedAchievements.target.displayName, + profileImageUrl: comparedAchievements.target.profileImageUrl, + totalAchievementCount: comparedAchievements.target.totalAchievementCount, unlockedAchievementCount: - comparedAchievements.otherUser.unlockedAchievementCount, + comparedAchievements.target.unlockedAchievementCount, }; }, [otherUserId, comparedAchievements]); diff --git a/src/renderer/src/pages/achievements/compared-achievement-list.tsx b/src/renderer/src/pages/achievements/compared-achievement-list.tsx index 6c0484c3..21f11936 100644 --- a/src/renderer/src/pages/achievements/compared-achievement-list.tsx +++ b/src/renderer/src/pages/achievements/compared-achievement-list.tsx @@ -21,7 +21,7 @@ export function ComparedAchievementList({ className={styles.listItem} style={{ display: "grid", - gridTemplateColumns: achievement.onwerUserStat + gridTemplateColumns: achievement.ownerStat ? "3fr 1fr 1fr" : "3fr 2fr", }} @@ -48,8 +48,8 @@ export function ComparedAchievementList({ - {achievement.onwerUserStat ? ( - achievement.onwerUserStat.unlocked ? ( + {achievement.ownerStat ? ( + achievement.ownerStat.unlocked ? (
    - {formatDateTime(achievement.onwerUserStat.unlockTime!)} + {formatDateTime(achievement.ownerStat.unlockTime!)}
    ) : ( @@ -77,7 +77,7 @@ export function ComparedAchievementList({ ) ) : null} - {achievement.otherUserStat.unlocked ? ( + {achievement.targetStat.unlocked ? (
    - {formatDateTime(achievement.otherUserStat.unlockTime!)} + {formatDateTime(achievement.targetStat.unlockTime!)}
    ) : ( diff --git a/src/types/index.ts b/src/types/index.ts index c3a91053..41a51e40 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -343,11 +343,11 @@ export interface GameArtifact { } export interface ComparedAchievements { - ownerUser: { + owner: { totalAchievementCount: number; unlockedAchievementCount: number; }; - otherUser: { + target: { displayName: string; profileImageUrl: string; totalAchievementCount: number; @@ -358,11 +358,11 @@ export interface ComparedAchievements { icon: string; displayName: string; description: string; - onwerUserStat?: { + ownerStat?: { unlocked: boolean; unlockTime: number; }; - otherUserStat: { + targetStat: { unlocked: boolean; unlockTime: number; }; From 36e6a8cef7be6201836abf6b667904f35177aa21 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 20 Oct 2024 21:29:17 -0300 Subject: [PATCH 140/163] feat: refactor --- .../events/autoupdater/check-for-updates.ts | 2 +- .../events/catalogue/get-game-achievements.ts | 147 ------------------ src/main/events/catalogue/get-game-stats.ts | 4 +- .../events/catalogue/get-trending-games.ts | 2 +- src/main/events/catalogue/search-games.ts | 2 +- .../cloud-save/get-game-backup-preview.ts | 2 +- .../events/cloud-save/upload-save-game.ts | 2 +- src/main/events/index.ts | 2 +- .../events/profile/get-friend-requests.ts | 2 +- src/main/events/profile/get-me.ts | 2 +- .../events/profile/sync-friend-requests.ts | 2 +- .../events/profile/update-friend-request.ts | 2 +- src/main/events/user/get-blocked-users.ts | 2 +- .../events/user/get-unlocked-achievements.ts | 68 ++++++++ src/main/events/user/get-user-friends.ts | 2 +- .../achievements/achievement-watcher.ts | 20 ++- .../achievements/get-game-achievement-data.ts | 4 +- .../achievements/merge-achievements.ts | 10 +- src/main/services/download/python-instance.ts | 2 +- src/main/workers/steam-games.worker.ts | 2 +- src/preload/index.ts | 4 +- .../header/auto-update-sub-header.tsx | 2 +- src/renderer/src/components/hero/hero.tsx | 2 +- .../game-details/game-details.context.tsx | 17 +- src/renderer/src/declaration.d.ts | 9 +- .../src/features/running-game-slice.ts | 2 +- .../achievements/achievements-content.tsx | 58 +++---- .../achievements/achievements-skeleton.tsx | 2 +- .../pages/achievements/achievements.css.ts | 12 +- .../src/pages/achievements/achievements.tsx | 2 +- .../src/pages/downloads/downloads.tsx | 2 +- .../src/pages/game-details/game-details.tsx | 2 +- .../modals/remove-from-library-modal.tsx | 2 +- .../profile-content/profile-content.tsx | 2 +- .../settings/add-download-source-modal.tsx | 2 +- .../user-friend-modal-list.tsx | 2 +- src/renderer/src/workers/repacks.worker.ts | 2 +- 37 files changed, 151 insertions(+), 254 deletions(-) delete mode 100644 src/main/events/catalogue/get-game-achievements.ts create mode 100644 src/main/events/user/get-unlocked-achievements.ts diff --git a/src/main/events/autoupdater/check-for-updates.ts b/src/main/events/autoupdater/check-for-updates.ts index 6c8d3cb0..1dcc80f3 100644 --- a/src/main/events/autoupdater/check-for-updates.ts +++ b/src/main/events/autoupdater/check-for-updates.ts @@ -1,4 +1,4 @@ -import { AppUpdaterEvent } from "@types"; +import type { AppUpdaterEvent } from "@types"; import { registerEvent } from "../register-event"; import updater, { UpdateInfo } from "electron-updater"; import { WindowManager } from "@main/services"; diff --git a/src/main/events/catalogue/get-game-achievements.ts b/src/main/events/catalogue/get-game-achievements.ts deleted file mode 100644 index 734366c8..00000000 --- a/src/main/events/catalogue/get-game-achievements.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { - AchievementData, - GameShop, - RemoteUnlockedAchievement, - UnlockedAchievement, - UserAchievement, -} from "@types"; -import { registerEvent } from "../register-event"; -import { - gameAchievementRepository, - userPreferencesRepository, -} from "@main/repository"; -import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; -import { HydraApi } from "@main/services"; - -const getAchievementLocalUser = async (shop: string, objectId: string) => { - const cachedAchievements = await gameAchievementRepository.findOne({ - where: { objectId, shop }, - }); - - const achievementsData = await getGameAchievementData(objectId, shop); - - const unlockedAchievements = JSON.parse( - cachedAchievements?.unlockedAchievements || "[]" - ) as UnlockedAchievement[]; - - return achievementsData - .map((achievementData) => { - const unlockedAchiementData = unlockedAchievements.find( - (localAchievement) => { - return ( - localAchievement.name.toUpperCase() == - achievementData.name.toUpperCase() - ); - } - ); - - const icongray = achievementData.icongray.endsWith("/") - ? achievementData.icon - : achievementData.icongray; - - if (unlockedAchiementData) { - return { - ...achievementData, - unlocked: true, - unlockTime: unlockedAchiementData.unlockTime, - }; - } - - return { - ...achievementData, - unlocked: false, - unlockTime: null, - icongray: icongray, - } as UserAchievement; - }) - .sort((a, b) => { - if (a.unlocked && !b.unlocked) return -1; - if (!a.unlocked && b.unlocked) return 1; - if (a.unlocked && b.unlocked) { - return b.unlockTime! - a.unlockTime!; - } - return Number(a.hidden) - Number(b.hidden); - }); -}; - -const getAchievementsRemoteUser = async ( - shop: string, - objectId: string, - userId: string -) => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - - const achievementsData: AchievementData[] = await getGameAchievementData( - objectId, - shop - ); - - const unlockedAchievements = await HydraApi.get( - `/users/${userId}/games/achievements`, - { shop, objectId, language: userPreferences?.language || "en" } - ); - - return achievementsData - .map((achievementData) => { - const unlockedAchiementData = unlockedAchievements.find( - (localAchievement) => { - return ( - localAchievement.name.toUpperCase() == - achievementData.name.toUpperCase() - ); - } - ); - - const icongray = achievementData.icongray.endsWith("/") - ? achievementData.icon - : achievementData.icongray; - - if (unlockedAchiementData) { - return { - ...achievementData, - unlocked: true, - unlockTime: unlockedAchiementData.unlockTime, - }; - } - - return { - ...achievementData, - unlocked: false, - unlockTime: null, - icongray: icongray, - } as UserAchievement; - }) - .sort((a, b) => { - if (a.unlocked && !b.unlocked) return -1; - if (!a.unlocked && b.unlocked) return 1; - if (a.unlocked && b.unlocked) { - return b.unlockTime! - a.unlockTime!; - } - return Number(a.hidden) - Number(b.hidden); - }); -}; - -export const getGameAchievements = async ( - objectId: string, - shop: GameShop, - userId?: string -): Promise => { - if (!userId) { - return getAchievementLocalUser(shop, objectId); - } - - return getAchievementsRemoteUser(shop, objectId, userId); -}; - -const getGameAchievementsEvent = async ( - _event: Electron.IpcMainInvokeEvent, - objectId: string, - shop: GameShop, - userId?: string -): Promise => { - return getGameAchievements(objectId, shop, userId); -}; - -registerEvent("getGameAchievements", getGameAchievementsEvent); diff --git a/src/main/events/catalogue/get-game-stats.ts b/src/main/events/catalogue/get-game-stats.ts index b64a42ce..9961a0b2 100644 --- a/src/main/events/catalogue/get-game-stats.ts +++ b/src/main/events/catalogue/get-game-stats.ts @@ -1,8 +1,6 @@ -import type { GameShop } from "@types"; - +import type { GameShop, GameStats } from "@types"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import type { GameStats } from "@types"; const getGameStats = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/events/catalogue/get-trending-games.ts b/src/main/events/catalogue/get-trending-games.ts index 8f8e02c0..acfebfd6 100644 --- a/src/main/events/catalogue/get-trending-games.ts +++ b/src/main/events/catalogue/get-trending-games.ts @@ -1,7 +1,7 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { userPreferencesRepository } from "@main/repository"; -import { TrendingGame } from "@types"; +import type { TrendingGame } from "@types"; const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => { const userPreferences = await userPreferencesRepository.findOne({ diff --git a/src/main/events/catalogue/search-games.ts b/src/main/events/catalogue/search-games.ts index 8f81d40e..4ce42fd8 100644 --- a/src/main/events/catalogue/search-games.ts +++ b/src/main/events/catalogue/search-games.ts @@ -1,6 +1,6 @@ import { registerEvent } from "../register-event"; import { convertSteamGameToCatalogueEntry } from "../helpers/search-games"; -import { CatalogueEntry } from "@types"; +import type { CatalogueEntry } from "@types"; import { HydraApi } from "@main/services"; const searchGamesEvent = async ( diff --git a/src/main/events/cloud-save/get-game-backup-preview.ts b/src/main/events/cloud-save/get-game-backup-preview.ts index ee95059f..13a00758 100644 --- a/src/main/events/cloud-save/get-game-backup-preview.ts +++ b/src/main/events/cloud-save/get-game-backup-preview.ts @@ -1,5 +1,5 @@ import { registerEvent } from "../register-event"; -import { GameShop } from "@types"; +import type { GameShop } from "@types"; import { Ludusavi } from "@main/services"; import path from "node:path"; import { backupsPath } from "@main/constants"; diff --git a/src/main/events/cloud-save/upload-save-game.ts b/src/main/events/cloud-save/upload-save-game.ts index 6cf68596..a573b3ba 100644 --- a/src/main/events/cloud-save/upload-save-game.ts +++ b/src/main/events/cloud-save/upload-save-game.ts @@ -4,7 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import * as tar from "tar"; import crypto from "node:crypto"; -import { GameShop } from "@types"; +import type { GameShop } from "@types"; import axios from "axios"; import os from "node:os"; import { backupsPath } from "@main/constants"; diff --git a/src/main/events/index.ts b/src/main/events/index.ts index ffdfc354..87fbdbe0 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -9,7 +9,6 @@ import "./catalogue/get-random-game"; import "./catalogue/search-games"; import "./catalogue/get-game-stats"; import "./catalogue/get-trending-games"; -import "./catalogue/get-game-achievements"; import "./hardware/get-disk-free-space"; import "./library/add-game-to-library"; import "./library/create-game-shortcut"; @@ -50,6 +49,7 @@ import "./user/unblock-user"; import "./user/get-user-friends"; import "./user/get-user-stats"; import "./user/report-user"; +import "./user/get-unlocked-achievements"; import "./user/get-compared-unlocked-achievements"; import "./profile/get-friend-requests"; import "./profile/get-me"; diff --git a/src/main/events/profile/get-friend-requests.ts b/src/main/events/profile/get-friend-requests.ts index 11d8a884..39573b67 100644 --- a/src/main/events/profile/get-friend-requests.ts +++ b/src/main/events/profile/get-friend-requests.ts @@ -1,6 +1,6 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import { FriendRequest } from "@types"; +import type { FriendRequest } from "@types"; const getFriendRequests = async ( _event: Electron.IpcMainInvokeEvent diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts index 65aa1d5a..474effbb 100644 --- a/src/main/events/profile/get-me.ts +++ b/src/main/events/profile/get-me.ts @@ -1,7 +1,7 @@ import { registerEvent } from "../register-event"; import * as Sentry from "@sentry/electron/main"; import { HydraApi, logger } from "@main/services"; -import { ProfileVisibility, UserDetails } from "@types"; +import type { ProfileVisibility, UserDetails } from "@types"; import { userAuthRepository, userSubscriptionRepository, diff --git a/src/main/events/profile/sync-friend-requests.ts b/src/main/events/profile/sync-friend-requests.ts index c7dfbd81..ffe995e6 100644 --- a/src/main/events/profile/sync-friend-requests.ts +++ b/src/main/events/profile/sync-friend-requests.ts @@ -1,7 +1,7 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { UserNotLoggedInError } from "@shared"; -import { FriendRequestSync } from "@types"; +import type { FriendRequestSync } from "@types"; const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => { return HydraApi.get(`/profile/friend-requests/sync`).catch( diff --git a/src/main/events/profile/update-friend-request.ts b/src/main/events/profile/update-friend-request.ts index 24929544..b265f88c 100644 --- a/src/main/events/profile/update-friend-request.ts +++ b/src/main/events/profile/update-friend-request.ts @@ -1,6 +1,6 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import { FriendRequestAction } from "@types"; +import type { FriendRequestAction } from "@types"; const updateFriendRequest = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/events/user/get-blocked-users.ts b/src/main/events/user/get-blocked-users.ts index 7df6bf9a..9696cd7b 100644 --- a/src/main/events/user/get-blocked-users.ts +++ b/src/main/events/user/get-blocked-users.ts @@ -1,7 +1,7 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { UserNotLoggedInError } from "@shared"; -import { UserBlocks } from "@types"; +import type { UserBlocks } from "@types"; export const getBlockedUsers = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/events/user/get-unlocked-achievements.ts b/src/main/events/user/get-unlocked-achievements.ts new file mode 100644 index 00000000..b6f3e0b7 --- /dev/null +++ b/src/main/events/user/get-unlocked-achievements.ts @@ -0,0 +1,68 @@ +import type { GameShop, UnlockedAchievement, UserAchievement } from "@types"; +import { registerEvent } from "../register-event"; +import { gameAchievementRepository } from "@main/repository"; +import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; + +export const getUnlockedAchievements = async ( + objectId: string, + shop: GameShop +): Promise => { + const cachedAchievements = await gameAchievementRepository.findOne({ + where: { objectId, shop }, + }); + + const achievementsData = await getGameAchievementData(objectId, shop); + + const unlockedAchievements = JSON.parse( + cachedAchievements?.unlockedAchievements || "[]" + ) as UnlockedAchievement[]; + + return achievementsData + .map((achievementData) => { + const unlockedAchiementData = unlockedAchievements.find( + (localAchievement) => { + return ( + localAchievement.name.toUpperCase() == + achievementData.name.toUpperCase() + ); + } + ); + + const icongray = achievementData.icongray.endsWith("/") + ? achievementData.icon + : achievementData.icongray; + + if (unlockedAchiementData) { + return { + ...achievementData, + unlocked: true, + unlockTime: unlockedAchiementData.unlockTime, + }; + } + + return { + ...achievementData, + unlocked: false, + unlockTime: null, + icongray: icongray, + } as UserAchievement; + }) + .sort((a, b) => { + if (a.unlocked && !b.unlocked) return -1; + if (!a.unlocked && b.unlocked) return 1; + if (a.unlocked && b.unlocked) { + return b.unlockTime! - a.unlockTime!; + } + return Number(a.hidden) - Number(b.hidden); + }); +}; + +const getGameAchievementsEvent = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop +): Promise => { + return getUnlockedAchievements(objectId, shop); +}; + +registerEvent("getUnlockedAchievements", getGameAchievementsEvent); diff --git a/src/main/events/user/get-user-friends.ts b/src/main/events/user/get-user-friends.ts index 5ff4c8a4..9a6f156c 100644 --- a/src/main/events/user/get-user-friends.ts +++ b/src/main/events/user/get-user-friends.ts @@ -1,7 +1,7 @@ import { userAuthRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import { UserFriends } from "@types"; +import type { UserFriends } from "@types"; export const getUserFriends = async ( userId: string, diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index 7e0009eb..f3b3ac51 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -16,7 +16,7 @@ import { IsNull, Not } from "typeorm"; const fileStats: Map = new Map(); const fltFiles: Map> = new Map(); -const watchAchiievementsWindows = async () => { +const watchAchievementsWindows = async () => { const games = await gameRepository.find({ where: { isDeleted: false, @@ -53,12 +53,17 @@ const watchAchievementsWithWine = async () => { if (games.length === 0) return; + // const user = app.getPath("home").split("/").pop() + + // for (const game of games) { + // } + // TODO: watch achievements with wine }; export const watchAchievements = async () => { if (process.platform === "win32") { - return watchAchiievementsWindows(); + return watchAchievementsWindows(); } watchAchievementsWithWine(); @@ -101,10 +106,9 @@ const compareFltFolder = async (game: Game, file: AchievementFile) => { } }; -const compareFile = async (game: Game, file: AchievementFile) => { +const compareFile = (game: Game, file: AchievementFile) => { if (file.type === Cracker.flt) { - await compareFltFolder(game, file); - return; + return compareFltFolder(game, file); } try { @@ -121,8 +125,7 @@ const compareFile = async (game: Game, file: AchievementFile) => { currentStat.mtimeMs ); - await processAchievementFileDiff(game, file); - return; + return processAchievementFileDiff(game, file); } } @@ -136,8 +139,9 @@ const compareFile = async (game: Game, file: AchievementFile) => { previousStat, currentStat.mtimeMs ); - await processAchievementFileDiff(game, file); + return processAchievementFileDiff(game, file); } catch (err) { fileStats.set(file.filePath, -1); + return; } }; diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index 552e66b7..b08914e8 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -3,13 +3,13 @@ import { userPreferencesRepository, } from "@main/repository"; import { HydraApi } from "../hydra-api"; -import { AchievementData } from "@types"; +import type { AchievementData, GameShop } from "@types"; import { UserNotLoggedInError } from "@shared"; import { logger } from "../logger"; export const getGameAchievementData = async ( objectId: string, - shop: string + shop: GameShop ) => { const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 }, diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 367a5550..5c37d436 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -6,11 +6,11 @@ import { import type { AchievementData, GameShop, UnlockedAchievement } from "@types"; import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; -import { getGameAchievements } from "@main/events/catalogue/get-game-achievements"; +import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; const saveAchievementsOnLocal = async ( objectId: string, - shop: string, + shop: GameShop, achievements: any[] ) => { return gameAchievementRepository @@ -23,7 +23,7 @@ const saveAchievementsOnLocal = async ( ["objectId", "shop"] ) .then(() => { - return getGameAchievements(objectId, shop as GameShop) + return getUnlockedAchievements(objectId, shop) .then((achievements) => { WindowManager.mainWindow?.webContents.send( `on-update-achievements-${objectId}-${shop}`, @@ -36,12 +36,12 @@ const saveAchievementsOnLocal = async ( export const mergeAchievements = async ( objectId: string, - shop: string, + shop: GameShop, achievements: UnlockedAchievement[], publishNotification: boolean ) => { const game = await gameRepository.findOne({ - where: { objectID: objectId, shop: shop as GameShop }, + where: { objectID: objectId, shop: shop }, }); if (!game) return; diff --git a/src/main/services/download/python-instance.ts b/src/main/services/download/python-instance.ts index 4a41c2dc..f59b20b8 100644 --- a/src/main/services/download/python-instance.ts +++ b/src/main/services/download/python-instance.ts @@ -7,7 +7,7 @@ import { startTorrentClient as startRPCClient, } from "./torrent-client"; import { gameRepository } from "@main/repository"; -import { DownloadProgress } from "@types"; +import type { DownloadProgress } from "@types"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { calculateETA } from "./helpers"; import axios from "axios"; diff --git a/src/main/workers/steam-games.worker.ts b/src/main/workers/steam-games.worker.ts index 9085082b..55792454 100644 --- a/src/main/workers/steam-games.worker.ts +++ b/src/main/workers/steam-games.worker.ts @@ -1,4 +1,4 @@ -import { SteamGame } from "@types"; +import type { SteamGame } from "@types"; import { slice } from "lodash-es"; import fs from "node:fs"; diff --git a/src/preload/index.ts b/src/preload/index.ts index 0eadd409..6d541b0b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -51,8 +51,6 @@ contextBridge.exposeInMainWorld("electron", { getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"), - getGameAchievements: (objectId: string, shop: GameShop, userId?: string) => - ipcRenderer.invoke("getGameAchievements", objectId, shop, userId), onAchievementUnlocked: ( cb: ( objectId: string, @@ -270,6 +268,8 @@ contextBridge.exposeInMainWorld("electron", { shop, userId ), + getUnlockedAchievements: (objectId: string, shop: GameShop) => + ipcRenderer.invoke("getUnlockedAchievements", objectId, shop), /* Auth */ signOut: () => ipcRenderer.invoke("signOut"), diff --git a/src/renderer/src/components/header/auto-update-sub-header.tsx b/src/renderer/src/components/header/auto-update-sub-header.tsx index 5dcfe841..005cdfda 100644 --- a/src/renderer/src/components/header/auto-update-sub-header.tsx +++ b/src/renderer/src/components/header/auto-update-sub-header.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { SyncIcon } from "@primer/octicons-react"; import { Link } from "../link/link"; import * as styles from "./header.css"; -import { AppUpdaterEvent } from "@types"; +import type { AppUpdaterEvent } from "@types"; export const releasesPageUrl = "https://github.com/hydralauncher/hydra/releases/latest"; diff --git a/src/renderer/src/components/hero/hero.tsx b/src/renderer/src/components/hero/hero.tsx index 9986a7d8..9bc5514d 100644 --- a/src/renderer/src/components/hero/hero.tsx +++ b/src/renderer/src/components/hero/hero.tsx @@ -1,7 +1,7 @@ import { useNavigate } from "react-router-dom"; import * as styles from "./hero.css"; import { useEffect, useState } from "react"; -import { TrendingGame } from "@types"; +import type { TrendingGame } from "@types"; import { useTranslation } from "react-i18next"; import Skeleton from "react-loading-skeleton"; 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 c308ab2f..1cd8a529 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -155,14 +155,15 @@ export function GameDetailsContextProvider({ setStats(result); }); - window.electron - .getGameAchievements(objectId, shop as GameShop) - .then((achievements) => { - if (abortController.signal.aborted) return; - if (!userDetails) return; - setAchievements(achievements); - }) - .catch(() => {}); + if (userDetails) { + window.electron + .getUnlockedAchievements(objectId, shop as GameShop) + .then((achievements) => { + if (abortController.signal.aborted) return; + setAchievements(achievements); + }) + .catch(() => {}); + } updateGame(); }, [ diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index e6a47959..31ff375e 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -66,11 +66,6 @@ declare global { searchGameRepacks: (query: string) => Promise; getGameStats: (objectId: string, shop: GameShop) => Promise; getTrendingGames: () => Promise; - getGameAchievements: ( - objectId: string, - shop: GameShop, - userId?: string - ) => Promise; onAchievementUnlocked: ( cb: ( objectId: string, @@ -208,6 +203,10 @@ declare global { shop: GameShop, userId: string ) => Promise; + getUnlockedAchievements: ( + objectId: string, + shop: GameShop + ) => Promise; /* Profile */ getMe: () => Promise; diff --git a/src/renderer/src/features/running-game-slice.ts b/src/renderer/src/features/running-game-slice.ts index b3fb0a9d..e1dd609f 100644 --- a/src/renderer/src/features/running-game-slice.ts +++ b/src/renderer/src/features/running-game-slice.ts @@ -1,5 +1,5 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { GameRunning } from "@types"; +import type { GameRunning } from "@types"; export interface GameRunningState { gameRunning: GameRunning | null; diff --git a/src/renderer/src/pages/achievements/achievements-content.tsx b/src/renderer/src/pages/achievements/achievements-content.tsx index 7a37358b..aa93bc30 100644 --- a/src/renderer/src/pages/achievements/achievements-content.tsx +++ b/src/renderer/src/pages/achievements/achievements-content.tsx @@ -11,14 +11,14 @@ import { import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; import { gameDetailsContext } from "@renderer/context"; -import { ComparedAchievements, UserAchievement } from "@types"; +import type { ComparedAchievements, UserAchievement } from "@types"; import { average } from "color.js"; import Color from "color"; import { Link } from "@renderer/components"; import { ComparedAchievementList } from "./compared-achievement-list"; interface UserInfo { - userId: string; + id: string; displayName: string; profileImageUrl: string | null; totalAchievementCount: number; @@ -43,7 +43,9 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { const { t } = useTranslation("achievement"); const { userDetails, hasActiveSubscription } = useUserDetails(); - const getProfileImage = (user: UserInfo) => { + const getProfileImage = ( + user: Pick + ) => { return (
    {user.profileImageUrl ? ( @@ -59,11 +61,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) { ); }; - if ( - isComparison && - userDetails?.id == user.userId && - !hasActiveSubscription - ) { + if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) { return (
    { - if (gameTitle) { - dispatch(setHeaderTitle(gameTitle)); - } + dispatch(setHeaderTitle(gameTitle)); }, [dispatch, gameTitle]); const handleHeroLoad = async () => { @@ -247,16 +242,15 @@ export function AchievementsContent({ }; const getProfileImage = ( - profileImageUrl: string | null, - displayName: string + user: Pick ) => { return (
    - {profileImageUrl ? ( + {user.profileImageUrl ? ( {displayName} ) : ( @@ -315,7 +309,6 @@ export function AchievementsContent({
    {hasActiveSubscription && (
    - {getProfileImage( - userDetails.profileImageUrl, - userDetails.displayName - )} + {getProfileImage({ ...userDetails })}
    )}
    - {getProfileImage( - otherUser.profileImageUrl, - otherUser.displayName - )} + {getProfileImage(otherUser)}
    )} -
    - {otherUser ? ( - - ) : ( - - )} -
    + {otherUser ? ( + + ) : ( + + )} ); diff --git a/src/renderer/src/pages/achievements/achievements-skeleton.tsx b/src/renderer/src/pages/achievements/achievements-skeleton.tsx index f26f3951..f9ae81ac 100644 --- a/src/renderer/src/pages/achievements/achievements-skeleton.tsx +++ b/src/renderer/src/pages/achievements/achievements-skeleton.tsx @@ -4,7 +4,7 @@ import * as styles from "./achievements.css"; export function AchievementsSkeleton() { return (
    -
    +
    diff --git a/src/renderer/src/pages/achievements/achievements.css.ts b/src/renderer/src/pages/achievements/achievements.css.ts index c4b66384..d5bed1e6 100644 --- a/src/renderer/src/pages/achievements/achievements.css.ts +++ b/src/renderer/src/pages/achievements/achievements.css.ts @@ -3,8 +3,8 @@ import { style } from "@vanilla-extract/css"; import { recipe } from "@vanilla-extract/recipes"; export const HERO_HEIGHT = 150; -export const LOGO_HEIGHT = 100; -export const LOGO_MAX_WIDTH = 200; +const LOGO_HEIGHT = 100; +const LOGO_MAX_WIDTH = 200; export const wrapper = style({ display: "flex", @@ -104,6 +104,7 @@ export const list = style({ gap: `${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 2}px`, width: "100%", + backgroundColor: vars.color.background, }); export const listItem = style({ @@ -162,12 +163,7 @@ export const heroLogoBackdrop = style({ }); export const heroImageSkeleton = style({ - height: "300px", - "@media": { - "(min-width: 1250px)": { - height: "350px", - }, - }, + height: "150px", }); export const heroPanelSkeleton = style({ diff --git a/src/renderer/src/pages/achievements/achievements.tsx b/src/renderer/src/pages/achievements/achievements.tsx index c831dd0e..605300ef 100644 --- a/src/renderer/src/pages/achievements/achievements.tsx +++ b/src/renderer/src/pages/achievements/achievements.tsx @@ -52,7 +52,7 @@ export default function Achievements() { if (!otherUserId || !comparedAchievements) return null; return { - userId: otherUserId, + id: otherUserId, displayName: comparedAchievements.target.displayName, profileImageUrl: comparedAchievements.target.profileImageUrl, totalAchievementCount: comparedAchievements.target.totalAchievementCount, diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index 925fb175..ed541745 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -7,7 +7,7 @@ import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal"; import * as styles from "./downloads.css"; import { DeleteGameModal } from "./delete-game-modal"; import { DownloadGroup } from "./download-group"; -import { LibraryGame } from "@types"; +import type { LibraryGame } from "@types"; import { orderBy } from "lodash-es"; import { ArrowDownIcon } from "@primer/octicons-react"; diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index bab9452f..d32841c7 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; -import { GameRepack, GameShop, Steam250Game } from "@types"; +import type { GameRepack, GameShop, Steam250Game } from "@types"; import { Button, ConfirmationModal } from "@renderer/components"; import { buildGameDetailsPath } from "@renderer/helpers"; diff --git a/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx b/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx index 09bf5d43..39789872 100644 --- a/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { Button, Modal } from "@renderer/components"; import * as styles from "./remove-from-library-modal.css"; -import { Game } from "@types"; +import type { Game } from "@types"; interface RemoveGameFromLibraryModalProps { visible: boolean; diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index a989b81d..d6b55490 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -14,7 +14,7 @@ import { LockedProfile } from "./locked-profile"; import { ReportProfile } from "../report-profile/report-profile"; import { FriendsBox } from "./friends-box"; import { RecentGamesBox } from "./recent-games-box"; -import { UserGame } from "@types"; +import type { UserGame } from "@types"; import { buildGameAchievementPath, buildGameDetailsPath, diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx index 5ec22827..5b05d5b8 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -9,7 +9,7 @@ import { useForm } from "react-hook-form"; import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; import { downloadSourcesTable } from "@renderer/dexie"; -import { DownloadSourceValidationResult } from "@types"; +import type { DownloadSourceValidationResult } from "@types"; import { downloadSourcesWorker } from "@renderer/workers"; interface AddDownloadSourceModalProps { diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx index 36ff7e14..d104e676 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx @@ -1,5 +1,5 @@ import { SPACING_UNIT, vars } from "@renderer/theme.css"; -import { UserFriend } from "@types"; +import type { UserFriend } from "@types"; import { useEffect, useRef, useState } from "react"; import { UserFriendItem } from "./user-friend-item"; import { useNavigate } from "react-router-dom"; diff --git a/src/renderer/src/workers/repacks.worker.ts b/src/renderer/src/workers/repacks.worker.ts index 6c3aca73..c2394510 100644 --- a/src/renderer/src/workers/repacks.worker.ts +++ b/src/renderer/src/workers/repacks.worker.ts @@ -1,6 +1,6 @@ import { repacksTable } from "@renderer/dexie"; import { formatName } from "@shared"; -import { GameRepack } from "@types"; +import type { GameRepack } from "@types"; import flexSearch from "flexsearch"; interface SerializedGameRepack extends Omit { From 22fc95ff5399538265b760dd5d00d2f66fdd43af Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:45:34 -0300 Subject: [PATCH 141/163] feat: wine prefix for achievements --- .../achievements/achievement-watcher.ts | 16 +++-- .../achievements/find-achivement-files.ts | 66 +++++++++++++++++-- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index f3b3ac51..4558e20d 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -5,6 +5,7 @@ import { mergeAchievements } from "./merge-achievements"; import fs, { readdirSync } from "node:fs"; import { findAchievementFileInExecutableDirectory, + findAchievementFiles, findAllAchievementFiles, getAlternativeObjectIds, } from "./find-achivement-files"; @@ -53,12 +54,19 @@ const watchAchievementsWithWine = async () => { if (games.length === 0) return; - // const user = app.getPath("home").split("/").pop() + for (const game of games) { + const gameAchievementFiles = findAchievementFiles(game); + const achievementFileInsideDirectory = + findAchievementFileInExecutableDirectory(game); - // for (const game of games) { - // } + gameAchievementFiles.push(...achievementFileInsideDirectory); - // TODO: watch achievements with wine + if (!gameAchievementFiles.length) continue; + + for (const file of gameAchievementFiles) { + compareFile(game, file); + } + } }; export const watchAchievements = async () => { diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 9a2c70ff..a48e5dd6 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -6,12 +6,59 @@ import { Cracker } from "@shared"; import { Game } from "@main/entity"; import { achievementsLogger } from "../logger"; +const getAppDataPath = () => { + if (process.platform === "win32") { + return app.getPath("appData"); + } + + const user = app.getPath("home").split("/").pop(); + + return path.join("drive_c", "users", user || "", "AppData", "Roaming"); +}; + +const getDocumentsPath = () => { + if (process.platform === "win32") { + return app.getPath("appData"); + } + + const user = app.getPath("home").split("/").pop(); + + return path.join("drive_c", "users", user || "", "Documents"); +}; + +const getPublicDocumentsPath = () => { + if (process.platform === "win32") { + return app.getPath("appData"); + } + // /media/jackenx/JED2/.newprefix/dosdevices/c:/users/Public/Documents/Steam/CODEX/489830 + + return path.join("drive_c", "users", "Public", "Documents"); +}; + +const getLocalAppDataPath = () => { + if (process.platform === "win32") { + return path.join(appData, "..", "Local"); + } + + const user = app.getPath("home").split("/").pop(); + + return path.join("drive_c", "users", user || "", "AppData", "Local"); +}; + +const getProgramDataPath = () => { + if (process.platform === "win32") { + return path.join("C:", "ProgramData"); + } + + return path.join("drive_c", "ProgramData"); +}; + //TODO: change to a automatized method -const publicDocuments = path.join("C:", "Users", "Public", "Documents"); -const programData = path.join("C:", "ProgramData"); -const appData = app.getPath("appData"); -const documents = app.getPath("documents"); -const localAppData = path.join(appData, "..", "Local"); +const publicDocuments = getPublicDocumentsPath(); +const programData = getProgramDataPath(); +const appData = getAppDataPath(); +const documents = getDocumentsPath(); +const localAppData = getLocalAppDataPath(); const crackers = [ Cracker.codex, @@ -190,7 +237,12 @@ export const findAchievementFiles = (game: Game) => { for (const cracker of crackers) { for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) { for (const objectId of getAlternativeObjectIds(game.objectID)) { - const filePath = path.join(folderPath, objectId, ...fileLocation); + const filePath = path.join( + game.winePrefixPath ?? "", + folderPath, + objectId, + ...fileLocation + ); if (fs.existsSync(filePath)) { achievementFiles.push({ @@ -216,6 +268,7 @@ export const findAchievementFileInExecutableDirectory = ( { type: Cracker.userstats, filePath: path.join( + game.winePrefixPath ?? "", game.executablePath, "..", "SteamData", @@ -225,6 +278,7 @@ export const findAchievementFileInExecutableDirectory = ( { type: Cracker._3dm, filePath: path.join( + game.winePrefixPath ?? "", game.executablePath, "..", "3DMGAME", From bb65d77fc6db10db57469b47d89439b9662c87e1 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 01:07:46 -0300 Subject: [PATCH 142/163] feat: adjustments on achievements --- src/main/entity/game-achievements.entity.ts | 4 +- .../achievements/achievement-watcher.ts | 21 +++-- .../achievements/find-achivement-files.ts | 9 +- .../update-local-unlocked-achivements.ts | 88 ++++++++++--------- 4 files changed, 66 insertions(+), 56 deletions(-) diff --git a/src/main/entity/game-achievements.entity.ts b/src/main/entity/game-achievements.entity.ts index 29cca558..0cb15f6e 100644 --- a/src/main/entity/game-achievements.entity.ts +++ b/src/main/entity/game-achievements.entity.ts @@ -12,8 +12,8 @@ export class GameAchievement { shop: string; @Column("text", { nullable: true }) - unlockedAchievements: string; + unlockedAchievements: string | null; @Column("text", { nullable: true }) - achievements: string; + achievements: string | null; } diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/achievement-watcher.ts index 4558e20d..0986f8be 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/achievement-watcher.ts @@ -25,21 +25,24 @@ const watchAchievementsWindows = async () => { }); if (games.length === 0) return; + const achievementFiles = findAllAchievementFiles(); for (const game of games) { + const gameAchievementFiles: AchievementFile[] = []; + for (const objectId of getAlternativeObjectIds(game.objectID)) { - const gameAchievementFiles = achievementFiles.get(objectId) || []; - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); + gameAchievementFiles.push(...(achievementFiles.get(objectId) || [])); - gameAchievementFiles.push(...achievementFileInsideDirectory); + gameAchievementFiles.push( + ...findAchievementFileInExecutableDirectory(game) + ); + } - if (!gameAchievementFiles.length) continue; + if (!gameAchievementFiles.length) continue; - for (const file of gameAchievementFiles) { - compareFile(game, file); - } + for (const file of gameAchievementFiles) { + await compareFile(game, file); } } }; @@ -64,7 +67,7 @@ const watchAchievementsWithWine = async () => { if (!gameAchievementFiles.length) continue; for (const file of gameAchievementFiles) { - compareFile(game, file); + await compareFile(game, file); } } }; diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index a48e5dd6..84984b58 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -13,12 +13,12 @@ const getAppDataPath = () => { const user = app.getPath("home").split("/").pop(); - return path.join("drive_c", "users", user || "", "AppData", "Roaming"); + return path.join("drive_c", "Users", user || "", "AppData", "Roaming"); }; const getDocumentsPath = () => { if (process.platform === "win32") { - return app.getPath("appData"); + return app.getPath("documents"); } const user = app.getPath("home").split("/").pop(); @@ -28,11 +28,10 @@ const getDocumentsPath = () => { const getPublicDocumentsPath = () => { if (process.platform === "win32") { - return app.getPath("appData"); + return path.join("C:", "Users", "Public", "Documents"); } - // /media/jackenx/JED2/.newprefix/dosdevices/c:/users/Public/Documents/Steam/CODEX/489830 - return path.join("drive_c", "users", "Public", "Documents"); + return path.join("drive_c", "Users", "Public", "Documents"); }; const getLocalAppDataPath = () => { diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index 38c1fa2d..177808d8 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -7,59 +7,69 @@ import { } from "./find-achivement-files"; import { parseAchievementFile } from "./parse-achievement-file"; import { mergeAchievements } from "./merge-achievements"; -import type { UnlockedAchievement } from "@types"; +import type { AchievementFile, UnlockedAchievement } from "@types"; import { getGameAchievementData } from "./get-game-achievement-data"; import { achievementsLogger } from "../logger"; import { Game } from "@main/entity"; export const updateAllLocalUnlockedAchievements = async () => { - const gameAchievementFilesMap = findAllAchievementFiles(); - const games = await gameRepository.find({ where: { isDeleted: false, }, }); + if (games.length === 0) return; + + const gameAchievementFilesMap = findAllAchievementFiles(); + for (const game of games) { - for (const objectId of getAlternativeObjectIds(game.objectID)) { - const gameAchievementFiles = gameAchievementFilesMap.get(objectId) || []; - const achievementFileInsideDirectory = - findAchievementFileInExecutableDirectory(game); - - gameAchievementFiles.push(...achievementFileInsideDirectory); - - gameAchievementRepository - .findOne({ - where: { objectId: game.objectID, shop: "steam" }, - }) - .then((localAchievements) => { - if (!localAchievements || !localAchievements.achievements) { - getGameAchievementData(game.objectID, "steam"); - } - }); - - const unlockedAchievements: UnlockedAchievement[] = []; - - for (const achievementFile of gameAchievementFiles) { - const parsedAchievements = parseAchievementFile( - achievementFile.filePath, - achievementFile.type - ); - - if (parsedAchievements.length) { - unlockedAchievements.push(...parsedAchievements); - - achievementsLogger.log( - "Achievement file for", - game.title, - achievementFile.filePath, - parsedAchievements - ); + gameAchievementRepository + .findOne({ + where: { objectId: game.objectID, shop: "steam" }, + }) + .then((localAchievements) => { + if (!localAchievements || !localAchievements.achievements) { + getGameAchievementData(game.objectID, "steam"); } + }); + + const gameAchievementFiles: AchievementFile[] = []; + const unlockedAchievements: UnlockedAchievement[] = []; + + for (const objectId of getAlternativeObjectIds(game.objectID)) { + gameAchievementFiles.push( + ...(gameAchievementFilesMap.get(objectId) || []) + ); + + gameAchievementFiles.push( + ...findAchievementFileInExecutableDirectory(game) + ); + } + + for (const achievementFile of gameAchievementFiles) { + const parsedAchievements = parseAchievementFile( + achievementFile.filePath, + achievementFile.type + ); + + if (parsedAchievements.length) { + unlockedAchievements.push(...parsedAchievements); + + achievementsLogger.log( + "Achievement file for", + game.title, + achievementFile.filePath, + parsedAchievements + ); } - mergeAchievements(game.objectID, "steam", unlockedAchievements, false); + await mergeAchievements( + game.objectID, + "steam", + unlockedAchievements, + false + ); } } }; @@ -72,8 +82,6 @@ export const updateLocalUnlockedAchivements = async (game: Game) => { gameAchievementFiles.push(...achievementFileInsideDirectory); - // console.log("Achievements files for", game.title, gameAchievementFiles); - const unlockedAchievements: UnlockedAchievement[] = []; for (const achievementFile of gameAchievementFiles) { From c6fda9b4d8b04a1ff137a6939933805db3d73304 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 01:48:47 -0300 Subject: [PATCH 143/163] feat: start watching new achievements only after finishing sync --- ...atcher.ts => AchievementWatcherManager.ts} | 130 ++++++++++++++---- .../update-local-unlocked-achivements.ts | 69 +--------- .../library-sync/upload-games-batch.ts | 4 +- src/main/services/main-loop.ts | 4 +- 4 files changed, 109 insertions(+), 98 deletions(-) rename src/main/services/achievements/{achievement-watcher.ts => AchievementWatcherManager.ts} (59%) diff --git a/src/main/services/achievements/achievement-watcher.ts b/src/main/services/achievements/AchievementWatcherManager.ts similarity index 59% rename from src/main/services/achievements/achievement-watcher.ts rename to src/main/services/achievements/AchievementWatcherManager.ts index 0986f8be..31c913f7 100644 --- a/src/main/services/achievements/achievement-watcher.ts +++ b/src/main/services/achievements/AchievementWatcherManager.ts @@ -1,4 +1,4 @@ -import { gameRepository } from "@main/repository"; +import { gameAchievementRepository, gameRepository } from "@main/repository"; import { parseAchievementFile } from "./parse-achievement-file"; import { Game } from "@main/entity"; import { mergeAchievements } from "./merge-achievements"; @@ -9,10 +9,11 @@ import { findAllAchievementFiles, getAlternativeObjectIds, } from "./find-achivement-files"; -import type { AchievementFile } from "@types"; +import type { AchievementFile, UnlockedAchievement } from "@types"; import { achievementsLogger } from "../logger"; import { Cracker } from "@shared"; import { IsNull, Not } from "typeorm"; +import { getGameAchievementData } from "./get-game-achievement-data"; const fileStats: Map = new Map(); const fltFiles: Map> = new Map(); @@ -72,30 +73,6 @@ const watchAchievementsWithWine = async () => { } }; -export const watchAchievements = async () => { - if (process.platform === "win32") { - return watchAchievementsWindows(); - } - - watchAchievementsWithWine(); -}; - -const processAchievementFileDiff = async ( - game: Game, - file: AchievementFile -) => { - const unlockedAchievements = parseAchievementFile(file.filePath, file.type); - - if (unlockedAchievements.length) { - return mergeAchievements( - game.objectID, - game.shop, - unlockedAchievements, - true - ); - } -}; - const compareFltFolder = async (game: Game, file: AchievementFile) => { try { const currentAchievements = new Set(readdirSync(file.filePath)); @@ -156,3 +133,104 @@ const compareFile = (game: Game, file: AchievementFile) => { return; } }; + +const processAchievementFileDiff = async ( + game: Game, + file: AchievementFile +) => { + const unlockedAchievements = parseAchievementFile(file.filePath, file.type); + + if (unlockedAchievements.length) { + return mergeAchievements( + game.objectID, + game.shop, + unlockedAchievements, + true + ); + } +}; + +export class AchievementWatcherManager { + private static hasFinishedMergingWithRemote = false; + + public static watchAchievements = async () => { + if (!this.hasFinishedMergingWithRemote) return; + + if (process.platform === "win32") { + return watchAchievementsWindows(); + } + + watchAchievementsWithWine(); + }; + + public static preSearchAchievements = async () => { + const games = await gameRepository.find({ + where: { + isDeleted: false, + }, + }); + + const gameAchievementFilesMap = findAllAchievementFiles(); + + await Promise.all( + games.map(async (game) => { + gameAchievementRepository + .findOne({ + where: { objectId: game.objectID, shop: "steam" }, + }) + .then((localAchievements) => { + if (!localAchievements || !localAchievements.achievements) { + getGameAchievementData(game.objectID, "steam"); + } + }); + + const gameAchievementFiles: AchievementFile[] = []; + const unlockedAchievements: UnlockedAchievement[] = []; + + for (const objectId of getAlternativeObjectIds(game.objectID)) { + gameAchievementFiles.push( + ...(gameAchievementFilesMap.get(objectId) || []) + ); + + gameAchievementFiles.push( + ...findAchievementFileInExecutableDirectory(game) + ); + } + + for (const achievementFile of gameAchievementFiles) { + const parsedAchievements = parseAchievementFile( + achievementFile.filePath, + achievementFile.type + ); + + try { + const currentStat = fs.statSync(achievementFile.filePath); + fileStats.set(achievementFile.filePath, currentStat.mtimeMs); + } catch { + fileStats.set(achievementFile.filePath, -1); + } + + if (parsedAchievements.length) { + unlockedAchievements.push(...parsedAchievements); + + achievementsLogger.log( + "Achievement file for", + game.title, + achievementFile.filePath, + parsedAchievements + ); + } + + await mergeAchievements( + game.objectID, + "steam", + unlockedAchievements, + false + ); + } + }) + ); + + this.hasFinishedMergingWithRemote = true; + }; +} diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index 177808d8..f579382f 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -1,79 +1,12 @@ -import { gameAchievementRepository, gameRepository } from "@main/repository"; import { - findAllAchievementFiles, findAchievementFiles, findAchievementFileInExecutableDirectory, - getAlternativeObjectIds, } from "./find-achivement-files"; import { parseAchievementFile } from "./parse-achievement-file"; import { mergeAchievements } from "./merge-achievements"; -import type { AchievementFile, UnlockedAchievement } from "@types"; -import { getGameAchievementData } from "./get-game-achievement-data"; -import { achievementsLogger } from "../logger"; +import type { UnlockedAchievement } from "@types"; import { Game } from "@main/entity"; -export const updateAllLocalUnlockedAchievements = async () => { - const games = await gameRepository.find({ - where: { - isDeleted: false, - }, - }); - - if (games.length === 0) return; - - const gameAchievementFilesMap = findAllAchievementFiles(); - - for (const game of games) { - gameAchievementRepository - .findOne({ - where: { objectId: game.objectID, shop: "steam" }, - }) - .then((localAchievements) => { - if (!localAchievements || !localAchievements.achievements) { - getGameAchievementData(game.objectID, "steam"); - } - }); - - const gameAchievementFiles: AchievementFile[] = []; - const unlockedAchievements: UnlockedAchievement[] = []; - - for (const objectId of getAlternativeObjectIds(game.objectID)) { - gameAchievementFiles.push( - ...(gameAchievementFilesMap.get(objectId) || []) - ); - - gameAchievementFiles.push( - ...findAchievementFileInExecutableDirectory(game) - ); - } - - for (const achievementFile of gameAchievementFiles) { - const parsedAchievements = parseAchievementFile( - achievementFile.filePath, - achievementFile.type - ); - - if (parsedAchievements.length) { - unlockedAchievements.push(...parsedAchievements); - - achievementsLogger.log( - "Achievement file for", - game.title, - achievementFile.filePath, - parsedAchievements - ); - } - - await mergeAchievements( - game.objectID, - "steam", - unlockedAchievements, - false - ); - } - } -}; - export const updateLocalUnlockedAchivements = async (game: Game) => { const gameAchievementFiles = findAchievementFiles(game); diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index 366f6c4e..b40bfcda 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -4,7 +4,7 @@ import { IsNull } from "typeorm"; import { HydraApi } from "../hydra-api"; import { mergeWithRemoteGames } from "./merge-with-remote-games"; import { WindowManager } from "../window-manager"; -import { updateAllLocalUnlockedAchievements } from "../achievements/update-local-unlocked-achivements"; +import { AchievementWatcherManager } from "../achievements/AchievementWatcherManager"; export const uploadGamesBatch = async () => { const games = await gameRepository.find({ @@ -29,7 +29,7 @@ export const uploadGamesBatch = async () => { await mergeWithRemoteGames(); - await updateAllLocalUnlockedAchievements(); + await AchievementWatcherManager.preSearchAchievements(); if (WindowManager.mainWindow) WindowManager.mainWindow.webContents.send("on-library-batch-complete"); diff --git a/src/main/services/main-loop.ts b/src/main/services/main-loop.ts index 5ba57fc3..48db4887 100644 --- a/src/main/services/main-loop.ts +++ b/src/main/services/main-loop.ts @@ -1,7 +1,7 @@ import { sleep } from "@main/helpers"; import { DownloadManager } from "./download"; import { watchProcesses } from "./process-watcher"; -import { watchAchievements } from "./achievements/achievement-watcher"; +import { AchievementWatcherManager } from "./achievements/AchievementWatcherManager"; export const startMainLoop = async () => { // eslint-disable-next-line no-constant-condition @@ -9,7 +9,7 @@ export const startMainLoop = async () => { await Promise.allSettled([ watchProcesses(), DownloadManager.watchDownloads(), - watchAchievements(), + AchievementWatcherManager.watchAchievements(), ]); await sleep(1500); From 36b98a7d73ce079453150918b7c0fa54def61105 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 02:31:06 -0300 Subject: [PATCH 144/163] fix: return on parse file --- src/main/services/achievements/parse-achievement-file.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/services/achievements/parse-achievement-file.ts b/src/main/services/achievements/parse-achievement-file.ts index eb6321a3..07854935 100644 --- a/src/main/services/achievements/parse-achievement-file.ts +++ b/src/main/services/achievements/parse-achievement-file.ts @@ -98,7 +98,7 @@ const iniParse = (filePath: string) => { return object; } catch (err) { achievementsLogger.error(`Error parsing ${filePath}`, err); - return null; + return {}; } }; @@ -107,7 +107,7 @@ const jsonParse = (filePath: string) => { return JSON.parse(readFileSync(filePath, "utf-8")); } catch (err) { achievementsLogger.error(`Error parsing ${filePath}`, err); - return null; + return {}; } }; From fd5262cd6ecbbd4e845f41428cd6f41526c38fc1 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 02:57:14 -0300 Subject: [PATCH 145/163] feat: change Users to users --- src/main/services/achievements/find-achivement-files.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 84984b58..4c12f82e 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -13,7 +13,7 @@ const getAppDataPath = () => { const user = app.getPath("home").split("/").pop(); - return path.join("drive_c", "Users", user || "", "AppData", "Roaming"); + return path.join("drive_c", "users", user || "", "AppData", "Roaming"); }; const getDocumentsPath = () => { @@ -31,7 +31,7 @@ const getPublicDocumentsPath = () => { return path.join("C:", "Users", "Public", "Documents"); } - return path.join("drive_c", "Users", "Public", "Documents"); + return path.join("drive_c", "users", "Public", "Documents"); }; const getLocalAppDataPath = () => { From 27e8a0820f07ed738e5b7fa7d73d12067bd1353a Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 04:28:04 -0300 Subject: [PATCH 146/163] feat: optimizations --- ...ager.ts => achievement-watcher-manager.ts} | 136 +++++++++++------- .../achievements/merge-achievements.ts | 22 ++- src/main/services/hydra-api.ts | 109 +++++++------- .../library-sync/upload-games-batch.ts | 4 +- src/main/services/main-loop.ts | 2 +- .../components/sidebar/sidebar-profile.tsx | 10 +- 6 files changed, 167 insertions(+), 116 deletions(-) rename src/main/services/achievements/{AchievementWatcherManager.ts => achievement-watcher-manager.ts} (65%) diff --git a/src/main/services/achievements/AchievementWatcherManager.ts b/src/main/services/achievements/achievement-watcher-manager.ts similarity index 65% rename from src/main/services/achievements/AchievementWatcherManager.ts rename to src/main/services/achievements/achievement-watcher-manager.ts index 31c913f7..42509276 100644 --- a/src/main/services/achievements/AchievementWatcherManager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -40,10 +40,8 @@ const watchAchievementsWindows = async () => { ); } - if (!gameAchievementFiles.length) continue; - for (const file of gameAchievementFiles) { - await compareFile(game, file); + compareFile(game, file); } } }; @@ -56,8 +54,6 @@ const watchAchievementsWithWine = async () => { }, }); - if (games.length === 0) return; - for (const game of games) { const gameAchievementFiles = findAchievementFiles(game); const achievementFileInsideDirectory = @@ -65,10 +61,8 @@ const watchAchievementsWithWine = async () => { gameAchievementFiles.push(...achievementFileInsideDirectory); - if (!gameAchievementFiles.length) continue; - for (const file of gameAchievementFiles) { - await compareFile(game, file); + compareFile(game, file); } } }; @@ -104,7 +98,7 @@ const compareFile = (game: Game, file: AchievementFile) => { const previousStat = fileStats.get(file.filePath); fileStats.set(file.filePath, currentStat.mtimeMs); - if (!previousStat) { + if (!previousStat || previousStat === -1) { if (currentStat.mtimeMs) { achievementsLogger.log( "First change in file", @@ -153,17 +147,55 @@ const processAchievementFileDiff = async ( export class AchievementWatcherManager { private static hasFinishedMergingWithRemote = false; - public static watchAchievements = async () => { + public static watchAchievements = () => { if (!this.hasFinishedMergingWithRemote) return; if (process.platform === "win32") { return watchAchievementsWindows(); } - watchAchievementsWithWine(); + return watchAchievementsWithWine(); }; - public static preSearchAchievements = async () => { + private static preProcessGameAchievementFiles = ( + game: Game, + gameAchievementFiles: AchievementFile[] + ) => { + const unlockedAchievements: UnlockedAchievement[] = []; + for (const achievementFile of gameAchievementFiles) { + const parsedAchievements = parseAchievementFile( + achievementFile.filePath, + achievementFile.type + ); + + try { + const currentStat = fs.statSync(achievementFile.filePath); + fileStats.set(achievementFile.filePath, currentStat.mtimeMs); + } catch { + fileStats.set(achievementFile.filePath, -1); + } + + if (parsedAchievements.length) { + unlockedAchievements.push(...parsedAchievements); + + achievementsLogger.log( + "Achievement file for", + game.title, + achievementFile.filePath, + parsedAchievements + ); + } + } + + return mergeAchievements( + game.objectID, + "steam", + unlockedAchievements, + false + ); + }; + + private static preSearchAchievementsWindows = async () => { const games = await gameRepository.find({ where: { isDeleted: false, @@ -172,20 +204,19 @@ export class AchievementWatcherManager { const gameAchievementFilesMap = findAllAchievementFiles(); - await Promise.all( - games.map(async (game) => { + return Promise.all( + games.map((game) => { gameAchievementRepository .findOne({ - where: { objectId: game.objectID, shop: "steam" }, + where: { objectId: game.objectID, shop: game.shop }, }) .then((localAchievements) => { if (!localAchievements || !localAchievements.achievements) { - getGameAchievementData(game.objectID, "steam"); + getGameAchievementData(game.objectID, game.shop); } }); const gameAchievementFiles: AchievementFile[] = []; - const unlockedAchievements: UnlockedAchievement[] = []; for (const objectId of getAlternativeObjectIds(game.objectID)) { gameAchievementFiles.push( @@ -197,39 +228,48 @@ export class AchievementWatcherManager { ); } - for (const achievementFile of gameAchievementFiles) { - const parsedAchievements = parseAchievementFile( - achievementFile.filePath, - achievementFile.type - ); - - try { - const currentStat = fs.statSync(achievementFile.filePath); - fileStats.set(achievementFile.filePath, currentStat.mtimeMs); - } catch { - fileStats.set(achievementFile.filePath, -1); - } - - if (parsedAchievements.length) { - unlockedAchievements.push(...parsedAchievements); - - achievementsLogger.log( - "Achievement file for", - game.title, - achievementFile.filePath, - parsedAchievements - ); - } - - await mergeAchievements( - game.objectID, - "steam", - unlockedAchievements, - false - ); - } + return this.preProcessGameAchievementFiles(game, gameAchievementFiles); }) ); + }; + + private static preSearchAchievementsWithWine = async () => { + const games = await gameRepository.find({ + where: { + isDeleted: false, + winePrefixPath: Not(IsNull()), + }, + }); + + return Promise.all( + games.map((game) => { + gameAchievementRepository + .findOne({ + where: { objectId: game.objectID, shop: game.shop }, + }) + .then((localAchievements) => { + if (!localAchievements || !localAchievements.achievements) { + getGameAchievementData(game.objectID, game.shop); + } + }); + + const gameAchievementFiles = findAchievementFiles(game); + const achievementFileInsideDirectory = + findAchievementFileInExecutableDirectory(game); + + gameAchievementFiles.push(...achievementFileInsideDirectory); + + return this.preProcessGameAchievementFiles(game, gameAchievementFiles); + }) + ); + }; + + public static preSearchAchievements = async () => { + if (process.platform === "win32") { + await this.preSearchAchievementsWindows(); + } else { + await this.preSearchAchievementsWithWine(); + } this.hasFinishedMergingWithRemote = true; }; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 5c37d436..3a330d8f 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -11,7 +11,8 @@ import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievem const saveAchievementsOnLocal = async ( objectId: string, shop: GameShop, - achievements: any[] + achievements: any[], + sendUpdateEvent: boolean ) => { return gameAchievementRepository .upsert( @@ -23,6 +24,8 @@ const saveAchievementsOnLocal = async ( ["objectId", "shop"] ) .then(() => { + if (!sendUpdateEvent) return; + return getUnlockedAchievements(objectId, shop) .then((achievements) => { WindowManager.mainWindow?.webContents.send( @@ -133,13 +136,24 @@ export const mergeAchievements = async ( return saveAchievementsOnLocal( response.objectId, response.shop, - response.achievements + response.achievements, + publishNotification ); }) .catch(() => { - return saveAchievementsOnLocal(objectId, shop, mergedLocalAchievements); + return saveAchievementsOnLocal( + objectId, + shop, + mergedLocalAchievements, + publishNotification + ); }); } - return saveAchievementsOnLocal(objectId, shop, mergedLocalAchievements); + return saveAchievementsOnLocal( + objectId, + shop, + mergedLocalAchievements, + publishNotification + ); }; diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 4ae6a32e..c6c60b72 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -14,6 +14,7 @@ import { } from "@shared"; // import { omit } from "lodash-es"; import { appVersion } from "@main/constants"; +import { omit } from "lodash-es"; interface HydraApiOptions { needsAuth?: boolean; @@ -24,7 +25,7 @@ export class HydraApi { private static instance: AxiosInstance; private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes - private static readonly ADD_LOG_INTERCEPTOR = true; + private static readonly ADD_LOG_INTERCEPTOR = false; private static secondsToMilliseconds = (seconds: number) => seconds * 1000; @@ -109,59 +110,59 @@ export class HydraApi { }); if (this.ADD_LOG_INTERCEPTOR) { - // this.instance.interceptors.request.use( - // (request) => { - // logger.log(" ---- REQUEST -----"); - // const data = Array.isArray(request.data) - // ? request.data - // : omit(request.data, ["refreshToken"]); - // logger.log(request.method, request.url, request.params, data); - // return request; - // }, - // (error) => { - // logger.error("request error", error); - // return Promise.reject(error); - // } - // ); - // this.instance.interceptors.response.use( - // (response) => { - // logger.log(" ---- RESPONSE -----"); - // const data = Array.isArray(response.data) - // ? response.data - // : omit(response.data, ["username", "accessToken", "refreshToken"]); - // logger.log( - // response.status, - // response.config.method, - // response.config.url, - // data - // ); - // return response; - // }, - // (error) => { - // logger.error(" ---- RESPONSE ERROR -----"); - // const { config } = error; - // logger.error( - // config.method, - // config.baseURL, - // config.url, - // config.headers, - // config.data - // ); - // if (error.response) { - // logger.error( - // "Response", - // error.response.status, - // error.response.data - // ); - // } else if (error.request) { - // logger.error("Request", error.request); - // } else { - // logger.error("Error", error.message); - // } - // logger.error(" ----- END RESPONSE ERROR -------"); - // return Promise.reject(error); - // } - // ); + this.instance.interceptors.request.use( + (request) => { + logger.log(" ---- REQUEST -----"); + const data = Array.isArray(request.data) + ? request.data + : omit(request.data, ["refreshToken"]); + logger.log(request.method, request.url, request.params, data); + return request; + }, + (error) => { + logger.error("request error", error); + return Promise.reject(error); + } + ); + this.instance.interceptors.response.use( + (response) => { + logger.log(" ---- RESPONSE -----"); + const data = Array.isArray(response.data) + ? response.data + : omit(response.data, ["username", "accessToken", "refreshToken"]); + logger.log( + response.status, + response.config.method, + response.config.url, + data + ); + return response; + }, + (error) => { + logger.error(" ---- RESPONSE ERROR -----"); + const { config } = error; + logger.error( + config.method, + config.baseURL, + config.url, + config.headers, + config.data + ); + if (error.response) { + logger.error( + "Response", + error.response.status, + error.response.data + ); + } else if (error.request) { + logger.error("Request", error.request); + } else { + logger.error("Error", error.message); + } + logger.error(" ----- END RESPONSE ERROR -------"); + return Promise.reject(error); + } + ); } const userAuth = await userAuthRepository.findOne({ diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index b40bfcda..79559a35 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -4,7 +4,7 @@ import { IsNull } from "typeorm"; import { HydraApi } from "../hydra-api"; import { mergeWithRemoteGames } from "./merge-with-remote-games"; import { WindowManager } from "../window-manager"; -import { AchievementWatcherManager } from "../achievements/AchievementWatcherManager"; +import { AchievementWatcherManager } from "../achievements/achievement-watcher-manager"; export const uploadGamesBatch = async () => { const games = await gameRepository.find({ @@ -29,7 +29,7 @@ export const uploadGamesBatch = async () => { await mergeWithRemoteGames(); - await AchievementWatcherManager.preSearchAchievements(); + AchievementWatcherManager.preSearchAchievements(); if (WindowManager.mainWindow) WindowManager.mainWindow.webContents.send("on-library-batch-complete"); diff --git a/src/main/services/main-loop.ts b/src/main/services/main-loop.ts index 48db4887..b4836b46 100644 --- a/src/main/services/main-loop.ts +++ b/src/main/services/main-loop.ts @@ -1,7 +1,7 @@ import { sleep } from "@main/helpers"; import { DownloadManager } from "./download"; import { watchProcesses } from "./process-watcher"; -import { AchievementWatcherManager } from "./achievements/AchievementWatcherManager"; +import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager"; export const startMainLoop = async () => { // eslint-disable-next-line no-constant-condition diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index 79acf414..bae3501a 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -2,7 +2,7 @@ import { useNavigate } from "react-router-dom"; import { PeopleIcon } from "@primer/octicons-react"; import * as styles from "./sidebar-profile.css"; import { useAppSelector, useUserDetails } from "@renderer/hooks"; -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; @@ -13,8 +13,6 @@ const LONG_POLLING_INTERVAL = 60_000; export function SidebarProfile() { const navigate = useNavigate(); - const pollingInterval = useRef(null); - const { t } = useTranslation("sidebar"); const { @@ -36,14 +34,12 @@ export function SidebarProfile() { }; useEffect(() => { - pollingInterval.current = setInterval(() => { + const pollingInterval = setInterval(() => { syncFriendRequests(); }, LONG_POLLING_INTERVAL); return () => { - if (pollingInterval.current) { - clearInterval(pollingInterval.current); - } + clearInterval(pollingInterval); }; }, [syncFriendRequests]); From 0bcf00536596594f13c99027e33063d28235ca6e Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 04:40:04 -0300 Subject: [PATCH 147/163] feat: refactor --- .../achievement-watcher-manager.ts | 14 ++-------- .../achievements/merge-achievements.ts | 27 +++++++------------ .../update-local-unlocked-achivements.ts | 2 +- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index 42509276..5f915c52 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -135,12 +135,7 @@ const processAchievementFileDiff = async ( const unlockedAchievements = parseAchievementFile(file.filePath, file.type); if (unlockedAchievements.length) { - return mergeAchievements( - game.objectID, - game.shop, - unlockedAchievements, - true - ); + return mergeAchievements(game, unlockedAchievements, true); } }; @@ -187,12 +182,7 @@ export class AchievementWatcherManager { } } - return mergeAchievements( - game.objectID, - "steam", - unlockedAchievements, - false - ); + return mergeAchievements(game, unlockedAchievements, false); }; private static preSearchAchievementsWindows = async () => { diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 3a330d8f..b7aabf9c 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -1,12 +1,12 @@ import { gameAchievementRepository, - gameRepository, userPreferencesRepository, } from "@main/repository"; import type { AchievementData, GameShop, UnlockedAchievement } from "@types"; import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; +import { Game } from "@main/entity"; const saveAchievementsOnLocal = async ( objectId: string, @@ -38,22 +38,15 @@ const saveAchievementsOnLocal = async ( }; export const mergeAchievements = async ( - objectId: string, - shop: GameShop, + game: Game, achievements: UnlockedAchievement[], publishNotification: boolean ) => { - const game = await gameRepository.findOne({ - where: { objectID: objectId, shop: shop }, - }); - - if (!game) return; - const [localGameAchievement, userPreferences] = await Promise.all([ gameAchievementRepository.findOne({ where: { - objectId, - shop, + objectId: game.objectID, + shop: game.shop, }, }), userPreferencesRepository.findOne({ where: { id: 1 } }), @@ -115,8 +108,8 @@ export const mergeAchievements = async ( WindowManager.notificationWindow?.webContents.send( "on-achievement-unlocked", - objectId, - shop, + game.objectID, + game.shop, achievementsInfo ); } @@ -142,8 +135,8 @@ export const mergeAchievements = async ( }) .catch(() => { return saveAchievementsOnLocal( - objectId, - shop, + game.objectID, + game.shop, mergedLocalAchievements, publishNotification ); @@ -151,8 +144,8 @@ export const mergeAchievements = async ( } return saveAchievementsOnLocal( - objectId, - shop, + game.objectID, + game.shop, mergedLocalAchievements, publishNotification ); diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index f579382f..0393477c 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -28,5 +28,5 @@ export const updateLocalUnlockedAchivements = async (game: Game) => { } } - mergeAchievements(game.objectID, "steam", unlockedAchievements, false); + mergeAchievements(game, unlockedAchievements, false); }; From d0f42e73ffdfe9dd5ca6364589d75080204d62f1 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 05:07:49 -0300 Subject: [PATCH 148/163] feat: get achievement data on demand --- .../events/user/get-unlocked-achievements.ts | 15 ++++++++---- .../achievement-watcher-manager.ts | 23 +------------------ .../achievements/get-game-achievement-data.ts | 13 ++++++++++- .../achievements/merge-achievements.ts | 2 +- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/main/events/user/get-unlocked-achievements.ts b/src/main/events/user/get-unlocked-achievements.ts index b6f3e0b7..a831bc50 100644 --- a/src/main/events/user/get-unlocked-achievements.ts +++ b/src/main/events/user/get-unlocked-achievements.ts @@ -5,13 +5,18 @@ import { getGameAchievementData } from "@main/services/achievements/get-game-ach export const getUnlockedAchievements = async ( objectId: string, - shop: GameShop + shop: GameShop, + useCachedData: boolean ): Promise => { const cachedAchievements = await gameAchievementRepository.findOne({ where: { objectId, shop }, }); - const achievementsData = await getGameAchievementData(objectId, shop); + const achievementsData = await getGameAchievementData( + objectId, + shop, + useCachedData + ); const unlockedAchievements = JSON.parse( cachedAchievements?.unlockedAchievements || "[]" @@ -57,12 +62,12 @@ export const getUnlockedAchievements = async ( }); }; -const getGameAchievementsEvent = async ( +const getUnlockedAchievementsEvent = async ( _event: Electron.IpcMainInvokeEvent, objectId: string, shop: GameShop ): Promise => { - return getUnlockedAchievements(objectId, shop); + return getUnlockedAchievements(objectId, shop, false); }; -registerEvent("getUnlockedAchievements", getGameAchievementsEvent); +registerEvent("getUnlockedAchievements", getUnlockedAchievementsEvent); diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index 5f915c52..ea06dfd9 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -1,4 +1,4 @@ -import { gameAchievementRepository, gameRepository } from "@main/repository"; +import { gameRepository } from "@main/repository"; import { parseAchievementFile } from "./parse-achievement-file"; import { Game } from "@main/entity"; import { mergeAchievements } from "./merge-achievements"; @@ -13,7 +13,6 @@ import type { AchievementFile, UnlockedAchievement } from "@types"; import { achievementsLogger } from "../logger"; import { Cracker } from "@shared"; import { IsNull, Not } from "typeorm"; -import { getGameAchievementData } from "./get-game-achievement-data"; const fileStats: Map = new Map(); const fltFiles: Map> = new Map(); @@ -196,16 +195,6 @@ export class AchievementWatcherManager { return Promise.all( games.map((game) => { - gameAchievementRepository - .findOne({ - where: { objectId: game.objectID, shop: game.shop }, - }) - .then((localAchievements) => { - if (!localAchievements || !localAchievements.achievements) { - getGameAchievementData(game.objectID, game.shop); - } - }); - const gameAchievementFiles: AchievementFile[] = []; for (const objectId of getAlternativeObjectIds(game.objectID)) { @@ -233,16 +222,6 @@ export class AchievementWatcherManager { return Promise.all( games.map((game) => { - gameAchievementRepository - .findOne({ - where: { objectId: game.objectID, shop: game.shop }, - }) - .then((localAchievements) => { - if (!localAchievements || !localAchievements.achievements) { - getGameAchievementData(game.objectID, game.shop); - } - }); - const gameAchievementFiles = findAchievementFiles(game); const achievementFileInsideDirectory = findAchievementFileInExecutableDirectory(game); diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index b08914e8..02019dab 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -9,8 +9,19 @@ import { logger } from "../logger"; export const getGameAchievementData = async ( objectId: string, - shop: GameShop + shop: GameShop, + useCachedData: boolean ) => { + if (useCachedData) { + const cachedAchievements = await gameAchievementRepository.findOne({ + where: { objectId, shop }, + }); + + if (cachedAchievements && cachedAchievements.achievements) { + return JSON.parse(cachedAchievements.achievements) as AchievementData[]; + } + } + const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 }, }); diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index b7aabf9c..8dce5aa8 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -26,7 +26,7 @@ const saveAchievementsOnLocal = async ( .then(() => { if (!sendUpdateEvent) return; - return getUnlockedAchievements(objectId, shop) + return getUnlockedAchievements(objectId, shop, true) .then((achievements) => { WindowManager.mainWindow?.webContents.send( `on-update-achievements-${objectId}-${shop}`, From 2f9fa6da48b5e47b40f25fe03a19572ade52154a Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:37:31 -0300 Subject: [PATCH 149/163] feat: add new possible path --- src/main/services/achievements/find-achivement-files.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 4c12f82e..5b1237b3 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -136,6 +136,10 @@ const getPathFromCracker = (cracker: Cracker) => { folderPath: path.join(programData, "Steam", "Player"), fileLocation: ["stats", "achievements.ini"], }, + { + folderPath: path.join(programData, "Steam", "RLD!"), + fileLocation: ["stats", "achievements.ini"], + }, { folderPath: path.join(programData, "Steam", "dodi"), fileLocation: ["stats", "achievements.ini"], From 9f76ae8c591b4f374663846677ca592f81aadef5 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:28:38 -0300 Subject: [PATCH 150/163] feat: re add log interceptor --- src/main/services/hydra-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index c6c60b72..bea6d0a6 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -25,7 +25,7 @@ export class HydraApi { private static instance: AxiosInstance; private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes - private static readonly ADD_LOG_INTERCEPTOR = false; + private static readonly ADD_LOG_INTERCEPTOR = true; private static secondsToMilliseconds = (seconds: number) => seconds * 1000; From d801bad49e02c8230d214e019e3baf888c85e0b7 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:57:07 -0300 Subject: [PATCH 151/163] feat: show notification on launch --- src/locales/en/translation.json | 3 ++- src/locales/pt-BR/translation.json | 3 ++- src/locales/pt-PT/translation.json | 3 ++- .../achievement-watcher-manager.ts | 18 +++++++++---- .../achievements/find-achivement-files.ts | 8 +++--- .../achievements/merge-achievements.ts | 16 ++++++------ src/preload/index.ts | 12 +++++++++ src/renderer/src/declaration.d.ts | 3 +++ .../notification/achievement-notification.tsx | 25 +++++++++++++++++++ 9 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a4426d2c..c94a6053 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -340,6 +340,7 @@ "user_achievements": "{{displayName}}'s Achievements", "your_achievements": "Your Achievements", "unlocked_at": "Unlocked at:", - "subscription_needed": "A Hydra Cloud subscription is needed to see this content" + "subscription_needed": "A Hydra Cloud subscription is needed to see this content", + "new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 77ebe906..85523328 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -342,6 +342,7 @@ "your_achievements": "Suas Conquistas", "user_achievements": "Conquistas de {{displayName}}", "unlocked_at": "Desbloqueado em:", - "subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo" + "subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo", + "new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos" } } diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index dac181ee..423c4518 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -284,6 +284,7 @@ "achievement": { "achievement_unlocked": "Conquista desbloqueada", "unlocked_at": "Desbloqueado em:", - "subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo" + "subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo", + "new_achievements_unlocked": "Encontradas {{achievementCount}} novas conquistas de {{gameCount}} jogos" } } diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index ea06dfd9..2345e3be 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -13,6 +13,7 @@ import type { AchievementFile, UnlockedAchievement } from "@types"; import { achievementsLogger } from "../logger"; import { Cracker } from "@shared"; import { IsNull, Not } from "typeorm"; +import { WindowManager } from "../window-manager"; const fileStats: Map = new Map(); const fltFiles: Map> = new Map(); @@ -136,6 +137,8 @@ const processAchievementFileDiff = async ( if (unlockedAchievements.length) { return mergeAchievements(game, unlockedAchievements, true); } + + return 0; }; export class AchievementWatcherManager { @@ -234,11 +237,16 @@ export class AchievementWatcherManager { }; public static preSearchAchievements = async () => { - if (process.platform === "win32") { - await this.preSearchAchievementsWindows(); - } else { - await this.preSearchAchievementsWithWine(); - } + const newAchievementsCount = + process.platform === "win32" + ? await this.preSearchAchievementsWindows() + : await this.preSearchAchievementsWithWine(); + + WindowManager.notificationWindow?.webContents.send( + "on-combined-achievements-unlocked", + newAchievementsCount.filter((achievements) => achievements).length, + newAchievementsCount.reduce((acc, val) => acc + val, 0) + ); this.hasFinishedMergingWithRemote = true; }; diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 5b1237b3..9195c13a 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -201,10 +201,10 @@ const getPathFromCracker = (cracker: Cracker) => { if (cracker === Cracker.flt) { return [ - { - folderPath: path.join(appData, "FLT"), - fileLocation: ["stats"], - }, + // { + // folderPath: path.join(appData, "FLT"), + // fileLocation: ["stats"], + // }, ]; } diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 8dce5aa8..05899e23 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -117,7 +117,7 @@ export const mergeAchievements = async ( const mergedLocalAchievements = unlockedAchievements.concat(newAchievements); if (game.remoteId) { - return HydraApi.put( + await HydraApi.put( "/profile/games/achievements", { id: game.remoteId, @@ -141,12 +141,14 @@ export const mergeAchievements = async ( publishNotification ); }); + } else { + await saveAchievementsOnLocal( + game.objectID, + game.shop, + mergedLocalAchievements, + publishNotification + ); } - return saveAchievementsOnLocal( - game.objectID, - game.shop, - mergedLocalAchievements, - publishNotification - ); + return newAchievements.length; }; diff --git a/src/preload/index.ts b/src/preload/index.ts index 6d541b0b..d75045ac 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -68,6 +68,18 @@ contextBridge.exposeInMainWorld("electron", { return () => ipcRenderer.removeListener("on-achievement-unlocked", listener); }, + onCombinedAchievementsUnlocked: ( + cb: (gameCount: number, achievementsCount: number) => void + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + gameCount: number, + achievementCount: number + ) => cb(gameCount, achievementCount); + ipcRenderer.on("on-combined-achievements-unlocked", listener); + return () => + ipcRenderer.removeListener("on-combined-achievements-unlocked", listener); + }, onUpdateAchievements: ( objectId: string, shop: GameShop, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 31ff375e..68e22d67 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -73,6 +73,9 @@ declare global { achievements?: { displayName: string; iconUrl: string }[] ) => void ) => () => Electron.IpcRenderer; + onCombinedAchievementsUnlocked: ( + cb: (gameCount: number, achievementCount: number) => void + ) => () => Electron.IpcRenderer; onUpdateAchievements: ( objectId: string, shop: GameShop, diff --git a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx index d33cb231..ec8e2d8a 100644 --- a/src/renderer/src/pages/achievements/notification/achievement-notification.tsx +++ b/src/renderer/src/pages/achievements/notification/achievement-notification.tsx @@ -31,6 +31,31 @@ export function AchievementNotification() { return audio; }, []); + useEffect(() => { + const unsubscribe = window.electron.onCombinedAchievementsUnlocked( + (gameCount, achievementCount) => { + if (gameCount === 0 || achievementCount === 0) return; + + setAchievements([ + { + displayName: t("new_achievements_unlocked", { + gameCount, + achievementCount, + }), + iconUrl: + "https://avatars.githubusercontent.com/u/164102380?s=400&u=01a13a7b4f0c642f7e547b8e1d70440ea06fa750&v=4", + }, + ]); + + audio.play(); + } + ); + + return () => { + unsubscribe(); + }; + }, [audio]); + useEffect(() => { const unsubscribe = window.electron.onAchievementUnlocked( (_object, _shop, achievements) => { From df5f82d47f032c1d459b0cd7c63c567a3309dd41 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:01:43 -0300 Subject: [PATCH 152/163] feat: getMe on launch --- src/main/events/profile/get-me.ts | 101 ++++++------------ src/main/main.ts | 4 +- .../achievements/merge-achievements.ts | 10 +- src/main/services/hydra-api.ts | 3 +- src/main/services/user/get-user-data.ts | 43 ++++++++ 5 files changed, 89 insertions(+), 72 deletions(-) create mode 100644 src/main/services/user/get-user-data.ts diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts index 474effbb..8ab78bf9 100644 --- a/src/main/events/profile/get-me.ts +++ b/src/main/events/profile/get-me.ts @@ -1,80 +1,43 @@ import { registerEvent } from "../register-event"; -import * as Sentry from "@sentry/electron/main"; -import { HydraApi, logger } from "@main/services"; +import { logger } from "@main/services"; import type { ProfileVisibility, UserDetails } from "@types"; -import { - userAuthRepository, - userSubscriptionRepository, -} from "@main/repository"; +import { userAuthRepository } from "@main/repository"; import { UserNotLoggedInError } from "@shared"; +import { getUserData } from "@main/services/user/get-user-data"; const getMe = async ( _event: Electron.IpcMainInvokeEvent ): Promise => { - return HydraApi.get(`/profile/me`) - .then((me) => { - userAuthRepository.upsert( - { - id: 1, - displayName: me.displayName, - profileImageUrl: me.profileImageUrl, - backgroundImageUrl: me.backgroundImageUrl, - userId: me.id, - }, - ["id"] - ); - - if (me.subscription) { - userSubscriptionRepository.upsert( - { - id: 1, - subscriptionId: me.subscription?.id || "", - status: me.subscription?.status || "", - planId: me.subscription?.plan.id || "", - planName: me.subscription?.plan.name || "", - expiresAt: me.subscription?.expiresAt || null, - user: { id: 1 }, - }, - ["id"] - ); - } else { - userSubscriptionRepository.delete({ id: 1 }); - } - - Sentry.setUser({ id: me.id, username: me.username }); - - return me; - }) - .catch(async (err) => { - if (err instanceof UserNotLoggedInError) { - return null; - } - logger.error("Failed to get logged user", err); - const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } }); - - if (loggedUser) { - return { - ...loggedUser, - id: loggedUser.userId, - username: "", - bio: "", - profileVisibility: "PUBLIC" as ProfileVisibility, - subscription: loggedUser.subscription - ? { - id: loggedUser.subscription.subscriptionId, - status: loggedUser.subscription.status, - plan: { - id: loggedUser.subscription.planId, - name: loggedUser.subscription.planName, - }, - expiresAt: loggedUser.subscription.expiresAt, - } - : null, - }; - } - + return getUserData().catch(async (err) => { + if (err instanceof UserNotLoggedInError) { return null; - }); + } + logger.error("Failed to get logged user", err); + const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } }); + + if (loggedUser) { + return { + ...loggedUser, + id: loggedUser.userId, + username: "", + bio: "", + profileVisibility: "PUBLIC" as ProfileVisibility, + subscription: loggedUser.subscription + ? { + id: loggedUser.subscription.subscriptionId, + status: loggedUser.subscription.status, + plan: { + id: loggedUser.subscription.planId, + name: loggedUser.subscription.planName, + }, + expiresAt: loggedUser.subscription.expiresAt, + } + : null, + }; + } + + return null; + }); }; registerEvent("getMe", getMe); diff --git a/src/main/main.ts b/src/main/main.ts index 69bc62e0..6794c204 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -12,6 +12,7 @@ import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; +import { getUserData } from "./services/user/get-user-data"; const loadState = async (userPreferences: UserPreferences | null) => { import("./events"); @@ -22,7 +23,8 @@ const loadState = async (userPreferences: UserPreferences | null) => { Ludusavi.addManifestToLudusaviConfig(); - HydraApi.setupApi().then(() => { + HydraApi.setupApi().then(async () => { + await getUserData().catch(() => {}); uploadGamesBatch(); }); diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 05899e23..277b265e 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -7,6 +7,7 @@ import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; import { Game } from "@main/entity"; +import { achievementsLogger } from "../logger"; const saveAchievementsOnLocal = async ( objectId: string, @@ -117,6 +118,12 @@ export const mergeAchievements = async ( const mergedLocalAchievements = unlockedAchievements.concat(newAchievements); if (game.remoteId) { + achievementsLogger.log( + "Syncing achievements with cloud", + game.title, + game.objectID, + game.remoteId + ); await HydraApi.put( "/profile/games/achievements", { @@ -133,7 +140,8 @@ export const mergeAchievements = async ( publishNotification ); }) - .catch(() => { + .catch((err) => { + achievementsLogger.error(err); return saveAchievementsOnLocal( game.objectID, game.shop, diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index bea6d0a6..1b1c3663 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -44,7 +44,8 @@ export class HydraApi { return userSubscriptionRepository .findOne({ where: { id: 1 } }) .then((userSubscription) => { - if (userSubscription?.status !== "active") return false; + if (!userSubscription) return false; + return ( !userSubscription.expiresAt || userSubscription!.expiresAt > new Date() diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts new file mode 100644 index 00000000..5035b296 --- /dev/null +++ b/src/main/services/user/get-user-data.ts @@ -0,0 +1,43 @@ +import type { UserDetails } from "@types"; +import { HydraApi } from "../hydra-api"; +import { + userAuthRepository, + userSubscriptionRepository, +} from "@main/repository"; +import * as Sentry from "@sentry/electron/main"; + +export const getUserData = () => { + return HydraApi.get(`/profile/me`).then(async (me) => { + userAuthRepository.upsert( + { + id: 1, + displayName: me.displayName, + profileImageUrl: me.profileImageUrl, + backgroundImageUrl: me.backgroundImageUrl, + userId: me.id, + }, + ["id"] + ); + + if (me.subscription) { + await userSubscriptionRepository.upsert( + { + id: 1, + subscriptionId: me.subscription?.id || "", + status: me.subscription?.status || "", + planId: me.subscription?.plan.id || "", + planName: me.subscription?.plan.name || "", + expiresAt: me.subscription?.expiresAt || null, + user: { id: 1 }, + }, + ["id"] + ); + } else { + await userSubscriptionRepository.delete({ id: 1 }); + } + + Sentry.setUser({ id: me.id, username: me.username }); + + return me; + }); +}; From 6ef1135ba28d40d20c03326f67733291a52fe72f Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:10:07 -0300 Subject: [PATCH 153/163] feat: update error log --- src/main/services/achievements/merge-achievements.ts | 6 +++++- src/main/services/hydra-api.ts | 8 ++------ src/shared/index.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 277b265e..84302e73 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -8,6 +8,7 @@ import { HydraApi } from "../hydra-api"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; import { Game } from "@main/entity"; import { achievementsLogger } from "../logger"; +import { SubscriptionRequiredError } from "@shared"; const saveAchievementsOnLocal = async ( objectId: string, @@ -141,7 +142,10 @@ export const mergeAchievements = async ( ); }) .catch((err) => { - achievementsLogger.error(err); + if (err! instanceof SubscriptionRequiredError) { + achievementsLogger.error(err); + } + return saveAchievementsOnLocal( game.objectID, game.shop, diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 1b1c3663..1c027dec 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -8,10 +8,7 @@ import url from "url"; import { uploadGamesBatch } from "./library-sync"; import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id"; import { logger } from "./logger"; -import { - UserNotLoggedInError, - UserWithoutCloudSubscriptionError, -} from "@shared"; +import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared"; // import { omit } from "lodash-es"; import { appVersion } from "@main/constants"; import { omit } from "lodash-es"; @@ -40,7 +37,6 @@ export class HydraApi { } private static async hasCloudSubscription() { - // TODO change this later, this is just a quick test return userSubscriptionRepository .findOne({ where: { id: 1 } }) .then((userSubscription) => { @@ -262,7 +258,7 @@ export class HydraApi { if (needsCloud) { if (!(await this.hasCloudSubscription())) { - throw new UserWithoutCloudSubscriptionError(); + throw new SubscriptionRequiredError(); } } } diff --git a/src/shared/index.ts b/src/shared/index.ts index c2a98c8a..1f17ac56 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -10,7 +10,7 @@ export class UserNotLoggedInError extends Error { } } -export class UserWithoutCloudSubscriptionError extends Error { +export class SubscriptionRequiredError extends Error { constructor() { super("user does not have hydra cloud subscription"); this.name = "UserWithoutCloudSubscriptionError"; From 34a33ccef306a2019cd77f75c1ed9b6fed78c6db Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 21 Oct 2024 19:19:18 +0100 Subject: [PATCH 154/163] feat: adding artifact limit --- package.json | 2 +- src/locales/en/translation.json | 18 +- src/locales/pt-BR/translation.json | 22 ++- src/main/events/catalogue/get-catalogue.ts | 2 +- src/main/events/index.ts | 1 + src/main/services/ludusavi.ts | 23 +-- src/preload/index.ts | 2 + .../components/bottom-panel/bottom-panel.tsx | 4 +- src/renderer/src/constants.ts | 2 +- .../context/cloud-sync/cloud-sync.context.tsx | 75 ++++---- src/renderer/src/declaration.d.ts | 1 + src/renderer/src/dexie.ts | 9 - src/renderer/src/hooks/use-user-details.ts | 4 +- .../cloud-sync-files-modal.css.ts | 25 +-- .../cloud-sync-files-modal.tsx | 163 ++++++++++++------ .../cloud-sync-modal/cloud-sync-modal.css.ts | 14 ++ .../cloud-sync-modal/cloud-sync-modal.tsx | 155 +++++++---------- .../game-details/game-details-content.tsx | 5 +- src/renderer/src/pages/home/home.tsx | 1 + .../profile/profile-hero/profile-hero.css.ts | 3 +- .../upload-background-image-button.tsx | 10 +- src/shared/constants.ts | 1 + src/types/index.ts | 2 +- src/types/ludusavi.types.ts | 1 + 24 files changed, 290 insertions(+), 255 deletions(-) diff --git a/package.json b/package.json index f869f42f..827f3faa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydralauncher", - "version": "2.1.7-preview", + "version": "3.0.0", "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a4426d2c..c5664c1e 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -148,7 +148,19 @@ "backup_deleted": "Backup deleted", "backup_restored": "Backup restored", "see_all_achievements": "See all achievements", - "sign_in_to_see_achievements": "Sign in to see achievements" + "sign_in_to_see_achievements": "Sign in to see achievements", + "mapping_method_automatic": "Automatic", + "mapping_method_manual": "Manual", + "mapping_method_label": "Mapping method", + "files_automatically_mapped": "Files automatically mapped", + "no_backups_created": "No backups created for this game", + "manage_files": "Manage files", + "loading_save_preview": "Searching for save games…", + "wine_prefix": "Wine Prefix", + "wine_prefix_description": "The Wine prefix used to run this game", + "no_download_option_info": "No information available", + "backup_deletion_failed": "Failed to delete backup", + "max_number_of_artifacts_reached": "Maximum number of backups reached for this game" }, "activation": { "title": "Activate Hydra", @@ -333,7 +345,9 @@ "report_reason_spam": "Spam", "report_reason_other": "Other", "profile_reported": "Profile reported", - "your_friend_code": "Your friend code:" + "your_friend_code": "Your friend code:", + "upload_banner": "Upload banner", + "uploading_banner": "Uploading banner…" }, "achievement": { "achievement_unlocked": "Achievement unlocked", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 77ebe906..925ea486 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -5,9 +5,9 @@ }, "home": { "featured": "Destaques", - "trending": "Populares", - "hot": "Populares agora", + "hot": "Populares", "weekly": "📅 Mais baixados da semana", + "achievements": "🏆 Pra platinar", "surprise_me": "Surpreenda-me", "no_results": "Nenhum resultado encontrado", "start_typing": "Comece a digitar para pesquisar…" @@ -144,7 +144,19 @@ "backup_deleted": "Backup apagado", "backup_restored": "Backup restaurado", "see_all_achievements": "Ver todas as conquistas", - "sign_in_to_see_achievements": "Faça login para ver as conquistas" + "sign_in_to_see_achievements": "Faça login para ver as conquistas", + "mapping_method_automatic": "Automático", + "mapping_method_manual": "Manual", + "mapping_method_label": "Método de mapeamento", + "files_automatically_mapped": "Arquivos automaticamente mapeados", + "no_backups_created": "Nenhum backup criado para este jogo", + "manage_files": "Gerenciar arquivos", + "loading_save_preview": "Buscando por arquivos de salvamento…", + "wine_prefix": "Prefixo Wine", + "wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo", + "no_download_option_info": "Sem informações disponíveis", + "backup_deletion_failed": "Falha ao apagar backup", + "max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo" }, "activation": { "title": "Ativação", @@ -335,7 +347,9 @@ "report_reason_spam": "Spam", "report_reason_other": "Outro", "profile_reported": "Perfil reportado", - "your_friend_code": "Seu código de amigo:" + "your_friend_code": "Seu código de amigo:", + "upload_banner": "Carregar banner", + "uploading_banner": "Carregando banner…" }, "achievement": { "achievement_unlocked": "Conquista desbloqueada", diff --git a/src/main/events/catalogue/get-catalogue.ts b/src/main/events/catalogue/get-catalogue.ts index 145f3166..a0542f25 100644 --- a/src/main/events/catalogue/get-catalogue.ts +++ b/src/main/events/catalogue/get-catalogue.ts @@ -15,7 +15,7 @@ const getCatalogue = async ( }); const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>( - `/games/${category}?${params.toString()}`, + `/catalogue/${category}?${params.toString()}`, {}, { needsAuth: false } ); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index ffdfc354..0fac695b 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -66,6 +66,7 @@ import "./cloud-save/upload-save-game"; import "./cloud-save/delete-game-artifact"; import "./notifications/publish-new-repacks-notification"; import { isPortableVersion } from "@main/helpers"; +import "./misc/show-item-in-folder"; ipcMain.handle("ping", () => "pong"); ipcMain.handle("getVersion", () => appVersion); diff --git a/src/main/services/ludusavi.ts b/src/main/services/ludusavi.ts index 8d0b2ee9..8b64ba11 100644 --- a/src/main/services/ludusavi.ts +++ b/src/main/services/ludusavi.ts @@ -7,9 +7,6 @@ import path from "node:path"; import YAML from "yaml"; import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath"; -import axios from "axios"; - -let a: Record | null = null; export class Ludusavi { private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi"); @@ -65,26 +62,14 @@ export class Ludusavi { } static async getBackupPreview( - _shop: GameShop, + shop: GameShop, objectId: string, backupPath: string ): Promise { - if (!a) { - await axios - .get( - "https://gist.githubusercontent.com/thegrannychaseroperation/b23d53e654e3ea060066a5c01b0cacc8/raw/57bf254a1c99dab9315136f660ff7b3d547de215/keys.json" - ) - .then((response) => { - a = response.data; - return response.data; - }); - } + const games = await this.findGames(shop, objectId); - const game = a?.[objectId]; - - // if (!games.length) return null; - - // const [game] = games; + if (!games.length) return null; + const [game] = games; const backupData = await this.worker.run( { title: game, backupPath, preview: true }, diff --git a/src/preload/index.ts b/src/preload/index.ts index 0eadd409..3d541408 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -215,6 +215,8 @@ contextBridge.exposeInMainWorld("electron", { openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), showOpenDialog: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke("showOpenDialog", options), + showItemInFolder: (path: string) => + ipcRenderer.invoke("showItemInFolder", path), platform: process.platform, /* Auto update */ diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index 045158a5..0d28a26e 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -81,10 +81,10 @@ export function BottomPanel() { {status} - {/* + {sessionHash ? `${sessionHash} -` : ""} v{version} " {VERSION_CODENAME}" - */} + ); } diff --git a/src/renderer/src/constants.ts b/src/renderer/src/constants.ts index 5835640f..1cea1d3a 100644 --- a/src/renderer/src/constants.ts +++ b/src/renderer/src/constants.ts @@ -1,6 +1,6 @@ import { Downloader } from "@shared"; -export const VERSION_CODENAME = "Leviticus"; +export const VERSION_CODENAME = "Skyscraper"; export const DOWNLOADER_NAME = { [Downloader.RealDebrid]: "Real-Debrid", diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx index 6db61332..5671ca56 100644 --- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -1,4 +1,3 @@ -import { gameBackupsTable } from "@renderer/dexie"; import { useToast } from "@renderer/hooks"; import { logger } from "@renderer/logger"; import type { LudusaviBackup, GameArtifact, GameShop } from "@types"; @@ -7,7 +6,6 @@ import React, { useCallback, useEffect, useMemo, - useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; @@ -31,8 +29,10 @@ export interface CloudSyncContext { deleteGameArtifact: (gameArtifactId: string) => Promise; setShowCloudSyncFilesModal: React.Dispatch>; getGameBackupPreview: () => Promise; + getGameArtifacts: () => Promise; restoringBackup: boolean; uploadingBackup: boolean; + loadingPreview: boolean; } export const cloudSyncContext = createContext({ @@ -47,8 +47,10 @@ export const cloudSyncContext = createContext({ showCloudSyncFilesModal: false, setShowCloudSyncFilesModal: () => {}, getGameBackupPreview: async () => {}, + getGameArtifacts: async () => {}, restoringBackup: false, uploadingBackup: false, + loadingPreview: false, }); const { Provider } = cloudSyncContext; @@ -67,8 +69,6 @@ export function CloudSyncContextProvider({ }: CloudSyncContextProviderProps) { const { t } = useTranslation("game_details"); - const backupPreviewLock = useRef(""); - const [artifacts, setArtifacts] = useState([]); const [showCloudSyncModal, setShowCloudSyncModal] = useState(false); const [backupPreview, setBackupPreview] = useState( @@ -77,6 +77,7 @@ export function CloudSyncContextProvider({ const [restoringBackup, setRestoringBackup] = useState(false); const [uploadingBackup, setUploadingBackup] = useState(false); const [showCloudSyncFilesModal, setShowCloudSyncFilesModal] = useState(false); + const [loadingPreview, setLoadingPreview] = useState(false); const { showSuccessToast } = useToast(); @@ -88,32 +89,25 @@ export function CloudSyncContextProvider({ [objectId, shop] ); - const getGameBackupPreview = useCallback(async () => { - const backupPreviewLockKey = `${objectId}-${shop}`; + const getGameArtifacts = useCallback(async () => { + const results = await window.electron.getGameArtifacts(objectId, shop); + setArtifacts(results); + }, [objectId, shop]); - if (backupPreviewLock.current !== backupPreviewLockKey) { - backupPreviewLock.current = backupPreviewLockKey; - await Promise.allSettled([ - window.electron.getGameArtifacts(objectId, shop).then((results) => { - setArtifacts(results); - }), - window.electron - .getGameBackupPreview(objectId, shop) - .then((preview) => { - backupPreviewLock.current = ""; - if (preview && Object.keys(preview.games).length) { - setBackupPreview(preview); - } - }) - .catch((err) => { - logger.error( - "Failed to get game backup preview", - objectId, - shop, - err - ); - }), - ]); + const getGameBackupPreview = useCallback(async () => { + setLoadingPreview(true); + + try { + const preview = await window.electron.getGameBackupPreview( + objectId, + shop + ); + + setBackupPreview(preview); + } catch (err) { + logger.error("Failed to get game backup preview", objectId, shop, err); + } finally { + setLoadingPreview(false); } }, [objectId, shop]); @@ -131,14 +125,8 @@ export function CloudSyncContextProvider({ shop, () => { showSuccessToast(t("backup_uploaded")); - setUploadingBackup(false); - gameBackupsTable.add({ - objectId, - shop, - createdAt: new Date(), - }); - + getGameArtifacts(); getGameBackupPreview(); } ); @@ -148,6 +136,7 @@ export function CloudSyncContextProvider({ showSuccessToast(t("backup_restored")); setRestoringBackup(false); + getGameArtifacts(); getGameBackupPreview(); }); @@ -155,15 +144,23 @@ export function CloudSyncContextProvider({ removeUploadCompleteListener(); removeDownloadCompleteListener(); }; - }, [objectId, shop, showSuccessToast, t, getGameBackupPreview]); + }, [ + objectId, + shop, + showSuccessToast, + t, + getGameBackupPreview, + getGameArtifacts, + ]); const deleteGameArtifact = useCallback( async (gameArtifactId: string) => { return window.electron.deleteGameArtifact(gameArtifactId).then(() => { getGameBackupPreview(); + getGameArtifacts(); }); }, - [getGameBackupPreview] + [getGameBackupPreview, getGameArtifacts] ); useEffect(() => { @@ -194,12 +191,14 @@ export function CloudSyncContextProvider({ restoringBackup, uploadingBackup, showCloudSyncFilesModal, + loadingPreview, setShowCloudSyncModal, uploadSaveGame, downloadGameArtifact, deleteGameArtifact, setShowCloudSyncFilesModal, getGameBackupPreview, + getGameArtifacts, }} > {children} diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index e6a47959..14bc828b 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -171,6 +171,7 @@ declare global { showOpenDialog: ( options: Electron.OpenDialogOptions ) => Promise; + showItemInFolder: (path: string) => Promise; platform: NodeJS.Platform; /* Auto update */ diff --git a/src/renderer/src/dexie.ts b/src/renderer/src/dexie.ts index e0e86a7f..2e3565ce 100644 --- a/src/renderer/src/dexie.ts +++ b/src/renderer/src/dexie.ts @@ -1,13 +1,6 @@ import type { GameShop, HowLongToBeatCategory } from "@types"; import { Dexie } from "dexie"; -export interface GameBackup { - id?: number; - shop: GameShop; - objectId: string; - createdAt: Date; -} - export interface HowLongToBeatEntry { id?: number; objectId: string; @@ -22,13 +15,11 @@ export const db = new Dexie("Hydra"); db.version(4).stores({ repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`, downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`, - gameBackups: `++id, [shop+objectId], createdAt`, howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`, }); export const downloadSourcesTable = db.table("downloadSources"); export const repacksTable = db.table("repacks"); -export const gameBackupsTable = db.table("gameBackups"); export const howLongToBeatEntriesTable = db.table( "howLongToBeatEntries" ); diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 1872c95d..5c06965c 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -14,7 +14,6 @@ import type { UserDetails, } from "@types"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; -import { gameBackupsTable } from "@renderer/dexie"; export function useUserDetails() { const dispatch = useAppDispatch(); @@ -33,7 +32,6 @@ export function useUserDetails() { dispatch(setUserDetails(null)); dispatch(setProfileBackground(null)); - await gameBackupsTable.clear(); window.localStorage.removeItem("userDetails"); }, [dispatch]); @@ -130,7 +128,7 @@ export function useUserDetails() { const unblockUser = (userId: string) => window.electron.unblockUser(userId); const hasActiveSubscription = useMemo(() => { - if (!userDetails?.subscription) { + if (!userDetails?.subscription?.plan) { return false; } diff --git a/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.css.ts b/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.css.ts index bb3335fa..f6a46a08 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.css.ts +++ b/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.css.ts @@ -1,26 +1,9 @@ import { style } from "@vanilla-extract/css"; -import { SPACING_UNIT, vars } from "../../../theme.css"; +import { SPACING_UNIT } from "../../../theme.css"; -export const artifacts = style({ - display: "flex", +export const mappingMethods = style({ + display: "grid", gap: `${SPACING_UNIT}px`, - flexDirection: "column", - listStyle: "none", - margin: "0", - padding: "0", -}); - -export const artifactButton = style({ - display: "flex", - textAlign: "left", - flexDirection: "row", - alignItems: "center", - gap: `${SPACING_UNIT}px`, - color: vars.color.body, - padding: `${SPACING_UNIT * 2}px`, - backgroundColor: vars.color.darkBackground, - border: `1px solid ${vars.color.border}`, - borderRadius: "4px", - justifyContent: "space-between", + gridTemplateColumns: "repeat(2, 1fr)", }); diff --git a/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx index ab416773..3bcba8a7 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-files-modal/cloud-sync-files-modal.tsx @@ -1,18 +1,34 @@ -import { Modal, ModalProps } from "@renderer/components"; -import { useContext, useMemo } from "react"; +import { Button, Modal, ModalProps } from "@renderer/components"; +import { useContext, useMemo, useState } from "react"; import { cloudSyncContext } from "@renderer/context"; -// import { useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; +import { CheckCircleFillIcon } from "@primer/octicons-react"; + +import * as styles from "./cloud-sync-files-modal.css"; +import { formatBytes } from "@shared"; +import { vars } from "@renderer/theme.css"; +// import { useToast } from "@renderer/hooks"; export interface CloudSyncFilesModalProps extends Omit {} +export enum FileMappingMethod { + Automatic = "AUTOMATIC", + Manual = "MANUAL", +} + export function CloudSyncFilesModal({ visible, onClose, }: CloudSyncFilesModalProps) { + const [selectedFileMappingMethod, setSelectedFileMappingMethod] = + useState(FileMappingMethod.Automatic); const { backupPreview } = useContext(cloudSyncContext); + // const { gameTitle } = useContext(gameDetailsContext); - // const { t } = useTranslation("game_details"); + const { t } = useTranslation("game_details"); + + // const { showSuccessToast } = useToast(); const files = useMemo(() => { if (!backupPreview) { @@ -20,6 +36,7 @@ export function CloudSyncFilesModal({ } const [game] = Object.values(backupPreview.games); + if (!game) return []; const entries = Object.entries(game.files); return entries.map(([key, value]) => { @@ -27,6 +44,19 @@ export function CloudSyncFilesModal({ }); }, [backupPreview]); + // const handleAddCustomPathClick = useCallback(async () => { + // const { filePaths } = await window.electron.showOpenDialog({ + // properties: ["openDirectory"], + // }); + + // if (filePaths && filePaths.length > 0) { + // const path = filePaths[0]; + // await window.electron.selectGameBackupDirectory(gameTitle, path); + // showSuccessToast("custom_backup_location_set"); + // getGameBackupPreview(); + // } + // }, [gameTitle, showSuccessToast, getGameBackupPreview]); + return ( - {/*
    - {["AUTOMATIC", "CUSTOM"].map((downloader) => ( - - ))} -
    */} +
    + {t("mapping_method_label")} - {/* - {t("select_directory")} - - } - /> */} - -
    - - - - - - - - - {files.map((file) => ( - - - - - +
    + {Object.values(FileMappingMethod).map((mappingMethod) => ( + ))} -
    -
    ArquivoHashTamanho
    {file.path}{file.change}{file.path}
    +
    +
    + +
    + {/* + + {t("select_executable")} + + } + /> */} + +

    {t("files_automatically_mapped")}

    + +
      + {files.map((file) => ( +
    • + +

      {formatBytes(file.bytes)}

      +
    • + ))} +
    +
    ); } diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts index 77231403..dc2d0031 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.css.ts @@ -49,3 +49,17 @@ export const progress = style({ backgroundColor: vars.color.muted, }, }); + +export const manageFilesButton = style({ + margin: "0", + padding: "0", + alignSelf: "flex-start", + fontSize: 14, + cursor: "pointer", + textDecoration: "underline", + color: vars.color.body, + ":disabled": { + cursor: "not-allowed", + opacity: vars.opacity.disabled, + }, +}); diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index 87260f59..7a546eb4 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -6,7 +6,6 @@ import * as styles from "./cloud-sync-modal.css"; import { formatBytes } from "@shared"; import { format } from "date-fns"; import { - CheckCircleFillIcon, ClockIcon, DeviceDesktopIcon, HistoryIcon, @@ -16,18 +15,16 @@ import { UploadIcon, } from "@primer/octicons-react"; import { useToast } from "@renderer/hooks"; -import { GameBackup, gameBackupsTable } from "@renderer/dexie"; import { useTranslation } from "react-i18next"; import { AxiosProgressEvent } from "axios"; import { formatDownloadProgress } from "@renderer/helpers"; -import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { SPACING_UNIT } from "@renderer/theme.css"; export interface CloudSyncModalProps extends Omit {} export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { const [deletingArtifact, setDeletingArtifact] = useState(false); - const [lastBackup, setLastBackup] = useState(null); const [backupDownloadProgress, setBackupDownloadProgress] = useState(null); @@ -38,6 +35,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { backupPreview, uploadingBackup, restoringBackup, + loadingPreview, uploadSaveGame, downloadGameArtifact, deleteGameArtifact, @@ -64,11 +62,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { }; useEffect(() => { - gameBackupsTable - .where({ shop: shop, objectId }) - .last() - .then((lastBackup) => setLastBackup(lastBackup || null)); - const removeBackupDownloadProgressListener = window.electron.onBackupDownloadProgress( objectId!, @@ -111,20 +104,19 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { ); } - if (lastBackup) { + if (loadingPreview) { return ( - - - - - {t("last_backup_date", { - date: format(lastBackup.createdAt, "dd/MM/yyyy HH:mm"), - })} + + {t("loading_save_preview")} ); } + if (artifacts.length >= 2) { + return t("max_number_of_artifacts_reached"); + } + if (!backupPreview) { return t("no_backup_preview"); } @@ -133,93 +125,76 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { }, [ uploadingBackup, backupDownloadProgress?.progress, - lastBackup, backupPreview, restoringBackup, + loadingPreview, + artifacts, t, ]); const disableActions = uploadingBackup || restoringBackup || deletingArtifact; return ( - <> - {/* {}} - onClose={() => {}} - visible - /> */} - - +
    +
    +

    {gameTitle}

    +

    {backupStateLabel}

    + + +
    + + +
    + +
    -
    -

    {gameTitle}

    -

    {backupStateLabel}

    - - -
    - - -
    - -
    -
    -

    {t("backups")}

    - {artifacts.length} / 2 -
    - -
    - Espaço usado - -
    +

    {t("backups")}

    + {artifacts.length} / 2
    +
    + {artifacts.length > 0 ? (
      - {artifacts.map((artifact) => ( + {artifacts.map((artifact, index) => (
    • -

      Backup do dia {format(artifact.createdAt, "dd/MM")}

      +

      {t("backup_title", { number: index + 1 })}

      {formatBytes(artifact.artifactLengthInBytes)}
      @@ -272,7 +247,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
    • ))}
    -
    - + ) : ( +

    {t("no_backups_created")}

    + )} + ); } 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 a112e9d1..b852916f 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -36,7 +36,7 @@ export function GameDetailsContent() { const { userDetails } = useUserDetails(); - const { setShowCloudSyncModal, getGameBackupPreview } = + const { setShowCloudSyncModal, getGameBackupPreview, getGameArtifacts } = useContext(cloudSyncContext); const aboutTheGame = useMemo(() => { @@ -108,7 +108,8 @@ export function GameDetailsContent() { useEffect(() => { getGameBackupPreview(); - }, [getGameBackupPreview]); + getGameArtifacts(); + }, [getGameBackupPreview, getGameArtifacts]); return (
    diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index ad306726..61417b1f 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -34,6 +34,7 @@ export default function Home() { >({ [CatalogueCategory.Hot]: [], [CatalogueCategory.Weekly]: [], + [CatalogueCategory.Achievements]: [], }); const getCatalogue = useCallback((category: CatalogueCategory) => { diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts index fd02d11f..2080e445 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.css.ts @@ -52,8 +52,7 @@ export const profileDisplayName = style({ display: "flex", alignItems: "center", position: "relative", - textShadow: - "0 0 40px rgb(0 0 0), 0 0 20px rgb(0 0 0 / 50%), 0 0 10px rgb(0 0 0 / 20%)", + textShadow: "0 0 5px rgb(0 0 0 / 40%)", }); export const heroPanel = style({ diff --git a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx index a11c9069..3e8e2971 100644 --- a/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx +++ b/src/renderer/src/pages/profile/upload-background-image-button/upload-background-image-button.tsx @@ -5,11 +5,14 @@ import { userProfileContext } from "@renderer/context"; import * as styles from "./upload-background-image-button.css"; import { useToast, useUserDetails } from "@renderer/hooks"; +import { useTranslation } from "react-i18next"; export function UploadBackgroundImageButton() { const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] = useState(false); - const { hasActiveSubscription } = useUserDetails(); + const { userDetails } = useUserDetails(); + + const { t } = useTranslation("user_profile"); const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext); const { patchUser, fetchUserDetails } = useUserDetails(); @@ -44,7 +47,8 @@ export function UploadBackgroundImageButton() { } }; - if (!isMe || !hasActiveSubscription) return null; + if (!isMe || !userDetails?.subscription) return null; + if (userDetails.subscription.plan.name !== "plus") return null; return ( ); } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 0a466695..f0f6d2f0 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -14,6 +14,7 @@ export enum DownloadSourceStatus { export enum CatalogueCategory { Hot = "hot", Weekly = "weekly", + Achievements = "achievements", } export enum SteamContentDescriptor { diff --git a/src/types/index.ts b/src/types/index.ts index c3a91053..2f6746f4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -237,7 +237,7 @@ export type SubscriptionStatus = "active" | "pending" | "cancelled"; export interface Subscription { id: string; status: SubscriptionStatus; - plan: { id: string; name: string }; + plan: { id: string; name: "basic" | "plus" }; expiresAt: Date | null; } diff --git a/src/types/ludusavi.types.ts b/src/types/ludusavi.types.ts index 01561b4d..55f3f506 100644 --- a/src/types/ludusavi.types.ts +++ b/src/types/ludusavi.types.ts @@ -1,6 +1,7 @@ export interface LudusaviScanChange { change: "New" | "Different" | "Removed" | "Same" | "Unknown"; decision: "Processed" | "Cancelled" | "Ignore"; + bytes: number; } export interface LudusaviGame extends LudusaviScanChange { From af8468066a66dac77344ad856331163b7f904436 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 21 Oct 2024 19:21:25 +0100 Subject: [PATCH 155/163] feat: adding artifact limit --- src/types/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/index.ts b/src/types/index.ts index 2f6746f4..c3a91053 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -237,7 +237,7 @@ export type SubscriptionStatus = "active" | "pending" | "cancelled"; export interface Subscription { id: string; status: SubscriptionStatus; - plan: { id: string; name: "basic" | "plus" }; + plan: { id: string; name: string }; expiresAt: Date | null; } From 21fecb2c4e00f0da7b6dc471754af2c7aa65b86c Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:30:38 -0300 Subject: [PATCH 156/163] feat: open checkout page --- src/main/events/index.ts | 1 + src/main/events/misc/open-checkout.ts | 22 +++++++++++++++++++ .../achievements/merge-achievements.ts | 8 +------ src/preload/index.ts | 1 + src/renderer/src/declaration.d.ts | 1 + .../game-details/game-details-content.tsx | 7 +++++- 6 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 src/main/events/misc/open-checkout.ts diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 87fbdbe0..eea8e3ed 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -25,6 +25,7 @@ import "./library/verify-executable-path"; import "./library/remove-game"; import "./library/remove-game-from-library"; import "./library/select-game-wine-prefix"; +import "./misc/open-checkout"; import "./misc/open-external"; import "./misc/show-open-dialog"; import "./torrenting/cancel-game-download"; diff --git a/src/main/events/misc/open-checkout.ts b/src/main/events/misc/open-checkout.ts new file mode 100644 index 00000000..e1e35674 --- /dev/null +++ b/src/main/events/misc/open-checkout.ts @@ -0,0 +1,22 @@ +import { shell } from "electron"; +import { registerEvent } from "../register-event"; +import { userAuthRepository } from "@main/repository"; +import { HydraApi } from "@main/services"; + +const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { + const userAuth = await userAuthRepository.findOne({ where: { id: 1 } }); + + if (!userAuth) { + return; + } + + const paymentToken = await HydraApi.post("/auth/payment", { + refreshToken: userAuth.refreshToken, + }).then((response) => response.accessToken); + + shell.openExternal( + "https://checkout.hydralauncher.gg/?token=" + paymentToken + ); +}; + +registerEvent("openCheckout", openCheckout); diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 84302e73..b1ae0273 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -119,12 +119,6 @@ export const mergeAchievements = async ( const mergedLocalAchievements = unlockedAchievements.concat(newAchievements); if (game.remoteId) { - achievementsLogger.log( - "Syncing achievements with cloud", - game.title, - game.objectID, - game.remoteId - ); await HydraApi.put( "/profile/games/achievements", { @@ -142,7 +136,7 @@ export const mergeAchievements = async ( ); }) .catch((err) => { - if (err! instanceof SubscriptionRequiredError) { + if (!(err instanceof SubscriptionRequiredError)) { achievementsLogger.error(err); } diff --git a/src/preload/index.ts b/src/preload/index.ts index d75045ac..6090e636 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -223,6 +223,7 @@ contextBridge.exposeInMainWorld("electron", { getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"), isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), + openCheckout: () => ipcRenderer.invoke("openCheckout"), showOpenDialog: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke("showOpenDialog", options), platform: process.platform, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 68e22d67..24e541e7 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -162,6 +162,7 @@ declare global { /* Misc */ openExternal: (src: string) => Promise; + openCheckout: () => Promise; getVersion: () => Promise; ping: () => string; getDefaultDownloadsPath: () => Promise; 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 a112e9d1..442446b2 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -34,7 +34,7 @@ export function GameDetailsContent() { hasNSFWContentBlocked, } = useContext(gameDetailsContext); - const { userDetails } = useUserDetails(); + const { userDetails, hasActiveSubscription } = useUserDetails(); const { setShowCloudSyncModal, getGameBackupPreview } = useContext(cloudSyncContext); @@ -103,6 +103,11 @@ export function GameDetailsContent() { return; } + if (!hasActiveSubscription) { + window.electron.openCheckout(); + return; + } + setShowCloudSyncModal(true); }; From e86daacad4341690f318981213d699dc125f16b8 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 21 Oct 2024 19:33:25 +0100 Subject: [PATCH 157/163] ci: testing cloudfront deployment --- src/main/events/misc/show-item-in-folder.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/events/misc/show-item-in-folder.ts diff --git a/src/main/events/misc/show-item-in-folder.ts b/src/main/events/misc/show-item-in-folder.ts new file mode 100644 index 00000000..bc774cda --- /dev/null +++ b/src/main/events/misc/show-item-in-folder.ts @@ -0,0 +1,11 @@ +import { shell } from "electron"; +import { registerEvent } from "../register-event"; + +const showItemInFolder = async ( + _event: Electron.IpcMainInvokeEvent, + filePath: string +) => { + return shell.showItemInFolder(filePath); +}; + +registerEvent("showItemInFolder", showItemInFolder); From b7f717a6f4f1cbeb1b1e9487ba305f47d1459172 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:38:43 -0300 Subject: [PATCH 158/163] feat: use env --- src/main/events/misc/open-checkout.ts | 6 +++++- src/main/vite-env.d.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/events/misc/open-checkout.ts b/src/main/events/misc/open-checkout.ts index e1e35674..ba48f03b 100644 --- a/src/main/events/misc/open-checkout.ts +++ b/src/main/events/misc/open-checkout.ts @@ -14,8 +14,12 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { refreshToken: userAuth.refreshToken, }).then((response) => response.accessToken); + const params = new URLSearchParams({ + token: paymentToken, + }); + shell.openExternal( - "https://checkout.hydralauncher.gg/?token=" + paymentToken + `${import.meta.env.MAIN_VITE_CHECKOUT_URL}?${params.toString()}` ); }; diff --git a/src/main/vite-env.d.ts b/src/main/vite-env.d.ts index 41f54e24..698bb7db 100644 --- a/src/main/vite-env.d.ts +++ b/src/main/vite-env.d.ts @@ -5,6 +5,7 @@ interface ImportMetaEnv { readonly MAIN_VITE_API_URL: string; readonly MAIN_VITE_AUTH_URL: string; readonly MAIN_VITE_SENTRY_DSN: string; + readonly MAIN_VITE_CHECKOUT_URL: string; } interface ImportMeta { From 8a24fd8ef9e9a8a56a025884a4baa8cb2ded082e Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:54:37 -0300 Subject: [PATCH 159/163] feat: add envs to gh action yml --- .github/workflows/build.yml | 2 ++ .github/workflows/release.yml | 2 ++ .../pages/profile/edit-profile-modal/edit-profile-modal.css.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05066c1c..539d1f0c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,7 @@ jobs: env: MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} + MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -52,6 +53,7 @@ jobs: env: MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }} + MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fc71ec1..bc03a2db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,7 @@ jobs: env: MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} + MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -54,6 +55,7 @@ jobs: env: MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }} + MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts index b4232096..8dd1df51 100644 --- a/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts +++ b/src/renderer/src/pages/profile/edit-profile-modal/edit-profile-modal.css.ts @@ -27,6 +27,7 @@ export const profileAvatarEditOverlay = style({ justifyContent: "center", transition: "all ease 0.2s", alignItems: "center", + borderRadius: "4px", opacity: "0", }); From 955725b6462e75f09489f85a567adef4de95e8d1 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 21 Oct 2024 20:10:18 +0100 Subject: [PATCH 160/163] fix: fixing games with colon ludusavi --- src/main/workers/ludusavi.worker.ts | 2 +- .../pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx | 5 +++++ src/renderer/src/pages/game-details/game-details-content.tsx | 5 ++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/workers/ludusavi.worker.ts b/src/main/workers/ludusavi.worker.ts index 469ab721..855d20bf 100644 --- a/src/main/workers/ludusavi.worker.ts +++ b/src/main/workers/ludusavi.worker.ts @@ -35,7 +35,7 @@ export const backupGame = ({ preview?: boolean; winePrefix?: string; }) => { - const args = ["backup", title, "--api", "--force"]; + const args = ["backup", `"${title}"`, "--api", "--force"]; if (preview) args.push("--preview"); if (backupPath) args.push("--path", backupPath); diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index 7a546eb4..325b64e5 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -40,6 +40,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { downloadGameArtifact, deleteGameArtifact, setShowCloudSyncFilesModal, + getGameBackupPreview, } = useContext(cloudSyncContext); const { objectId, shop, gameTitle, lastDownloadedOption } = @@ -81,6 +82,10 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { downloadGameArtifact(artifactId); }; + useEffect(() => { + getGameBackupPreview(); + }, [getGameBackupPreview]); + const backupStateLabel = useMemo(() => { if (uploadingBackup) { return ( 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 b852916f..c80863c8 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -36,7 +36,7 @@ export function GameDetailsContent() { const { userDetails } = useUserDetails(); - const { setShowCloudSyncModal, getGameBackupPreview, getGameArtifacts } = + const { setShowCloudSyncModal, getGameArtifacts } = useContext(cloudSyncContext); const aboutTheGame = useMemo(() => { @@ -107,9 +107,8 @@ export function GameDetailsContent() { }; useEffect(() => { - getGameBackupPreview(); getGameArtifacts(); - }, [getGameBackupPreview, getGameArtifacts]); + }, [getGameArtifacts]); return (
    From a72eb768e7d5a6e033d34fae6a3ce62171fd91e0 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:12:10 -0300 Subject: [PATCH 161/163] feat: show buttons to open checkout --- src/locales/en/translation.json | 3 +- src/locales/pt-BR/translation.json | 3 +- .../game-details/game-details.context.tsx | 7 ++++ .../game-details.context.types.ts | 1 + .../game-details/game-details-content.tsx | 3 +- .../pages/game-details/sidebar/sidebar.css.ts | 13 ++++++++ .../pages/game-details/sidebar/sidebar.tsx | 32 ++++++++++++++++--- 7 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 51aa7b7e..e4a795d1 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -160,7 +160,8 @@ "wine_prefix_description": "The Wine prefix used to run this game", "no_download_option_info": "No information available", "backup_deletion_failed": "Failed to delete backup", - "max_number_of_artifacts_reached": "Maximum number of backups reached for this game" + "max_number_of_artifacts_reached": "Maximum number of backups reached for this game", + "achievements_not_sync": "Your achievements are not synchronized" }, "activation": { "title": "Activate Hydra", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 839fd969..32d1ca26 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -156,7 +156,8 @@ "wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo", "no_download_option_info": "Sem informações disponíveis", "backup_deletion_failed": "Falha ao apagar backup", - "max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo" + "max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo", + "achievements_not_sync": "Suas conquistas não estão sincronizadas" }, "activation": { "title": "Ativação", 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 1cd8a529..77aaab5d 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -53,6 +53,7 @@ export const gameDetailsContext = createContext({ setShowGameOptionsModal: () => {}, setShowRepacksModal: () => {}, setHasNSFWContentBlocked: () => {}, + handleClickOpenCheckout: () => {}, }); const { Provider } = gameDetailsContext; @@ -110,6 +111,11 @@ export function GameDetailsContextProvider({ (state) => state.userPreferences.value ); + const handleClickOpenCheckout = () => { + // TODO: show modal before redirecting to checkout page + window.electron.openCheckout(); + }; + const updateGame = useCallback(async () => { return window.electron .getGameByObjectId(objectId!) @@ -282,6 +288,7 @@ export function GameDetailsContextProvider({ updateGame, setShowRepacksModal, setShowGameOptionsModal, + handleClickOpenCheckout, }} > {children} diff --git a/src/renderer/src/context/game-details/game-details.context.types.ts b/src/renderer/src/context/game-details/game-details.context.types.ts index 49718430..842065cc 100644 --- a/src/renderer/src/context/game-details/game-details.context.types.ts +++ b/src/renderer/src/context/game-details/game-details.context.types.ts @@ -29,4 +29,5 @@ export interface GameDetailsContext { setShowRepacksModal: React.Dispatch>; setShowGameOptionsModal: React.Dispatch>; setHasNSFWContentBlocked: React.Dispatch>; + handleClickOpenCheckout: () => void; } 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 6e26c5ca..28102854 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,7 @@ export function GameDetailsContent() { gameColor, setGameColor, hasNSFWContentBlocked, + handleClickOpenCheckout, } = useContext(gameDetailsContext); const { userDetails, hasActiveSubscription } = useUserDetails(); @@ -104,7 +105,7 @@ export function GameDetailsContent() { } if (!hasActiveSubscription) { - window.electron.openCheckout(); + handleClickOpenCheckout(); return; } diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts index 1f0e0515..aa27cd42 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts @@ -154,3 +154,16 @@ export const listItemImage = recipe({ }, }, }); + +export const subscriptionRequiredButton = style({ + textDecoration: "none", + display: "flex", + justifyContent: "center", + width: "100%", + gap: `${SPACING_UNIT / 2}px`, + color: vars.color.warning, + cursor: "pointer", + ":hover": { + textDecoration: "underline", + }, +}); diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index a6a24850..07443fee 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -10,12 +10,17 @@ import { Button, Link } from "@renderer/components"; import * as styles from "./sidebar.css"; import { gameDetailsContext } from "@renderer/context"; import { useDate, useFormat, useUserDetails } from "@renderer/hooks"; -import { DownloadIcon, LockIcon, PeopleIcon } from "@primer/octicons-react"; +import { + CloudOfflineIcon, + DownloadIcon, + LockIcon, + PeopleIcon, +} from "@primer/octicons-react"; import { HowLongToBeatSection } from "./how-long-to-beat-section"; import { howLongToBeatEntriesTable } from "@renderer/dexie"; import { SidebarSection } from "../sidebar-section/sidebar-section"; import { buildGameAchievementPath } from "@renderer/helpers"; -import { SPACING_UNIT } from "@renderer/theme.css"; +import { SPACING_UNIT, vars } from "@renderer/theme.css"; const fakeAchievements: UserAchievement[] = [ { @@ -57,13 +62,20 @@ export function Sidebar() { data: HowLongToBeatCategory[] | null; }>({ isLoading: true, data: null }); - const { userDetails } = useUserDetails(); + const { userDetails, hasActiveSubscription } = useUserDetails(); const [activeRequirement, setActiveRequirement] = useState("minimum"); - const { gameTitle, shopDetails, objectId, shop, stats, achievements } = - useContext(gameDetailsContext); + const { + gameTitle, + shopDetails, + objectId, + shop, + stats, + achievements, + handleClickOpenCheckout, + } = useContext(gameDetailsContext); const { t } = useTranslation("game_details"); const { formatDateTime } = useDate(); @@ -162,6 +174,16 @@ export function Sidebar() { })} >
      + {!hasActiveSubscription && ( + + )} + {achievements.slice(0, 4).map((achievement, index) => (
    • Date: Mon, 21 Oct 2024 17:26:15 -0300 Subject: [PATCH 162/163] feat: creating subscribe modal --- src/locales/en/translation.json | 10 ++ src/locales/pt-BR/translation.json | 9 ++ src/renderer/src/app.tsx | 35 +++++- .../shared-modals/subscription-tour-modal.tsx | 104 ++++++++++++++++++ 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/pages/shared-modals/subscription-tour-modal.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e4a795d1..3d58c86d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -357,5 +357,15 @@ "unlocked_at": "Unlocked at:", "subscription_needed": "A Hydra Cloud subscription is needed to see this content", "new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games" + }, + "tour": { + "subscription_tour_title": "Hydra Cloud Subscription", + "subscribe_now": "Subscribe now", + "cloud_saving": "Cloud saving (up to {{gameCount}} games)", + "cloud_achievements": "Save your achievements on the cloud", + "animated_profile_picture": "Animated profile pictures", + "premium_support": "Premium Support", + "show_and_compare_achievements": "Show and compare your achievements to other users", + "animated_profile_banner": "Animated profile banner" } } diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 32d1ca26..c79c0847 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -359,5 +359,14 @@ "unlocked_at": "Desbloqueado em:", "subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo", "new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos" + }, + "tour": { + "subscription_tour_title": "Assinatura Hydra Cloud", + "subscribe_now": "Inscreva-se agora", + "cloud_achievements": "Salvamento de conquistas em nuvem", + "animated_profile_picture": "Fotos de perfil animadas", + "premium_support": "Suporte Premium", + "show_and_compare_achievements": "Exiba e compare suas conquistas com outros usuários", + "animated_profile_banner": "Banner animado no perfil" } } diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 7c572a56..941fb75d 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useRef } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; @@ -29,6 +29,11 @@ import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { downloadSourcesWorker } from "./workers"; import { repacksContext } from "./context"; import { logger } from "./logger"; +import { SubscriptionTourModal } from "./pages/shared-modals/subscription-tour-modal"; + +interface TourModals { + subscriptionModal?: boolean; +} export interface AppProps { children: React.ReactNode; @@ -72,6 +77,9 @@ export function App() { const { showSuccessToast } = useToast(); + const [showSubscritionTourModal, setShowSubscritionTourModal] = + useState(false); + useEffect(() => { Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then( ([preferences]) => { @@ -117,6 +125,16 @@ export function App() { }); }, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]); + useEffect(() => { + const tourModalsString = window.localStorage.getItem("tourModals") || "{}"; + + const tourModals = JSON.parse(tourModalsString) as TourModals; + + if (!tourModals.subscriptionModal) { + setShowSubscritionTourModal(true); + } + }, []); + const onSignIn = useCallback(() => { fetchUserDetails().then((response) => { if (response) { @@ -262,6 +280,14 @@ export function App() { }); }, [indexRepacks]); + const handleCloseSubscriptionTourModal = () => { + setShowSubscritionTourModal(false); + window.localStorage.setItem( + "tourModals", + JSON.stringify({ subscriptionModal: true } as TourModals) + ); + }; + const handleToastClose = useCallback(() => { dispatch(closeToast()); }, [dispatch]); @@ -281,6 +307,13 @@ export function App() { onClose={handleToastClose} /> + {showSubscritionTourModal && ( + + )} + {userDetails && ( void; +} + +export const SubscriptionTourModal = ({ + visible, + onClose, +}: UserFriendsModalProps) => { + const { t } = useTranslation("tour"); + + const handleSubscribeClick = () => { + window.electron.openCheckout().finally(onClose); + }; + + return ( + +
      +
      +
      +

      Hydra Cloud

      +
        +
      • + {t("cloud_saving", { gameCount: 15 })} +
      • +
      • + {t("cloud_achievements")} +
      • +
      • + {t("show_and_compare_achievements")} +
      • +
      • + {t("animated_profile_picture")} +
      • +
      • + {t("premium_support")} +
      • +
      +
      + +
      +

      Hydra Cloud+

      +
        +
      • + {t("cloud_saving", { gameCount: 30 })} +
      • +
      • + {t("cloud_achievements")} +
      • +
      • + {t("show_and_compare_achievements")} +
      • +
      • + {t("animated_profile_banner")} +
      • +
      • + {t("animated_profile_picture")} +
      • +
      • + {t("premium_support")} +
      • +
      +
      +
      + +
      +
      + ); +}; From 1c63fc11eee1dc0e8071e20e683e8d784a00ae6f Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:45:42 -0300 Subject: [PATCH 163/163] chore: docs --- .github/workflows/lint.yml | 1 - README.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fa7fadc2..cf74c4e8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20.11.1 - cache: "yarn" - name: Install dependencies run: yarn diff --git a/README.md b/README.md index 427a5f59..1012c05d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ [![da](https://img.shields.io/badge/lang-da-red)](./README.da.md) [![nb](https://img.shields.io/badge/lang-nb-blue)](./README.nb.md) -![Hydra Catalogue](./screenshot.png) +![Hydra Catalogue](./docs/screenshot.png)