Compare commits

..

2 Commits

Author SHA1 Message Date
Chubby Granny Chaser
3928770ef8 Merge branch 'main' into fix/HYD-860 2025-06-03 13:35:10 +01:00
Chubby Granny Chaser
de1dfca57e fix: fixing playtime dir on linux 2025-06-03 13:33:48 +01:00
53 changed files with 738 additions and 728 deletions

View File

@@ -7,6 +7,7 @@ import {
} from "electron-vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig(({ mode }) => {
loadEnv(mode);
@@ -47,7 +48,15 @@ export default defineConfig(({ mode }) => {
"@shared": resolve("src/shared"),
},
},
plugins: [svgr(), react()],
plugins: [
svgr(),
react(),
sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: "hydra-launcher",
project: "hydra-renderer",
}),
],
},
};
});

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.6.6",
"version": "3.6.0",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -40,6 +40,8 @@
"@primer/octicons-react": "^19.9.0",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@reduxjs/toolkit": "^2.2.3",
"@sentry/react": "^8.47.0",
"@sentry/vite-plugin": "^2.22.7",
"auto-launch": "^5.0.6",
"axios": "^1.7.9",
"axios-cookiejar-support": "^5.0.5",

View File

@@ -4,6 +4,7 @@ from torrent_downloader import TorrentDownloader
from http_downloader import HttpDownloader
from profile_image_processor import ProfileImageProcessor
import libtorrent as lt
import os
app = Flask(__name__)
@@ -101,14 +102,27 @@ def process_list():
auth_error = validate_rpc_password()
if auth_error:
return auth_error
iter_list = ['exe', 'pid', 'name']
if sys.platform != 'win32':
iter_list.append('cwd')
iter_list.append('environ')
process_list = [proc.info for proc in psutil.process_iter(iter_list)]
return jsonify(process_list), 200
processes = []
for proc in psutil.process_iter(['exe', 'cwd', 'pid', 'cmdline']):
try:
info = proc.info
cmdline = info.get('cmdline') or []
wine_launched_exe = None
for arg in cmdline:
if isinstance(arg, str) and arg.lower().endswith(".exe"):
wine_launched_exe = os.path.basename(arg)
break
exe_path = info.get('exe') or ''
info['name'] = wine_launched_exe or os.path.basename(exe_path)
processes.append(info)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
return jsonify(processes), 200
@app.route("/profile-image", methods=["POST"])
def profile_image():

View File

@@ -89,7 +89,6 @@
"amount_minutes": "{{amount}} minutes",
"accuracy": "{{accuracy}}% accuracy",
"add_to_library": "Add to library",
"already_in_library": "Already in library",
"remove_from_library": "Remove from library",
"no_downloads": "No downloads available",
"play_time": "Played for {{amount}}",
@@ -380,7 +379,6 @@
"installing_common_redist": "Installing…",
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
"extract_files_by_default": "Extract files by default after download",
"enable_steam_achievements": "Enable search for Steam achievements",
"achievement_custom_notification_position": "Achievement custom notification position",
"top-left": "Top left",
"top-center": "Top center",
@@ -517,8 +515,7 @@
"earned_points": "Earned points",
"show_achievements_on_profile": "Show your achievements on your profile",
"show_points_on_profile": "Show your earned points on your profile",
"error_adding_friend": "Could not send friend request. Please check friend code",
"friend_code_length_error": "Friend code must have 8 characters"
"error_adding_friend": "Could not send friend request. Please check friend code"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",

View File

