mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
Merge branch 'feature/game-achievements' of github.com:hydralauncher/hydra into feature/cloud-sync
This commit is contained in:
commit
bdaf68ad23
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -18,6 +18,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.11.1
|
node-version: 20.11.1
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
@ -26,6 +27,7 @@ jobs:
|
|||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install -r requirements.txt
|
run: pip install -r requirements.txt
|
||||||
|
1
.github/workflows/lint.yml
vendored
1
.github/workflows/lint.yml
vendored
@ -14,6 +14,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.11.1
|
node-version: 20.11.1
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
|
@ -53,18 +53,17 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"electron-log": "^5.2.0",
|
"electron-log": "^5.2.0",
|
||||||
"electron-updater": "^6.3.4",
|
"electron-updater": "^6.3.9",
|
||||||
"fetch-cookie": "^3.0.1",
|
|
||||||
"flexsearch": "^0.7.43",
|
"flexsearch": "^0.7.43",
|
||||||
"i18next": "^23.11.2",
|
"i18next": "^23.11.2",
|
||||||
"i18next-browser-languagedetector": "^7.2.1",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
"icojs": "^0.19.3",
|
"icojs": "^0.19.4",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"parse-torrent": "^11.0.16",
|
"parse-torrent": "^11.0.17",
|
||||||
"piscina": "^4.5.1",
|
"piscina": "^4.5.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
@ -101,7 +100,7 @@
|
|||||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"electron": "^30.3.0",
|
"electron": "^30.3.0",
|
||||||
"electron-builder": "^25.1.6",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.0.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
|
@ -4,4 +4,3 @@ cx_Logging; sys_platform == 'win32'
|
|||||||
pywin32; sys_platform == 'win32'
|
pywin32; sys_platform == 'win32'
|
||||||
psutil
|
psutil
|
||||||
Pillow
|
Pillow
|
||||||
requests
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -131,7 +131,8 @@
|
|||||||
"executable_path_in_use": "Executable already in use by \"{{game}}\"",
|
"executable_path_in_use": "Executable already in use by \"{{game}}\"",
|
||||||
"warning": "Warning:",
|
"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.",
|
"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": "Cloud save",
|
||||||
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
@ -145,7 +146,9 @@
|
|||||||
"no_backups": "You haven't created any backups for this game yet",
|
"no_backups": "You haven't created any backups for this game yet",
|
||||||
"backup_uploaded": "Backup uploaded",
|
"backup_uploaded": "Backup uploaded",
|
||||||
"backup_deleted": "Backup deleted",
|
"backup_deleted": "Backup deleted",
|
||||||
"backup_restored": "Backup restored"
|
"backup_restored": "Backup restored",
|
||||||
|
"see_all_achievements": "See all achievements",
|
||||||
|
"sign_in_to_see_achievements": "Sign in to see achievements"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
@ -232,7 +235,8 @@
|
|||||||
"source_already_exists": "This source has been already added",
|
"source_already_exists": "This source has been already added",
|
||||||
"must_be_valid_url": "The source must be a valid URL",
|
"must_be_valid_url": "The source must be a valid URL",
|
||||||
"blocked_users": "Blocked users",
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
@ -332,6 +336,9 @@
|
|||||||
"your_friend_code": "Your friend code:"
|
"your_friend_code": "Your friend code:"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Achievement unlocked"
|
"achievement_unlocked": "Achievement unlocked",
|
||||||
|
"user_achievements": "{{displayName}}'s Achievements",
|
||||||
|
"your_achievements": "Your Achievements",
|
||||||
|
"unlocked_at": "Unlocked at:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
"trending": "Tendencias",
|
"trending": "Tendencias",
|
||||||
"surprise_me": "¡Sorpréndeme!",
|
"surprise_me": "¡Sorpréndeme!",
|
||||||
"no_results": "No se encontraron resultados",
|
"no_results": "No se encontraron resultados",
|
||||||
"hot": "Caliente ahora",
|
"hot": "Popular Ahora",
|
||||||
"weekly": "📅 Los mejores juegos de la semana",
|
"weekly": "📅 Destacados de esta semana",
|
||||||
"start_typing": "Empieza a escribir para buscar..."
|
"start_typing": "Empieza a escribir para buscar..."
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
@ -123,11 +123,11 @@
|
|||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
"download_count": "Descargas",
|
"download_count": "Descargas",
|
||||||
"download_error": "Esta opción de descarga no está disponible.",
|
"download_error": "Esta opción de descarga no está disponible.",
|
||||||
"executable_path_in_use": "Ejecutable ya en uso por \"{{game}}\"",
|
"executable_path_in_use": "El ejecutable se encuentra en uso por \"{{game}}\"",
|
||||||
"nsfw_content_description": "{{title}} incluye contenido que puede no ser adecuado para todas las edades. \n¿Estás seguro de que quieres continuar?",
|
"nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?",
|
||||||
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
||||||
"player_count": "Jugadores activos",
|
"player_count": "Jugadores activos",
|
||||||
"refuse_nsfw_content": "Volver",
|
"refuse_nsfw_content": "No, gracias",
|
||||||
"stats": "Estadísticas"
|
"stats": "Estadísticas"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
@ -209,7 +209,7 @@
|
|||||||
"download_options_one": "",
|
"download_options_one": "",
|
||||||
"download_options_other": "",
|
"download_options_other": "",
|
||||||
"download_options_zero": "",
|
"download_options_zero": "",
|
||||||
"friends_only": "solo amigos",
|
"friends_only": "Solo amigos",
|
||||||
"must_be_valid_url": "La fuente debe ser una URL válida.",
|
"must_be_valid_url": "La fuente debe ser una URL válida.",
|
||||||
"privacy": "Privacidad",
|
"privacy": "Privacidad",
|
||||||
"private": "Privado",
|
"private": "Privado",
|
||||||
@ -296,21 +296,21 @@
|
|||||||
"no_blocked_users": "No has bloqueado a ningún usuario",
|
"no_blocked_users": "No has bloqueado a ningún usuario",
|
||||||
"friend_code_copied": "Código de amigo copiado",
|
"friend_code_copied": "Código de amigo copiado",
|
||||||
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}",
|
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}",
|
||||||
"displayname_max_length": "El nombre para mostrar debe tener como máximo 50 caracteres",
|
"displayname_max_length": "El nombre a mostrar debe tener como máximo 50 caracteres",
|
||||||
"displayname_min_length": "El nombre para mostrar debe tener al menos 3 caracteres",
|
"displayname_min_length": "El nombre a mostrar debe tener al menos 3 caracteres",
|
||||||
"locked_profile": "Este perfil es privado.",
|
"locked_profile": "Este perfil es privado.",
|
||||||
"privacy_hint": "Para ajustar quién puede ver esto, ve a <0>Configuración</0>.",
|
"privacy_hint": "Para ajustar quién puede ver esto, ve a <0>Configuración</0>.",
|
||||||
"profile_locked": "",
|
"profile_locked": "Este perfil es privado",
|
||||||
"profile_reported": "Perfil reportado",
|
"profile_reported": "Perfil reportado",
|
||||||
"report": "Informe",
|
"report": "Reportar",
|
||||||
"report_description": "Información adicional",
|
"report_description": "Información adicional",
|
||||||
"report_description_placeholder": "Información adicional",
|
"report_description_placeholder": "Información adicional",
|
||||||
"report_profile": "Reportar este perfil",
|
"report_profile": "Reportar este perfil",
|
||||||
"report_reason": "¿Por qué estás denunciando este perfil?",
|
"report_reason": "¿Cual es el motivo del reporte?",
|
||||||
"report_reason_hate": "Discurso de odio",
|
"report_reason_hate": "Discursos de odio",
|
||||||
"report_reason_other": "Otro",
|
"report_reason_other": "Otro",
|
||||||
"report_reason_sexual_content": "Contenido sexual",
|
"report_reason_sexual_content": "Contenido sexual",
|
||||||
"report_reason_spam": "Correo basura",
|
"report_reason_spam": "Spam/Contenido no deseado",
|
||||||
"report_reason_violence": "Violencia",
|
"report_reason_violence": "Violencia",
|
||||||
"required_field": "Este campo es obligatorio",
|
"required_field": "Este campo es obligatorio",
|
||||||
"image_process_failure": "Error al procesar la imagen"
|
"image_process_failure": "Error al procesar la imagen"
|
||||||
|
@ -127,7 +127,8 @@
|
|||||||
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
||||||
"warning": "Aviso:",
|
"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.",
|
"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": "Salvamento em nuvem",
|
||||||
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
|
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
|
||||||
"backups": "Backups",
|
"backups": "Backups",
|
||||||
@ -141,7 +142,9 @@
|
|||||||
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
||||||
"backup_uploaded": "Backup criado",
|
"backup_uploaded": "Backup criado",
|
||||||
"backup_deleted": "Backup apagado",
|
"backup_deleted": "Backup apagado",
|
||||||
"backup_restored": "Backup restaurado"
|
"backup_restored": "Backup restaurado",
|
||||||
|
"see_all_achievements": "Ver todas as conquistas",
|
||||||
|
"sign_in_to_see_achievements": "Faça login para ver as conquistas"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@ -231,7 +234,8 @@
|
|||||||
"source_already_exists": "Essa fonte já foi adicionada",
|
"source_already_exists": "Essa fonte já foi adicionada",
|
||||||
"must_be_valid_url": "A fonte deve ser uma URL válida",
|
"must_be_valid_url": "A fonte deve ser uma URL válida",
|
||||||
"blocked_users": "Usuários bloqueados",
|
"blocked_users": "Usuários bloqueados",
|
||||||
"user_unblocked": "Usuário desbloqueado"
|
"user_unblocked": "Usuário desbloqueado",
|
||||||
|
"enable_achievement_notifications": "Quando uma conquista é desbloqueada"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
@ -334,6 +338,9 @@
|
|||||||
"your_friend_code": "Seu código de amigo:"
|
"your_friend_code": "Seu código de amigo:"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada"
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
"your_achievements": "Suas Conquistas",
|
||||||
|
"user_achievements": "Conquistas de {{displayName}}",
|
||||||
|
"unlocked_at": "Desbloqueado em:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,9 @@
|
|||||||
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
|
||||||
"warning": "Aviso:",
|
"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.",
|
"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": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@ -280,6 +282,7 @@
|
|||||||
"your_friend_code": "Seu código de amigo:"
|
"your_friend_code": "Seu código de amigo:"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada"
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
"unlocked_at": "Desbloqueado em:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
UserPreferences,
|
UserPreferences,
|
||||||
UserAuth,
|
UserAuth,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
|
UserSubscription,
|
||||||
} from "@main/entity";
|
} from "@main/entity";
|
||||||
|
|
||||||
import { databasePath } from "./constants";
|
import { databasePath } from "./constants";
|
||||||
@ -17,11 +18,12 @@ export const dataSource = new DataSource({
|
|||||||
entities: [
|
entities: [
|
||||||
Game,
|
Game,
|
||||||
Repack,
|
Repack,
|
||||||
|
UserAuth,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
|
UserSubscription,
|
||||||
GameShopCache,
|
GameShopCache,
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
DownloadQueue,
|
DownloadQueue,
|
||||||
UserAuth,
|
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
],
|
],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
export * from "./game.entity";
|
export * from "./game.entity";
|
||||||
export * from "./repack.entity";
|
export * from "./repack.entity";
|
||||||
|
export * from "./user-auth.entity";
|
||||||
export * from "./user-preferences.entity";
|
export * from "./user-preferences.entity";
|
||||||
|
export * from "./user-subscription.entity";
|
||||||
export * from "./game-shop-cache.entity";
|
export * from "./game-shop-cache.entity";
|
||||||
export * from "./game.entity";
|
export * from "./game.entity";
|
||||||
export * from "./game-achievements.entity";
|
export * from "./game-achievements.entity";
|
||||||
export * from "./download-source.entity";
|
export * from "./download-source.entity";
|
||||||
export * from "./download-queue.entity";
|
export * from "./download-queue.entity";
|
||||||
export * from "./user-auth";
|
|
||||||
|
@ -4,7 +4,9 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
|
OneToOne,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
|
import { UserSubscription } from "./user-subscription.entity";
|
||||||
|
|
||||||
@Entity("user_auth")
|
@Entity("user_auth")
|
||||||
export class UserAuth {
|
export class UserAuth {
|
||||||
@ -29,6 +31,9 @@ export class UserAuth {
|
|||||||
@Column("int", { default: 0 })
|
@Column("int", { default: 0 })
|
||||||
tokenExpirationTimestamp: number;
|
tokenExpirationTimestamp: number;
|
||||||
|
|
||||||
|
@OneToOne("UserSubscription", "user")
|
||||||
|
subscription: UserSubscription | null;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
@ -26,6 +26,9 @@ export class UserPreferences {
|
|||||||
@Column("boolean", { default: false })
|
@Column("boolean", { default: false })
|
||||||
repackUpdatesNotificationsEnabled: boolean;
|
repackUpdatesNotificationsEnabled: boolean;
|
||||||
|
|
||||||
|
@Column("boolean", { default: true })
|
||||||
|
achievementNotificationsEnabled: boolean;
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
@Column("boolean", { default: false })
|
||||||
preferQuitInsteadOfHiding: boolean;
|
preferQuitInsteadOfHiding: boolean;
|
||||||
|
|
||||||
|
42
src/main/entity/user-subscription.entity.ts
Normal file
42
src/main/entity/user-subscription.entity.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -1,57 +1,32 @@
|
|||||||
import type { GameAchievement, GameShop, UnlockedAchievement } from "@types";
|
import type {
|
||||||
|
AchievementData,
|
||||||
|
GameShop,
|
||||||
|
RemoteUnlockedAchievement,
|
||||||
|
UnlockedAchievement,
|
||||||
|
UserAchievement,
|
||||||
|
} from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import {
|
import {
|
||||||
gameAchievementRepository,
|
gameAchievementRepository,
|
||||||
userAuthRepository,
|
userPreferencesRepository,
|
||||||
} from "@main/repository";
|
} from "@main/repository";
|
||||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
const getAchievements = async (
|
const getAchievementLocalUser = async (shop: string, objectId: string) => {
|
||||||
shop: string,
|
|
||||||
objectId: string,
|
|
||||||
userId?: string
|
|
||||||
) => {
|
|
||||||
const userAuth = await userAuthRepository.findOne({ where: { userId } });
|
|
||||||
|
|
||||||
const cachedAchievements = await gameAchievementRepository.findOne({
|
const cachedAchievements = await gameAchievementRepository.findOne({
|
||||||
where: { objectId, shop },
|
where: { objectId, shop },
|
||||||
});
|
});
|
||||||
|
|
||||||
const achievementsData = cachedAchievements?.achievements
|
const achievementsData = await getGameAchievementData(objectId, shop);
|
||||||
? JSON.parse(cachedAchievements.achievements)
|
|
||||||
: await getGameAchievementData(objectId, shop);
|
|
||||||
|
|
||||||
if (!userId || userAuth) {
|
const unlockedAchievements = JSON.parse(
|
||||||
const unlockedAchievements = JSON.parse(
|
cachedAchievements?.unlockedAchievements || "[]"
|
||||||
cachedAchievements?.unlockedAchievements || "[]"
|
) as UnlockedAchievement[];
|
||||||
) as UnlockedAchievement[];
|
|
||||||
|
|
||||||
return { achievementsData, unlockedAchievements };
|
|
||||||
}
|
|
||||||
|
|
||||||
const unlockedAchievements = await HydraApi.get<UnlockedAchievement[]>(
|
|
||||||
`/users/${userId}/games/achievements`,
|
|
||||||
{ shop, objectId, language: "en" }
|
|
||||||
);
|
|
||||||
|
|
||||||
return { achievementsData, unlockedAchievements };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGameAchievements = async (
|
|
||||||
objectId: string,
|
|
||||||
shop: GameShop,
|
|
||||||
userId?: string
|
|
||||||
): Promise<GameAchievement[]> => {
|
|
||||||
const { achievementsData, unlockedAchievements } = await getAchievements(
|
|
||||||
shop,
|
|
||||||
objectId,
|
|
||||||
userId
|
|
||||||
);
|
|
||||||
|
|
||||||
return achievementsData
|
return achievementsData
|
||||||
.map((achievementData) => {
|
.map((achievementData) => {
|
||||||
const unlockedAchiement = unlockedAchievements.find(
|
const unlockedAchiementData = unlockedAchievements.find(
|
||||||
(localAchievement) => {
|
(localAchievement) => {
|
||||||
return (
|
return (
|
||||||
localAchievement.name.toUpperCase() ==
|
localAchievement.name.toUpperCase() ==
|
||||||
@ -60,29 +35,112 @@ export const getGameAchievements = async (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (unlockedAchiement) {
|
const icongray = achievementData.icongray.endsWith("/")
|
||||||
|
? achievementData.icon
|
||||||
|
: achievementData.icongray;
|
||||||
|
|
||||||
|
if (unlockedAchiementData) {
|
||||||
return {
|
return {
|
||||||
...achievementData,
|
...achievementData,
|
||||||
unlocked: true,
|
unlocked: true,
|
||||||
unlockTime: unlockedAchiement.unlockTime,
|
unlockTime: unlockedAchiementData.unlockTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...achievementData, unlocked: false, unlockTime: null };
|
return {
|
||||||
|
...achievementData,
|
||||||
|
unlocked: false,
|
||||||
|
unlockTime: null,
|
||||||
|
icon: icongray,
|
||||||
|
} as UserAchievement;
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.unlocked && !b.unlocked) return -1;
|
if (a.unlocked && !b.unlocked) return -1;
|
||||||
if (!a.unlocked && b.unlocked) return 1;
|
if (!a.unlocked && b.unlocked) return 1;
|
||||||
return b.unlockTime - a.unlockTime;
|
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<RemoteUnlockedAchievement[]>(
|
||||||
|
`/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,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGameAchievements = async (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
userId?: string
|
||||||
|
): Promise<UserAchievement[]> => {
|
||||||
|
if (!userId) {
|
||||||
|
return getAchievementLocalUser(shop, objectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAchievementsRemoteUser(shop, objectId, userId);
|
||||||
|
};
|
||||||
|
|
||||||
const getGameAchievementsEvent = async (
|
const getGameAchievementsEvent = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
userId?: string
|
userId?: string
|
||||||
): Promise<GameAchievement[]> => {
|
): Promise<UserAchievement[]> => {
|
||||||
return getGameAchievements(objectId, shop, userId);
|
return getGameAchievements(objectId, shop, userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -50,7 +50,8 @@ const openGameInstaller = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fs.lstatSync(gamePath).isFile()) {
|
if (fs.lstatSync(gamePath).isFile()) {
|
||||||
return executeGameInstaller(gamePath);
|
shell.showItemInFolder(gamePath);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupPath = path.join(gamePath, "setup.exe");
|
const setupPath = path.join(gamePath, "setup.exe");
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi, logger } from "@main/services";
|
||||||
import { ProfileVisibility, UserDetails } from "@types";
|
import { ProfileVisibility, UserDetails } from "@types";
|
||||||
import { userAuthRepository } from "@main/repository";
|
import {
|
||||||
|
userAuthRepository,
|
||||||
|
userSubscriptionRepository,
|
||||||
|
} from "@main/repository";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
|
|
||||||
const getMe = async (
|
const getMe = async (
|
||||||
_event: Electron.IpcMainInvokeEvent
|
_event: Electron.IpcMainInvokeEvent
|
||||||
): Promise<UserDetails | null> => {
|
): Promise<UserDetails | null> => {
|
||||||
return HydraApi.get<UserDetails>(`/profile/me`)
|
return HydraApi.get<UserDetails>(`/profile/me`)
|
||||||
.then(async (me) => {
|
.then((me) => {
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -20,6 +23,23 @@ const getMe = async (
|
|||||||
["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 });
|
Sentry.setUser({ id: me.id, username: me.username });
|
||||||
|
|
||||||
return me;
|
return me;
|
||||||
@ -28,7 +48,7 @@ const getMe = async (
|
|||||||
if (err instanceof UserNotLoggedInError) {
|
if (err instanceof UserNotLoggedInError) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
logger.error("Failed to get logged user", err);
|
||||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||||
|
|
||||||
if (loggedUser) {
|
if (loggedUser) {
|
||||||
@ -38,6 +58,17 @@ const getMe = async (
|
|||||||
username: "",
|
username: "",
|
||||||
bio: "",
|
bio: "",
|
||||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
||||||
import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement";
|
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 };
|
export type HydraMigration = Knex.Migration & { name: string };
|
||||||
|
|
||||||
@ -19,6 +21,8 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
EnsureRepackUris,
|
EnsureRepackUris,
|
||||||
FixMissingColumns,
|
FixMissingColumns,
|
||||||
CreateGameAchievement,
|
CreateGameAchievement,
|
||||||
|
AddAchievementNotificationPreference,
|
||||||
|
CreateUserSubscription,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
getMigrationName(migration: HydraMigration): string {
|
getMigrationName(migration: HydraMigration): string {
|
||||||
|
@ -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");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
@ -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");
|
||||||
|
},
|
||||||
|
};
|
@ -3,8 +3,8 @@ import type { Knex } from "knex";
|
|||||||
|
|
||||||
export const MigrationName: HydraMigration = {
|
export const MigrationName: HydraMigration = {
|
||||||
name: "MigrationName",
|
name: "MigrationName",
|
||||||
up: async (knex: Knex) => {
|
up: (knex: Knex) => {
|
||||||
await knex.schema.createTable("table_name", (table) => {});
|
return knex.schema.createTable("table_name", async (table) => {});
|
||||||
},
|
},
|
||||||
|
|
||||||
down: async (knex: Knex) => {},
|
down: async (knex: Knex) => {},
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
UserPreferences,
|
UserPreferences,
|
||||||
UserAuth,
|
UserAuth,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
|
UserSubscription,
|
||||||
} from "@main/entity";
|
} from "@main/entity";
|
||||||
|
|
||||||
export const gameRepository = dataSource.getRepository(Game);
|
export const gameRepository = dataSource.getRepository(Game);
|
||||||
@ -26,5 +27,8 @@ export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
|||||||
|
|
||||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||||
|
|
||||||
|
export const userSubscriptionRepository =
|
||||||
|
dataSource.getRepository(UserSubscription);
|
||||||
|
|
||||||
export const gameAchievementRepository =
|
export const gameAchievementRepository =
|
||||||
dataSource.getRepository(GameAchievement);
|
dataSource.getRepository(GameAchievement);
|
||||||
|
@ -113,8 +113,8 @@ const compareFile = async (game: Game, file: AchievementFile) => {
|
|||||||
logger.log(
|
logger.log(
|
||||||
"Detected change in file",
|
"Detected change in file",
|
||||||
file.filePath,
|
file.filePath,
|
||||||
currentStat.mtimeMs,
|
previousStat,
|
||||||
fileStats.get(file.filePath)
|
currentStat.mtimeMs
|
||||||
);
|
);
|
||||||
await processAchievementFileDiff(game, file);
|
await processAchievementFileDiff(game, file);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -53,9 +53,13 @@ const getPathFromCracker = (cracker: Cracker) => {
|
|||||||
if (cracker === Cracker.onlineFix) {
|
if (cracker === Cracker.onlineFix) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
folderPath: path.join(publicDocuments, Cracker.onlineFix),
|
folderPath: path.join(publicDocuments, "OnlineFix"),
|
||||||
fileLocation: ["Stats", "Achievements.ini"],
|
fileLocation: ["Stats", "Achievements.ini"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
folderPath: path.join(publicDocuments, "OnlineFix"),
|
||||||
|
fileLocation: ["Achievements.ini"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,9 @@ import {
|
|||||||
userPreferencesRepository,
|
userPreferencesRepository,
|
||||||
} from "@main/repository";
|
} from "@main/repository";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
|
import { AchievementData } from "@types";
|
||||||
|
import { UserNotLoggedInError } from "@shared";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
export const getGameAchievementData = async (
|
export const getGameAchievementData = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@ -12,13 +15,13 @@ export const getGameAchievementData = async (
|
|||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
return HydraApi.get("/games/achievements", {
|
return HydraApi.get<AchievementData[]>("/games/achievements", {
|
||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
language: userPreferences?.language || "en",
|
language: userPreferences?.language || "en",
|
||||||
})
|
})
|
||||||
.then(async (achievements) => {
|
.then((achievements) => {
|
||||||
await gameAchievementRepository.upsert(
|
gameAchievementRepository.upsert(
|
||||||
{
|
{
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
@ -29,5 +32,17 @@ export const getGameAchievementData = async (
|
|||||||
|
|
||||||
return achievements;
|
return achievements;
|
||||||
})
|
})
|
||||||
.catch(() => []);
|
.catch((err) => {
|
||||||
|
if (err instanceof UserNotLoggedInError) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
logger.error("Failed to get game achievements", err);
|
||||||
|
return gameAchievementRepository
|
||||||
|
.findOne({
|
||||||
|
where: { objectId, shop },
|
||||||
|
})
|
||||||
|
.then((gameAchievements) => {
|
||||||
|
return JSON.parse(gameAchievements?.achievements || "[]");
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
import {
|
||||||
import type { GameShop, UnlockedAchievement } from "@types";
|
gameAchievementRepository,
|
||||||
|
gameRepository,
|
||||||
|
userPreferencesRepository,
|
||||||
|
} from "@main/repository";
|
||||||
|
import type { AchievementData, GameShop, UnlockedAchievement } from "@types";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { getGameAchievements } from "@main/events/catalogue/get-game-achievements";
|
import { getGameAchievements } from "@main/events/catalogue/get-game-achievements";
|
||||||
@ -18,11 +22,15 @@ const saveAchievementsOnLocal = async (
|
|||||||
},
|
},
|
||||||
["objectId", "shop"]
|
["objectId", "shop"]
|
||||||
)
|
)
|
||||||
.then(async () => {
|
.then(() => {
|
||||||
WindowManager.mainWindow?.webContents.send(
|
return getGameAchievements(objectId, shop as GameShop)
|
||||||
`on-update-achievements-${objectId}-${shop}`,
|
.then((achievements) => {
|
||||||
await getGameAchievements(objectId, shop as GameShop)
|
WindowManager.mainWindow?.webContents.send(
|
||||||
);
|
`on-update-achievements-${objectId}-${shop}`,
|
||||||
|
achievements
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,16 +46,23 @@ export const mergeAchievements = async (
|
|||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
const localGameAchievement = await gameAchievementRepository.findOne({
|
const [localGameAchievement, userPreferences] = await Promise.all([
|
||||||
where: {
|
gameAchievementRepository.findOne({
|
||||||
objectId,
|
where: {
|
||||||
shop,
|
objectId,
|
||||||
},
|
shop,
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
|
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const achievementsData = JSON.parse(
|
||||||
|
localGameAchievement?.achievements || "[]"
|
||||||
|
) as AchievementData[];
|
||||||
|
|
||||||
const unlockedAchievements = JSON.parse(
|
const unlockedAchievements = JSON.parse(
|
||||||
localGameAchievement?.unlockedAchievements || "[]"
|
localGameAchievement?.unlockedAchievements || "[]"
|
||||||
).filter((achievement) => achievement.name);
|
).filter((achievement) => achievement.name) as UnlockedAchievement[];
|
||||||
|
|
||||||
const newAchievements = achievements
|
const newAchievements = achievements
|
||||||
.filter((achievement) => {
|
.filter((achievement) => {
|
||||||
@ -64,26 +79,28 @@ export const mergeAchievements = async (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newAchievements.length && publishNotification) {
|
if (
|
||||||
|
newAchievements.length &&
|
||||||
|
publishNotification &&
|
||||||
|
userPreferences?.achievementNotificationsEnabled
|
||||||
|
) {
|
||||||
const achievementsInfo = newAchievements
|
const achievementsInfo = newAchievements
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.unlockTime - b.unlockTime;
|
return a.unlockTime - b.unlockTime;
|
||||||
})
|
})
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
return JSON.parse(localGameAchievement?.achievements || "[]").find(
|
return achievementsData.find((steamAchievement) => {
|
||||||
(steamAchievement) => {
|
return (
|
||||||
return (
|
achievement.name.toUpperCase() ===
|
||||||
achievement.name.toUpperCase() ===
|
steamAchievement.name.toUpperCase()
|
||||||
steamAchievement.name.toUpperCase()
|
);
|
||||||
);
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.filter((achievement) => achievement)
|
.filter((achievement) => achievement)
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
return {
|
return {
|
||||||
displayName: achievement.displayName,
|
displayName: achievement!.displayName,
|
||||||
iconUrl: achievement.icon,
|
iconUrl: achievement!.icon,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,10 +115,14 @@ export const mergeAchievements = async (
|
|||||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||||
|
|
||||||
if (game?.remoteId) {
|
if (game?.remoteId) {
|
||||||
return HydraApi.put("/profile/games/achievements", {
|
return HydraApi.put(
|
||||||
id: game.remoteId,
|
"/profile/games/achievements",
|
||||||
achievements: mergedLocalAchievements,
|
{
|
||||||
})
|
id: game.remoteId,
|
||||||
|
achievements: mergedLocalAchievements,
|
||||||
|
},
|
||||||
|
{ needsCloud: true }
|
||||||
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return saveAchievementsOnLocal(
|
return saveAchievementsOnLocal(
|
||||||
response.objectId,
|
response.objectId,
|
||||||
|
@ -73,7 +73,12 @@ export const parseAchievementFile = (
|
|||||||
|
|
||||||
const iniParse = (filePath: string) => {
|
const iniParse = (filePath: string) => {
|
||||||
try {
|
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 = "";
|
let objectName = "";
|
||||||
const object: Record<string, Record<string, string | number>> = {};
|
const object: Record<string, Record<string, string | number>> = {};
|
||||||
@ -112,11 +117,21 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
if (unlockedAchievement?.achieved) {
|
if (unlockedAchievement?.achieved == "true") {
|
||||||
parsedUnlockedAchievements.push({
|
parsedUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
unlockTime: unlockedAchievement.timestamp * 1000,
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +144,7 @@ const processCreamAPI = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
if (unlockedAchievement?.achieved) {
|
if (unlockedAchievement?.achieved == "true") {
|
||||||
const unlockTime = unlockedAchievement.unlocktime;
|
const unlockTime = unlockedAchievement.unlocktime;
|
||||||
parsedUnlockedAchievements.push({
|
parsedUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
@ -207,7 +222,7 @@ const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||||
const unlockedAchievement = unlockedAchievements[achievement];
|
const unlockedAchievement = unlockedAchievements[achievement];
|
||||||
|
|
||||||
if (unlockedAchievement?.Achieved) {
|
if (unlockedAchievement?.Achieved == "1") {
|
||||||
newUnlockedAchievements.push({
|
newUnlockedAchievements.push({
|
||||||
name: achievement,
|
name: achievement,
|
||||||
unlockTime: unlockedAchievement.UnlockTime * 1000,
|
unlockTime: unlockedAchievement.UnlockTime * 1000,
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
import { userAuthRepository } from "@main/repository";
|
import {
|
||||||
|
userAuthRepository,
|
||||||
|
userSubscriptionRepository,
|
||||||
|
} from "@main/repository";
|
||||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
import { uploadGamesBatch } from "./library-sync";
|
import { uploadGamesBatch } from "./library-sync";
|
||||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import {
|
||||||
|
UserNotLoggedInError,
|
||||||
|
UserWithoutCloudSubscriptionError,
|
||||||
|
} from "@shared";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import { appVersion } from "@main/constants";
|
import { appVersion } from "@main/constants";
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth: boolean;
|
needsAuth?: boolean;
|
||||||
|
needsCloud?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HydraApi {
|
export class HydraApi {
|
||||||
@ -31,6 +38,19 @@ export class HydraApi {
|
|||||||
return this.userAuth.authToken !== "";
|
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) {
|
static async handleExternalAuth(uri: string) {
|
||||||
const { payload } = url.parse(uri, true).query;
|
const { payload } = url.parse(uri, true).query;
|
||||||
|
|
||||||
@ -234,15 +254,28 @@ export class HydraApi {
|
|||||||
throw err;
|
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<T = any>(
|
static async get<T = any>(
|
||||||
url: string,
|
url: string,
|
||||||
params?: any,
|
params?: any,
|
||||||
options?: HydraApiOptions
|
options?: HydraApiOptions
|
||||||
) {
|
) {
|
||||||
if (!options || options.needsAuth) {
|
await this.validateOptions(options);
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.instance
|
return this.instance
|
||||||
.get<T>(url, { params, ...this.getAxiosConfig() })
|
.get<T>(url, { params, ...this.getAxiosConfig() })
|
||||||
@ -255,10 +288,7 @@ export class HydraApi {
|
|||||||
data?: any,
|
data?: any,
|
||||||
options?: HydraApiOptions
|
options?: HydraApiOptions
|
||||||
) {
|
) {
|
||||||
if (!options || options.needsAuth) {
|
await this.validateOptions(options);
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.instance
|
return this.instance
|
||||||
.post<T>(url, data, this.getAxiosConfig())
|
.post<T>(url, data, this.getAxiosConfig())
|
||||||
@ -271,10 +301,7 @@ export class HydraApi {
|
|||||||
data?: any,
|
data?: any,
|
||||||
options?: HydraApiOptions
|
options?: HydraApiOptions
|
||||||
) {
|
) {
|
||||||
if (!options || options.needsAuth) {
|
await this.validateOptions(options);
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.instance
|
return this.instance
|
||||||
.put<T>(url, data, this.getAxiosConfig())
|
.put<T>(url, data, this.getAxiosConfig())
|
||||||
@ -287,10 +314,7 @@ export class HydraApi {
|
|||||||
data?: any,
|
data?: any,
|
||||||
options?: HydraApiOptions
|
options?: HydraApiOptions
|
||||||
) {
|
) {
|
||||||
if (!options || options.needsAuth) {
|
await this.validateOptions(options);
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.instance
|
return this.instance
|
||||||
.patch<T>(url, data, this.getAxiosConfig())
|
.patch<T>(url, data, this.getAxiosConfig())
|
||||||
@ -299,10 +323,7 @@ export class HydraApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async delete<T = any>(url: string, options?: HydraApiOptions) {
|
static async delete<T = any>(url: string, options?: HydraApiOptions) {
|
||||||
if (!options || options.needsAuth) {
|
await this.validateOptions(options);
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.instance
|
return this.instance
|
||||||
.delete<T>(url, this.getAxiosConfig())
|
.delete<T>(url, this.getAxiosConfig())
|
||||||
|
@ -39,6 +39,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (location.pathname.startsWith("/game")) return headerTitle;
|
if (location.pathname.startsWith("/game")) return headerTitle;
|
||||||
|
if (location.pathname.startsWith("/achievements")) return headerTitle;
|
||||||
if (location.pathname.startsWith("/profile")) return headerTitle;
|
if (location.pathname.startsWith("/profile")) return headerTitle;
|
||||||
if (location.pathname.startsWith("/search")) return t("search_results");
|
if (location.pathname.startsWith("/search")) return t("search_results");
|
||||||
|
|
||||||
|
@ -3,20 +3,26 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { getSteamLanguage } from "@renderer/helpers";
|
import { getSteamLanguage } from "@renderer/helpers";
|
||||||
import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks";
|
import {
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
useDownload,
|
||||||
|
useUserDetails,
|
||||||
|
} from "@renderer/hooks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Game,
|
Game,
|
||||||
GameAchievement,
|
|
||||||
GameRepack,
|
GameRepack,
|
||||||
GameShop,
|
GameShop,
|
||||||
GameStats,
|
GameStats,
|
||||||
ShopDetails,
|
ShopDetails,
|
||||||
|
UserAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -37,7 +43,7 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
|||||||
showRepacksModal: false,
|
showRepacksModal: false,
|
||||||
showGameOptionsModal: false,
|
showGameOptionsModal: false,
|
||||||
stats: null,
|
stats: null,
|
||||||
achievements: [],
|
achievements: null,
|
||||||
hasNSFWContentBlocked: false,
|
hasNSFWContentBlocked: false,
|
||||||
setGameColor: () => {},
|
setGameColor: () => {},
|
||||||
selectGameExecutable: async () => null,
|
selectGameExecutable: async () => null,
|
||||||
@ -64,9 +70,12 @@ export function GameDetailsContextProvider({
|
|||||||
shop,
|
shop,
|
||||||
}: GameDetailsContextProps) {
|
}: GameDetailsContextProps) {
|
||||||
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
||||||
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
|
const [achievements, setAchievements] = useState<UserAchievement[] | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const [game, setGame] = useState<Game | null>(null);
|
||||||
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
|
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const [stats, setStats] = useState<GameStats | null>(null);
|
const [stats, setStats] = useState<GameStats | null>(null);
|
||||||
|
|
||||||
@ -93,6 +102,7 @@ export function GameDetailsContextProvider({
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { lastPacket } = useDownload();
|
const { lastPacket } = useDownload();
|
||||||
|
const { userDetails } = useUserDetails();
|
||||||
|
|
||||||
const userPreferences = useAppSelector(
|
const userPreferences = useAppSelector(
|
||||||
(state) => state.userPreferences.value
|
(state) => state.userPreferences.value
|
||||||
@ -111,6 +121,10 @@ export function GameDetailsContextProvider({
|
|||||||
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (abortControllerRef.current) abortControllerRef.current.abort();
|
||||||
|
const abortController = new AbortController();
|
||||||
|
abortControllerRef.current = abortController;
|
||||||
|
|
||||||
window.electron
|
window.electron
|
||||||
.getGameShopDetails(
|
.getGameShopDetails(
|
||||||
objectId!,
|
objectId!,
|
||||||
@ -118,6 +132,8 @@ export function GameDetailsContextProvider({
|
|||||||
getSteamLanguage(i18n.language)
|
getSteamLanguage(i18n.language)
|
||||||
)
|
)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
|
|
||||||
setShopDetails(result);
|
setShopDetails(result);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -133,28 +149,36 @@ export function GameDetailsContextProvider({
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.electron.getGameStats(objectId, shop as GameShop).then((result) => {
|
window.electron.getGameStats(objectId, shop as GameShop).then((result) => {
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
setStats(result);
|
setStats(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electron
|
window.electron
|
||||||
.getGameAchievements(objectId, shop as GameShop)
|
.getGameAchievements(objectId, shop as GameShop)
|
||||||
.then((achievements) => {
|
.then((achievements) => {
|
||||||
// TODO: race condition
|
if (abortController.signal.aborted) return;
|
||||||
|
if (!userDetails) return;
|
||||||
setAchievements(achievements);
|
setAchievements(achievements);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {});
|
||||||
// TODO: handle user not logged in error
|
|
||||||
});
|
|
||||||
|
|
||||||
updateGame();
|
updateGame();
|
||||||
}, [updateGame, dispatch, gameTitle, objectId, shop, i18n.language]);
|
}, [
|
||||||
|
updateGame,
|
||||||
|
dispatch,
|
||||||
|
gameTitle,
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
i18n.language,
|
||||||
|
userDetails,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShopDetails(null);
|
setShopDetails(null);
|
||||||
setGame(null);
|
setGame(null);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setisGameRunning(false);
|
setisGameRunning(false);
|
||||||
setAchievements([]);
|
setAchievements(null);
|
||||||
dispatch(setHeaderTitle(gameTitle));
|
dispatch(setHeaderTitle(gameTitle));
|
||||||
}, [objectId, gameTitle, dispatch]);
|
}, [objectId, gameTitle, dispatch]);
|
||||||
|
|
||||||
@ -180,6 +204,7 @@ export function GameDetailsContextProvider({
|
|||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
(achievements) => {
|
(achievements) => {
|
||||||
|
if (!userDetails) return;
|
||||||
setAchievements(achievements);
|
setAchievements(achievements);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -187,7 +212,7 @@ export function GameDetailsContextProvider({
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [objectId, shop]);
|
}, [objectId, shop, userDetails]);
|
||||||
|
|
||||||
const getDownloadsPath = async () => {
|
const getDownloadsPath = async () => {
|
||||||
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
Game,
|
Game,
|
||||||
GameAchievement,
|
|
||||||
GameRepack,
|
GameRepack,
|
||||||
GameShop,
|
GameShop,
|
||||||
GameStats,
|
GameStats,
|
||||||
ShopDetails,
|
ShopDetails,
|
||||||
|
UserAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
export interface GameDetailsContext {
|
export interface GameDetailsContext {
|
||||||
@ -20,7 +20,7 @@ export interface GameDetailsContext {
|
|||||||
showRepacksModal: boolean;
|
showRepacksModal: boolean;
|
||||||
showGameOptionsModal: boolean;
|
showGameOptionsModal: boolean;
|
||||||
stats: GameStats | null;
|
stats: GameStats | null;
|
||||||
achievements: GameAchievement[];
|
achievements: UserAchievement[] | null;
|
||||||
hasNSFWContentBlocked: boolean;
|
hasNSFWContentBlocked: boolean;
|
||||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||||
selectGameExecutable: () => Promise<string | null>;
|
selectGameExecutable: () => Promise<string | null>;
|
||||||
|
3
src/renderer/src/declaration.d.ts
vendored
3
src/renderer/src/declaration.d.ts
vendored
@ -28,6 +28,7 @@ import type {
|
|||||||
GameAchievement,
|
GameAchievement,
|
||||||
GameArtifact,
|
GameArtifact,
|
||||||
LudusaviBackup,
|
LudusaviBackup,
|
||||||
|
UserAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
@ -68,7 +69,7 @@ declare global {
|
|||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
userId?: string
|
userId?: string
|
||||||
) => Promise<GameAchievement[]>;
|
) => Promise<UserAchievement[]>;
|
||||||
onAchievementUnlocked: (
|
onAchievementUnlocked: (
|
||||||
cb: (
|
cb: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
|
@ -34,5 +34,21 @@ export const buildGameDetailsPath = (
|
|||||||
return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`;
|
return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const buildGameAchievementPath = (
|
||||||
|
game: { shop: GameShop; objectId: string; title: string },
|
||||||
|
user?: { userId: string; displayName: string; profileImageUrl: string | null }
|
||||||
|
) => {
|
||||||
|
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()}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||||
new Color(color).darken(amount).alpha(alpha).toString();
|
new Color(color).darken(amount).alpha(alpha).toString();
|
||||||
|
@ -68,12 +68,17 @@ export function useDate() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
format: (timestamp: number): string => {
|
formatDateTime: (date: number | Date | string): string => {
|
||||||
const locale = getDateLocale();
|
const locale = getDateLocale();
|
||||||
return format(
|
return format(
|
||||||
timestamp,
|
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"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
formatDate: (date: number | Date | string): string => {
|
||||||
|
const locale = getDateLocale();
|
||||||
|
return format(date, locale == enUS ? "MM/dd/yyyy" : "dd/MM/yyyy");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,7 @@ export function useUserDetails() {
|
|||||||
return updateUserDetails({
|
return updateUserDetails({
|
||||||
...response,
|
...response,
|
||||||
username: userDetails?.username || "",
|
username: userDetails?.username || "",
|
||||||
|
subscription: userDetails?.subscription || null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[updateUserDetails, userDetails?.username]
|
[updateUserDetails, userDetails?.username]
|
||||||
|
483
src/renderer/src/pages/achievement/achievements-content.tsx
Normal file
483
src/renderer/src/pages/achievement/achievements-content.tsx
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
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 { useTranslation } from "react-i18next";
|
||||||
|
import * as styles from "./achievements.css";
|
||||||
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
|
import { PersonIcon, TrophyIcon } 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;
|
||||||
|
achievements: UserAchievement[];
|
||||||
|
profileImageUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AchievementsContentProps {
|
||||||
|
otherUser: UserInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AchievementListProps {
|
||||||
|
achievements: UserAchievement[];
|
||||||
|
otherUserAchievements?: UserAchievement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AchievementPanelProps {
|
||||||
|
user: UserInfo;
|
||||||
|
otherUser: UserInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AchievementPanel({ user, otherUser }: AchievementPanelProps) {
|
||||||
|
const { t } = useTranslation("achievement");
|
||||||
|
|
||||||
|
const getProfileImage = (imageUrl: string | null | undefined) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.profileAvatar}>
|
||||||
|
{imageUrl ? (
|
||||||
|
<img className={styles.profileAvatar} src={imageUrl} alt={"teste"} />
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const userTotalAchievementCount = user.achievements.length;
|
||||||
|
const userUnlockedAchievementCount = user.achievements.filter(
|
||||||
|
(achievement) => achievement.unlocked
|
||||||
|
).length;
|
||||||
|
|
||||||
|
if (!otherUser) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
width: "100%",
|
||||||
|
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getProfileImage(user.profileImageUrl)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ fontSize: "1.2em", marginBottom: "8px" }}>
|
||||||
|
{t("your_achievements")}
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 8,
|
||||||
|
width: "100%",
|
||||||
|
color: vars.color.muted,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={13} />
|
||||||
|
<span>
|
||||||
|
{userUnlockedAchievementCount} / {userTotalAchievementCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{formatDownloadProgress(
|
||||||
|
userUnlockedAchievementCount / userTotalAchievementCount
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<progress
|
||||||
|
max={1}
|
||||||
|
value={userUnlockedAchievementCount / userTotalAchievementCount}
|
||||||
|
className={styles.achievementsProgressBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherUserUnlockedAchievementCount = otherUser.achievements.filter(
|
||||||
|
(achievement) => achievement.unlocked
|
||||||
|
).length;
|
||||||
|
const otherUserTotalAchievementCount = otherUser.achievements.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
width: "100%",
|
||||||
|
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getProfileImage(otherUser.profileImageUrl)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ fontSize: "1.2em", marginBottom: "8px" }}>
|
||||||
|
{t("user_achievements", {
|
||||||
|
displayName: otherUser.displayName,
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 8,
|
||||||
|
width: "100%",
|
||||||
|
color: vars.color.muted,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={13} />
|
||||||
|
<span>
|
||||||
|
{otherUserUnlockedAchievementCount} /{" "}
|
||||||
|
{otherUserTotalAchievementCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{formatDownloadProgress(
|
||||||
|
otherUserUnlockedAchievementCount /
|
||||||
|
otherUserTotalAchievementCount
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<progress
|
||||||
|
max={1}
|
||||||
|
value={
|
||||||
|
otherUserUnlockedAchievementCount / otherUserTotalAchievementCount
|
||||||
|
}
|
||||||
|
className={styles.achievementsProgressBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row-reverse",
|
||||||
|
width: "100%",
|
||||||
|
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getProfileImage(user.profileImageUrl)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ fontSize: "1.2em", marginBottom: "8px" }}>
|
||||||
|
{t("your_achievements")}
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 8,
|
||||||
|
width: "100%",
|
||||||
|
color: vars.color.muted,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={13} />
|
||||||
|
<span>
|
||||||
|
{userUnlockedAchievementCount} / {userTotalAchievementCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{formatDownloadProgress(
|
||||||
|
userUnlockedAchievementCount / userTotalAchievementCount
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<progress
|
||||||
|
max={1}
|
||||||
|
value={userUnlockedAchievementCount / userTotalAchievementCount}
|
||||||
|
className={styles.achievementsProgressBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AchievementList({
|
||||||
|
achievements,
|
||||||
|
otherUserAchievements,
|
||||||
|
}: AchievementListProps) {
|
||||||
|
const { t } = useTranslation("achievement");
|
||||||
|
const { formatDateTime } = useDate();
|
||||||
|
|
||||||
|
if (!otherUserAchievements || otherUserAchievements.length === 0) {
|
||||||
|
return (
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{achievements.map((achievement, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className={styles.listItem}
|
||||||
|
style={{ display: "flex" }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={styles.listItemImage({
|
||||||
|
unlocked: achievement.unlocked,
|
||||||
|
})}
|
||||||
|
src={achievement.icon}
|
||||||
|
alt={achievement.displayName}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<h4>{achievement.displayName}</h4>
|
||||||
|
<p>{achievement.description}</p>
|
||||||
|
</div>
|
||||||
|
{achievement.unlockTime && (
|
||||||
|
<div style={{ whiteSpace: "nowrap" }}>
|
||||||
|
<small>{t("unlocked_at")}</small>
|
||||||
|
<p>{formatDateTime(achievement.unlockTime)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{otherUserAchievements.map((otherUserAchievement, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className={styles.listItem}
|
||||||
|
style={{ display: "grid", gridTemplateColumns: "1fr auto 1fr" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={styles.listItemImage({
|
||||||
|
unlocked: otherUserAchievement.unlocked,
|
||||||
|
})}
|
||||||
|
src={otherUserAchievement.icon}
|
||||||
|
alt={otherUserAchievement.displayName}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{otherUserAchievement.unlockTime && (
|
||||||
|
<div style={{ whiteSpace: "nowrap" }}>
|
||||||
|
<small>{t("unlocked_at")}</small>
|
||||||
|
<p>{formatDateTime(otherUserAchievement.unlockTime)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<h4>{otherUserAchievement.displayName}</h4>
|
||||||
|
<p>{otherUserAchievement.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row-reverse",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
textAlign: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={styles.listItemImage({
|
||||||
|
unlocked: achievements[index].unlocked,
|
||||||
|
})}
|
||||||
|
src={achievements[index].icon}
|
||||||
|
alt={achievements[index].displayName}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{achievements[index].unlockTime && (
|
||||||
|
<div style={{ whiteSpace: "nowrap" }}>
|
||||||
|
<small>{t("unlocked_at")}</small>
|
||||||
|
<p>{formatDateTime(achievements[index].unlockTime)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AchievementsContent({ otherUser }: AchievementsContentProps) {
|
||||||
|
const heroRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
||||||
|
const [backdropOpactiy, setBackdropOpacity] = useState(1);
|
||||||
|
|
||||||
|
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 } = useUserDetails();
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScroll: React.UIEventHandler<HTMLElement> = (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 || !userDetails) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.libraryHero(objectId)}
|
||||||
|
alt={gameTitle}
|
||||||
|
className={styles.hero}
|
||||||
|
onLoad={handleHeroLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section
|
||||||
|
ref={containerRef}
|
||||||
|
onScroll={onScroll}
|
||||||
|
className={styles.container}
|
||||||
|
>
|
||||||
|
<div ref={heroRef} className={styles.header}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: gameColor,
|
||||||
|
flex: 1,
|
||||||
|
opacity: Math.min(1, 1 - backdropOpactiy),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.heroLogoBackdrop}
|
||||||
|
style={{ opacity: backdropOpactiy }}
|
||||||
|
>
|
||||||
|
<div className={styles.heroContent}>
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.logo(objectId)}
|
||||||
|
className={styles.gameLogo}
|
||||||
|
alt={gameTitle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.panel({ stuck: isHeaderStuck })}>
|
||||||
|
<AchievementPanel
|
||||||
|
user={{
|
||||||
|
...userDetails,
|
||||||
|
userId: userDetails.id,
|
||||||
|
achievements: achievements!,
|
||||||
|
}}
|
||||||
|
otherUser={otherUser}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AchievementList
|
||||||
|
achievements={sortedAchievements}
|
||||||
|
otherUserAchievements={otherUser?.achievements}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
src/renderer/src/pages/achievement/achievements-skeleton.tsx
Normal file
13
src/renderer/src/pages/achievement/achievements-skeleton.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
import * as styles from "./achievements.css";
|
||||||
|
|
||||||
|
export function AchievementsSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.hero}>
|
||||||
|
<Skeleton className={styles.heroImageSkeleton} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.heroPanelSkeleton}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
190
src/renderer/src/pages/achievement/achievements.css.ts
Normal file
190
src/renderer/src/pages/achievement/achievements.css.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
||||||
|
export const HERO_HEIGHT = 300;
|
||||||
|
|
||||||
|
export const wrapper = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
transition: "all ease 0.3s",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const header = style({
|
||||||
|
display: "flex",
|
||||||
|
height: `${HERO_HEIGHT}px`,
|
||||||
|
minHeight: `${HERO_HEIGHT}px`,
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
flexDirection: "column",
|
||||||
|
position: "relative",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
"@media": {
|
||||||
|
"(min-width: 1250px)": {
|
||||||
|
height: "350px",
|
||||||
|
minHeight: "350px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const hero = style({
|
||||||
|
position: "absolute",
|
||||||
|
inset: "0",
|
||||||
|
borderRadius: "4px",
|
||||||
|
objectFit: "cover",
|
||||||
|
cursor: "pointer",
|
||||||
|
width: "100%",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const heroContent = style({
|
||||||
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gameLogo = style({
|
||||||
|
width: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "auto",
|
||||||
|
zIndex: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const panel = recipe({
|
||||||
|
base: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100px",
|
||||||
|
minHeight: "100px",
|
||||||
|
padding: `${SPACING_UNIT * 2}px 0`,
|
||||||
|
backgroundColor: vars.color.darkBackground,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
borderBottom: `solid 1px ${vars.color.border}`,
|
||||||
|
position: "sticky",
|
||||||
|
overflow: "hidden",
|
||||||
|
top: "0",
|
||||||
|
zIndex: "1",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
stuck: {
|
||||||
|
true: {
|
||||||
|
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.8)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const list = style({
|
||||||
|
listStyle: "none",
|
||||||
|
margin: "0",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listItem = style({
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatar = style({
|
||||||
|
height: "65px",
|
||||||
|
width: "65px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
position: "relative",
|
||||||
|
objectFit: "cover",
|
||||||
|
});
|
@ -1,71 +1,93 @@
|
|||||||
import { useDate } from "@renderer/hooks";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { useAppDispatch, useUserDetails } from "@renderer/hooks";
|
||||||
import { GameAchievement, GameShop } from "@types";
|
import type { GameShop, UserAchievement } from "@types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { vars } from "@renderer/theme.css";
|
||||||
|
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() {
|
export function Achievement() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const objectId = searchParams.get("objectId");
|
const objectId = searchParams.get("objectId");
|
||||||
const shop = searchParams.get("shop");
|
const shop = searchParams.get("shop");
|
||||||
|
const title = searchParams.get("title");
|
||||||
const userId = searchParams.get("userId");
|
const userId = searchParams.get("userId");
|
||||||
|
const displayName = searchParams.get("displayName");
|
||||||
|
const profileImageUrl = searchParams.get("profileImageUrl");
|
||||||
|
|
||||||
const { format } = useDate();
|
const { userDetails } = useUserDetails();
|
||||||
|
|
||||||
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
|
const [otherUserAchievements, setOtherUserAchievements] = useState<
|
||||||
|
UserAchievement[] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (objectId && shop) {
|
if (title) {
|
||||||
|
dispatch(setHeaderTitle(title));
|
||||||
|
}
|
||||||
|
}, [dispatch, title]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOtherUserAchievements(null);
|
||||||
|
if (userDetails?.id == userId) {
|
||||||
|
setOtherUserAchievements([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectId && shop && userId) {
|
||||||
window.electron
|
window.electron
|
||||||
.getGameAchievements(objectId, shop as GameShop, userId || undefined)
|
.getGameAchievements(objectId, shop as GameShop, userId)
|
||||||
.then((achievements) => {
|
.then((achievements) => {
|
||||||
setAchievements(achievements);
|
setOtherUserAchievements(achievements);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [objectId, shop, userId]);
|
}, [objectId, shop, userId]);
|
||||||
|
|
||||||
return (
|
if (!objectId || !shop || !title) return null;
|
||||||
<div>
|
|
||||||
<h1>Achievement</h1>
|
|
||||||
|
|
||||||
<div
|
const otherUserId = userDetails?.id === userId ? null : userId;
|
||||||
style={{
|
|
||||||
display: "flex",
|
const otherUser = otherUserId
|
||||||
flexDirection: "column",
|
? {
|
||||||
gap: `${SPACING_UNIT}px`,
|
userId: otherUserId,
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
displayName: displayName || "",
|
||||||
|
achievements: otherUserAchievements || [],
|
||||||
|
profileImageUrl: profileImageUrl || "",
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GameDetailsContextProvider
|
||||||
|
gameTitle={title}
|
||||||
|
shop={shop as GameShop}
|
||||||
|
objectId={objectId}
|
||||||
|
>
|
||||||
|
<GameDetailsContextConsumer>
|
||||||
|
{({ isLoading, achievements }) => {
|
||||||
|
return (
|
||||||
|
<SkeletonTheme
|
||||||
|
baseColor={vars.color.background}
|
||||||
|
highlightColor="#444"
|
||||||
|
>
|
||||||
|
{isLoading ||
|
||||||
|
achievements === null ||
|
||||||
|
(otherUserId && otherUserAchievements === null) ? (
|
||||||
|
<AchievementsSkeleton />
|
||||||
|
) : (
|
||||||
|
<AchievementsContent otherUser={otherUser} />
|
||||||
|
)}
|
||||||
|
</SkeletonTheme>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
</GameDetailsContextConsumer>
|
||||||
{achievements.map((achievement, index) => (
|
</GameDetailsContextProvider>
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
title={achievement.description}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
style={{
|
|
||||||
height: "60px",
|
|
||||||
width: "60px",
|
|
||||||
filter: achievement.unlocked ? "none" : "grayscale(100%)",
|
|
||||||
}}
|
|
||||||
src={
|
|
||||||
achievement.unlocked ? achievement.icon : achievement.icongray
|
|
||||||
}
|
|
||||||
alt={achievement.displayName}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p>{achievement.displayName}</p>
|
|
||||||
{achievement.unlockTime && format(achievement.unlockTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,11 @@ import type { GameRepack } from "@types";
|
|||||||
import * as styles from "./repacks-modal.css";
|
import * as styles from "./repacks-modal.css";
|
||||||
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { DownloadSettingsModal } from "./download-settings-modal";
|
import { DownloadSettingsModal } from "./download-settings-modal";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { orderBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
|
import { useDate } from "@renderer/hooks";
|
||||||
|
|
||||||
export interface RepacksModalProps {
|
export interface RepacksModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -36,6 +36,8 @@ export function RepacksModal({
|
|||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const { formatDate } = useDate();
|
||||||
|
|
||||||
const sortedRepacks = useMemo(() => {
|
const sortedRepacks = useMemo(() => {
|
||||||
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
|
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
|
||||||
}, [repacks]);
|
}, [repacks]);
|
||||||
@ -109,9 +111,7 @@ export function RepacksModal({
|
|||||||
|
|
||||||
<p style={{ fontSize: "12px" }}>
|
<p style={{ fontSize: "12px" }}>
|
||||||
{repack.fileSize} - {repack.repacker} -{" "}
|
{repack.fileSize} - {repack.repacker} -{" "}
|
||||||
{repack.uploadDate
|
{repack.uploadDate ? formatDate(repack.uploadDate!) : ""}
|
||||||
? format(repack.uploadDate, "dd/MM/yyyy")
|
|
||||||
: ""}
|
|
||||||
</p>
|
</p>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -29,6 +29,7 @@ export function SidebarSection({ title, children }: SidebarSectionProps) {
|
|||||||
maxHeight: isOpen ? `${content.current?.scrollHeight}px` : "0",
|
maxHeight: isOpen ? `${content.current?.scrollHeight}px` : "0",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)",
|
transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)",
|
||||||
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { globalStyle, style } from "@vanilla-extract/css";
|
import { globalStyle, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
||||||
export const contentSidebar = style({
|
export const contentSidebar = style({
|
||||||
borderLeft: `solid 1px ${vars.color.border}`,
|
borderLeft: `solid 1px ${vars.color.border}`,
|
||||||
@ -110,3 +111,46 @@ globalStyle(`${requirementsDetails} a`, {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const list = style({
|
||||||
|
listStyle: "none",
|
||||||
|
margin: "0",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listItem = style({
|
||||||
|
display: "flex",
|
||||||
|
cursor: "pointer",
|
||||||
|
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%)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1,16 +1,49 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { Button, Link } from "@renderer/components";
|
import { Button, Link } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar.css";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { useDate, useFormat } from "@renderer/hooks";
|
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
|
||||||
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
import { DownloadIcon, LockIcon, PeopleIcon } from "@primer/octicons-react";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
||||||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
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() {
|
export function Sidebar() {
|
||||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||||
@ -18,6 +51,8 @@ export function Sidebar() {
|
|||||||
data: HowLongToBeatCategory[] | null;
|
data: HowLongToBeatCategory[] | null;
|
||||||
}>({ isLoading: true, data: null });
|
}>({ isLoading: true, data: null });
|
||||||
|
|
||||||
|
const { userDetails } = useUserDetails();
|
||||||
|
|
||||||
const [activeRequirement, setActiveRequirement] =
|
const [activeRequirement, setActiveRequirement] =
|
||||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||||
|
|
||||||
@ -25,15 +60,10 @@ export function Sidebar() {
|
|||||||
useContext(gameDetailsContext);
|
useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
const { format } = useDate();
|
const { formatDateTime } = useDate();
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const buildGameAchievementPath = () => {
|
|
||||||
const urlParams = new URLSearchParams({ objectId: objectId!, shop });
|
|
||||||
return `/achievements?${urlParams.toString()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (objectId) {
|
if (objectId) {
|
||||||
setHowLongToBeat({ isLoading: true, data: null });
|
setHowLongToBeat({ isLoading: true, data: null });
|
||||||
@ -73,56 +103,99 @@ export function Sidebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.contentSidebar}>
|
<aside className={styles.contentSidebar}>
|
||||||
{achievements.length > 0 && (
|
{userDetails === null && (
|
||||||
|
<SidebarSection title={t("achievements")}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 2,
|
||||||
|
inset: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
background: "rgba(0, 0, 0, 0.7)",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LockIcon size={36} />
|
||||||
|
<h3>{t("sign_in_to_see_achievements")}</h3>
|
||||||
|
</div>
|
||||||
|
<ul className={styles.list} style={{ filter: "blur(4px)" }}>
|
||||||
|
{fakeAchievements.map((achievement, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<div className={styles.listItem}>
|
||||||
|
<img
|
||||||
|
style={{ filter: "blur(8px)" }}
|
||||||
|
className={styles.listItemImage({
|
||||||
|
unlocked: achievement.unlocked,
|
||||||
|
})}
|
||||||
|
src={achievement.icon}
|
||||||
|
alt={achievement.displayName}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p>{achievement.displayName}</p>
|
||||||
|
<small>
|
||||||
|
{achievement.unlockTime &&
|
||||||
|
formatDateTime(achievement.unlockTime)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</SidebarSection>
|
||||||
|
)}
|
||||||
|
{userDetails && achievements && achievements.length > 0 && (
|
||||||
<SidebarSection
|
<SidebarSection
|
||||||
title={t("achievements", {
|
title={t("achievements_count", {
|
||||||
unlockedCount: achievements.filter((a) => a.unlocked).length,
|
unlockedCount: achievements.filter((a) => a.unlocked).length,
|
||||||
achievementsCount: achievements.length,
|
achievementsCount: achievements.length,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>
|
<ul className={styles.list}>
|
||||||
<Link to={buildGameAchievementPath()}>Ver todas</Link>
|
{achievements.slice(0, 4).map((achievement, index) => (
|
||||||
</span>
|
<li key={index}>
|
||||||
<div
|
<Link
|
||||||
style={{
|
to={buildGameAchievementPath({
|
||||||
display: "flex",
|
shop: shop,
|
||||||
flexDirection: "column",
|
objectId: objectId!,
|
||||||
gap: `${SPACING_UNIT}px`,
|
title: gameTitle,
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
})}
|
||||||
}}
|
className={styles.listItem}
|
||||||
>
|
title={achievement.description}
|
||||||
{achievements.slice(0, 6).map((achievement, index) => (
|
>
|
||||||
<div
|
<img
|
||||||
key={index}
|
className={styles.listItemImage({
|
||||||
style={{
|
unlocked: achievement.unlocked,
|
||||||
display: "flex",
|
})}
|
||||||
flexDirection: "row",
|
src={achievement.icon}
|
||||||
alignItems: "center",
|
alt={achievement.displayName}
|
||||||
gap: `${SPACING_UNIT}px`,
|
/>
|
||||||
}}
|
<div>
|
||||||
title={achievement.description}
|
<p>{achievement.displayName}</p>
|
||||||
>
|
<small>
|
||||||
<img
|
{achievement.unlockTime &&
|
||||||
style={{
|
formatDateTime(achievement.unlockTime)}
|
||||||
height: "60px",
|
</small>
|
||||||
width: "60px",
|
</div>
|
||||||
filter: achievement.unlocked ? "none" : "grayscale(100%)",
|
</Link>
|
||||||
}}
|
</li>
|
||||||
src={
|
|
||||||
achievement.unlocked
|
|
||||||
? achievement.icon
|
|
||||||
: achievement.icongray
|
|
||||||
}
|
|
||||||
alt={achievement.displayName}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p>{achievement.displayName}</p>
|
|
||||||
{achievement.unlockTime && format(achievement.unlockTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
|
<Link
|
||||||
|
style={{ textAlign: "center" }}
|
||||||
|
to={buildGameAchievementPath({
|
||||||
|
shop: shop,
|
||||||
|
objectId: objectId!,
|
||||||
|
title: gameTitle,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t("see_all_achievements")}
|
||||||
|
</Link>
|
||||||
|
</ul>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import { FriendsBox } from "./friends-box";
|
|||||||
import { RecentGamesBox } from "./recent-games-box";
|
import { RecentGamesBox } from "./recent-games-box";
|
||||||
import { UserGame } from "@types";
|
import { UserGame } from "@types";
|
||||||
import {
|
import {
|
||||||
|
buildGameAchievementPath,
|
||||||
buildGameDetailsPath,
|
buildGameDetailsPath,
|
||||||
formatDownloadProgress,
|
formatDownloadProgress,
|
||||||
} from "@renderer/helpers";
|
} from "@renderer/helpers";
|
||||||
@ -44,11 +45,24 @@ export function ProfileContent() {
|
|||||||
return userProfile?.relation?.status === "ACCEPTED";
|
return userProfile?.relation?.status === "ACCEPTED";
|
||||||
}, [userProfile]);
|
}, [userProfile]);
|
||||||
|
|
||||||
const buildUserGameDetailsPath = (game: UserGame) =>
|
const buildUserGameDetailsPath = (game: UserGame) => {
|
||||||
buildGameDetailsPath({
|
if (!userProfile?.hasActiveSubscription) {
|
||||||
...game,
|
return buildGameDetailsPath({
|
||||||
objectId: game.objectId,
|
...game,
|
||||||
});
|
objectId: game.objectId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userParams = userProfile
|
||||||
|
? {
|
||||||
|
userId: userProfile.id,
|
||||||
|
displayName: userProfile.displayName,
|
||||||
|
profileImageUrl: userProfile.profileImageUrl,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return buildGameAchievementPath({ ...game }, userParams);
|
||||||
|
};
|
||||||
|
|
||||||
const formatPlayTime = useCallback(
|
const formatPlayTime = useCallback(
|
||||||
(playTimeInSeconds = 0) => {
|
(playTimeInSeconds = 0) => {
|
||||||
@ -160,53 +174,55 @@ export function ProfileContent() {
|
|||||||
{formatPlayTime(game.playTimeInSeconds)}
|
{formatPlayTime(game.playTimeInSeconds)}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<div
|
{userProfile.hasActiveSubscription && (
|
||||||
style={{
|
|
||||||
color: "white",
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
color: "white",
|
||||||
|
width: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
flexDirection: "column",
|
||||||
marginBottom: 8,
|
|
||||||
color: vars.color.muted,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
gap: 8,
|
marginBottom: 8,
|
||||||
|
color: vars.color.muted,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TrophyIcon size={13} />
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={13} />
|
||||||
|
<span>
|
||||||
|
{game.unlockedAchievementCount} /{" "}
|
||||||
|
{game.achievementCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{game.unlockedAchievementCount} /{" "}
|
{formatDownloadProgress(
|
||||||
{game.achievementCount}
|
game.unlockedAchievementCount /
|
||||||
|
game.achievementCount
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span>
|
<progress
|
||||||
{formatDownloadProgress(
|
max={1}
|
||||||
|
value={
|
||||||
game.unlockedAchievementCount /
|
game.unlockedAchievementCount /
|
||||||
game.achievementCount
|
game.achievementCount
|
||||||
)}
|
}
|
||||||
</span>
|
className={styles.achievementsProgressBar}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<progress
|
|
||||||
max={1}
|
|
||||||
value={
|
|
||||||
game.unlockedAchievementCount /
|
|
||||||
game.achievementCount
|
|
||||||
}
|
|
||||||
className={styles.achievementsProgressBar}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
|
@ -30,6 +30,7 @@ export function SettingsGeneral() {
|
|||||||
downloadsPath: "",
|
downloadsPath: "",
|
||||||
downloadNotificationsEnabled: false,
|
downloadNotificationsEnabled: false,
|
||||||
repackUpdatesNotificationsEnabled: false,
|
repackUpdatesNotificationsEnabled: false,
|
||||||
|
achievementNotificationsEnabled: false,
|
||||||
language: "",
|
language: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -103,6 +104,8 @@ export function SettingsGeneral() {
|
|||||||
userPreferences.downloadNotificationsEnabled,
|
userPreferences.downloadNotificationsEnabled,
|
||||||
repackUpdatesNotificationsEnabled:
|
repackUpdatesNotificationsEnabled:
|
||||||
userPreferences.repackUpdatesNotificationsEnabled,
|
userPreferences.repackUpdatesNotificationsEnabled,
|
||||||
|
achievementNotificationsEnabled:
|
||||||
|
userPreferences.achievementNotificationsEnabled,
|
||||||
language: language ?? "en",
|
language: language ?? "en",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -155,6 +158,17 @@ export function SettingsGeneral() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CheckboxField
|
||||||
|
label={t("enable_achievement_notifications")}
|
||||||
|
checked={form.achievementNotificationsEnabled}
|
||||||
|
onChange={() =>
|
||||||
|
handleChange({
|
||||||
|
achievementNotificationsEnabled:
|
||||||
|
!form.achievementNotificationsEnabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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"];
|
const FORMAT = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
|
||||||
export const formatBytes = (bytes: number): string => {
|
export const formatBytes = (bytes: number): string => {
|
||||||
|
@ -28,8 +28,37 @@ export interface GameRepack {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AchievementData {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description?: string;
|
||||||
|
icon: string;
|
||||||
|
icongray: string;
|
||||||
|
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 {
|
export interface GameAchievement {
|
||||||
name: string;
|
name: string;
|
||||||
|
hidden: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
unlocked: boolean;
|
unlocked: boolean;
|
||||||
@ -125,6 +154,7 @@ export interface UserPreferences {
|
|||||||
language: string;
|
language: string;
|
||||||
downloadNotificationsEnabled: boolean;
|
downloadNotificationsEnabled: boolean;
|
||||||
repackUpdatesNotificationsEnabled: boolean;
|
repackUpdatesNotificationsEnabled: boolean;
|
||||||
|
achievementNotificationsEnabled: boolean;
|
||||||
realDebridApiToken: string | null;
|
realDebridApiToken: string | null;
|
||||||
preferQuitInsteadOfHiding: boolean;
|
preferQuitInsteadOfHiding: boolean;
|
||||||
runAtStartup: boolean;
|
runAtStartup: boolean;
|
||||||
@ -200,6 +230,15 @@ export interface UserProfileCurrentGame extends Omit<GameRunning, "objectId"> {
|
|||||||
|
|
||||||
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
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 {
|
export interface UserDetails {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -208,6 +247,7 @@ export interface UserDetails {
|
|||||||
backgroundImageUrl: string | null;
|
backgroundImageUrl: string | null;
|
||||||
profileVisibility: ProfileVisibility;
|
profileVisibility: ProfileVisibility;
|
||||||
bio: string;
|
bio: string;
|
||||||
|
subscription: Subscription | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
@ -223,6 +263,7 @@ export interface UserProfile {
|
|||||||
relation: UserRelation | null;
|
relation: UserRelation | null;
|
||||||
currentGame: UserProfileCurrentGame | null;
|
currentGame: UserProfileCurrentGame | null;
|
||||||
bio: string;
|
bio: string;
|
||||||
|
hasActiveSubscription: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProfileRequest {
|
export interface UpdateProfileRequest {
|
||||||
|
121
yarn.lock
121
yarn.lock
@ -789,10 +789,10 @@
|
|||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
plist "^3.0.5"
|
plist "^3.0.5"
|
||||||
|
|
||||||
"@electron/rebuild@3.6.0":
|
"@electron/rebuild@3.6.1":
|
||||||
version "3.6.0"
|
version "3.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.6.0.tgz#60211375a5f8541a71eb07dd2f97354ad0b2b96f"
|
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.6.1.tgz#59e8e36c3f6e6b94a699425dfb61f0394c3dd4df"
|
||||||
integrity sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw==
|
integrity sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@malept/cross-spawn-promise" "^2.0.0"
|
"@malept/cross-spawn-promise" "^2.0.0"
|
||||||
chalk "^4.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"
|
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz#cf12e593b6b847fb9d04027fa755c6c6610d778b"
|
||||||
integrity sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==
|
integrity sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==
|
||||||
|
|
||||||
app-builder-lib@25.1.6:
|
app-builder-lib@25.1.8:
|
||||||
version "25.1.6"
|
version "25.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-25.1.6.tgz#a9618e186a5fa7b38cfa2d51ebba36d176ac31b6"
|
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-25.1.8.tgz#ae376039c5f269c7d562af494a087e5bc6310f1b"
|
||||||
integrity sha512-iui5q65skaawkTBcsaf2wCaegCWO+JoK5VaPnaNdIWXm2bq8a/g53W88FHjR535L9viLeGbuBp/iU1XZSe2DQQ==
|
integrity sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@develar/schema-utils" "~2.6.5"
|
"@develar/schema-utils" "~2.6.5"
|
||||||
"@electron/notarize" "2.5.0"
|
"@electron/notarize" "2.5.0"
|
||||||
"@electron/osx-sign" "1.3.1"
|
"@electron/osx-sign" "1.3.1"
|
||||||
"@electron/rebuild" "3.6.0"
|
"@electron/rebuild" "3.6.1"
|
||||||
"@electron/universal" "2.0.1"
|
"@electron/universal" "2.0.1"
|
||||||
"@malept/flatpak-bundler" "^0.4.0"
|
"@malept/flatpak-bundler" "^0.4.0"
|
||||||
"@types/fs-extra" "9.0.13"
|
"@types/fs-extra" "9.0.13"
|
||||||
async-exit-hook "^2.0.1"
|
async-exit-hook "^2.0.1"
|
||||||
bluebird-lst "^1.0.9"
|
bluebird-lst "^1.0.9"
|
||||||
builder-util "25.1.6"
|
builder-util "25.1.7"
|
||||||
builder-util-runtime "9.2.9"
|
builder-util-runtime "9.2.10"
|
||||||
chromium-pickle-js "^0.2.0"
|
chromium-pickle-js "^0.2.0"
|
||||||
config-file-ts "0.2.8-rc1"
|
config-file-ts "0.2.8-rc1"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
dotenv "^16.4.5"
|
dotenv "^16.4.5"
|
||||||
dotenv-expand "^11.0.6"
|
dotenv-expand "^11.0.6"
|
||||||
ejs "^3.1.8"
|
ejs "^3.1.8"
|
||||||
electron-publish "25.1.6"
|
electron-publish "25.1.7"
|
||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
fs-extra "^10.1.0"
|
fs-extra "^10.1.0"
|
||||||
hosted-git-info "^4.1.0"
|
hosted-git-info "^4.1.0"
|
||||||
@ -3369,32 +3369,24 @@ buffer@^6.0.3:
|
|||||||
base64-js "^1.3.1"
|
base64-js "^1.3.1"
|
||||||
ieee754 "^1.2.1"
|
ieee754 "^1.2.1"
|
||||||
|
|
||||||
builder-util-runtime@9.2.5:
|
builder-util-runtime@9.2.10:
|
||||||
version "9.2.5"
|
version "9.2.10"
|
||||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz#0afdffa0adb5c84c14926c7dd2cf3c6e96e9be83"
|
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz#a0f7d9e214158402e78b74a745c8d9f870c604bc"
|
||||||
integrity sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==
|
integrity sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
sax "^1.2.4"
|
sax "^1.2.4"
|
||||||
|
|
||||||
builder-util-runtime@9.2.9:
|
builder-util@25.1.7:
|
||||||
version "9.2.9"
|
version "25.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.9.tgz#e7d5dcb3a7caa9575406edea28940f6088d7cbb3"
|
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-25.1.7.tgz#a07b404f0cb1a635aa165902be65297d58932ff8"
|
||||||
integrity sha512-DWeHdrRFVvNnVyD4+vMztRpXegOGaQHodsAjyhstTbUNBIjebxM1ahxokQL+T1v8vpW8SY7aJ5is/zILH82lAw==
|
integrity sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==
|
||||||
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==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"7zip-bin" "~5.2.0"
|
"7zip-bin" "~5.2.0"
|
||||||
"@types/debug" "^4.1.6"
|
"@types/debug" "^4.1.6"
|
||||||
app-builder-bin "5.0.0-alpha.10"
|
app-builder-bin "5.0.0-alpha.10"
|
||||||
bluebird-lst "^1.0.9"
|
bluebird-lst "^1.0.9"
|
||||||
builder-util-runtime "9.2.9"
|
builder-util-runtime "9.2.10"
|
||||||
chalk "^4.1.2"
|
chalk "^4.1.2"
|
||||||
cross-spawn "^7.0.3"
|
cross-spawn "^7.0.3"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
@ -4078,14 +4070,14 @@ dir-glob@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-type "^4.0.0"
|
path-type "^4.0.0"
|
||||||
|
|
||||||
dmg-builder@25.1.6:
|
dmg-builder@25.1.8:
|
||||||
version "25.1.6"
|
version "25.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.6.tgz#ac9c9c35e09727610f342f400256f329cc7ba064"
|
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.1.8.tgz#41f3b725edd896156e891016a44129e1bd580430"
|
||||||
integrity sha512-TeKjLNKBu6ODewl36Q1FH0+Bv/Rnb76x1vzPJ0Xw1T/YlAByYl1G+1PaKoEpVEDisrLRSrM9a0k3vDj53xQi9w==
|
integrity sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib "25.1.6"
|
app-builder-lib "25.1.8"
|
||||||
builder-util "25.1.6"
|
builder-util "25.1.7"
|
||||||
builder-util-runtime "9.2.9"
|
builder-util-runtime "9.2.10"
|
||||||
fs-extra "^10.1.0"
|
fs-extra "^10.1.0"
|
||||||
iconv-lite "^0.6.2"
|
iconv-lite "^0.6.2"
|
||||||
js-yaml "^4.1.0"
|
js-yaml "^4.1.0"
|
||||||
@ -4166,16 +4158,16 @@ ejs@^3.1.8:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jake "^10.8.5"
|
jake "^10.8.5"
|
||||||
|
|
||||||
electron-builder@^25.1.6:
|
electron-builder@^25.1.8:
|
||||||
version "25.1.6"
|
version "25.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-25.1.6.tgz#e2a8a4b4a7b6db615405a9b9b18fb3dcf7c84d8c"
|
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-25.1.8.tgz#b0e310f1600787610bb84c3f39bc7aadb2548486"
|
||||||
integrity sha512-a+w0leNGcr57u9fiQBsVvVuXtTsg/6VpMOps1Q1TPAceHtK4kWtX2wc7X2cnJQrIME8bWbdm/YR9Swr/xF1/ng==
|
integrity sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==
|
||||||
dependencies:
|
dependencies:
|
||||||
app-builder-lib "25.1.6"
|
app-builder-lib "25.1.8"
|
||||||
builder-util "25.1.6"
|
builder-util "25.1.7"
|
||||||
builder-util-runtime "9.2.9"
|
builder-util-runtime "9.2.10"
|
||||||
chalk "^4.1.2"
|
chalk "^4.1.2"
|
||||||
dmg-builder "25.1.6"
|
dmg-builder "25.1.8"
|
||||||
fs-extra "^10.1.0"
|
fs-extra "^10.1.0"
|
||||||
is-ci "^3.0.0"
|
is-ci "^3.0.0"
|
||||||
lazy-val "^1.0.5"
|
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"
|
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.2.0.tgz#505716926dfcf9cb3e74f42b1003be6d865bcb88"
|
||||||
integrity sha512-VjLkvaLmbP3AOGOh5Fob9M8bFU0mmeSAb5G2EoTBx+kQLf2XA/0byzjsVGBTHhikbT+m1AB27NEQUv9wX9nM8w==
|
integrity sha512-VjLkvaLmbP3AOGOh5Fob9M8bFU0mmeSAb5G2EoTBx+kQLf2XA/0byzjsVGBTHhikbT+m1AB27NEQUv9wX9nM8w==
|
||||||
|
|
||||||
electron-publish@25.1.6:
|
electron-publish@25.1.7:
|
||||||
version "25.1.6"
|
version "25.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-25.1.6.tgz#09cd851b3af6b5ca334ee4dd35581251efe0323e"
|
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-25.1.7.tgz#14e50c2a3fafdc1c454eadbbc47ead89a48bb554"
|
||||||
integrity sha512-Hiy/tVyu57cbkyxS0GrzEnxm7+B/9IeFcBTia2QQBCTg10zvHURdAj7Sk7XHKys8kKLr1tVNThuG165uTnNJdQ==
|
integrity sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/fs-extra" "^9.0.11"
|
"@types/fs-extra" "^9.0.11"
|
||||||
builder-util "25.1.6"
|
builder-util "25.1.7"
|
||||||
builder-util-runtime "9.2.9"
|
builder-util-runtime "9.2.10"
|
||||||
chalk "^4.1.2"
|
chalk "^4.1.2"
|
||||||
fs-extra "^10.1.0"
|
fs-extra "^10.1.0"
|
||||||
lazy-val "^1.0.5"
|
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"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.30.tgz#5b264b489cfe0c3dd71097c164d795444834e7c7"
|
||||||
integrity sha512-sXI35EBN4lYxzc/pIGorlymYNzDBOqkSlVRe6MkgBsW/hW1tpC/HDJ2fjG7XnjakzfLEuvdmux0Mjs6jHq4UOA==
|
integrity sha512-sXI35EBN4lYxzc/pIGorlymYNzDBOqkSlVRe6MkgBsW/hW1tpC/HDJ2fjG7XnjakzfLEuvdmux0Mjs6jHq4UOA==
|
||||||
|
|
||||||
electron-updater@^6.3.4:
|
electron-updater@^6.3.9:
|
||||||
version "6.3.4"
|
version "6.3.9"
|
||||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.4.tgz#3934bc89875bb524c2cbbd11041114e97c0c2496"
|
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.9.tgz#e1e7f155624c58e6f3760f376c3a584028165ec4"
|
||||||
integrity sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==
|
integrity sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==
|
||||||
dependencies:
|
dependencies:
|
||||||
builder-util-runtime "9.2.5"
|
builder-util-runtime "9.2.10"
|
||||||
fs-extra "^10.1.0"
|
fs-extra "^10.1.0"
|
||||||
js-yaml "^4.1.0"
|
js-yaml "^4.1.0"
|
||||||
lazy-val "^1.0.5"
|
lazy-val "^1.0.5"
|
||||||
@ -4766,14 +4758,6 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
|||||||
node-domexception "^1.0.0"
|
node-domexception "^1.0.0"
|
||||||
web-streams-polyfill "^3.0.3"
|
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:
|
file-entry-cache@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
||||||
@ -5363,7 +5347,7 @@ i18next@^23.11.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.23.2"
|
"@babel/runtime" "^7.23.2"
|
||||||
|
|
||||||
icojs@^0.19.3:
|
icojs@^0.19.4:
|
||||||
version "0.19.4"
|
version "0.19.4"
|
||||||
resolved "https://registry.yarnpkg.com/icojs/-/icojs-0.19.4.tgz#fdbc9e61a0945ed1d331beb358d67f72cf7d78dc"
|
resolved "https://registry.yarnpkg.com/icojs/-/icojs-0.19.4.tgz#fdbc9e61a0945ed1d331beb358d67f72cf7d78dc"
|
||||||
integrity sha512-86oNepPk2jAmbb96BPeucZI7HoSBobFlXDhhjIbwRb3wkQpvdBO5HO9KtMUNzMFT3qqQZsjLsfW+L0/9Rl9VqA==
|
integrity sha512-86oNepPk2jAmbb96BPeucZI7HoSBobFlXDhhjIbwRb3wkQpvdBO5HO9KtMUNzMFT3qqQZsjLsfW+L0/9Rl9VqA==
|
||||||
@ -6851,7 +6835,7 @@ parse-json@^5.2.0:
|
|||||||
json-parse-even-better-errors "^2.3.0"
|
json-parse-even-better-errors "^2.3.0"
|
||||||
lines-and-columns "^1.1.6"
|
lines-and-columns "^1.1.6"
|
||||||
|
|
||||||
parse-torrent@^11.0.16:
|
parse-torrent@^11.0.17:
|
||||||
version "11.0.17"
|
version "11.0.17"
|
||||||
resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-11.0.17.tgz#60614845b28e24b869a60adce492d37c2b1a3133"
|
resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-11.0.17.tgz#60614845b28e24b869a60adce492d37c2b1a3133"
|
||||||
integrity sha512-bkfEtrqIMT4+bSWs+m7+Ktd7LSJsDefA9qfJQ3UFwOeBqipiQ+347guu79zX++nRwMnrdvRecLmgaRcdiYjE4w==
|
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"
|
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
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:
|
set-function-length@^1.2.1:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
|
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"
|
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
|
||||||
integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
|
integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
|
||||||
|
|
||||||
tough-cookie@^4.0.0, tough-cookie@^4.1.4:
|
tough-cookie@^4.1.4:
|
||||||
version "4.1.4"
|
version "4.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
|
||||||
integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
|
integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
|
||||||
|
Loading…
Reference in New Issue
Block a user