@@ -141,7 +141,7 @@
"allow_nsfw_content": "Continuar",
"refuse_nsfw_content": "No, gracias",
"stats": "Estadísticas",
"download_count": "Descargas",
"download_count": "Downloads",
"player_count": "Jugadores activos",
"download_error": "Esta opción de descarga no está disponible.",
"download": "Descargar",
@@ -199,12 +199,12 @@
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y aún no se puede verificar el estado de la descarga.",
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y el estado de descarga del sondeo aún no está disponible.",
"game_added_to_favorites": "Juego añadido a favoritos",
"game_removed_from_favorites": "Juego removido de favoritos",
"invalid_wine_prefix_path": "Ruta de prefijo de Wine inválida",
"invalid_wine_prefix_path_description": "La ruta del prefijo Wine es inválida. Por favor, checa la ruta y vuelve a intentarlo.",
"missing_wine_prefix": "Se requiere el prefijo Wine para crear una copia de seguridad en Linux"
"invalid_wine_prefix_path": "Ruta de prefixo Wine inválida",
"invalid_wine_prefix_path_description": "La ruta al prefixo Wine es inválida. Por favor, verifica la ruta y vuelve a intentarlo.",
"missing_wine_prefix": ""
},
"activation": {
"title": "Activar Hydra",

View File

@@ -76,7 +76,6 @@
"amount_minutes": "{{amount}} minutos",
"accuracy": "{{accuracy}}% de precisão",
"add_to_library": "Adicionar à biblioteca",
"already_in_library": "Já está na biblioteca",
"remove_from_library": "Remover da biblioteca",
"no_downloads": "Nenhum download disponível",
"play_time": "Jogou por {{amount}}",
@@ -365,7 +364,6 @@
"installing_common_redist": "Instalando…",
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
"enable_steam_achievements": "Habilitar busca por conquistas da Steam",
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
"top-left": "Superior esquerdo",
"top-center": "Superior central",
@@ -510,8 +508,7 @@
"earned_points": "Pontos ganhos",
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido",
"friend_code_length_error": "Código de amigo deve ter 8 caracteres"
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",

View File

@@ -365,14 +365,14 @@
"installing_common_redist": "Установка…",
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
"achievement_custom_notification_position": "Позиция уведомлений достижений",
"achievement_custom_notification_position": "Позиция настраиваемых уведомлений о достижениях",
"top-left": "Верхний левый угол",
"top-center": "Верхний центр",
"top-right": "Верхний правый угол",
"bottom-left": "Нижний левый угол",
"bottom-center": "Нижний центр",
"bottom-right": "Нижний правый угол",
"enable_achievement_custom_notifications": "Включить уведомления о достижениях",
"enable_achievement_custom_notifications": "Включить настраиваемые уведомления о достижениях",
"alignment": "Выравнивание",
"variation": "Вариация",
"default": "По умолчанию",

View File

@@ -1,5 +1,5 @@
{
"language_name": "简体中文",
"language_name": "中文",
"app": {
"successfully_signed_in": "已成功登录"
},
@@ -26,9 +26,7 @@
"game_has_no_executable": "未选择游戏的可执行文件",
"sign_in": "登入",
"friends": "好友",
"favorites": "收藏",
"need_help": "需要帮助?",
"playable_button_title": "仅显示现在可以游玩的游戏"
"favorites": "收藏"
},
"header": {
"search": "搜索游戏",
@@ -45,23 +43,11 @@
"downloading_metadata": "正在下载{{title}}的元数据…",
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}",
"calculating_eta": "正在下载 {{title}}… (已完成{{percentage}}.) - 正在计算剩余时间...",
"checking_files": "正在校验 {{title}} 的文件... ({{percentage}} 已完成)",
"installation_complete": "安装完成",
"installation_complete_message": "通用可再发行组件安装成功",
"installing_common_redist": "{{log}}…"
"checking_files": "正在校验 {{title}} 的文件... ({{percentage}} 已完成)"
},
"catalogue": {
"next_page": "下一页",
"previous_page": "上一页",
"clear_filters": "清除已选的 {{filterCount}} 项",
"developers": "开发商",
"download_sources": "下载源",
"filter_count": "{{filterCount}} 项可用",
"genres": "类型",
"publishers": "发行商",
"result_count": "{{resultCount}} 个结果",
"search": "筛选…",
"tags": "标签"
"previous_page": "上一页"
},
"game_details": {
"open_download_options": "打开下载菜单",
@@ -180,48 +166,7 @@
"manage_files_description": "管理哪些文件要备份和恢复",
"select_folder": "选择文件夹",
"backup_from": "{{date}} 时备份",
"custom_backup_location_set": "自定义备份文件位置",
"artifact_name_label": "备份名称",
"artifact_name_placeholder": "为备份输入名称",
"artifact_renamed": "备份重命名成功",
"automatic_backup_from": "{{date}} 的自动备份",
"automatically_extract_downloaded_files": "自动解压下载的文件",
"backup_freeze_failed": "固定备份失败",
"backup_freeze_failed_description": "您必须至少保留一个空位用于自动备份",
"backup_frozen": "备份已固定",
"backup_unfrozen": "备份已取消固定",
"clear": "清除",
"create_start_menu_shortcut": "创建开始菜单快捷方式",
"create_steam_shortcut": "创建Steam快捷方式",
"download_error_gofile_quota_exceeded": "您已超出Gofile的月度配额。请等待配额重置。",
"download_error_not_cached_on_hydra": "此下载在Nimbus上不可用。",
"download_error_not_cached_on_real_debrid": "此下载在Real-Debrid上不可用且暂不支持从Real-Debrid轮询下载状态。",
"download_error_not_cached_on_torbox": "此下载在TorBox上不可用且暂不支持从TorBox轮询下载状态。",
"download_error_real_debrid_account_not_authorized": "您的Real-Debrid账户未被授权进行新下载。请检查您的账户设置并重试。",
"enable_automatic_cloud_sync": "启用自动云同步",
"freeze_backup": "固定以免被自动备份覆盖",
"game_added_to_favorites": "游戏已添加到收藏",
"game_removed_from_favorites": "游戏已从收藏中移除",
"invalid_wine_prefix_path": "无效的Wine前置路径",
"invalid_wine_prefix_path_description": "Wine前置的路径无效。请检查路径并重试。",
"launch_options": "启动选项",
"launch_options_description": "高级用户可以选择修改启动选项(实验性功能)",
"launch_options_placeholder": "未指定参数",
"max_length_field": "此字段必须少于 {{length}} 个字符",
"missing_wine_prefix": "在Linux上创建备份需要Wine前置",
"no_directory_selected": "未选择目录",
"no_write_permission": "无法下载到此目录。点击此处了解更多。",
"rename_artifact": "重命名备份",
"rename_artifact_description": "将备份重命名为更具描述性的名称",
"required_field": "此字段为必填项",
"reset_achievements": "重置成就",
"reset_achievements_description": "这将重置 {{game}} 的所有成就",
"reset_achievements_error": "重置成就失败",
"reset_achievements_success": "成就重置成功",
"reset_achievements_title": "您确定吗?",
"save_changes": "保存更改",
"unfreeze_backup": "取消固定",
"you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改"
"custom_backup_location_set": "自定义备份文件位置"
},
"activation": {
"title": "激活 Hydra",
@@ -254,13 +199,7 @@
"queued": "下载列表",
"no_downloads_title": "空空如也",
"no_downloads_description": "你还未使用Hydra下载任何游戏,但什么时候开始,都为时不晚。",
"checking_files": "正在校验文件…",
"extract": "解压文件",
"extracting": "正在解压文件…",
"options": "管理",
"resume_seeding": "恢复做种",
"seeding": "做种中",
"stop_seeding": "停止做种"
"checking_files": "正在校验文件…"
},
"settings": {
"downloads_path": "下载路径",
@@ -321,83 +260,7 @@
"must_be_valid_url": "来源必须是有效的 URL",
"blocked_users": "已屏蔽用户",
"user_unblocked": "用户已经被屏蔽",
"enable_achievement_notifications": "当成就解锁时",
"account": "账户",
"account_data_updated_successfully": "账户数据更新成功",
"achievement_custom_notification_position": "成就自定义通知位置",
"alignment": "对齐",
"appearance": "外观",
"become_subscriber": "成为Hydra Cloud用户",
"bill_sent_until": "您的下一张账单将在此日期前发送",
"bottom-center": "底部中央",
"bottom-left": "底部左侧",
"bottom-right": "底部右侧",
"cancel": "取消",
"clear_themes": "清除",
"common_redist": "通用可再发行组件",
"common_redist_description": "运行某些游戏需要通用可再发行组件。建议安装以避免问题。",
"create_real_debrid_account": "如果您还没有Real-Debrid账户请点击此处",
"create_theme": "创建",
"create_theme_modal_description": "创建新主题以自定义Hydra的外观",
"create_theme_modal_title": "创建自定义主题",
"create_torbox_account": "如果您还没有TorBox账户请点击此处",
"current_email": "当前邮箱:",
"default": "默认",
"delete_all_themes": "删除所有主题",
"delete_all_themes_description": "这将删除所有您的自定义主题",
"delete_theme": "删除主题",
"delete_theme_description": "这将删除主题 {{theme}}",
"disable_nsfw_alert": "禁用NSFW警告",
"edit_theme": "编辑主题",
"editor_tab_code": "代码",
"editor_tab_info": "信息",
"editor_tab_save": "保存",
"enable_achievement_custom_notifications": "启用成就自定义通知",
"enable_auto_install": "自动下载更新",
"enable_friend_request_notifications": "当收到好友请求时",
"enable_friend_start_game_notifications": "当好友开始游戏时",
"enable_torbox": "启用TorBox",
"error_importing_theme": "导入主题时出错",
"extract_files_by_default": "下载后默认解压文件",
"hidden": "隐藏",
"import_theme": "导入主题",
"import_theme_description": "您将从主题商店导入 {{theme}}",
"insert_theme_name": "输入主题名称",
"install_common_redist": "安装",
"installing_common_redist": "正在安装…",
"launch_minimized": "最小化启动Hydra",
"manage_subscription": "管理订阅",
"name_min_length": "主题名称必须至少3个字符长",
"no_email_account": "您尚未设置邮箱",
"no_subscription": "以最佳方式享受Hydra",
"no_themes": "看起来您还没有任何主题,但别担心,点击这里创建您的第一个杰作。",
"no_users_blocked": "您没有屏蔽任何用户",
"notification_preview": "成就通知预览",
"platinum": "白金",
"rare": "稀有",
"real_debrid_account_linked": "Real-Debrid账户已连接",
"renew_subscription": "续费Hydra Cloud",
"seed_after_download_complete": "下载完成后做种",
"set_theme": "设置主题",
"show_download_speed_in_megabytes": "以兆字节每秒显示下载速度",
"show_hidden_achievement_description": "在解锁前显示隐藏成就描述",
"subscription_active_until": "您的Hydra Cloud活跃至 {{date}}",
"subscription_expired_at": "您的订阅已于 {{date}} 到期",
"subscription_renew_cancelled": "自动续费已禁用",
"subscription_renews_on": "您的订阅将于 {{date}} 续费",
"test_notification": "测试通知",
"theme_imported": "主题导入成功",
"theme_name": "名称",
"top-center": "顶部中央",
"top-left": "顶部左侧",
"top-right": "顶部右侧",
"torbox_account_linked": "TorBox账户已连接",
"torbox_description": "TorBox是您的高级种子盒服务甚至可与市场上最好的服务器相媲美。",
"unset_theme": "取消设置主题",
"update_email": "更新邮箱",
"update_password": "更新密码",
"variation": "变体",
"web_store": "网络商店"
"enable_achievement_notifications": "当成就解锁时"
},
"notifications": {
"download_complete": "下载完成",
@@ -408,23 +271,14 @@
"new_update_available": "版本 {{version}} 可用",
"restart_to_install_update": "重启 Hydra 以安装更新",
"notification_achievement_unlocked_title": "{{game}} 的成绩已解锁",
"notification_achievement_unlocked_body": "{{achievement}} 和其他 {{count}} 已解锁",
"extraction_complete": "解压完成",
"friend_started_playing_game": "{{displayName}} 开始玩游戏",
"game_extracted": "{{title}} 解压成功",
"new_friend_request_description": "{{displayName}} 向您发送了好友请求",
"new_friend_request_title": "新好友请求",
"test_achievement_notification_description": "非常酷,对吧?",
"test_achievement_notification_title": "这是一个测试通知"
"notification_achievement_unlocked_body": "{{achievement}} 和其他 {{count}} 已解锁"
},
"system_tray": {
"open": "打开 Hydra",
"quit": "退出"
},
"game_card": {
"no_downloads": "无可用下载选项",
"available_one": "可用",
"available_other": "可用"
"no_downloads": "无可用下载选项"
},
"binary_not_found_modal": {
"title": "程序未安装",
@@ -497,7 +351,7 @@
"report_description": "额外信息",
"report_description_placeholder": "额外信息",
"report": "举报",
"report_reason_hate": "仇恨言论",
"report_reason_hate": "Hate speech",
"report_reason_sexual_content": "色情内容",
"report_reason_violence": "暴力",
"report_reason_spam": "骚扰",
@@ -506,19 +360,7 @@
"your_friend_code": "你的好友代码:",
"upload_banner": "上传横幅",
"uploading_banner": "上传横幅中…",
"background_image_updated": "背景图片已更新",
"achievements": "成就",
"achievements_unlocked": "成就已解锁",
"earned_points": "获得积分",
"error_adding_friend": "无法发送好友请求。请检查好友代码",
"friend_code_length_error": "好友代码必须为8个字符",
"games": "游戏",
"playing": "正在玩 {{game}}",
"ranking_updated_weekly": "排名每周更新",
"show_achievements_on_profile": "在您的个人资料上显示成就",
"show_points_on_profile": "在您的个人资料上显示获得的积分",
"stats": "统计",
"top_percentile": "前 {{percentile}}%"
"background_image_updated": "背景图片已更新"
},
"achievement": {
"achievement_unlocked": "成就已解锁",
@@ -526,14 +368,7 @@
"your_achievements": "你的成就",
"unlocked_at": "解锁于: {{date}}",
"subscription_needed": "需要订阅 Hydra Cloud 才能看到此内容",
"new_achievements_unlocked": "从 {{gameCount}} 游戏中解锁 {{achievementCount}} 新成就",
"achievement_earn_points": "通过此成就获得 {{points}} 积分",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} 成就",
"achievements_unlocked_for_game": "为 {{gameTitle}} 解锁了 {{achievementCount}} 个新成就",
"available_points": "可用积分:",
"earned_points": "获得积分:",
"hidden_achievement_tooltip": "这是一个隐藏成就",
"how_to_earn_achievements_points": "如何获得成就积分?"
"new_achievements_unlocked": "从 {{gameCount}} 游戏中解锁 {{achievementCount}} 新成就"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra 云订阅",
@@ -543,10 +378,6 @@
"animated_profile_picture": "动画头像",
"premium_support": "高级技术支持",
"show_and_compare_achievements": "展示并与其他用户比较您的成就",
"animated_profile_banner": "动态个人简介横幅",
"debrid_description": "使用Nimbus下载速度提升4倍",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "您刚刚发现了一个Hydra Cloud功能",
"learn_more": "了解更多"
"animated_profile_banner": "动态个人简介横幅"
}
}

View File

@@ -41,4 +41,4 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
export const MAIN_LOOP_INTERVAL = 2000;
export const MAIN_LOOP_INTERVAL = 1500;

View File

@@ -1,38 +1,17 @@
import type { GameShop, GameStats } from "@types";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { gamesStatsCacheSublevel, levelKeys } from "@main/level";
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 30; // 30 minutes
const getGameStats = async (
_event: Electron.IpcMainInvokeEvent,
objectId: string,
shop: GameShop
) => {
const cachedStats = await gamesStatsCacheSublevel.get(
levelKeys.game(shop, objectId)
);
if (
cachedStats &&
cachedStats.updatedAt + LOCAL_CACHE_EXPIRATION > Date.now()
) {
return cachedStats;
}
return HydraApi.get<GameStats>(
`/games/stats`,
{ objectId, shop },
{ needsAuth: false }
).then(async (data) => {
await gamesStatsCacheSublevel.put(levelKeys.game(shop, objectId), {
...data,
updatedAt: Date.now(),
});
return data;
});
);
};
registerEvent("getGameStats", getGameStats);

View File

@@ -8,9 +8,7 @@ const saveGameShopAssets = async (
shop: GameShop,
assets: ShopAssets
): Promise<void> => {
const key = levelKeys.game(shop, objectId);
const existingAssets = await gamesShopAssetsSublevel.get(key);
return gamesShopAssetsSublevel.put(key, { ...existingAssets, ...assets });
return gamesShopAssetsSublevel.put(levelKeys.game(shop, objectId), assets);
};
registerEvent("saveGameShopAssets", saveGameShopAssets);

View File

@@ -20,6 +20,7 @@ import "./library/create-game-shortcut";
import "./library/close-game";
import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/sync-game-by-object-id";
import "./library/get-library";
import "./library/extract-game-download";
import "./library/open-game";

View File

@@ -1,13 +1,13 @@
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { createGame } from "@main/services/library-sync";
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
import {
downloadsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
levelKeys,
} from "@main/level";
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent,
@@ -43,10 +43,7 @@ const addGameToLibrary = async (
await createGame(game).catch(() => {});
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
game.shop,
game.objectId
);
updateLocalUnlockedAchievements(game);
};
registerEvent("addGameToLibrary", addGameToLibrary);

View File

@@ -94,7 +94,7 @@ const createSteamShortcut = async (
if (!steamUserIds.length) {
logger.error("No Steam user ID found");
throw new Error("No Steam user ID found");
return;
}
const [iconImage, heroImage, logoImage, coverImage, libraryImage] =

View File

@@ -16,8 +16,7 @@ const resetGameAchievements = async (
objectId: string
) => {
try {
const levelKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(levelKey);
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
if (!game) return;
@@ -30,6 +29,8 @@ const resetGameAchievements = async (
}
}
const levelKey = levelKeys.game(game.shop, game.objectId);
await gameAchievementsSublevel
.get(levelKey)
.then(async (gameAchievements) => {

View File

@@ -0,0 +1,32 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { HydraApi } from "@main/services";
import type { GameShop, UserGameDetails } from "@types";
const syncGameByObjectId = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
return HydraApi.get<UserGameDetails>(
`/profile/games/${shop}/${objectId}`
).then(async (res) => {
const { id, playTimeInSeconds, isFavorite, ...rest } = res;
const gameKey = levelKeys.game(shop, objectId);
const currentData = await gamesSublevel.get(gameKey);
await gamesSublevel.put(gameKey, {
...currentData,
...rest,
remoteId: id,
playTimeInMilliseconds: playTimeInSeconds * 1000,
favorite: isFavorite ?? currentData?.favorite,
});
return res;
});
};
registerEvent("syncGameByObjectId", syncGameByObjectId);

View File

@@ -3,7 +3,6 @@ import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { db, levelKeys } from "@main/level";
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
const getComparedUnlockedAchievements = async (
_event: Electron.IpcMainInvokeEvent,
@@ -11,8 +10,6 @@ const getComparedUnlockedAchievements = async (
shop: GameShop,
userId: string
) => {
await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{

View File

@@ -2,7 +2,6 @@ import type { GameShop, UserAchievement, UserPreferences } from "@types";
import { registerEvent } from "../register-event";
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
export const getUnlockedAchievements = async (
objectId: string,
@@ -63,7 +62,7 @@ export const getUnlockedAchievements = async (
!achievementData.hidden || showHiddenAchievementsDescription
? achievementData.description
: undefined,
};
} as UserAchievement;
})
.sort((a, b) => {
if (a.unlocked && !b.unlocked) return -1;
@@ -80,7 +79,6 @@ const getUnlockedAchievementsEvent = async (
objectId: string,
shop: GameShop
): Promise<UserAchievement[]> => {
await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
return getUnlockedAchievements(objectId, shop, false);
};

View File

@@ -1,11 +0,0 @@
import type { GameStats } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";
export const gamesStatsCacheSublevel = db.sublevel<
string,
GameStats & { updatedAt: number }
>(levelKeys.gameStatsCache, {
valueEncoding: "json",
});

View File

@@ -2,7 +2,6 @@ export * from "./downloads";
export * from "./games";
export * from "./game-shop-assets";
export * from "./game-shop-cache";
export * from "./game-stats-cache";
export * from "./game-achievements";
export * from "./keys";
export * from "./themes";

View File

@@ -7,7 +7,6 @@ export const levelKeys = {
auth: "auth",
themes: "themes",
gameShopAssets: "gameShopAssets",
gameStatsCache: "gameStatsAssets",
gameShopCache: "gameShopCache",
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
`${shop}:${objectId}:${language}`,

View File

@@ -3,7 +3,6 @@ import { mergeAchievements } from "./merge-achievements";
import fs, { readdirSync } from "node:fs";
import {
findAchievementFileInExecutableDirectory,
findAchievementFileInSteamPath,
findAchievementFiles,
findAllAchievementFiles,
getAlternativeObjectIds,
@@ -11,7 +10,6 @@ import {
import type {
AchievementFile,
Game,
GameShop,
UnlockedAchievement,
UserPreferences,
} from "@types";
@@ -20,7 +18,7 @@ import { Cracker } from "@shared";
import { publishCombinedNewAchievementNotification } from "../notifications";
import { db, gamesSublevel, levelKeys } from "@main/level";
import { WindowManager } from "../window-manager";
import { setTimeout } from "node:timers/promises";
import { sleep } from "@main/helpers";
const fileStats: Map<string, number> = new Map();
const fltFiles: Map<string, Set<string>> = new Map();
@@ -39,19 +37,15 @@ const watchAchievementsWindows = async () => {
const gameAchievementFiles: AchievementFile[] = [];
for (const objectId of getAlternativeObjectIds(game.objectId)) {
gameAchievementFiles.push(...(achievementFiles.get(objectId) ?? []));
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
gameAchievementFiles.push(
...findAchievementFileInExecutableDirectory(game)
);
gameAchievementFiles.push(
...(await findAchievementFileInSteamPath(game))
);
}
for (const file of gameAchievementFiles) {
await compareFile(game, file);
compareFile(game, file);
}
}
};
@@ -66,11 +60,13 @@ const watchAchievementsWithWine = async () => {
for (const game of games) {
const gameAchievementFiles = findAchievementFiles(game);
const achievementFileInsideDirectory =
findAchievementFileInExecutableDirectory(game);
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
gameAchievementFiles.push(...achievementFileInsideDirectory);
for (const file of gameAchievementFiles) {
await compareFile(game, file);
compareFile(game, file);
}
}
};
@@ -131,11 +127,6 @@ const compareFile = (game: Game, file: AchievementFile) => {
);
return processAchievementFileDiff(game, file);
} catch (err) {
achievementsLogger.error(
"Error reading file",
file.filePath,
err instanceof Error ? err.message : err
);
fileStats.set(file.filePath, -1);
return;
}
@@ -145,66 +136,20 @@ const processAchievementFileDiff = async (
game: Game,
file: AchievementFile
) => {
const parsedAchievements = parseAchievementFile(file.filePath, file.type);
const unlockedAchievements = parseAchievementFile(file.filePath, file.type);
if (parsedAchievements.length) {
return mergeAchievements(game, parsedAchievements, true);
if (unlockedAchievements.length) {
return mergeAchievements(game, unlockedAchievements, true);
}
return 0;
};
export class AchievementWatcherManager {
private static _hasFinishedPreSearch = false;
public static get hasFinishedPreSearch() {
return this._hasFinishedPreSearch;
}
public static readonly alreadySyncedGames: Map<string, boolean> = new Map();
public static async firstSyncWithRemoteIfNeeded(
shop: GameShop,
objectId: string
) {
const gameKey = levelKeys.game(shop, objectId);
if (this.alreadySyncedGames.get(gameKey)) return;
this.alreadySyncedGames.set(gameKey, true);
const game = await gamesSublevel.get(gameKey).catch(() => null);
if (!game || game.isDeleted) return;
const gameAchievementFiles = findAchievementFiles(game);
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
const unlockedAchievements: UnlockedAchievement[] = [];
for (const achievementFile of gameAchievementFiles) {
const localAchievementFile = parseAchievementFile(
achievementFile.filePath,
achievementFile.type
);
if (localAchievementFile.length) {
unlockedAchievements.push(...localAchievementFile);
}
}
const newAchievements = await mergeAchievements(
game,
unlockedAchievements,
false
);
if (newAchievements > 0) {
this.notifyCombinedAchievementsUnlocked(1, newAchievements);
}
}
private static hasFinishedMergingWithRemote = false;
public static watchAchievements() {
if (!this.hasFinishedPreSearch) return;
if (!this.hasFinishedMergingWithRemote) return;
if (process.platform === "win32") {
return watchAchievementsWindows();
@@ -243,11 +188,7 @@ export class AchievementWatcherManager {
}
}
if (unlockedAchievements.length) {
return mergeAchievements(game, unlockedAchievements, false);
}
return 0;
return mergeAchievements(game, unlockedAchievements, false);
}
private static async getGameAchievementFilesWindows() {
@@ -259,7 +200,7 @@ export class AchievementWatcherManager {
const gameAchievementFilesMap = findAllAchievementFiles();
return Promise.all(
games.map(async (game) => {
games.map((game) => {
const achievementFiles: AchievementFile[] = [];
for (const objectId of getAlternativeObjectIds(game.objectId)) {
@@ -270,10 +211,6 @@ export class AchievementWatcherManager {
achievementFiles.push(
...findAchievementFileInExecutableDirectory(game)
);
achievementFiles.push(
...(await findAchievementFileInSteamPath(game))
);
}
return { game, achievementFiles };
@@ -288,54 +225,37 @@ export class AchievementWatcherManager {
.then((games) => games.filter((game) => !game.isDeleted));
return Promise.all(
games.map(async (game) => {
games.map((game) => {
const achievementFiles = findAchievementFiles(game);
const achievementFileInsideDirectory =
findAchievementFileInExecutableDirectory(game);
achievementFiles.push(...(await findAchievementFileInSteamPath(game)));
achievementFiles.push(...achievementFileInsideDirectory);
return { game, achievementFiles };
})
);
}
private static async notifyCombinedAchievementsUnlocked(
totalNewGamesWithAchievements: number,
totalNewAchievements: number
) {
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
WindowManager.notificationWindow?.webContents.send(
"on-combined-achievements-unlocked",
totalNewGamesWithAchievements,
totalNewAchievements,
userPreferences.achievementCustomNotificationPosition ?? "top-left"
);
} else {
publishCombinedNewAchievementNotification(
totalNewAchievements,
totalNewGamesWithAchievements
);
}
}
public static async preSearchAchievements() {
await sleep(2000);
try {
const gameAchievementFiles =
process.platform === "win32"
? await this.getGameAchievementFilesWindows()
: await this.getGameAchievementFilesLinux();
const newAchievementsCount = await Promise.all(
gameAchievementFiles.map(({ game, achievementFiles }) => {
return this.preProcessGameAchievementFiles(game, achievementFiles);
})
);
const newAchievementsCount: number[] = [];
for (const { game, achievementFiles } of gameAchievementFiles) {
const result = await this.preProcessGameAchievementFiles(
game,
achievementFiles
);
newAchievementsCount.push(result);
}
const totalNewGamesWithAchievements = newAchievementsCount.filter(
(achievements) => achievements
@@ -347,16 +267,34 @@ export class AchievementWatcherManager {
);
if (totalNewAchievements > 0) {
await setTimeout(4000);
this.notifyCombinedAchievementsUnlocked(
totalNewGamesWithAchievements,
totalNewAchievements
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (userPreferences.achievementNotificationsEnabled !== false) {
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
WindowManager.notificationWindow?.webContents.send(
"on-combined-achievements-unlocked",
totalNewGamesWithAchievements,
totalNewAchievements,
userPreferences.achievementCustomNotificationPosition ??
"top-left"
);
} else {
publishCombinedNewAchievementNotification(
totalNewAchievements,
totalNewGamesWithAchievements
);
}
}
}
} catch (err) {
achievementsLogger.error("Error on preSearchAchievements", err);
}
this._hasFinishedPreSearch = true;
this.hasFinishedMergingWithRemote = true;
}
}

View File

@@ -1,11 +1,9 @@
import path from "node:path";
import fs from "node:fs";
import type { Game, AchievementFile, UserPreferences } from "@types";
import type { Game, AchievementFile } from "@types";
import { Cracker } from "@shared";
import { achievementsLogger } from "../logger";
import { SystemPath } from "../system-path";
import { getSteamLocation, getSteamUsersIds } from "../steam";
import { db, levelKeys } from "@main/level";
const getAppDataPath = () => {
if (process.platform === "win32") {
@@ -272,55 +270,6 @@ export const findAchievementFiles = (game: Game) => {
}
}
const achievementFileInsideDirectory =
findAchievementFileInExecutableDirectory(game);
return achievementFiles.concat(achievementFileInsideDirectory);
};
const steamUserIds = await getSteamUsersIds();
const steamPath = await getSteamLocation().catch(() => null);
export const findAchievementFileInSteamPath = async (game: Game) => {
if (!steamUserIds.length) {
return [];
}
if (!steamPath) {
return [];
}
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (!userPreferences?.enableSteamAchievements) {
return [];
}
const achievementFiles: AchievementFile[] = [];
for (const steamUserId of steamUserIds) {
const gameAchievementPath = path.join(
steamPath,
"userdata",
steamUserId.toString(),
"config",
"librarycache",
`${game.objectId}.json`
);
if (fs.existsSync(gameAchievementPath)) {
achievementFiles.push({
type: Cracker.Steam,
filePath: gameAchievementPath,
});
}
}
return achievementFiles;
};
@@ -354,7 +303,7 @@ export const findAchievementFileInExecutableDirectory = (
"achievements.ini"
),
},
].filter((file) => fs.existsSync(file.filePath)) as AchievementFile[];
];
};
const mapFileLocationWithObjectId = (

View File

@@ -1,39 +1,24 @@
import { HydraApi } from "../hydra-api";
import type { GameAchievement, GameShop, SteamAchievement } from "@types";
import type { GameShop, SteamAchievement } from "@types";
import { UserNotLoggedInError } from "@shared";
import { logger } from "../logger";
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
import { AxiosError } from "axios";
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60; // 1 hour
const getModifiedSinceHeader = (
cachedAchievements: GameAchievement | undefined
): Date | undefined => {
if (!cachedAchievements) {
return undefined;
}
return cachedAchievements.updatedAt
? new Date(cachedAchievements.updatedAt)
: undefined;
};
export const getGameAchievementData = async (
objectId: string,
shop: GameShop,
useCachedData: boolean
) => {
const gameKey = levelKeys.game(shop, objectId);
const cachedAchievements = await gameAchievementsSublevel.get(
levelKeys.game(shop, objectId)
);
const cachedAchievements = await gameAchievementsSublevel.get(gameKey);
if (cachedAchievements?.achievements && useCachedData)
if (cachedAchievements && useCachedData)
return cachedAchievements.achievements;
if (
cachedAchievements?.achievements &&
Date.now() < (cachedAchievements.updatedAt ?? 0) + LOCAL_CACHE_EXPIRATION
cachedAchievements &&
Date.now() < (cachedAchievements.cacheExpiresTimestamp ?? 0)
) {
return cachedAchievements.achievements;
}
@@ -44,22 +29,18 @@ export const getGameAchievementData = async (
})
.then((language) => language || "en");
return HydraApi.get<SteamAchievement[]>(
"/games/achievements",
{
shop,
objectId,
language,
},
{
ifModifiedSince: getModifiedSinceHeader(cachedAchievements),
}
)
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
shop,
objectId,
language,
})
.then(async (achievements) => {
await gameAchievementsSublevel.put(gameKey, {
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
achievements,
updatedAt: Date.now() + LOCAL_CACHE_EXPIRATION,
cacheExpiresTimestamp: achievements.length
? Date.now() + 1000 * 60 * 30 // 30 minutes
: undefined,
});
return achievements;
@@ -69,14 +50,8 @@ export const getGameAchievementData = async (
throw err;
}
const isNotModified = (err as AxiosError)?.response?.status === 304;
if (isNotModified) {
return cachedAchievements?.achievements ?? [];
}
logger.error("Failed to get game achievements for", objectId, err);
return cachedAchievements?.achievements ?? [];
return [];
});
};

View File

@@ -14,7 +14,6 @@ import { SubscriptionRequiredError } from "@shared";
import { achievementsLogger } from "../logger";
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
import { getGameAchievementData } from "./get-game-achievement-data";
import { AchievementWatcherManager } from "./achievement-watcher-manager";
const isRareAchievement = (points: number) => {
const rawPercentage = (50 - Math.sqrt(points)) * 2;
@@ -36,7 +35,7 @@ const saveAchievementsOnLocal = async (
await gameAchievementsSublevel.put(levelKey, {
achievements: gameAchievement?.achievements ?? [],
unlockedAchievements: unlockedAchievements,
updatedAt: gameAchievement?.updatedAt,
cacheExpiresTimestamp: gameAchievement?.cacheExpiresTimestamp,
});
if (!sendUpdateEvent) return;
@@ -57,9 +56,9 @@ export const mergeAchievements = async (
achievements: UnlockedAchievement[],
publishNotification: boolean
) => {
const gameKey = levelKeys.game(game.shop, game.objectId);
let localGameAchievement = await gameAchievementsSublevel.get(gameKey);
let localGameAchievement = await gameAchievementsSublevel.get(
levelKeys.game(game.shop, game.objectId)
);
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
@@ -68,8 +67,10 @@ export const mergeAchievements = async (
);
if (!localGameAchievement) {
await getGameAchievementData(game.objectId, game.shop, false);
localGameAchievement = await gameAchievementsSublevel.get(gameKey);
await getGameAchievementData(game.objectId, game.shop, true);
localGameAchievement = await gameAchievementsSublevel.get(
levelKeys.game(game.shop, game.objectId)
);
}
const achievementsData = localGameAchievement?.achievements ?? [];
@@ -135,12 +136,6 @@ export const mergeAchievements = async (
};
});
achievementsLogger.log(
"Publishing achievement notification",
game.objectId,
game.title
);
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
WindowManager.notificationWindow?.webContents.send(
"on-achievement-unlocked",
@@ -158,11 +153,7 @@ export const mergeAchievements = async (
}
}
const shouldSyncWithRemote =
game.remoteId &&
(newAchievements.length || AchievementWatcherManager.hasFinishedPreSearch);
if (shouldSyncWithRemote) {
if (game.remoteId) {
await HydraApi.put<UpdatedUnlockedAchievements | undefined>(
"/profile/games/achievements",
{
@@ -203,11 +194,8 @@ export const mergeAchievements = async (
mergedLocalAchievements,
publishNotification
);
})
.finally(() => {
AchievementWatcherManager.alreadySyncedGames.set(gameKey, true);
});
} else if (newAchievements.length) {
} else {
await saveAchievementsOnLocal(
game.objectId,
game.shop,

View File

@@ -75,11 +75,6 @@ export const parseAchievementFile = (
return processRazor1911(filePath);
}
if (type === Cracker.Steam) {
const parsed = jsonParse(filePath);
return processSteamCacheAchievement(parsed);
}
achievementsLogger.log(
`Unprocessed ${type} achievements found on ${filePath}`
);
@@ -239,35 +234,6 @@ const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
return newUnlockedAchievements;
};
const processSteamCacheAchievement = (
unlockedAchievements: any[]
): UnlockedAchievement[] => {
const newUnlockedAchievements: UnlockedAchievement[] = [];
const achievementIndex = unlockedAchievements.findIndex(
(element) => element[0] === "achievements"
);
if (achievementIndex === -1) {
achievementsLogger.info("No achievements found in Steam cache file");
return [];
}
const unlockedAchievementsData =
unlockedAchievements[achievementIndex][1]["data"]["vecHighlight"];
for (const achievement of unlockedAchievementsData) {
if (achievement.bAchieved) {
newUnlockedAchievements.push({
name: achievement.strID,
unlockTime: achievement.rtUnlocked * 1000,
});
}
}
return newUnlockedAchievements;
};
const process3DM = (unlockedAchievements: any): UnlockedAchievement[] => {
const newUnlockedAchievements: UnlockedAchievement[] = [];

View File

@@ -0,0 +1,31 @@
import {
findAchievementFiles,
findAchievementFileInExecutableDirectory,
} from "./find-achivement-files";
import { parseAchievementFile } from "./parse-achievement-file";
import { mergeAchievements } from "./merge-achievements";
import type { Game, UnlockedAchievement } from "@types";
export const updateLocalUnlockedAchievements = async (game: Game) => {
const gameAchievementFiles = findAchievementFiles(game);
const achievementFileInsideDirectory =
findAchievementFileInExecutableDirectory(game);
gameAchievementFiles.push(...achievementFileInsideDirectory);
const unlockedAchievements: UnlockedAchievement[] = [];
for (const achievementFile of gameAchievementFiles) {
const localAchievementFile = parseAchievementFile(
achievementFile.filePath,
achievementFile.type
);
if (localAchievementFile.length) {
unlockedAchievements.push(...localAchievementFile);
}
}
mergeAchievements(game, unlockedAchievements, false);
};

View File

@@ -31,8 +31,8 @@ export interface ProcessPayload {
exe: string | null;
pid: number;
name: string;
environ?: Record<string, string> | null;
cwd?: string | null;
environ: Record<string, string> | null;
cwd: string | null;
}
export interface PauseSeedingPayload {

View File

@@ -16,7 +16,6 @@ import { WSClient } from "./ws/ws-client";
interface HydraApiOptions {
needsAuth?: boolean;
needsSubscription?: boolean;
ifModifiedSince?: Date;
}
interface HydraApiUserAuth {
@@ -338,13 +337,8 @@ export class HydraApi {
) {
await this.validateOptions(options);
const headers = {
...this.getAxiosConfig().headers,
"Hydra-If-Modified-Since": options?.ifModifiedSince?.toUTCString(),
};
return this.instance
.get<T>(url, { params, ...this.getAxiosConfig(), headers })
.get<T>(url, { params, ...this.getAxiosConfig() })
.then((response) => response.data)
.catch(this.handleUnauthorizedError);
}

View File

@@ -13,8 +13,9 @@ export const mergeWithRemoteGames = async () => {
return HydraApi.get<ProfileGame[]>("/profile/games")
.then(async (response) => {
for (const game of response) {
const gameKey = levelKeys.game(game.shop, game.objectId);
const localGame = await gamesSublevel.get(gameKey);
const localGame = await gamesSublevel.get(
levelKeys.game(game.shop, game.objectId)
);
if (localGame) {
const updatedLastTimePlayed =
@@ -29,7 +30,7 @@ export const mergeWithRemoteGames = async () => {
? game.playTimeInMilliseconds
: localGame.playTimeInMilliseconds;
await gamesSublevel.put(gameKey, {
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...localGame,
remoteId: game.id,
lastTimePlayed: updatedLastTimePlayed,
@@ -37,7 +38,7 @@ export const mergeWithRemoteGames = async () => {
favorite: game.isFavorite ?? localGame.favorite,
});
} else {
await gamesSublevel.put(gameKey, {
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
objectId: game.objectId,
title: game.title,
remoteId: game.id,
@@ -50,17 +51,20 @@ export const mergeWithRemoteGames = async () => {
});
}
await gamesShopAssetsSublevel.put(gameKey, {
shop: game.shop,
objectId: game.objectId,
title: game.title,
coverImageUrl: game.coverImageUrl,
libraryHeroImageUrl: game.libraryHeroImageUrl,
libraryImageUrl: game.libraryImageUrl,
logoImageUrl: game.logoImageUrl,
iconUrl: game.iconUrl,
logoPosition: game.logoPosition,
});
await gamesShopAssetsSublevel.put(
levelKeys.game(game.shop, game.objectId),
{
shop: game.shop,
objectId: game.objectId,
title: game.title,
coverImageUrl: game.coverImageUrl,
libraryHeroImageUrl: game.libraryHeroImageUrl,
libraryImageUrl: game.libraryImageUrl,
logoImageUrl: game.logoImageUrl,
iconUrl: game.iconUrl,
logoPosition: game.logoPosition,
}
);
}
})
.catch(() => {});

View File

@@ -34,7 +34,9 @@ export const uploadGamesBatch = async () => {
await mergeWithRemoteGames();
AchievementWatcherManager.preSearchAchievements();
if (HydraApi.isLoggedIn()) {
AchievementWatcherManager.preSearchAchievements();
}
if (WindowManager.mainWindow)
WindowManager.mainWindow.webContents.send("on-library-batch-complete");

View File

@@ -8,8 +8,6 @@ import { gamesSublevel, levelKeys } from "@main/level";
import { CloudSync } from "./cloud-sync";
import { logger } from "./logger";
import path from "path";
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
import { MAIN_LOOP_INTERVAL } from "@main/constants";
export const gamesPlaytime = new Map<
string,
@@ -26,7 +24,7 @@ interface GameExecutables {
[key: string]: ExecutableInfo[];
}
const TICKS_TO_UPDATE_API = (3 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 3 minutes
const TICKS_TO_UPDATE_API = 80;
let currentTick = 1;
const platform = process.platform;
@@ -192,11 +190,6 @@ export const watchProcesses = async () => {
function onOpenGame(game: Game) {
const now = performance.now();
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
game.shop,
game.objectId
);
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
lastTick: now,
firstTick: now,

View File

@@ -17,18 +17,30 @@ export interface SteamAppDetailsResponse {
};
}
export const getSteamLocation = async () => {
export const getSteamLocation = async (): Promise<string> => {
const home = SystemPath.getPath("home");
if (process.platform === "linux") {
return path.join(SystemPath.getPath("home"), ".local", "share", "Steam");
const candidates = [
path.join(home, ".local", "share", "Steam"),
path.join(home, ".steam", "steam"),
path.join(home, ".steam", "root"),
];
for (const candidate of candidates) {
try {
fs.accessSync(candidate, fs.constants.F_OK);
return candidate;
} catch {
continue;
}
}
throw new Error("Steam installation not found on Linux");
}
if (process.platform === "darwin") {
return path.join(
SystemPath.getPath("home"),
"Library",
"Application Support",
"Steam"
);
return path.join(home, "Library", "Application Support", "Steam");
}
const regKey = new WinReg({
@@ -39,11 +51,7 @@ export const getSteamLocation = async () => {
return new Promise<string>((resolve, reject) => {
regKey.get("SteamPath", (err, value) => {
if (err) {
reject(err);
}
if (!value) {
reject(new Error("SteamPath not found in registry"));
return reject(err);
}
resolve(value.value);
@@ -82,15 +90,7 @@ export const getSteamAppDetails = async (
};
export const getSteamUsersIds = async () => {
const userDataPath = await getSteamLocation().catch(() => null);
if (!userDataPath) {
return [];
}
if (!fs.existsSync(userDataPath)) {
return [];
}
const userDataPath = await getSteamLocation();
const userIds = fs.readdirSync(path.join(userDataPath, "userdata"), {
withFileTypes: true,

View File

@@ -370,11 +370,14 @@ export class WindowManager {
},
});
this.notificationWindow.setIgnoreMouseEvents(true);
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
// visibleOnFullScreen: true,
// });
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
this.loadNotificationWindowURL();
if (!app.isPackaged || isStaging) {
if (isStaging) {
this.notificationWindow.webContents.openDevTools();
}
}
@@ -461,7 +464,7 @@ export class WindowManager {
editorWindow.once("ready-to-show", () => {
editorWindow.show();
this.mainWindow?.webContents.openDevTools();
if (!app.isPackaged || isStaging) {
if (isStaging) {
editorWindow.webContents.openDevTools();
}
});
@@ -582,7 +585,7 @@ export class WindowManager {
tray.popUpContextMenu(contextMenu);
};
tray.setToolTip("Hydra Launcher");
tray.setToolTip("Hydra");
if (process.platform !== "darwin") {
await updateSystemTray();

View File

@@ -187,6 +187,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("deleteGameFolder", shop, objectId),
getGameByObjectId: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
syncGameByObjectId: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("syncGameByObjectId", shop, objectId),
resetGameAchievements: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
extractGameDownload: (shop: GameShop, objectId: string) =>

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hydra Launcher</title>
<title>Hydra</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline' * data: local:;"

View File

@@ -74,6 +74,6 @@
}
&__error-label {
color: globals.$error-color;
color: globals.$danger-color;
}
}

View File

@@ -182,6 +182,11 @@ export function GameDetailsContextProvider({
})
.catch(() => {});
}
window.electron.syncGameByObjectId(shop, objectId).then(() => {
if (abortController.signal.aborted) return;
updateGame();
});
}, [
updateGame,
dispatch,

View File

@@ -155,6 +155,7 @@ declare global {
shop: GameShop,
objectId: string
) => Promise<LibraryGame | null>;
syncGameByObjectId: (shop: GameShop, objectId: string) => Promise<void>;
onGamesRunning: (
cb: (
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]

View File

@@ -12,6 +12,7 @@ import type {
UpdateProfileRequest,
UserDetails,
} from "@types";
import * as Sentry from "@sentry/react";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
export function useUserDetails() {
@@ -28,6 +29,8 @@ export function useUserDetails() {
} = useAppSelector((state) => state.userDetails);
const clearUserDetails = useCallback(async () => {
Sentry.setUser(null);
dispatch(setUserDetails(null));
dispatch(setProfileBackground(null));
@@ -42,6 +45,12 @@ export function useUserDetails() {
const updateUserDetails = useCallback(
async (userDetails: UserDetails) => {
Sentry.setUser({
id: userDetails.id,
username: userDetails.username,
email: userDetails.email ?? undefined,
});
dispatch(setUserDetails(userDetails));
window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
},

View File

@@ -21,6 +21,7 @@ import resources from "@locales";
import { logger } from "./logger";
import { addCookieInterceptor } from "./cookies";
import * as Sentry from "@sentry/react";
import Catalogue from "./pages/catalogue/catalogue";
import Home from "./pages/home/home";
import Downloads from "./pages/downloads/downloads";
@@ -31,6 +32,18 @@ import Achievements from "./pages/achievements/achievements";
import ThemeEditor from "./pages/theme-editor/theme-editor";
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
Sentry.init({
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
release: await window.electron.getVersion(),
});
console.log = logger.log;
const isStaging = await window.electron.isStaging();

View File

@@ -158,8 +158,8 @@ $logo-max-width: 200px;
&-points {
display: flex;
align-items: center;
justify-content: end;
gap: 4px;
margin-right: 4px;
font-weight: 600;
&--locked {

View File

@@ -55,6 +55,7 @@ export function AchievementNotification() {
isHidden: false,
isRare: false,
isPlatinum: false,
points: 0,
iconUrl: "https://cdn.losbroxas.org/favicon.svg",
},
]);

View File

@@ -16,25 +16,6 @@
&:hover {
background-color: rgba(255, 255, 255, 0.05);
.game-item__plus-wrapper {
opacity: 1;
pointer-events: auto;
}
}
&__plus-wrapper {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
cursor: pointer;
}
&__plus-wrapper--added {
opacity: 0.5;
}
&__cover {

View File

@@ -1,14 +1,13 @@
import { Badge } from "@renderer/components";
import { buildGameDetailsPath } from "@renderer/helpers";
import { useAppSelector, useRepacks, useLibrary } from "@renderer/hooks";
import { useMemo, useState, useEffect } from "react";
import { useAppSelector, useRepacks } from "@renderer/hooks";
import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
import "./game-item.scss";
import { useTranslation } from "react-i18next";
import { CatalogueSearchResult } from "@types";
import { QuestionIcon, PlusIcon, CheckIcon } from "@primer/octicons-react";
import cn from "classnames";
import { QuestionIcon } from "@primer/octicons-react";
export interface GameItemProps {
game: CatalogueSearchResult;
@@ -17,9 +16,7 @@ export interface GameItemProps {
export function GameItem({ game }: GameItemProps) {
const navigate = useNavigate();
const { i18n, t } = useTranslation("game_details");
const language = i18n.language.split("-")[0];
const { i18n } = useTranslation();
const { steamGenres } = useAppSelector((state) => state.catalogueSearch);
@@ -27,41 +24,7 @@ export function GameItem({ game }: GameItemProps) {
const repacks = getRepacksForObjectId(game.objectId);
const [isAddingToLibrary, setIsAddingToLibrary] = useState(false);
const [added, setAdded] = useState(false);
const { library, updateLibrary } = useLibrary();
useEffect(() => {
const exists = library.some(
(libItem) =>
libItem.shop === game.shop && libItem.objectId === game.objectId
);
setAdded(exists);
}, [library, game.shop, game.objectId]);
const addGameToLibrary = async (
event: React.MouseEvent | React.KeyboardEvent
) => {
event.stopPropagation();
if (added || isAddingToLibrary) return;
setIsAddingToLibrary(true);
try {
await window.electron.addGameToLibrary(
game.shop,
game.objectId,
game.title
);
updateLibrary();
} catch (error) {
console.error(error);
} finally {
setIsAddingToLibrary(false);
}
};
const language = i18n.language.split("-")[0];
const uniqueRepackers = useMemo(() => {
return Array.from(new Set(repacks.map((repack) => repack.repacker)));
@@ -73,11 +36,7 @@ export function GameItem({ game }: GameItemProps) {
(steamGenre) => steamGenre === genre
);
if (
index !== undefined &&
steamGenres[language] &&
steamGenres[language][index]
) {
if (index && steamGenres[language] && steamGenres[language][index]) {
return steamGenres[language][index];
}
@@ -122,23 +81,6 @@ export function GameItem({ game }: GameItemProps) {
))}
</div>
</div>
<div
className={cn("game-item__plus-wrapper", {
"game-item__plus-wrapper--added": added,
})}
role="button"
tabIndex={0}
onClick={addGameToLibrary}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
addGameToLibrary(e);
}
}}
title={added ? t("already_in_library") : t("add_to_library")}
>
{added ? <CheckIcon size={16} /> : <PlusIcon size={16} />}
</div>
</button>
);
}

View File

@@ -9,16 +9,5 @@
opacity: 1;
cursor: pointer;
}
&--with-tooltip {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
}
&--tooltip {
cursor: pointer;
}
}
}

View File

@@ -5,7 +5,6 @@ import { CheckboxField } from "@renderer/components";
import { useAppSelector } from "@renderer/hooks";
import { settingsContext } from "@renderer/context";
import "./settings-behavior.scss";
import { QuestionIcon } from "@primer/octicons-react";
export function SettingsBehavior() {
const userPreferences = useAppSelector(
@@ -26,7 +25,6 @@ export function SettingsBehavior() {
showHiddenAchievementsDescription: false,
showDownloadSpeedInMegabytes: false,
extractFilesByDefault: true,
enableSteamAchievements: false,
});
const { t } = useTranslation("settings");
@@ -47,8 +45,6 @@ export function SettingsBehavior() {
showDownloadSpeedInMegabytes:
userPreferences.showDownloadSpeedInMegabytes ?? false,
extractFilesByDefault: userPreferences.extractFilesByDefault ?? true,
enableSteamAchievements:
userPreferences.enableSteamAchievements ?? false,
});
}
}, [userPreferences]);
@@ -168,25 +164,6 @@ export function SettingsBehavior() {
})
}
/>
<div className={`settings-behavior__checkbox-container--with-tooltip`}>
<CheckboxField
label={t("enable_steam_achievements")}
checked={form.enableSteamAchievements}
onChange={() =>
handleChange({
enableSteamAchievements: !form.enableSteamAchievements,
})
}
/>
<small
className="settings-behavior__checkbox-container--tooltip"
data-open-article="steam-achievements"
>
<QuestionIcon size={12} />
</small>
</div>
</>
);
}

View File

@@ -51,14 +51,6 @@ export const UserFriendModalAddFriend = ({
}
};
const validateFriendCode = (callback: () => void) => {
if (friendCode.length === 8) {
return callback();
}
showErrorToast(t("friend_code_length_error"));
};
const handleCancelFriendRequest = (userId: string) => {
updateFriendRequestState(userId, "CANCEL").catch(() => {
showErrorToast(t("try_again"));
@@ -99,13 +91,13 @@ export const UserFriendModalAddFriend = ({
disabled={isAddingFriend}
className="user-friend-modal-add-friend__button"
type="button"
onClick={() => validateFriendCode(handleClickAddFriend)}
onClick={handleClickAddFriend}
>
{isAddingFriend ? t("sending") : t("add")}
</Button>
<Button
onClick={() => validateFriendCode(handleClickSeeProfile)}
onClick={handleClickSeeProfile}
disabled={isAddingFriend}
className="user-friend-modal-add-friend__button"
type="button"

View File

@@ -7,7 +7,6 @@ $body-color: #8e919b;
$border-color: rgba(255, 255, 255, 0.15);
$success-color: #1c9749;
$danger-color: #801d1e;
$error-color: #e11d48;
$warning-color: #ffc107;
$brand-teal: #16b195;

View File

@@ -35,7 +35,6 @@ export enum Cracker {
onlineFix = "OnlineFix",
goldberg = "Goldberg",
userstats = "user_stats",
Steam = "Steam",
rld = "RLD!",
empress = "EMPRESS",
skidrow = "SKIDROW",

View File

@@ -112,6 +112,8 @@ export interface UserFriend {
id: string;
displayName: string;
profileImageUrl: string | null;
createdAt: string;
updatedAt: string;
currentGame:
| (ShopAssets & {
sessionDurationInSeconds: number;
@@ -144,6 +146,8 @@ export interface UserRelation {
AId: string;
BId: string;
status: "ACCEPTED" | "PENDING";
createdAt: string;
updatedAt: string;
}
export type UserProfileCurrentGame = GameRunning &
@@ -322,11 +326,17 @@ export interface CatalogueSearchPayload {
export type CatalogueSearchResult = {
id: string;
objectId: string;
title: string;
shop: GameShop;
tags: string[];
genres: string[];
} & Pick<ShopAssets, "libraryImageUrl">;
objectId: string;
shop: GameShop;
createdAt: Date;
updatedAt: Date;
title: string;
installCount: number;
achievementCount: number;
shopData: string;
} & ShopAssets;
export type LibraryGame = Game &
Partial<ShopAssets> & {

View File

@@ -68,7 +68,7 @@ export interface Download {
export interface GameAchievement {
achievements: SteamAchievement[];
unlockedAchievements: UnlockedAchievement[];
updatedAt: number | undefined;
cacheExpiresTimestamp: number | undefined;
}
export type AchievementCustomNotificationPosition =
@@ -101,7 +101,6 @@ export interface UserPreferences {
friendStartGameNotificationsEnabled?: boolean;
showDownloadSpeedInMegabytes?: boolean;
extractFilesByDefault?: boolean;
enableSteamAchievements?: boolean;
}
export interface ScreenState {

422
yarn.lock
View File

@@ -625,6 +625,15 @@
"@babel/highlight" "^7.24.2"
picocolors "^1.0.0"
"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
dependencies:
"@babel/helper-validator-identifier" "^7.25.9"
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/code-frame@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
@@ -639,11 +648,37 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz"
integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==
"@babel/compat-data@^7.25.9":
version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02"
integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==
"@babel/compat-data@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.2.tgz#4183f9e642fd84e74e3eea7ffa93a412e3b102c9"
integrity sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==
"@babel/core@^7.18.5":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40"
integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.26.0"
"@babel/generator" "^7.26.0"
"@babel/helper-compilation-targets" "^7.25.9"
"@babel/helper-module-transforms" "^7.26.0"
"@babel/helpers" "^7.26.0"
"@babel/parser" "^7.26.0"
"@babel/template" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.26.0"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
"@babel/core@^7.21.3", "@babel/core@^7.23.5":
version "7.24.5"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz"
@@ -696,6 +731,17 @@
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^2.5.1"
"@babel/generator@^7.26.0", "@babel/generator@^7.26.3":
version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019"
integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==
dependencies:
"@babel/parser" "^7.26.3"
"@babel/types" "^7.26.3"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
"@babel/generator@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.1.tgz#862d4fad858f7208edd487c28b58144036b76230"
@@ -718,6 +764,17 @@
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-compilation-targets@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875"
integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==
dependencies:
"@babel/compat-data" "^7.25.9"
"@babel/helper-validator-option" "^7.25.9"
browserslist "^4.24.0"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-compilation-targets@^7.27.1":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d"
@@ -756,6 +813,14 @@
dependencies:
"@babel/types" "^7.24.0"
"@babel/helper-module-imports@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715"
integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==
dependencies:
"@babel/traverse" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/helper-module-imports@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204"
@@ -775,6 +840,15 @@
"@babel/helper-split-export-declaration" "^7.24.5"
"@babel/helper-validator-identifier" "^7.24.5"
"@babel/helper-module-transforms@^7.26.0":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae"
integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==
dependencies:
"@babel/helper-module-imports" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/traverse" "^7.25.9"
"@babel/helper-module-transforms@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz#e1663b8b71d2de948da5c4fb2a20ca4f3ec27a6f"
@@ -813,6 +887,11 @@
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz"
integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-string-parser@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
@@ -823,6 +902,11 @@
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz"
integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-identifier@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
@@ -833,6 +917,11 @@
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz"
integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==
"@babel/helper-validator-option@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==
"@babel/helper-validator-option@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
@@ -847,6 +936,14 @@
"@babel/traverse" "^7.24.5"
"@babel/types" "^7.24.5"
"@babel/helpers@^7.26.0":
version "7.26.0"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4"
integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==
dependencies:
"@babel/template" "^7.25.9"
"@babel/types" "^7.26.0"
"@babel/helpers@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.1.tgz#ffc27013038607cdba3288e692c3611c06a18aa4"
@@ -870,6 +967,13 @@
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz"
integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==
"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3":
version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234"
integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==
dependencies:
"@babel/types" "^7.26.3"
"@babel/parser@^7.27.1", "@babel/parser@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.2.tgz#577518bedb17a2ce4212afd052e01f7df0941127"
@@ -921,6 +1025,15 @@
"@babel/parser" "^7.24.0"
"@babel/types" "^7.24.0"
"@babel/template@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016"
integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==
dependencies:
"@babel/code-frame" "^7.25.9"
"@babel/parser" "^7.25.9"
"@babel/types" "^7.25.9"
"@babel/template@^7.27.1":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
@@ -946,6 +1059,19 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/traverse@^7.25.9":
version "7.26.4"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd"
integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==
dependencies:
"@babel/code-frame" "^7.26.2"
"@babel/generator" "^7.26.3"
"@babel/parser" "^7.26.3"
"@babel/template" "^7.25.9"
"@babel/types" "^7.26.3"
debug "^4.3.1"
globals "^11.1.0"
"@babel/traverse@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.1.tgz#4db772902b133bbddd1c4f7a7ee47761c1b9f291"
@@ -968,6 +1094,14 @@
"@babel/helper-validator-identifier" "^7.24.5"
to-fast-properties "^2.0.0"
"@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3":
version "7.26.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@babel/types@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560"
@@ -1688,7 +1822,7 @@
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/sourcemap-codec@^1.5.0":
"@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
@@ -2147,6 +2281,142 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz#54e3562ebd264ef5839f8091618310c40d43d8a9"
integrity sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==
"@sentry-internal/browser-utils@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.47.0.tgz#39f2766a1bbdffc2d211e2f61f8ed8c258245b3d"
integrity sha512-vOXzYzHTKkahTLDzWWIA4EiVCQ+Gk+7xGWUlNcR2ZiEPBqYZVb5MjsUozAcc7syrSUy6WicyFjcomZ3rlCVQhg==
dependencies:
"@sentry/core" "8.47.0"
"@sentry-internal/feedback@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.47.0.tgz#22bceac03b61ab8509e79c0875fb140f214b7c4f"
integrity sha512-IAiIemTQIalxAOYhUENs9bZ8pMNgJnX3uQSuY7v0gknEqClOGpGkG04X/cxCmtJUj1acZ9ShTGDxoh55a+ggAQ==
dependencies:
"@sentry/core" "8.47.0"
"@sentry-internal/replay-canvas@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.47.0.tgz#5bbd04c81235b2bf627aa216185ae1993d2102c4"
integrity sha512-M4W9UGouEeELbGbP3QsXLDVtGiQSZoWJlKwqMWyqdQgZuLoKw0S33+60t6teLVMhuQZR0UI9VJTF5coiXysnnA==
dependencies:
"@sentry-internal/replay" "8.47.0"
"@sentry/core" "8.47.0"
"@sentry-internal/replay@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.47.0.tgz#4f7bd359df2de25d919a378295cab67dfa05a406"
integrity sha512-G/S40ZBORj0HSMLw/uVC6YDEPN/dqVk901vf4VYfml686DEhJrZesfAfp5SydJumQ0NKZQrdtvny+BWnlI5H1w==
dependencies:
"@sentry-internal/browser-utils" "8.47.0"
"@sentry/core" "8.47.0"
"@sentry/babel-plugin-component-annotate@2.22.7":
version "2.22.7"
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd"
integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ==
"@sentry/browser@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.47.0.tgz#fe0b6b65c0394f54438d6704039adeaec214ce18"
integrity sha512-K6BzHisykmbFy/wORtGyfsAlw7ShevLALzu3ReZZZ18dVubO1bjSNjkZQU9MJD5Jcb9oLwkq89n3N9XIBfvdRA==
dependencies:
"@sentry-internal/browser-utils" "8.47.0"
"@sentry-internal/feedback" "8.47.0"
"@sentry-internal/replay" "8.47.0"
"@sentry-internal/replay-canvas" "8.47.0"
"@sentry/core" "8.47.0"
"@sentry/bundler-plugin-core@2.22.7":
version "2.22.7"
resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.7.tgz#28204a224cd1fef58d157e5beeb2493947a9bc35"
integrity sha512-ouQh5sqcB8vsJ8yTTe0rf+iaUkwmeUlGNFi35IkCFUQlWJ22qS6OfvNjOqFI19e6eGUXks0c/2ieFC4+9wJ+1g==
dependencies:
"@babel/core" "^7.18.5"
"@sentry/babel-plugin-component-annotate" "2.22.7"
"@sentry/cli" "2.39.1"
dotenv "^16.3.1"
find-up "^5.0.0"
glob "^9.3.2"
magic-string "0.30.8"
unplugin "1.0.1"
"@sentry/cli-darwin@2.39.1":
version "2.39.1"
resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz#75c338a53834b4cf72f57599f4c72ffb36cf0781"
integrity sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ==
"@sentry/cli-linux-arm64@2.39.1":
version "2.39.1"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.39.1.tgz#27db44700c33fcb1e8966257020b43f8494373e6"
integrity sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw==
"@sentry/cli-linux-arm@2.39.1":
version "2.39.1"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.39.1.tgz#451683fa9a5a60b1359d104ec71334ed16f4b63c"
integrity sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ==
"@sentry/cli-linux-i686@2.39.1":
version "2.39.1"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.39.1.tgz#9965a81f97a94e8b6d1d15589e43fee158e35201"
integrity sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg==
"@sentry/cli-linux-x64@2.39.1":
version "2.39.1"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.39.1.tgz#31fe008b02f92769543dc9919e2a5cbc4cda7889"
integrity sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA==
"@sentry/cli-win32-i686@2.39.1":
version "2.39.1"
resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.39.1.tgz#609e8790c49414011445e397130560c777850b35"
integrity sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q==
"@sentry/cli-win32-x64@2.39.1":
version "2.39.1"
resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.39.1.tgz#1a874a5570c6d162b35d9d001c96e5389d07d2cb"
integrity sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw==
"@sentry/cli@2.39.1":
version "2.39.1"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.39.1.tgz#916bb5b7567ccf7fdf94ef6cf8a2b9ab78370d29"
integrity sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ==
dependencies:
https-proxy-agent "^5.0.0"
node-fetch "^2.6.7"
progress "^2.0.3"
proxy-from-env "^1.1.0"
which "^2.0.2"
optionalDependencies:
"@sentry/cli-darwin" "2.39.1"
"@sentry/cli-linux-arm" "2.39.1"
"@sentry/cli-linux-arm64" "2.39.1"
"@sentry/cli-linux-i686" "2.39.1"
"@sentry/cli-linux-x64" "2.39.1"
"@sentry/cli-win32-i686" "2.39.1"
"@sentry/cli-win32-x64" "2.39.1"
"@sentry/core@8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.47.0.tgz#e811444552f7a91b5de573875a318a6cd4e802f8"
integrity sha512-iSEJZMe3DOcqBFZQAqgA3NB2lCWBc4Gv5x/SCri/TVg96wAlss4VrUunSI2Mp0J4jJ5nJcJ2ChqHSBAU48k3FA==
"@sentry/react@^8.47.0":
version "8.47.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.47.0.tgz#0d9c120b0d4a2efd6d8e8fb9acff332c63afd50d"
integrity sha512-SRk2Up+qBTow4rQGiRXViC2i4M5w/tae5w8I/rmX+IxFoPyh8wXERcLAj/8xbbRm8aR+A4i5gNgfFtrYsyFJFA==
dependencies:
"@sentry/browser" "8.47.0"
"@sentry/core" "8.47.0"
hoist-non-react-statics "^3.3.2"
"@sentry/vite-plugin@^2.22.7":
version "2.22.7"
resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.22.7.tgz#9b63452d1d8cd02e6ba6234395a611ae7656c67a"
integrity sha512-sYRNiNm4toQGq2BfZSJPdw36em3eQaLu+3NTFpA7Hl4g3Sp2Rt3CYObnW5bxlFEruRhxzvdyB383N9OefVZ6KA==
dependencies:
"@sentry/bundler-plugin-core" "2.22.7"
unplugin "1.0.1"
"@sindresorhus/is@^4.0.0":
version "4.6.0"
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz"
@@ -3285,6 +3555,11 @@ acorn@^8.11.0, acorn@^8.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
acorn@^8.8.1:
version "8.14.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
acorn@^8.9.0:
version "8.11.3"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
@@ -3378,6 +3653,14 @@ ansi-styles@^6.1.0:
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
app-builder-bin@5.0.0-alpha.12:
version "5.0.0-alpha.12"
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz#2daf82f8badc698e0adcc95ba36af4ff0650dc80"
@@ -3673,6 +3956,11 @@ bep53-range@^2.0.0:
resolved "https://registry.yarnpkg.com/bep53-range/-/bep53-range-2.0.0.tgz#a1770475661b4b814c4359e4b66f7cbd88de2b10"
integrity sha512-sMm2sV5PRs0YOVk0LTKtjuIprVzxgTQUsrGX/7Yph2Rm4FO2Fqqtq7hNjsOB5xezM4v4+5rljCgK++UeQJZguA==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
bl@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
@@ -3707,7 +3995,7 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@^3.0.3:
braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
@@ -3935,6 +4223,21 @@ chalk@^5.3.0:
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
@@ -4487,6 +4790,11 @@ dotenv-expand@^11.0.6:
dependencies:
dotenv "^16.4.4"
dotenv@^16.3.1:
version "16.4.7"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26"
integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==
dotenv@^16.4.4, dotenv@^16.4.5:
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
@@ -5538,7 +5846,7 @@ git-raw-commits@^4.0.0:
meow "^12.0.1"
split2 "^4.0.0"
glob-parent@^5.1.2:
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -5587,6 +5895,16 @@ glob@^8.0.1, glob@^8.1.0:
minimatch "^5.0.1"
once "^1.3.0"
glob@^9.3.2:
version "9.3.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21"
integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==
dependencies:
fs.realpath "^1.0.0"
minimatch "^8.0.2"
minipass "^4.2.4"
path-scurry "^1.6.1"
global-agent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6"
@@ -5735,6 +6053,13 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hosted-git-info@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224"
@@ -6002,6 +6327,13 @@ is-bigint@^1.1.0:
dependencies:
has-bigints "^1.0.2"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
@@ -6097,7 +6429,7 @@ is-generator-function@^1.0.10:
dependencies:
has-tostringtag "^1.0.0"
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@@ -6720,6 +7052,13 @@ lru-cache@^7.7.1:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
magic-string@0.30.8:
version "0.30.8"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613"
integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
magic-string@^0.30.17:
version "0.30.17"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
@@ -6856,6 +7195,13 @@ minimatch@^5.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^8.0.2:
version "8.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229"
integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.3, minimatch@^9.0.4:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
@@ -6914,6 +7260,11 @@ minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6:
dependencies:
yallist "^4.0.0"
minipass@^4.2.4:
version "4.2.8"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a"
integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==
minipass@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
@@ -7036,6 +7387,13 @@ node-domexception@^1.0.0:
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^2.6.7:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
dependencies:
whatwg-url "^5.0.0"
node-fetch@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
@@ -7067,6 +7425,11 @@ nopt@^6.0.0:
dependencies:
abbrev "^1.0.0"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-url@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
@@ -7320,7 +7683,7 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-scurry@^1.11.1:
path-scurry@^1.11.1, path-scurry@^1.6.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
@@ -7358,7 +7721,7 @@ picocolors@^1.1.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.3.1:
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@@ -7527,7 +7890,7 @@ react-i18next@^14.1.0:
"@babel/runtime" "^7.23.9"
html-parse-stringify "^3.0.1"
react-is@^16.13.1:
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -7636,6 +7999,13 @@ readable-stream@^3.4.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
@@ -8656,6 +9026,11 @@ tr46@^5.0.0:
dependencies:
punycode "^2.3.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
truncate-utf8-bytes@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
@@ -8889,6 +9264,16 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unplugin@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.0.1.tgz#83b528b981cdcea1cad422a12cd02e695195ef3f"
integrity sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==
dependencies:
acorn "^8.8.1"
chokidar "^3.5.3"
webpack-sources "^3.2.3"
webpack-virtual-modules "^0.5.0"
untildify@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9"
@@ -9022,11 +9407,26 @@ web-streams-polyfill@^3.0.3:
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webidl-conversions@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack-virtual-modules@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c"
integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==
whatwg-encoding@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
@@ -9047,6 +9447,14 @@ whatwg-url@^14.0.0:
tr46 "^5.0.0"
webidl-conversions "^7.0.0"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"