diff --git a/WowUp-Unlimited/.github/workflows/build.yml b/WowUp-Unlimited/.github/workflows/build.yml new file mode 100644 index 0000000..34b996f --- /dev/null +++ b/WowUp-Unlimited/.github/workflows/build.yml @@ -0,0 +1,69 @@ +name: Build WowUp Unlimited +on: + workflow_dispatch: + inputs: + wowup_branch: + description: 'WowUp Branch' + default: 'master' + required: true + wowupcf_branch: + description: 'WowUp.CF Branch' + default: 'main' + required: true + release_name: + description: 'WowUp Unlimited Release' + default: 'vX.X.X' + required: true + +jobs: + build: + name: Build + runs-on: 'ubuntu-latest' + steps: + - name: Initialize git config + run: | + git config --global user.name "GitHub Actions" + git config --global user.email noreply@github.com + git config --global core.autocrlf false + git config --global core.eol lf + - name: Checkout WowUp-Unlimited (orphaned) + uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.WORKFLOW_PAT }} + - name: Checkout WowUp-Unlimited + uses: actions/checkout@v3 + with: + fetch-depth: 0 + path: 'WowUp-Unlimited' + - name: Checkout WowUp + uses: actions/checkout@v3 + with: + repository: 'WowUp/WowUp' + ref: ${{ inputs.wowup_branch }} + path: 'WowUp' + fetch-depth: 0 + - name: Create Release Branch + run: git switch --orphan "${{ inputs.release_name }}-Src" + - name: Patch WowUp Workflow + run: | + mkdir -p .github/workflows + cp WowUp/.github/workflows/electron-all-build.yml .github/workflows + + patch -p0 < ./WowUp-Unlimited/workflow-patches/01-WorkflowCheckouts.patch + patch -p0 < ./WowUp-Unlimited/workflow-patches/02-CurseForgeMerge.patch + patch -p0 < ./WowUp-Unlimited/workflow-patches/03-DefaultShell.patch + patch -p0 < ./WowUp-Unlimited/workflow-patches/04-Trigger.patch + patch -p0 < ./WowUp-Unlimited/workflow-patches/05-Env.patch + patch -p0 < ./WowUp-Unlimited/workflow-patches/06-WorkspaceLocation.patch + patch -p0 < ./WowUp-Unlimited/workflow-patches/07-RunMerge.patch + patch -p0 < ./WowUp-Unlimited/workflow-patches/08-DisableCodeSigning.patch + patch -p0 < ./WowUp-Unlimited/workflow-patches/09-WorkflowName.patch + + sed -i "s/WOWUP_BRANCH/${{ inputs.wowup_branch }}/g" .github/workflows/electron-all-build.yml + sed -i "s/WOWUPCF_BRANCH/${{ inputs.wowupcf_branch }}/g" .github/workflows/electron-all-build.yml + sed -i "s/RELEASE_NAME/${{ inputs.release_name }}/g" .github/workflows/electron-all-build.yml + + git add .github/workflows/electron-all-build.yml + git commit -m "Patched WowUp Build Workflow: WowUp ${{ inputs.wowup_branch }} WowUp.CF ${{ inputs.wowupcf_branch }}" + git push --force origin "${{ inputs.release_name }}-Src" \ No newline at end of file diff --git a/WowUp-Unlimited/merge.sh b/WowUp-Unlimited/merge.sh new file mode 100755 index 0000000..f0eb19f --- /dev/null +++ b/WowUp-Unlimited/merge.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +GITHUB_WORKSPACE="${GITHUB_WORKSPACE:=${SCRIPT_DIR}/../}" + +CURR_REPO_DIR="${GITHUB_WORKSPACE}/WowUp" +MERGE_REPO_DIR="${GITHUB_WORKSPACE}/WowUp.CF" + +while IFS= read -r f ; do + d=$(dirname "${f}") + if [ ! -d "${CURR_REPO_DIR}/${d}" ] ; then + mkdir -p "${CURR_REPO_DIR}/${d}" + fi + echo "${f}" + cp -af "${MERGE_REPO_DIR}/${f}" "${CURR_REPO_DIR}/${f}" +done < "${SCRIPT_DIR}/unlimited-patches/orphans.lst" + +find "${SCRIPT_DIR}/unlimited-patches/" -name "*.patch" -type f -print | sort -n | +while IFS= read -r f ; do + echo "$(basename ${f})" + patch -p0 -d "${CURR_REPO_DIR}" < "${f}" +done diff --git a/WowUp-Unlimited/unlimited-patches/01-CurseForgeMerge.patch b/WowUp-Unlimited/unlimited-patches/01-CurseForgeMerge.patch new file mode 100644 index 0000000..dfff89f --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/01-CurseForgeMerge.patch @@ -0,0 +1,2661 @@ +--- wowup-electron/src/environments/environment.ts ++++ wowup-electron/src/environments/environment.ts +@@ -13,16 +13,17 @@ + wago: { + termsUrl: "https://addons.wago.io/agreements/terms-of-service", + dataConsentUrl: "https://addons.wago.io/agreements/wowup-data-consent", + }, + curseforge: { + httpTimeoutMs: 60000, ++ apiKey: "{{CURSEFORGE_API_KEY}}", + }, + autoUpdateIntervalMs: 3600000, // 1 hour + appUpdateIntervalMs: 3600000, // 1 hour + defaultHttpTimeoutMs: 10000, + defaultHttpResetTimeoutMs: 30000, + wowUpHubHttpTimeoutMs: 10000, + wagoHttpTimeoutMs: 10000, + newsRefreshIntervalMs: 3600000, // 1 hour + featuredAddonsCacheTimeSec: 30, // 30 sec + }; +--- wowup-electron/src/environments/environment.prod.ts ++++ wowup-electron/src/environments/environment.prod.ts +@@ -13,16 +13,17 @@ + wago: { + termsUrl: "https://addons.wago.io/agreements/terms-of-service", + dataConsentUrl: "https://addons.wago.io/agreements/wowup-data-consent", + }, + curseforge: { + httpTimeoutMs: 60000, ++ apiKey: "{{CURSEFORGE_API_KEY}}", + }, + autoUpdateIntervalMs: 3600000, // 1 hour + appUpdateIntervalMs: 3600000, // 1 hour + defaultHttpTimeoutMs: 10000, + defaultHttpResetTimeoutMs: 30000, + wowUpHubHttpTimeoutMs: 10000, + wagoHttpTimeoutMs: 10000, + newsRefreshIntervalMs: 3600000, // 1 hour + featuredAddonsCacheTimeSec: 30, // 30 sec + }; +--- wowup-electron/src/environments/environment.dev.ts ++++ wowup-electron/src/environments/environment.dev.ts +@@ -18,16 +18,17 @@ + wago: { + termsUrl: "https://addons.wago.io/agreements/terms-of-service", + dataConsentUrl: "https://addons.wago.io/agreements/wowup-data-consent", + }, + curseforge: { + httpTimeoutMs: 60000, ++ apiKey: "{{CURSEFORGE_API_KEY}}", + }, + autoUpdateIntervalMs: 3600000, // 1 hour + appUpdateIntervalMs: 3600000, // 1 hour + defaultHttpTimeoutMs: 10000, + defaultHttpResetTimeoutMs: 30000, + wowUpHubHttpTimeoutMs: 10000, + wagoHttpTimeoutMs: 10000, + newsRefreshIntervalMs: 3600000, // 1 hour + featuredAddonsCacheTimeSec: 30, // 30 sec + }; +--- wowup-electron/src/common/wowup.d.ts ++++ wowup-electron/src/common/wowup.d.ts +@@ -36,12 +36,13 @@ + | "clipboard-read-text" + | "close-window" + | "copy-file" + | "create-app-menu" + | "create-directory" + | "create-tray-menu" ++ | "curse-get-scan-results" + | "decode-product-db" + | "delete-directory" + | "focus-window" + | "get-app-version" + | "get-asset-file-path" + | "get-directory-tree" +@@ -59,12 +60,14 @@ + | "list-directories" + | "list-disks-win32" + | "list-entries" + | "list-files" + | "maximize-window" + | "minimize-window" ++ | "ow-is-cmp-required" ++ | "ow-open-cmp" + | "path-exists" + | "push-init" + | "push-register" + | "push-subscribe" + | "push-unregister" + | "quit-app" +--- wowup-electron/src/common/constants.ts ++++ wowup-electron/src/common/constants.ts +@@ -14,19 +14,21 @@ + export const ADDON_PROVIDER_HUB = "WowUpHub"; + export const ADDON_PROVIDER_WAGO = "Wago"; + export const ADDON_PROVIDER_WOWUP_COMPANION = "WowUpCompanion"; + export const ADDON_PROVIDER_ZIP = "Zip"; + + export const APP_PROTOCOL_NAME = "wowup"; ++export const CURSE_PROTOCOL_NAME = "curseforge"; + + // WOWUP ADDON + export const WOWUP_ADDON_FOLDER_NAME = "WowUp"; + export const WOWUP_DATA_ADDON_FOLDER_NAME = "wowup_data_addon"; + export const WOWUP_ASSET_FOLDER_NAME = "WowUpAddon"; + + // IPC CHANNELS ++export const IPC_CURSE_GET_SCAN_RESULTS = "curse-get-scan-results"; + export const IPC_DOWNLOAD_FILE_CHANNEL = "download-file"; + export const IPC_COPY_DIRECTORY_CHANNEL = "copy-directory"; + export const IPC_CREATE_DIRECTORY_CHANNEL = "create-directory"; + export const IPC_DELETE_DIRECTORY_CHANNEL = "delete-directory"; + export const IPC_STAT_DIRECTORY_CHANNEL = "stat-directory"; + export const IPC_LIST_DIRECTORIES_CHANNEL = "list-directories"; +@@ -42,12 +44,14 @@ + export const IPC_WOWUP_GET_SCAN_RESULTS = "wowup-get-scan-results"; + export const IPC_GET_HOME_DIR = "get-home-dir"; + export const IPC_GET_ASSET_FILE_PATH = "get-asset-file-path"; + export const IPC_CREATE_TRAY_MENU_CHANNEL = "create-tray-menu"; + export const IPC_LIST_DISKS_WIN32 = "list-disks-win32"; + export const IPC_CREATE_APP_MENU_CHANNEL = "create-app-menu"; ++export const IPC_OW_IS_CMP_REQUIRED = "ow-is-cmp-required"; ++export const IPC_OW_OPEN_CMP = "ow-open-cmp"; + export const IPC_MENU_ZOOM_OUT_CHANNEL = "menu-zoom-out"; + export const IPC_MENU_ZOOM_IN_CHANNEL = "menu-zoom-in"; + export const IPC_MENU_ZOOM_RESET_CHANNEL = "menu-zoom-reset"; + export const IPC_MAXIMIZE_WINDOW = "maximize-window"; + export const IPC_WINDOW_IS_MAXIMIZED = "window-is-maximized"; + export const IPC_MINIMIZE_WINDOW = "minimize-window"; +--- wowup-electron/src/assets/i18n/zh-TW.json ++++ wowup-electron/src/assets/i18n/zh-TW.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "重置", + "VERSION_MISMATCH": "版本號不匹配" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "為什麼會看見廣告?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "顯示此廣告是為了支援 wago.io 上辛勤工作、開發出優秀插件的作者們。如果不想看到此廣告,可以在「選項」中禁用 Wago.io 安裝源。", ++ "MESSAGE": "顯示此廣告是為了支援 wago.io / CurseForge 上辛勤工作、開發出優秀插件的作者們。如果不想看到此廣告,可以在「選項」中禁用 Wago.io / CurseForge 安裝源。", + "TITLE": "為什麼會看見廣告?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "從 URL 安裝" + }, + "NEW_VERSION_POPUP": { + "TITLE": "更新記錄 {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "在開始使用 WowUp 之前,請設定此應用程式的許可權。", + "POSITIVE_BUTTON": "確認", + "TELEMETRY": { + "DESCRIPTION": "是否傳送匿名的插件統計資料和錯誤報告以幫助改進 WowUp?", + "TOGGLE_LABEL": "允許遙測" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "設定應用程式更新通道", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "我明白了", + "APP_RELEASE_CHANNEL_DESCRIPTION": "切換此應用程式的穩定版和 Beta 通道", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "更新通道", + "APP_RELEASE_CHANNEL_LABEL": "應用程式更新通道", + "CURRENT_LANGUAGE_LABEL": "當前語言", ++ "CURSE_PROTOCOL_DESCRIPTION": "從 CurseForge 網站下載插件時,WowUp 將會接管安裝過程", ++ "CURSE_PROTOCOL_LABEL": "接管 CurseForge 下載連結", + "ENABLE_APP_BADGE_DESCRIPTION": "在應用程式圖示上用角標顯示可更新的插件數量。", + "ENABLE_APP_BADGE_LABEL": "啟用數字角標通知", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "啟用各種系統通知彈窗,如自動更新插件通知。", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "啟用系統通知", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "開啟插件詳情頁時,自動切換到上一次開啟的選項卡", + "KEEP_LAST_OPENED_TAB_LABEL": "記住上一次開啟的選項卡", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "啟用硬體加速", + "USE_SYMLINK_SUPPORT": "啟用符號連結", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "啟用符號連結使得 WowUp 在重新掃描時識別符號連結。警告:如果您不知道什麼是符號連結,請禁用此項。在當前版本中,符號連結將會在更新插件時被刪除,並被替換為真正的資料夾。", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "啟用符號連結?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "允許 WowUp 掃描插件所在路徑下的符號連結。警告:符號連結在安裝或更新插件時會被替換。" + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "顯示配置檔案", + "CONFIG_FILES_DESCRIPTION": "開啟儲存 addons.json、preferences.json 等配置檔案的資料夾。", + "CONFIG_FILES_LABEL": "配置檔案", + "DEBUG_DATA_BUTTON": "轉儲除錯資料", + "DEBUG_DATA_DESCRIPTION": "記錄除錯資料以幫助診斷潛在的問題。除錯資料可以在最新的日誌檔案中找到。", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "關於", + "ADDONS": "插件", + "APPLICATION": "應用程式", + "CLIENTS": "客戶端", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "除錯", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "新增", + "AUTO_UPDATE_DESCRIPTION": "新安裝的插件將預設設定為自動更新", +--- wowup-electron/src/assets/i18n/zh.json ++++ wowup-electron/src/assets/i18n/zh.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "重置", + "VERSION_MISMATCH": "版本号不匹配" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "为什么会看见广告?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "显示此广告是为了支持 wago.io 上辛勤工作、开发出优秀插件的作者们。如果不想看到此广告,可以在 “选项” 中禁用 Wago.io 安装源。", ++ "MESSAGE": "显示此广告是为了支持 wago.io / CurseForge 上辛勤工作、开发出优秀插件的作者们。如果不想看到此广告,可以在 “选项” 中禁用 Wago.io / CurseForge 安装源。", + "TITLE": "为什么会看见广告?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "从 URL 安装" + }, + "NEW_VERSION_POPUP": { + "TITLE": "更新记录 {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "在开始使用 WowUp 之前,请设置此应用程序的权限。", + "POSITIVE_BUTTON": "确认", + "TELEMETRY": { + "DESCRIPTION": "是否发送匿名的插件统计数据和错误报告以帮助改进 WowUp?", + "TOGGLE_LABEL": "允许遥测" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "设置应用程序更新通道", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "我明白了", + "APP_RELEASE_CHANNEL_DESCRIPTION": "切换此应用程序的稳定版和 Beta 通道", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "更新通道", + "APP_RELEASE_CHANNEL_LABEL": "应用程序更新通道", + "CURRENT_LANGUAGE_LABEL": "当前语言", ++ "CURSE_PROTOCOL_DESCRIPTION": "从 CurseForge 网站下载插件时,WowUp 将会接管安装过程", ++ "CURSE_PROTOCOL_LABEL": "接管 CurseForge 下载链接", + "ENABLE_APP_BADGE_DESCRIPTION": "在应用程序图标上用角标显示可更新的插件数量。", + "ENABLE_APP_BADGE_LABEL": "启用数字角标通知", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "启用各种系统通知弹窗,如自动更新插件通知。", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "启用系统通知", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "打开插件详情页时,自动切换到上一次打开的选项卡", + "KEEP_LAST_OPENED_TAB_LABEL": "记住上一次打开的选项卡", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "启用硬件加速", + "USE_SYMLINK_SUPPORT": "启用符号链接", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "启用符号链接使得 WowUp 在重新扫描时识别符号链接。警告:如果您不知道什么是符号链接,请禁用此项。在当前版本中,符号链接将会在更新插件时被删除,并被替换为真正的文件夹。", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "启用符号链接?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "允许 WowUp 扫描插件所在路径下的符号链接。警告:符号链接在安装或更新插件时会被替换。" + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "显示配置文件", + "CONFIG_FILES_DESCRIPTION": "打开存储 addons.json、preferences.json 等配置文件的文件夹。", + "CONFIG_FILES_LABEL": "配置文件", + "DEBUG_DATA_BUTTON": "转储调试数据", + "DEBUG_DATA_DESCRIPTION": "记录调试数据以帮助诊断潜在的问题。调试数据可以在最新的日志文件中找到。", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "关于", + "ADDONS": "插件", + "APPLICATION": "应用程序", + "CLIENTS": "客户端", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "调试", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "新增", + "AUTO_UPDATE_DESCRIPTION": "新安装的插件将默认设置为自动更新", +--- wowup-electron/src/assets/i18n/ru.json ++++ wowup-electron/src/assets/i18n/ru.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Очистить", + "VERSION_MISMATCH": "Не совпадают версии" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Почему я вижу эту рекламу?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "Чтобы использовать wago.io в виде источника модификаций и для поддержки их авторов за тяжёлый труд над Вашими любимыми модификациями, мы должны показывать эту рекламу.\n\nЕсли Вы не хотите видеть эту рекламу, то всегда можете отключить wago.io как источник модификаций во вкладке настроек.", ++ "MESSAGE": "Чтобы использовать wago.io / CurseForge в виде источника модификаций и для поддержки их авторов за тяжёлый труд над Вашими любимыми модификациями, мы должны показывать эту рекламу.\n\nЕсли Вы не хотите видеть эту рекламу, то всегда можете отключить wago.io / CurseForge как источник модификаций во вкладке настроек.", + "TITLE": "Почему я вижу эту рекламу?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "Ссылка на установку модификации" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Список изменений в версии {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "поставщики рекламы", ++ "DESCRIPTION_BOTTOM": ". Нажмите на кнопку 'Управление', чтобы контролировать свои согласия или возражать против обработки Ваших данных. Вы можете изменить свои предпочтения в любое время через экран настроек.", ++ "DESCRIPTION_TOP": " Для того, чтобы использовать интеграцию этого приложения с CurseForge, они требуют, чтобы Вы разрешили им показывать Вам рекламу от одного из своих приложений ", ++ "MANAGE_BUTTON": "Управление", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Прежде чем мы начнем, нам нужно настроить несколько разрешений для приложения.", + "POSITIVE_BUTTON": "Подтвердить", + "TELEMETRY": { + "DESCRIPTION": "Хотите помочь улучшить WowUp, анонимно отправляя данные об установке и ошибках?", + "TOGGLE_LABEL": "Разрешить телеметрию" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Установка канала выпуска приложения", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Да, я понимаю", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Переключение между бета-версией и стабильной версией приложения", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Канал", + "APP_RELEASE_CHANNEL_LABEL": "Канал выпуска приложения", + "CURRENT_LANGUAGE_LABEL": "Текущий язык", ++ "CURSE_PROTOCOL_DESCRIPTION": "При загрузке модификаций с сайта CurseForge, WowUp будет брать установку на себя.", ++ "CURSE_PROTOCOL_LABEL": "Обрабатывать ссылки на установку с CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Показывать количество доступных обновлений на значке приложения.", + "ENABLE_APP_BADGE_LABEL": "Включить уведомления на значке приложения", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Включить различные окна системных уведомлений, такие как автоматически обновлённые модификации.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Включить системные уведомления", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "При открытии страницы сведений о модификации автоматически выбирает последнюю открытую вкладку.", + "KEEP_LAST_OPENED_TAB_LABEL": "Сохранять последнюю открытую вкладку", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Включить аппаратное ускорение", + "USE_SYMLINK_SUPPORT": "Включить поддержку символических ссылок", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Включение поддержки символических ссылок позволит WowUp распознавать их при повторном сканировании. Предупреждение: Если Вы не знаете, что такое символическая ссылка, то, скорее всего, Вам это не нужно. При обновлении символические ссылки в настоящее время будут заменены фактической папкой, и ссылка будет потеряна.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Включить поддержку символических ссылок?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Разрешает WowUp сканировать папки символических ссылок в папке Вашей модификации. Предупреждение: они будут заменены при обновлении/установке." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "Просмотр и управление тем, как рекламодатели CurseForge могут использовать Ваши данные для персонализации рекламы", ++ "ADS_OPTION_LABEL": "Персонализация рекламы и данные", ++ "ADS_OPTION_MANAGE": "Управление", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Показать файлы настроек", + "CONFIG_FILES_DESCRIPTION": "Открыть папку, в которой хранятся Ваши файлы addons.json и preferences.json.", + "CONFIG_FILES_LABEL": "Файлы настроек", + "DEBUG_DATA_BUTTON": "Дамп отладочных данных", + "DEBUG_DATA_DESCRIPTION": "Записывать отладочные данные, чтобы помочь в диагностике потенциальных проблем. Его можно найти в последнем лог-файле, если необходимо.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "О приложении", + "ADDONS": "Модификации", + "APPLICATION": "Приложение", + "CLIENTS": "Клиенты", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Отладка", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Добавить новый", + "AUTO_UPDATE_DESCRIPTION": "Новые установленные модификации будут автоматически обновляться по умолчанию", +--- wowup-electron/src/assets/i18n/pt.json ++++ wowup-electron/src/assets/i18n/pt.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Redefinir", + "VERSION_MISMATCH": "Versões não correspondem" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Por que estou vendo este anúncio?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "Para poder usar wago.io como provedor de addon e apoiar seus autores pelo trabalho duro em seus addons favoritos nós precisamos mostrar este anúncio.\n\nSe você não quer mais ver esse anúncio, você pode sempre desabilitar wago.io como provedor na guia de opções.", ++ "MESSAGE": "Para poder usar wago.io / CurseForge como provedor de addon e apoiar seus autores pelo trabalho duro em seus addons favoritos nós precisamos mostrar este anúncio.\n\nSe você não quer mais ver esse anúncio, você pode sempre desabilitar wago.io / CurseForge como provedor na guia de opções.", + "TITLE": "Por que estou vendo este anúncio?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "Instalar Addon pela URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Notas do Patch {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Antes de começarmos nós precisamos configurar algumas permissões para o app.", + "POSITIVE_BUTTON": "Confirmar", + "TELEMETRY": { + "DESCRIPTION": "Ajudar a melhorar o WowUp enviando dados e/ou erros anônimos de instalação do app?", + "TOGGLE_LABEL": "Permitir Telemetria" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Configurar canal de release do app", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Sim, eu entendo", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Alternar entre releases Beta e Estável para a aplicação", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Canal", + "APP_RELEASE_CHANNEL_LABEL": "Canal de release da aplicação", + "CURRENT_LANGUAGE_LABEL": "Idioma Atual", ++ "CURSE_PROTOCOL_DESCRIPTION": "Quando baixando addons do site CurseForge, WowUp ira cuidar da instalação", ++ "CURSE_PROTOCOL_LABEL": "Gerenciar links de download CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Mostrar um selo no icone da aplicação com o número de atualizações de addon disponíveis.", + "ENABLE_APP_BADGE_LABEL": "Habilitar selo de notificação do app", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Habilitar várias notificações e popups do sistema, como os de aviso de Addons atualizados automaticamente.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Habilitar notificações do sistema", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Habilitar Aceleração de Hardware", + "USE_SYMLINK_SUPPORT": "Habilitar suporte a Symlink", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Habilitando o suporte à symlink irá permitir ao WowUp reconhecer symlinks quando fazer um re-escaneamento. Aviso: Se você não sabe o que é um symlink, você não precisa disso. Quando atualizar os symlinks serão atualmente substituídos com uma pasta e o link será perdido.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Habilitar suporte a symlink?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Permitir ao WowUp escanear pastas symlink na sua pasta de Addons. Aviso: elas serão substituidas quando atualizar/instalar." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Esvaziar log de depuração de dados", + "DEBUG_DATA_DESCRIPTION": "Registra os dados de depuração e ajuda a diagnosticar problemas potenciais. Apenas por o curiosidade, isso pode ser encontrado em seu último arquivo de registro.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "Sobre", + "ADDONS": "Addons", + "APPLICATION": "Aplicação", + "CLIENTS": "Clientes", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Adicionar Novo", + "AUTO_UPDATE_DESCRIPTION": "Addons recém-instalados serão definidos para atualizar automáticamente por padrão", +--- wowup-electron/src/assets/i18n/pl.json ++++ wowup-electron/src/assets/i18n/pl.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Ten addon jest już zainstalowany, ale wersja nie zgadza się" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Czemu widzę tę reklamę?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "Aby używać wago.io jako dostawcę addonu i pomagać autorom za ich ciężką pracę nad twoimi ulubionimi addonami, jesteśmy zmuszeni pokazywać tę reklamę.\n\nJeśli nie chcesz widzieć tej reklamy, zawsze możesz zablokować Wago jako dostawcę w zakładce opcje.", ++ "MESSAGE": "Aby używać wago.io / CurseForge jako dostawcę addonu i pomagać autorom za ich ciężką pracę nad twoimi ulubionimi addonami, jesteśmy zmuszeni pokazywać tę reklamę.\n\nJeśli nie chcesz widzieć tej reklamy, zawsze możesz zablokować Wago / CurseForge jako dostawcę w zakładce opcje.", + "TITLE": "Czemu widzę tę reklamę?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "Zainstaluj addon z URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Opis łatki {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Przed rozpoczęciem potrzebujemy ustawić klika uprawień dla aplikacji.", + "POSITIVE_BUTTON": "Potwierdź", + "TELEMETRY": { + "DESCRIPTION": "Pomóż ulepszyć WowUp poprzez wysyłanie anonimowych danych o instalacji aplikacji lub o ich błędach.", + "TOGGLE_LABEL": "Zezwalaj na telemetrię" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Ustawianie kanału wydania aplikacji", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Tak, rozumiem.", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Przełączanie pomiędzy wersjami Beta i Stabilną aplikacji", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Kanał", + "APP_RELEASE_CHANNEL_LABEL": "Kanał udostępniania aplikacji", + "CURRENT_LANGUAGE_LABEL": "Aktualny język", ++ "CURSE_PROTOCOL_DESCRIPTION": "Podczas pobierania dodatków ze strony CurseForge, WowUp zajmie się instalacją", ++ "CURSE_PROTOCOL_LABEL": "Obsługa pobranych linków CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Pokaż plakietkę na ikonie aplikacji z liczbą dodatków z dostępnymi aktualizacjami.", + "ENABLE_APP_BADGE_LABEL": "Włącz powiadamianie o odznakach aplikacji", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Włącz różne wyskakujące okienka powiadomień, np. o automatycznie aktualizowanych addonach.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Włącz powiadomienia systemowe", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "Podczas otwierania strony szczegółów addonu, automatycznie wybieraj ostatnio otwartą kartę", + "KEEP_LAST_OPENED_TAB_LABEL": "Zachowaj ostatnio otwartą kartę", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Włącz akcelerację sprzętową", + "USE_SYMLINK_SUPPORT": "Włącz obsługę Symlink", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Włączenie obsługi symlink pozwoli WowUp na rozpoznawanie symlinków podczas ponownego skanowania. Ostrzeżenie: Jeśli nie wiesz, co to jest symlink, nie potrzebujesz tego. Podczas aktualizacji symlink będzie obecnie zastępowany rzeczywistym folderem, a link zostanie utracony.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Włączyć obsługę symlink?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Pozwól WowUp na skanowanie folderów z symlinkami w folderze addonów. Ostrzeżenie: zostaną one zastąpione podczas aktualizacji/instalacji." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Pokaż pliki konfiguracyjne", + "CONFIG_FILES_DESCRIPTION": "Otwórz folder, w którym przechowywane są na przykład pliki addons.json oraz preferences.json.", + "CONFIG_FILES_LABEL": "Pliki konfiguracyjne", + "DEBUG_DATA_BUTTON": "Zrzut danych debugowania", + "DEBUG_DATA_DESCRIPTION": "Rejestruj dane debugowania, aby pomóc w diagnozowaniu potencjalnych problemów. Można je znaleźć w najnowszym pliku dziennika dla ciekawskich.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "Informacje", + "ADDONS": "Addony", + "APPLICATION": "Aplikacja", + "CLIENTS": "Klienty", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Debugowanie", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Dodaj nowy", + "AUTO_UPDATE_DESCRIPTION": "Wszystkie istniejące i nowo zainstalowane addony będą domyślnie ustawione na automatyczną aktualizację.", +--- wowup-electron/src/assets/i18n/nb.json ++++ wowup-electron/src/assets/i18n/nb.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Tilbakestill", + "VERSION_MISMATCH": "Versjonene samsvarer ikke" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Hvorfor ser jeg denne annonsen?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "For å kunne bruke wago.io som leverandør av utvidelser, og støtte forfatterene deres for deres harde arbeid med favorittutvidelsene dine, er vi nødt til å vise denne annonsen.\n\nHvis du ikke vil se denne annonsen kan du alltids deaktivere Wago som leverandør i fanen for alternativer.", ++ "MESSAGE": "For å kunne bruke wago.io / CurseForge som leverandør av utvidelser, og støtte forfatterene deres for deres harde arbeid med favorittutvidelsene dine, er vi nødt til å vise denne annonsen.\n\nHvis du ikke vil se denne annonsen kan du alltids deaktivere Wago / CurseForge som leverandør i fanen for alternativer.", + "TITLE": "Hvorfor ser jeg denne annonsen?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "Installer utvidelse fra URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Nytt i versjon {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Før vi kan komme igang må vi sette opp noen tillatelser for appen.", + "POSITIVE_BUTTON": "Bekreft", + "TELEMETRY": { + "DESCRIPTION": "Vil du bidra til å forbedre WowUp ved å sende anonyme installasjonsdata og/eller feil?", + "TOGGLE_LABEL": "Tillat telemetri" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Konfigurer apputgivelseskanalen", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Ja, jeg forstår", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Veksle mellom beta- og stabilutgivelsene av applikasjonen", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Kanal", + "APP_RELEASE_CHANNEL_LABEL": "Apputgivelseskanal", + "CURRENT_LANGUAGE_LABEL": "Nåværende språk", ++ "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", ++ "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Viser en teller på appikonet med antall tilgjengelige oppdateringer.", + "ENABLE_APP_BADGE_LABEL": "Aktiver notifikasjonsbadge for appen", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Aktiver ulike systemvarsler, som for eksempel for automatisk oppdaterte utvidelser.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Aktiver systemvarsler", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "Når du åpner en detaljside for en utvidelse, åpnes automatisk den sist åpnede fanen", + "KEEP_LAST_OPENED_TAB_LABEL": "Behold siste åpnede fane", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Aktiver maskinvareakselerasjon", + "USE_SYMLINK_SUPPORT": "Aktiver symlink-støtte", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Aktivering av symlink-støtte lar WowUp oppdage symlinker ved reskanning. Advarsel: Hvis du ikke vet hva en symlink er, trenger du ikke denne. For øyeblikket, når du oppdaterer, erstattes symlinker med en faktisk mappe, og koblingen går tapt.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Aktiver symlink-støtte?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Tillat WowUp å skanne symlink-mapper i utvidelsesmappen din. Advarsel: de vil bli erstattet ved oppdatering/installering." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Vis konfigurasjonsfiler", + "CONFIG_FILES_DESCRIPTION": "Åpne mappen der for eksempel addons.json og preferences.json er lagret.", + "CONFIG_FILES_LABEL": "Konfigurasjonsfiler", + "DEBUG_DATA_BUTTON": "Lagre debugdata", + "DEBUG_DATA_DESCRIPTION": "Logg debugdata for å hjelpe deg med å diagnostisere potensielle problemer. Hvis du er nyskjerrig kan du finne dette i den siste loggfilen.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "Om", + "ADDONS": "Utvidelser", + "APPLICATION": "Applikasjon", + "CLIENTS": "Klienter", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Legg til ny", + "AUTO_UPDATE_DESCRIPTION": "Alle eksisterende og nyinstallerte utvidelser vil bli satt til å oppdateres automatisk som standard", +--- wowup-electron/src/assets/i18n/ko.json ++++ wowup-electron/src/assets/i18n/ko.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "In order to use wago.io as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io as a provider in the options tab.", ++ "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "설치할 애드온 URL 주소" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Patch Notes {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Confirm", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "현재 언어", ++ "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", ++ "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Show a badge on the app icon with the number of addons with available updates.", + "ENABLE_APP_BADGE_LABEL": "Enable app badge notification", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "애드온 자동업데이트 등과 같은 다양한 시스템 알림 팝업 활성화 여부", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "시스템 알림 활성화", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "하드웨어 가속 활성화", + "USE_SYMLINK_SUPPORT": "Enable Symlink Support", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Enabling symlink support will allow WowUp to recognize symlinks when performing a re-scan. Warning: If you do not know what a symlink is, you do not need this. When updating symlinks will currently be replaced with an actual folder and the link lost.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Enable symlink support?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Allow WowUp to scan symlink folders in your addon folder. Warning: they will be replaced when updating/installing." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "디버그 데이터 덤프", + "DEBUG_DATA_DESCRIPTION": "잠재적인 문제를 진단하는데 도움을 주기 위해 디버그 데이터를 기록합니다. 궁금하시다면 최신 로그 파일에서 확인하실 수 있습니다.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "애드온", + "APPLICATION": "게임 애플리케이션", + "CLIENTS": "클라이언트", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "디버그", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Add New", + "AUTO_UPDATE_DESCRIPTION": "새로 설치되는 애드온의 자동업데이트 옵션을 활성화합니다.", +--- wowup-electron/src/assets/i18n/it.json ++++ wowup-electron/src/assets/i18n/it.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "In order to use wago.io as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io as a provider in the options tab.", ++ "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "Installa un Addon tramite URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Note della Patch {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Confirm", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "Lingua Corrente", ++ "CURSE_PROTOCOL_DESCRIPTION": "Quando si scaricano addons dal sito Web CurseForge, WowUp ne gestirà l'installazione", ++ "CURSE_PROTOCOL_LABEL": "Gestisci i link dei downloads da CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Mostra un indicatore di notifica sull'icona dell'applicazione con il numero di addons con un aggiornamento disponibile.", + "ENABLE_APP_BADGE_LABEL": "Abilita indicatore di notifica", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Abilita i vari popup di notifica del sistema, come gli addons aggiornati automaticamente.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Notifiche di Sistema", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Accelerazione Hardware", + "USE_SYMLINK_SUPPORT": "Abilita il Supporto Symlink", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "L'abilitazione del supporto symlink consentirà a WowUp di riconoscere i symlinks durante l'esecuzione di una nuova scansione. Attenzione: se non sai cos'è un symlink, puoi anche non procedere. Durante l'aggiornamento, i symlinks verranno sostituiti con una cartella reale e il collegamento viene perso.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Abilitare il supporto symlink?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Consenti a WowUp di scansionare le cartelle dei symlinks nella cartella dell'addon. Attenzione: verranno sostituiti durante l'aggiornamento/installazione." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Dump Dati Di Debug", + "DEBUG_DATA_DESCRIPTION": "Registra i dati di debug per aiutare a diagnosticare potenziali problemi. Questi dati possono essere trovati nei tuoi ultimi file di log.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "Addons", + "APPLICATION": "Applicazione", + "CLIENTS": "Clients", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Aggiungi Nuovo", + "AUTO_UPDATE_DESCRIPTION": "I nuovi addons installati saranno impostati per l'aggiornamento automatico ", +--- wowup-electron/src/assets/i18n/fr.json ++++ wowup-electron/src/assets/i18n/fr.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "In order to use wago.io as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io as a provider in the options tab.", ++ "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "URL de l'addon à installer" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Patch Notes {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Confirm", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "Langue actuelle", ++ "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", ++ "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Show a badge on the app icon with the number of addons with available updates.", + "ENABLE_APP_BADGE_LABEL": "Enable app badge notification", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Activer les fenêtres contextuelles système comme la mise à jour auto des addons.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Activer le système de notification", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "A l'ouverture de la fenêtre de détails d'un addon, selectionne automatiquement le dernier onglet utilisé", + "KEEP_LAST_OPENED_TAB_LABEL": "Garder le dernier onglet de détail ouvert", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Activer l'accélération matérielle", + "USE_SYMLINK_SUPPORT": "Enable Symlink Support", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Enabling symlink support will allow WowUp to recognize symlinks when performing a re-scan. Warning: If you do not know what a symlink is, you do not need this. When updating symlinks will currently be replaced with an actual folder and the link lost.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Enable symlink support?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Allow WowUp to scan symlink folders in your addon folder. Warning: they will be replaced when updating/installing." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Dump des données de débogage", + "DEBUG_DATA_DESCRIPTION": "Log les données de débogage pour aider à diagnostiquer les problèmes potentiels. Cela peut être trouvé dans votre dernier fichier journal pour les curieux.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "Addons", + "APPLICATION": "Application", + "CLIENTS": "Clients", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Add New", + "AUTO_UPDATE_DESCRIPTION": "Les addons nouvellement installés seront mises à jour automatiquement par défaut", +--- wowup-electron/src/assets/i18n/es.json ++++ wowup-electron/src/assets/i18n/es.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Reiniciar", + "VERSION_MISMATCH": "Las versiones no coinciden" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "¿Por qué estoy viendo este anuncio?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "Para poder usar wago.io como proveedor de addons y apoyar a sus autores por su duro trabajo, se nos requiere mostrar esta publicidad.\n\nSi no quiere ver estos anuncios, puede desactivar el proveedor wago.io en la pestaña de Opciones.", ++ "MESSAGE": "Para poder usar wago.io / CurseForge como proveedor de addons y apoyar a sus autores por su duro trabajo, se nos requiere mostrar esta publicidad.\n\nSi no quiere ver estos anuncios, puede desactivar el proveedor wago.io / CurseForge en la pestaña de Opciones.", + "TITLE": "¿Por qué estoy viendo este anuncio?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "Instalar addon desde URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Notas de la versión {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "distribuidores de anuncios", ++ "DESCRIPTION_BOTTOM": ". Haga clic en el botón Administrar para controlar sus consentimientos o para oponerse al procesado de sus datos. Puede cambiar sus preferencias en cualquier momento a través de la pantalla de configuración.", ++ "DESCRIPTION_TOP": " Para poder utilizar la integración con CurseForge de esta aplicación, es necesario su consentimiento para mostrarle publicidad de uno de sus ", ++ "MANAGE_BUTTON": "Administrar", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Antes de empezar, necesitamos configurar algunos permisos para la aplicación.", + "POSITIVE_BUTTON": "Confirmar", + "TELEMETRY": { + "DESCRIPTION": "¿Ayudar a mejorar WowUp enviando datos anónimos de instalación y/o error?", + "TOGGLE_LABEL": "Permitir telemetría" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Configuración del canal de publicación de WowUp", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Sí, lo entiendo", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Cambia entre las versiones Beta y Estable de la aplicación", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Canal", + "APP_RELEASE_CHANNEL_LABEL": "Canal de publicación de la aplicación", + "CURRENT_LANGUAGE_LABEL": "Idioma actual", ++ "CURSE_PROTOCOL_DESCRIPTION": "Al descargar addons desde la web de CurseForge, WowUp se hará cargo de la instalación", ++ "CURSE_PROTOCOL_LABEL": "Tramitar enlaces de descarga de CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Muestra un contador en el icono de la aplicación en la barra de tareas con el número de addons con actualizaciones disponibles.", + "ENABLE_APP_BADGE_LABEL": "Activar contador en el icono", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Activa varios mensajes de notificación del sistema, tales como cuando los addons son actualizados automáticamente.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Activar notificaciones del sistema", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "Al abrir los detalles de un addon, selecciona automáticamente la última pestaña abierta.", + "KEEP_LAST_OPENED_TAB_LABEL": "Recordar la última pestaña abierta", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Activar aceleración por hardware", + "USE_SYMLINK_SUPPORT": "Activar soporte de enlaces simbólicos (symlink)", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Activar el soporte de enlaces simbólicos permite a WowUp reconocerlos durante el reescaneo.\n\nAdvertencia: Si no sabe lo que es un enlace simbólico, no necesita activar esto. Al actualizar addons, los enlaces simbólicos serán reemplazados por copias de las carpetas y el enlace se perderá.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "¿Activar soporte de enlaces simbólicos?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Permite a WowUp escanear enlaces simbólicos en la carpeta de addons.\nAdvertecia: Los enlaces serán reemplazados al actualizar/instalar los addons." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "Ver y administrar la manera en que los anunciantes de CurseForge pueden usar sus datos para la personalización de la publicidad", ++ "ADS_OPTION_LABEL": "Personalización de publicidad y datos", ++ "ADS_OPTION_MANAGE": "Administrar", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Mostrar archivos de configuración", + "CONFIG_FILES_DESCRIPTION": "Abre la carpeta donde se encuentra almacenado addons.json y preferences.json por ejemplo.", + "CONFIG_FILES_LABEL": "Archivos de configuración", + "DEBUG_DATA_BUTTON": "Eliminar datos de depuración", + "DEBUG_DATA_DESCRIPTION": "Registra datos de depuración y ayuda a diagnosticar problemas potenciales. Puede curiosearlo abriendo el último archivo de registro.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "Acerca de", + "ADDONS": "Addons", + "APPLICATION": "Aplicación", + "CLIENTS": "Clientes", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Depuración", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Añadir nuevo", + "AUTO_UPDATE_DESCRIPTION": "Los addons instalados después de activar esta opción se configurarán para actualizarse automáticamente de forma predeterminada. No afecta a los addons instalados con anterioridad.", +--- wowup-electron/src/assets/i18n/en.json ++++ wowup-electron/src/assets/i18n/en.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "This addon is already installed, but the versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "In order to use wago.io as an addon provider and help support authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable Wago as a provider in the options tab.", ++ "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and help support authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable Wago / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,14 +314,21 @@ + "TITLE": "Install Addon URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Patch Notes {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", +- "POSITIVE_BUTTON": "Confirm", ++ "POSITIVE_BUTTON": "Accept & Continue", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, + "TITLE": "WowUp Permissions Setup", + "WAGO": { +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "Current Language", ++ "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", ++ "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Show a badge on the app icon with the number of addons with available updates.", + "ENABLE_APP_BADGE_LABEL": "Enable App Badge Notification", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Enable various system notification popups, such as auto updated addons.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Enable System Notifications", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Enable Hardware Acceleration", + "USE_SYMLINK_SUPPORT": "Enable Symlink Support", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Enabling symlink support will allow WowUp to recognize symlinks when performing a re-scan. Warning: If you do not know what a symlink is, you do not need this. When updating symlinks will currently be replaced with an actual folder and the link lost.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Enable symlink support?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Allow WowUp to scan symlink folders in your addon folder. Warning: they will be replaced when updating/installing." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Dump Debug Data", + "DEBUG_DATA_DESCRIPTION": "Log debug data to help with diagnosing potential issues. This can be found in your latest log file for the curious.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "Addons", + "APPLICATION": "Application", + "CLIENTS": "Clients", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Add New", + "AUTO_UPDATE_DESCRIPTION": "All existing and newly installed addons will be set to auto update by default", +--- wowup-electron/src/assets/i18n/de.json ++++ wowup-electron/src/assets/i18n/de.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Zurücksetzen", + "VERSION_MISMATCH": "Versionen stimmen nicht überein" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Warum sehe ich diese Werbung?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "Um wago.io als Addon-Anbieter zu nutzen und seine Autoren für ihre harte Arbeit an deinen bevorzugten Addons zu unterstützen, müssen wir diese Werbung zeigen.\n\nWenn du diese Werbung nicht sehen möchtest, kannst du wago.io jederzeit als Addon-Anbieter in den Einstellungen deaktivieren.", ++ "MESSAGE": "Um wago.io / CurseForge als Addon-Anbieter zu nutzen und seine Autoren für ihre harte Arbeit an deinen bevorzugten Addons zu unterstützen, müssen wir diese Werbung zeigen.\n\nWenn du diese Werbung nicht sehen möchtest, kannst du wago.io / CurseForge jederzeit als Addon-Anbieter in den Einstellungen deaktivieren.", + "TITLE": "Warum sehe ich diese Werbung?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "Installation eines Addons über eine externe URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Neu in Version {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "Werbepartner", ++ "DESCRIPTION_BOTTOM": ". Klicke auf die Schaltfläche 'Verwalten', um deine Einwilligungen zu steuern oder Widerspruch gegen die Verarbeitung deiner Daten einzulegen. Du kannst deine Einstellungen jederzeit über das Einstellungsmenü ändern.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Verwalten", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Bevor wir beginnen, müssen wir einige Berechtigungen für die App einrichten.", + "POSITIVE_BUTTON": "Bestätigen", + "TELEMETRY": { + "DESCRIPTION": "Möchest Du helfen, WowUp zu verbessern, indem anonyme Installationsdaten und/oder Fehler gesendet werden?", + "TOGGLE_LABEL": "Telemetrie zulassen" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Einstellen des App-Release-Kanals", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Ja ich verstehe", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Wechseln zwischen der Beta- und Stabilen-Versionen der Anwendung", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Kanal", + "APP_RELEASE_CHANNEL_LABEL": "App-Release-Kanal", + "CURRENT_LANGUAGE_LABEL": "Aktuelle Sprache", ++ "CURSE_PROTOCOL_DESCRIPTION": "Beim Herunterladen von Addons von der CurseForge-Website übernimmt WowUp die Installation", ++ "CURSE_PROTOCOL_LABEL": "CurseForge-Download-Links verarbeiten", + "ENABLE_APP_BADGE_DESCRIPTION": "Zeigt auf dem App-Symbol einen Zähler, mit der Anzahl an verfügbaren Updates an.", + "ENABLE_APP_BADGE_LABEL": "App-Badge-Benachrichtigung aktivieren", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Aktivieren/Deaktivieren verschiedener Systembenachrichtigungen", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Systembenachrichtigungen aktivieren", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "Beim Öffnen einer AddOn-Detailansicht wird automatisch der zuletzt geöffnete Tab ausgewählt", + "KEEP_LAST_OPENED_TAB_LABEL": "Zuletzt geöffneten Tab beibehalten", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Hardwarebeschleunigung aktivieren", + "USE_SYMLINK_SUPPORT": "Symlink-Unterstützung aktivieren", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Durch das Aktivieren der Symlink-Unterstützung kann WowUp Symlinks erkennen, wenn ein erneuter Scan durchgeführt wird. Warnung: Wenn Du nicht weißt, was ein Symlink ist, brauchst Du dies nicht. Beim Aktualisieren werden Symlinks derzeit durch einen tatsächlichen Ordner ersetzt und der Link geht verloren.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Symlink-Unterstützung aktivieren?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Erlaubt WowUp Symlink-Ordner in deinem Addon-Ordner zu scannen. Warnung: Diese werden beim Aktualisieren/Installieren ersetzt." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "Sehe ein und verwalte, wie CurseForge-Werbetreibende deine Daten für Werbepersonalisierung nutzen können.", ++ "ADS_OPTION_LABEL": "Werbepersonalisierung und Daten", ++ "ADS_OPTION_MANAGE": "Verwalten", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Konfigurationsdateien anzeigen", + "CONFIG_FILES_DESCRIPTION": "Öffne den Ordner, in dem beispielsweise deine addons.json und preferences.json gespeichert sind.", + "CONFIG_FILES_LABEL": "Konfigurationsdateien", + "DEBUG_DATA_BUTTON": "Debug-Daten speichern", + "DEBUG_DATA_DESCRIPTION": "Protokolliere Debug-Daten, um mögliche Probleme zu diagnostizieren. Dies findest Du in Deiner aktuellen Protokolldatei (für Neugierige).", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "Über", + "ADDONS": "Addons", + "APPLICATION": "Anwendung", + "CLIENTS": "Clients", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Neuen hinzufügen", + "AUTO_UPDATE_DESCRIPTION": "Neu installierte Addons werden standardmäßig auf 'Automatisches Update' eingestellt", +@@ -600,14 +616,14 @@ + "RESCAN_CLIENTS_BUTTON": "Neu scannen", + "RESCAN_CLIENTS_LABEL": "Installierte World of Warcraft-Produkte erneut durchsuchen", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Speichern", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { +- "FOLDER_PATH_LABEL": "Folder: ", +- "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", ++ "FOLDER_PATH_LABEL": "Ordner: ", ++ "PAGE_EXPLANATION": "Der WTF Explorer ermöglicht dir, alle Daten zu inspizieren, die deine Add-Ons gespeichert haben.\nDateien in Grau sind in der Regel Dinge, die du ignorieren kannst, wie beispielsweise Sicherungskopien.\nDateien in Rot sollten zu Add-Ons gehören, die du nicht mehr installiert hast.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { +--- wowup-electron/src/assets/i18n/cs.json ++++ wowup-electron/src/assets/i18n/cs.json +@@ -31,13 +31,13 @@ + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { +- "MESSAGE": "In order to use wago.io as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io as a provider in the options tab.", ++ "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { +@@ -314,12 +314,19 @@ + "TITLE": "URL adresa pro instalaci addonu" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Patch Notes {versionNumber}" + }, + "PERMISSIONS": { ++ "CURSEFORGE": { ++ "DESCRIPTION_AD_LINK": "ad vendors", ++ "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", ++ "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", ++ "MANAGE_BUTTON": "Manage", ++ "TITLE": "CurseForge" ++ }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Confirm", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, +@@ -516,12 +523,14 @@ + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "Jazyk", ++ "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", ++ "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Show a badge on the app icon with the number of addons with available updates.", + "ENABLE_APP_BADGE_LABEL": "Enable app badge notification", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Povolí různé systémové notifikace, např. po automatické aktualizaci addonů.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Povolit systémové notifikace", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", +@@ -554,12 +563,18 @@ + "USE_HARDWARE_ACCELERATION_LABEL": "Zapnout hardwarovou akceleraci", + "USE_SYMLINK_SUPPORT": "Enable Symlink Support", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Enabling symlink support will allow WowUp to recognize symlinks when performing a re-scan. Warning: If you do not know what a symlink is, you do not need this. When updating symlinks will currently be replaced with an actual folder and the link lost.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Enable symlink support?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Allow WowUp to scan symlink folders in your addon folder. Warning: they will be replaced when updating/installing." + }, ++ "CURSEFORGE": { ++ "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", ++ "ADS_OPTION_LABEL": "Ads Personalization & Data", ++ "ADS_OPTION_MANAGE": "Manage", ++ "TITLE": "CurseForge" ++ }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Uložit debugovací data", + "DEBUG_DATA_DESCRIPTION": "Zaloguje debugovací data do logovacího souboru pro případnou diagnostiku problémů s aplikací. Pro zvědavce: Tato data naleznete v nejnovějším logovacím souboru.", +@@ -571,12 +586,13 @@ + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "Addony", + "APPLICATION": "Aplikace", + "CLIENTS": "WoW klienti", ++ "CURSEFORGE": "CurseForge", + "DEBUG": "Debug aplikace", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Add New", + "AUTO_UPDATE_DESCRIPTION": "Nově instalované addony budou automaticky označeny k automatické aktualizaci pomocí WowUp.", +--- wowup-electron/src/app/services/wowup/patch-notes.service.ts ++++ wowup-electron/src/app/services/wowup/patch-notes.service.ts +@@ -19,24 +19,35 @@ + Version: "2.12.0", + html: ` +

Features

+ +-

Changes

++

Changes

+ + `, + }, + { ++ Version: "2.11.1", ++ html: ` ++

Changes

++ ++ `, ++ }, ++ { + Version: "2.11.0", + html: ` +

Changes

+ + `, + }, + { + Version: "2.10.0", +@@ -82,37 +93,43 @@ + +

Changes

+ +

Fixes

+ + `, + }, + { + Version: "2.9.1", + html: ` +

New Features

+ +

Changes

+ + `, + }, +--- wowup-electron/src/app/services/addons/addon-fingerprint.service.ts ++++ wowup-electron/src/app/services/addons/addon-fingerprint.service.ts +@@ -1,9 +1,9 @@ + import { Injectable } from "@angular/core"; + import { AddonFolder, AddonScanResult } from "wowup-lib-core"; +-import { IPC_WOWUP_GET_SCAN_RESULTS } from "../../../common/constants"; ++import { IPC_CURSE_GET_SCAN_RESULTS, IPC_WOWUP_GET_SCAN_RESULTS } from "../../../common/constants"; + import { ElectronService } from "../electron/electron.service"; + + @Injectable({ + providedIn: "root", + }) + export class AddonFingerprintService { +@@ -16,11 +16,19 @@ + const wowUpScanResults: AddonScanResult[] = await this._electronService.invoke( + IPC_WOWUP_GET_SCAN_RESULTS, + filePaths + ); + console.timeEnd("WowUpScan"); + ++ console.time("CFScan"); ++ const cfScanResults: AddonScanResult[] = await this._electronService.invoke( ++ IPC_CURSE_GET_SCAN_RESULTS, ++ filePaths ++ ); ++ console.timeEnd("CFScan"); ++ + addonFolders.forEach((af) => { + af.wowUpScanResults = wowUpScanResults.find((wur) => wur.path === af.path); ++ af.cfScanResults = cfScanResults.find((cfr) => cfr.path === af.path); + }); + } + } +--- wowup-electron/src/app/services/addons/addon.service.ts ++++ wowup-electron/src/app/services/addons/addon.service.ts +@@ -36,12 +36,13 @@ + import { TocService } from "../toc/toc.service"; + import { WarcraftInstallationService } from "../warcraft/warcraft-installation.service"; + import { WarcraftService } from "../warcraft/warcraft.service"; + import { WowUpService } from "../wowup/wowup.service"; + import { AddonProviderFactory } from "./addon.provider.factory"; + import { AddonFingerprintService } from "./addon-fingerprint.service"; ++import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider"; + import { + Addon, + AddonCategory, + AddonChannelType, + AddonDependency, + AddonDependencyType, +@@ -597,13 +598,17 @@ + } else { + this._activeInstalls.splice(itemIdx, 1, updateEvent); + } + }; + + public async logDebugData(): Promise { ++ const curseProvider = this._addonProviderService.getProvider(ADDON_PROVIDER_CURSEFORGE); + const hubProvider = this._addonProviderService.getProvider(ADDON_PROVIDER_HUB); ++ if (curseProvider === undefined) { ++ throw new Error("curse provider not found"); ++ } + if (hubProvider === undefined) { + throw new Error("hub provider not found"); + } + + const clientMap = {}; + const installations = await this._warcraftInstallationService.getWowInstallationsAsync(); +@@ -611,20 +616,26 @@ + const clientTypeName = getEnumName(WowClientType, installation.clientType); + + const useSymlinkMode = await this._wowUpService.getUseSymlinkMode(); + const addonFolders = await this._warcraftService.listAddons(installation, useSymlinkMode); + await this._addonFingerprintService.getFingerprints(addonFolders); + ++ const curseMap = {}; + const hubMap = {}; + addonFolders.forEach((af) => { ++ if (af.cfScanResults !== undefined) { ++ curseMap[af.cfScanResults.folderName] = af.cfScanResults.fingerprint; ++ } ++ + if (af.wowUpScanResults !== undefined) { + hubMap[af.wowUpScanResults.folderName] = af.wowUpScanResults.fingerprint; + } + }); + + clientMap[clientTypeName] = { ++ curse: curseMap, + hub: hubMap, + }; + + console.log(`clientType ${clientTypeName} addon fingerprints`); + } + +--- wowup-electron/src/app/services/addons/addon.provider.factory.ts ++++ wowup-electron/src/app/services/addons/addon.provider.factory.ts +@@ -16,12 +16,13 @@ + import { AddonProviderState } from "../../models/wowup/addon-provider-state"; + import { ADDON_PROVIDER_UNKNOWN, WAGO_PROMPT_KEY } from "../../../common/constants"; + import { Subject } from "rxjs"; + import { PreferenceStorageService } from "../storage/preference-storage.service"; + import { SensitiveStorageService } from "../storage/sensitive-storage.service"; + import { UiMessageService } from "../ui-message/ui-message.service"; ++import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider"; + import { WowUpAddonProvider, WowInterfaceAddonProvider, TukUiAddonProvider } from "wowup-lib-core"; + import { AppConfig } from "../../../environments/environment"; + import { GenericNetworkInterface } from "../../business-objects/generic-network-interface"; + + @Injectable({ + providedIn: "root", +@@ -83,12 +84,13 @@ + const providers: AddonProvider[] = [ + this.createZipAddonProvider(), + this.createRaiderIoAddonProvider(), + this.createWowUpCompanionAddonProvider(), + this.createWowUpAddonProvider(), + this.createWagoAddonProvider(), ++ this.createCurseProvider(), + this.createTukUiAddonProvider(), + this.createWowInterfaceAddonProvider(), + this.createGitHubAddonProvider(), + ]; + + for (const provider of providers) { +@@ -140,12 +142,16 @@ + public createWowUpCompanionAddonProvider(): WowUpCompanionAddonProvider { + return new WowUpCompanionAddonProvider(this._fileService, this._tocService); + } + + public createRaiderIoAddonProvider(): RaiderIoAddonProvider { + return new RaiderIoAddonProvider(this._tocService); ++ } ++ ++ public createCurseProvider(): CurseAddonProvider { ++ return new CurseAddonProvider(this._cachingService, this._networkService, this._tocService); + } + + public createTukUiAddonProvider(): TukUiAddonProvider { + return new TukUiAddonProvider(this._tukuiNetworkInterface); + } + +--- wowup-electron/src/app/pages/options/options.component.ts ++++ wowup-electron/src/app/pages/options/options.component.ts +@@ -1,22 +1,34 @@ +-import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; ++import { ChangeDetectionStrategy, Component, Input, OnInit } from "@angular/core"; ++import { IPC_OW_IS_CMP_REQUIRED } from "../../../common/constants"; + import { ElectronService } from "../../services"; + import { SessionService } from "../../services/session/session.service"; + import { WowUpService } from "../../services/wowup/wowup.service"; ++// import { IPC_OW_IS_CMP_REQUIRED, IPC_OW_OPEN_CMP } from "../../../../common/constants"; + + @Component({ + selector: "app-options", + templateUrl: "./options.component.html", + styleUrls: ["./options.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, + }) +-export class OptionsComponent { ++export class OptionsComponent implements OnInit { + @Input("tabIndex") public tabIndex!: number; + + public optionTabIndex = 0; ++ public isCMPRequired = false; + + public constructor( + public wowUpService: WowUpService, + public sessionService: SessionService, + public electronService: ElectronService + ) {} ++ ++ public ngOnInit(): void { ++ this.electronService ++ .invoke(IPC_OW_IS_CMP_REQUIRED) ++ .then((cmpRequired) => { ++ this.isCMPRequired = cmpRequired; ++ }) ++ .catch((e) => console.error("IPC_OW_IS_CMP_REQUIRED failed", e)); ++ } + } +--- wowup-electron/src/app/pages/options/options.component.html ++++ wowup-electron/src/app/pages/options/options.component.html +@@ -29,12 +29,15 @@ + + ++ + + + + + + + + +--- wowup-electron/src/app/pages/home/home.component.ts ++++ wowup-electron/src/app/pages/home/home.component.ts +@@ -1,14 +1,15 @@ +-import { from, Subscription } from "rxjs"; +-import { filter, first, map, switchMap } from "rxjs/operators"; ++import { from, of, Subject, Subscription } from "rxjs"; ++import { catchError, filter, first, map, switchMap, takeUntil, tap } from "rxjs/operators"; + + import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from "@angular/core"; + import { MatSnackBar } from "@angular/material/snack-bar"; + import { TranslateService } from "@ngx-translate/core"; + + import { ++ CURSE_PROTOCOL_NAME, + IPC_POWER_MONITOR_RESUME, + IPC_POWER_MONITOR_UNLOCK, + TAB_INDEX_ABOUT, + TAB_INDEX_GET_ADDONS, + TAB_INDEX_MY_ADDONS, + TAB_INDEX_NEWS, +@@ -25,23 +26,25 @@ + import { DialogFactory } from "../../services/dialog/dialog.factory"; + import { SessionService } from "../../services/session/session.service"; + import { SnackbarService } from "../../services/snackbar/snackbar.service"; + import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service"; + import { WowUpService } from "../../services/wowup/wowup.service"; + import { WowUpProtocolService } from "../../services/wowup/wowup-protocol.service"; ++import { getProtocol } from "../../utils/string.utils"; + import { WowInstallation } from "wowup-lib-core"; + + @Component({ + selector: "app-home", + templateUrl: "./home.component.html", + styleUrls: ["./home.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, + }) + export class HomeComponent implements AfterViewInit, OnDestroy { + private _appUpdateInterval?: number; + private _subscriptions: Subscription[] = []; ++ private _onDestroy$ = new Subject(); + + public readonly TAB_INDEX_MY_ADDONS = TAB_INDEX_MY_ADDONS; + public readonly TAB_INDEX_GET_ADDONS = TAB_INDEX_GET_ADDONS; + public readonly TAB_INDEX_ABOUT = TAB_INDEX_ABOUT; + public readonly TAB_INDEX_NEWS = TAB_INDEX_NEWS; + public readonly TAB_INDEX_SETTINGS = TAB_INDEX_SETTINGS; +@@ -63,12 +66,24 @@ + private _dialogFactory: DialogFactory, + private _wowUpProtocolService: WowUpProtocolService, + ) { + const wowInstalledSub = this._warcraftInstallationService.wowInstallations$.subscribe((installations) => { + this.hasWowClient = installations.length > 0; + }); ++ ++ this.electronService.customProtocol$ ++ .pipe( ++ takeUntil(this._onDestroy$), ++ filter((protocol) => getProtocol(protocol) === CURSE_PROTOCOL_NAME), ++ tap((protocol) => this.handleAddonInstallProtocol(protocol)), ++ catchError((e) => { ++ console.error(e); ++ return of(undefined); ++ }), ++ ) ++ .subscribe(); + + const scanErrorSub = this._addonService.scanError$.subscribe(this.onAddonScanError); + const addonInstallErrorSub = this._addonService.addonInstalled$.subscribe(this.onAddonInstalledEvent); + + const scanUpdateSub = this._addonService.scanUpdate$ + .pipe(filter((update) => update.type !== ScanUpdateType.Unknown)) +@@ -114,12 +129,14 @@ + this.initAppUpdateCheck(); + + this._subscriptions.push(powerMonitorSub); + } + + public ngOnDestroy(): void { ++ this._onDestroy$.next(true); ++ this._onDestroy$.complete(); + window.clearInterval(this._appUpdateInterval); + this._subscriptions.forEach((sub) => sub.unsubscribe()); + } + + private initAppUpdateCheck() { + if (this._appUpdateInterval !== undefined) { +--- wowup-electron/src/app/pages/get-addons/get-addons.component.ts ++++ wowup-electron/src/app/pages/get-addons/get-addons.component.ts +@@ -613,13 +613,13 @@ + addons, + [ + (sr) => (sr.providerName === ADDON_PROVIDER_HUB ? 1 : 0), + (sr) => (sr.providerName === ADDON_PROVIDER_WAGO ? 1 : 0), + "downloadCount", + ], +- ["desc", "desc", "desc"] ++ ["desc", "desc", "desc"], + ); + } + + private setPageContextText(rowCount: number) { + const contextStr: string = + rowCount > 0 +--- wowup-electron/src/app/modules/options.module.ts ++++ wowup-electron/src/app/modules/options.module.ts +@@ -10,21 +10,23 @@ + import { OptionsWowSectionComponent } from "../components/options/options-wow-section/options-wow-section.component"; + import { WowClientOptionsComponent } from "../components/options/wow-client-options/wow-client-options.component"; + import { WtfExplorerComponent } from "../components/options/wtf-explorer/wtf-explorer.component"; + import { MatModule } from "./mat-module"; + import { PipesModule } from "./pipes.module"; + import { DirectiveModule } from "./directive.module"; ++import { OptionsCurseforgeSectionComponent } from "../components/options/options-curseforge-section/options-curseforge-section.component"; + + @NgModule({ + declarations: [ + WtfExplorerComponent, + AboutComponent, + OptionsAddonSectionComponent, + OptionsAppSectionComponent, + OptionsDebugSectionComponent, + OptionsWowSectionComponent, ++ OptionsCurseforgeSectionComponent, + WowClientOptionsComponent, + ], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, +@@ -36,11 +38,12 @@ + exports: [ + WtfExplorerComponent, + AboutComponent, + OptionsAddonSectionComponent, + OptionsAppSectionComponent, + OptionsDebugSectionComponent, ++ OptionsCurseforgeSectionComponent, + OptionsWowSectionComponent, + WowClientOptionsComponent, + ], + }) + export class OptionsModule {} +--- wowup-electron/src/app/components/options/options-app-section/options-app-section.component.ts ++++ wowup-electron/src/app/components/options/options-app-section/options-app-section.component.ts +@@ -8,12 +8,13 @@ + import { TranslateService } from "@ngx-translate/core"; + + import { + ALLIANCE_LIGHT_THEME, + ALLIANCE_THEME, + APP_PROTOCOL_NAME, ++ CURSE_PROTOCOL_NAME, + DEFAULT_LIGHT_THEME, + DEFAULT_THEME, + HORDE_LIGHT_THEME, + HORDE_THEME, + } from "../../../../common/constants"; + import { ThemeGroup } from "../../../models/wowup/theme"; +@@ -42,12 +43,13 @@ + selector: "app-options-app-section", + templateUrl: "./options-app-section.component.html", + styleUrls: ["./options-app-section.component.scss"], + }) + export class OptionsAppSectionComponent implements OnInit { + public readonly wowupProtocolName = APP_PROTOCOL_NAME; ++ public readonly curseProtocolName = CURSE_PROTOCOL_NAME; + + public minimizeOnCloseDescription = ""; + public protocolRegistered = false; + public zoomScale = ZOOM_SCALE; + public currentScale = 1; + public languages: LocaleListItem[] = [ +@@ -87,12 +89,13 @@ + + public releaseChannels: ReleaseChannelViewModel[] = [ + { value: WowUpReleaseChannelType.Stable, labelKey: "COMMON.ENUM.ADDON_CHANNEL_TYPE.STABLE" }, + { value: WowUpReleaseChannelType.Beta, labelKey: "COMMON.ENUM.ADDON_CHANNEL_TYPE.BETA" }, + ]; + ++ public curseforgeProtocolHandled$ = from(this.electronService.isDefaultProtocolClient(CURSE_PROTOCOL_NAME)); + public wowupProtocolHandled$ = from(this.electronService.isDefaultProtocolClient(APP_PROTOCOL_NAME)); + + private _currentTheme: string; + public get currentTheme() { + return this._currentTheme; + } +--- wowup-electron/src/app/components/options/options-app-section/options-app-section.component.html ++++ wowup-electron/src/app/components/options/options-app-section/options-app-section.component.html +@@ -206,17 +206,34 @@ + {{ "PAGES.OPTIONS.APPLICATION.PROTOCOL_DESCRIPTION" | translate }} + + + +
+ --> +- ++ +
+
+
+
++ {{ "PAGES.OPTIONS.APPLICATION.CURSE_PROTOCOL_LABEL" | translate }} ++
++ {{ "PAGES.OPTIONS.APPLICATION.CURSE_PROTOCOL_DESCRIPTION" | translate }} ++
++ ++ ++
++
++
++ ++
++
++
++
+ {{ "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_DESCRIPTION" | translate }} +
+ + {{ "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_DROPDOWN_LABEL" | translate }} +--- wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts ++++ wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts +@@ -56,13 +56,13 @@ + }); + + public constructor( + private _addonProviderService: AddonProviderFactory, + private _sensitiveStorageService: SensitiveStorageService, + private _translateService: TranslateService, +- private _dialogFactory: DialogFactory ++ private _dialogFactory: DialogFactory, + ) { + this._addonProviderService.addonProviderChange$.subscribe(() => { + this.loadProviderStates(); + }); + + this.preferenceForm.valueChanges +@@ -70,24 +70,24 @@ + takeUntil(this.destroy$), + debounceTime(300), + switchMap((ch) => { + const tasks: Observable[] = []; + if (typeof ch?.ghPersonalAccessToken === "string") { + tasks.push( +- from(this._sensitiveStorageService.setAsync(PREF_GITHUB_PERSONAL_ACCESS_TOKEN, ch.ghPersonalAccessToken)) ++ from(this._sensitiveStorageService.setAsync(PREF_GITHUB_PERSONAL_ACCESS_TOKEN, ch.ghPersonalAccessToken)), + ); + } + if (typeof ch?.wagoAccessToken === "string") { + tasks.push(from(this.onWagoAccessTokenChange(ch.wagoAccessToken))); + } + return zip(tasks); + }), + catchError((e) => { + console.error(e); + return of(undefined); +- }) ++ }), + ) + .subscribe(); + } + + public ngOnInit(): void { + this.loadProviderStates(); +--- wowup-electron/src/app/components/common/webview/webview.component.ts ++++ wowup-electron/src/app/components/common/webview/webview.component.ts +@@ -72,17 +72,17 @@ + // const preload = this.options.preloadFilePath ? `preload="${preloadPath}"` : ""; + const partition = this.options.partition ?? "memcache"; + + console.debug("initWebview", this.options); + + const placeholder = document.createElement("div"); +- placeholder.style.width = "100%"; +- placeholder.style.height = "100%"; ++ placeholder.style.width = "400px"; ++ placeholder.style.height = "300px"; + + /* eslint-disable no-irregular-whitespace */ +- const webview: Electron.WebviewTag = document.createElement("webview"); ++ const webview: any = document.createElement("owadview"); + webview.id = this._id; + webview.src = this.options.pageUrl; + webview.setAttribute("style", "width: 100%; height: 100%;"); + webview.nodeintegration = false; + webview.nodeintegrationinsubframes = false; + webview.plugins = false; +@@ -119,12 +119,14 @@ + + placeholder.appendChild(webview); + element.nativeElement.appendChild(placeholder); + } + + private onWebviewReady = () => { ++ console.debug("onWebviewReady", this._tag); ++ + this._webviewReady = true; + + this._sessionService.debugAdFrame$.pipe(takeUntil(this.destroy$)).subscribe(() => { + if (!this._tag.isDevToolsOpened()) { + this._tag?.openDevTools(); + } +--- wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.scss ++++ wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.scss +@@ -5,13 +5,13 @@ + } + + .tab-strip { + box-sizing: border-box; + display: flex; + flex-direction: column; +- max-width: 300px; ++ max-width: 400px; + height: 100%; + padding-top: calc(1em + 50px); + + &.collapsed { + .theme-logo { + width: 50px; +@@ -137,15 +137,27 @@ + width: 150px; + background: var(--background-primary); + background: radial-gradient(circle, var(--background-primary) 0%, rgba(0, 0, 0, 0) 70%); + } + + .ad-space { +- width: 300px; ++ width: 400px; + display: flex; + flex-direction: column; ++ ++ .addon-info-btn { ++ display: block; ++ width: 100%; ++ text-align: center; ++ line-height: 40px; ++ ++ &:hover { ++ border: 2px solid var(--background-primary); ++ box-sizing: border-box; ++ } ++ } + + .ad-details { + flex-grow: 1; + + .center-col { + display: flex; +@@ -154,15 +166,15 @@ + height: 100%; + } + } + + .ad { + flex-shrink: 0; +- width: 300px; +- height: 250px; ++ width: 400px; ++ height: 300px; + background-color: var(--background-secondary-4); + background-image: var(--ad-placeholder); + background-position: top 32px center; + background-repeat: no-repeat; + background-size: 55%; + } + } +--- wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.html ++++ wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.html +@@ -125,14 +125,14 @@ + +
{{ "PAGES.HOME.COLLAPSE_BUTTON_TITLE" | translate }}
+
+ + +
+-
++
+ {{ "ADS.AD_EXPLAINER_BUTTON" | translate }} +
+
+ +
+
+
+--- wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.ts ++++ wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.ts +@@ -1,39 +1,67 @@ +-import { AfterViewChecked, Component, ElementRef, ViewChild } from "@angular/core"; ++import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from "@angular/core"; + import { UntypedFormControl, UntypedFormGroup } from "@angular/forms"; + + import { MatDialogRef } from "@angular/material/dialog"; ++import { IPC_OW_IS_CMP_REQUIRED, IPC_OW_OPEN_CMP } from "../../../../common/constants"; ++import { ElectronService } from "../../../services"; + import { AppConfig } from "../../../../environments/environment"; + + export interface ConsentDialogResult { + telemetry: boolean; + wagoProvider: boolean; + } + + @Component({ + selector: "app-consent-dialog", + templateUrl: "./consent-dialog.component.html", + styleUrls: ["./consent-dialog.component.scss"], + }) +-export class ConsentDialogComponent implements AfterViewChecked { ++export class ConsentDialogComponent implements AfterViewChecked, OnInit { + @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef; + + public consentOptions: UntypedFormGroup; ++ public requiresCmp = false; + + public readonly wagoTermsUrl = AppConfig.wago.termsUrl; + public readonly wagoDataUrl = AppConfig.wago.dataConsentUrl; + +- public constructor(public dialogRef: MatDialogRef) { ++ public constructor( ++ public dialogRef: MatDialogRef, ++ private _electronService: ElectronService, ++ ) { + this.consentOptions = new UntypedFormGroup({ + telemetry: new UntypedFormControl(true), + wagoProvider: new UntypedFormControl(true), + }); + } + ++ public ngOnInit(): void { ++ this._electronService ++ .invoke(IPC_OW_IS_CMP_REQUIRED) ++ .then((cmpRequired) => { ++ console.log("cmpRequired", cmpRequired); ++ this.requiresCmp = cmpRequired; ++ }) ++ .catch((e) => console.error("IPC_OW_IS_CMP_REQUIRED failed", e)); ++ } ++ + public ngAfterViewChecked(): void { + // formatDynamicLinks(descriptionContainer, this.onOpenLink); ++ } ++ ++ public onClickAdVendors(evt: MouseEvent): void { ++ evt.preventDefault(); ++ ++ this._electronService.invoke(IPC_OW_OPEN_CMP, "vendors").catch((e) => console.error("onClickAdVendors failed", e)); ++ } ++ ++ public onClickManage(evt: MouseEvent): void { ++ evt.preventDefault(); ++ ++ this._electronService.invoke(IPC_OW_OPEN_CMP).catch((e) => console.error("onClickManage failed", e)); + } + + public onNoClick(): void { + this.dialogRef.close(); + } + +--- wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.html ++++ wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.html +@@ -7,12 +7,22 @@ + {{ + "DIALOGS.PERMISSIONS.TELEMETRY.TOGGLE_LABEL" | translate + }} +
+ {{ "DIALOGS.PERMISSIONS.TELEMETRY.DESCRIPTION" | translate }} + ++
++

{{ "DIALOGS.PERMISSIONS.CURSEFORGE.TITLE" | translate }}

++ ++ {{ "DIALOGS.PERMISSIONS.CURSEFORGE.DESCRIPTION_TOP" | translate }} ++ {{ ++ "DIALOGS.PERMISSIONS.CURSEFORGE.DESCRIPTION_AD_LINK" | translate ++ }} ++ {{ "DIALOGS.PERMISSIONS.CURSEFORGE.DESCRIPTION_BOTTOM" | translate }} ++ ++
+
+
+ {{ + "DIALOGS.PERMISSIONS.WAGO.TOGGLE_LABEL" | translate + }} +
+@@ -22,11 +32,12 @@ + 'DIALOGS.PERMISSIONS.WAGO.DESCRIPTION' | translate: { termsUrl: wagoTermsUrl, dataUrl: wagoDataUrl } + " + > +
+ +
++ + +
+ +--- wowup-electron/src/index.html ++++ wowup-electron/src/index.html +@@ -1,21 +1,24 @@ +- ++ + +- +- +- +- WowUp.io +- +- +- +- +- +- +- +- +- +- +-
++ WowUp.io ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++
+- +-
+-
+- +- +- +- +\ No newline at end of file ++ ++ ++ +--- wowup-electron/app/main.ts ++++ wowup-electron/app/main.ts +@@ -153,22 +153,43 @@ + } + + // This method will be called when Electron has finished + // initialization and is ready to create browser windows. + // Some APIs can only be used after this event occurs. + // Added 400 ms to fix the black background issue while using transparent window. More details at https://github.com/electron/electron/issues/15947 +-if (app.isReady()) { +- log.info(`App already ready: ${Date.now() - startedAt}ms`); +-} else { +- app.once("ready", () => { ++app ++ .whenReady() ++ .then(() => { ++ powerMonitor.on("resume", () => { ++ log.info("powerMonitor resume"); ++ win?.webContents?.send(IPC_POWER_MONITOR_RESUME); ++ }); ++ ++ powerMonitor.on("suspend", () => { ++ log.info("powerMonitor suspend"); ++ win?.webContents?.send(IPC_POWER_MONITOR_SUSPEND); ++ }); ++ ++ powerMonitor.on("lock-screen", () => { ++ log.info("powerMonitor lock-screen"); ++ win?.webContents?.send(IPC_POWER_MONITOR_LOCK); ++ }); ++ ++ powerMonitor.on("unlock-screen", () => { ++ log.info("powerMonitor unlock-screen"); ++ win?.webContents?.send(IPC_POWER_MONITOR_UNLOCK); ++ }); ++ + log.info(`App ready: ${Date.now() - startedAt}ms`); + // setTimeout(() => { + createWindow(); + // }, 400); ++ }) ++ .catch((e) => { ++ log.error("whenready failed", e); + }); +-} + + app.on("before-quit", () => { + windowState.saveWindowConfig(win); + win = null; + appIsQuitting = true; + appUpdater?.dispose(); +@@ -207,32 +228,12 @@ + log.info(`Custom protocol detected: ${url}`); + win?.webContents.send(IPC_CUSTOM_PROTOCOL_RECEIVED, url); + setPendingOpenUrl(url); + } + }); + } +- +-powerMonitor.on("resume", () => { +- log.info("powerMonitor resume"); +- win?.webContents?.send(IPC_POWER_MONITOR_RESUME); +-}); +- +-powerMonitor.on("suspend", () => { +- log.info("powerMonitor suspend"); +- win?.webContents?.send(IPC_POWER_MONITOR_SUSPEND); +-}); +- +-powerMonitor.on("lock-screen", () => { +- log.info("powerMonitor lock-screen"); +- win?.webContents?.send(IPC_POWER_MONITOR_LOCK); +-}); +- +-powerMonitor.on("unlock-screen", () => { +- log.info("powerMonitor unlock-screen"); +- win?.webContents?.send(IPC_POWER_MONITOR_UNLOCK); +-}); + + let lastCrash = 0; + + const crashMap = new Map(); + const exitOnCrashServices = ["network.mojom.NetworkService"]; + /** If a particular child process crashes too many times, notify the user and exit the app to attempt preventing softlock of system */ +@@ -345,24 +346,36 @@ + webContents.session.setUserAgent(webContents.userAgent); + + webContents.on("preload-error", (evt, path, e) => { + log.error("[webview] preload-error", e.message); + }); + ++ webContents.on("did-fail-provisional-load", (evt) => { ++ log.error("[webview] did-fail-provisional-load", evt); ++ }); ++ + webContents.session.setPermissionRequestHandler((contents, permission, callback) => { + log.warn("[webview] setPermissionRequestHandler", permission); + return callback(false); + }); + + webContents.session.setPermissionCheckHandler((contents, permission, origin) => { + if (["background-sync"].includes(permission)) { + return true; + } + + log.warn("[webview] setPermissionCheckHandler", permission, origin); + return false; ++ }); ++ ++ // webview allowpopups must be enabled for any link to work ++ // https://www.electronjs.org/docs/latest/api/webview-tag#allowpopups ++ webContents.setWindowOpenHandler((details) => { ++ log.debug("[webview] setWindowOpenHandler"); ++ win?.webContents.send("webview-new-window", details); // forward this new window to the app for processing ++ return { action: "deny" }; + }); + + webContents.on("did-start-navigation", (evt, url) => { + if (url === "https://addons.wago.io/wowup_ad") { + log.debug("[webview] did-start-navigation", url); + wagoHandler.initializeWebContents(webContents); +--- wowup-electron/app/ipc-events.ts ++++ wowup-electron/app/ipc-events.ts +@@ -74,12 +74,15 @@ + IPC_PUSH_INIT, + IPC_PUSH_REGISTER, + IPC_PUSH_UNREGISTER, + IPC_PUSH_SUBSCRIBE, + IPC_WINDOW_IS_FULLSCREEN, + IPC_WINDOW_IS_MAXIMIZED, ++ IPC_CURSE_GET_SCAN_RESULTS, ++ IPC_OW_IS_CMP_REQUIRED, ++ IPC_OW_OPEN_CMP, + ZOOM_FACTOR_KEY, + } from "../src/common/constants"; + import { CopyFileRequest } from "../src/common/models/copy-file-request"; + import { DownloadRequest } from "../src/common/models/download-request"; + import { DownloadStatus } from "../src/common/models/download-status"; + import { DownloadStatusType } from "../src/common/models/download-status-type"; +@@ -107,12 +110,13 @@ + import { WowUpFolderScanner } from "./wowup-folder-scanner"; + import * as push from "./push"; + import { GetDirectoryTreeRequest } from "../src/common/models/ipc-request"; + import { ProductDb } from "../src/common/wowup/product-db"; + import { restoreWindow } from "./window-state"; + import { firstValueFrom, from, mergeMap, toArray } from "rxjs"; ++import { CurseFolderScanner } from "./curse-folder-scanner"; + import { Addon, AddonScanResult, FsStats } from "wowup-lib-core"; + + let PENDING_OPEN_URLS: string[] = []; + + interface SymlinkDir { + original: fs.Dirent; +@@ -215,12 +219,31 @@ + handle(IPC_CREATE_DIRECTORY_CHANNEL, async (evt, directoryPath: string): Promise => { + log.info(`[CreateDirectory] '${directoryPath}'`); + await fsp.mkdir(directoryPath, { recursive: true }); + return true; + }); + ++ handle(IPC_OW_IS_CMP_REQUIRED, async (): Promise => { ++ // NOTE(twolf): Next version of the ow-electron will fix the types ++ try { ++ return await (app as any).overwolf.isCMPRequired(); ++ } catch (e) { ++ console.error("IPC_OW_IS_CMP_REQUIRED failed", e); ++ return false; ++ } ++ }); ++ ++ handle(IPC_OW_OPEN_CMP, (evt, cmpTab?: string) => { ++ const options = {} as any; ++ if (cmpTab) { ++ options.tab = cmpTab; ++ } ++ ++ (app as any).overwolf.openCMPWindow(options); ++ }); ++ + handle(IPC_GET_ZOOM_FACTOR, () => { + return window?.webContents?.getZoomFactor(); + }); + + handle(IPC_UPDATE_APP_BADGE, (evt, count: number) => { + return app.setBadgeCount(count); +@@ -374,12 +397,29 @@ + return false; + } + + return true; + }); + ++ handle(IPC_CURSE_GET_SCAN_RESULTS, async (evt, filePaths: string[]): Promise => { ++ // Scan addon folders in parallel for speed!? ++ try { ++ const taskResults = await firstValueFrom( ++ from(filePaths).pipe( ++ mergeMap((folder) => from(new CurseFolderScanner().scanFolder(folder)), 2), ++ toArray() ++ ) ++ ); ++ ++ return taskResults; ++ } catch (e) { ++ log.error("Failed during curse scan", e); ++ throw e; ++ } ++ }); ++ + handle(IPC_WOWUP_GET_SCAN_RESULTS, async (evt, filePaths: string[]): Promise => { + const taskResults = await firstValueFrom( + from(filePaths).pipe( + mergeMap((folder) => from(new WowUpFolderScanner(folder).scanFolder()), 3), + toArray() + ) +--- wowup-electron/.vscode/launch.json ++++ wowup-electron/.vscode/launch.json +@@ -29,22 +29,22 @@ + { + "name": "Main", + "type": "node", + "request": "launch", + "protocol": "inspector", + "cwd": "${workspaceFolder}", +- "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", ++ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ow-electron", + "trace": "verbose", +- "runtimeArgs": ["--serve", ".", "--remote-debugging-port=9876"], ++ "runtimeArgs": ["--force-cmp", "--serve", ".", "--remote-debugging-port=9876"], + "windows": { +- "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" ++ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ow-electron.cmd" + }, + "preLaunchTask": "Build.Main" + } + ], + "compounds": [ + { + "name": "Application Debug", + "configurations": ["Renderer", "Main"] + } + ] + } +--- wowup-electron/package-lock.json ++++ wowup-electron/package-lock.json +@@ -54,12 +54,14 @@ + "@fortawesome/free-regular-svg-icons": "6.4.2", + "@fortawesome/free-solid-svg-icons": "6.4.2", + "@messageformat/core": "3.2.0", + "@microsoft/applicationinsights-web": "3.0.5", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", ++ "@overwolf/ow-electron": "28.2.5", ++ "@overwolf/ow-electron-builder": "24.13.4", + "@types/adm-zip": "0.5.1", + "@types/flat": "5.0.2", + "@types/globrex": "0.1.2", + "@types/jasmine": "4.3.5", + "@types/jasminewd2": "2.0.10", + "@types/lodash": "4.14.198", +@@ -74,23 +76,27 @@ + "@typescript-eslint/parser": "^7.2.0", + "ag-grid-angular": "31.2.1", + "ag-grid-community": "31.2.1", + "chai": "4.3.8", + "core-js": "3.21.1", + "cross-env": "7.0.3", ++ "curseforge-v2": "1.3.0", + "del": "7.1.0", + "dotenv": "16.4.5", + "electron": "30.0.2", + "electron-builder": "24.13.3", + "electron-notarize": "1.2.2", + "electron-reload": "2.0.0-alpha.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "9.1.0", ++ "eslint-plugin-import": "2.28.1", ++ "eslint-plugin-jsdoc": "46.5.1", + "eslint-plugin-prefer-arrow": "1.2.3", + "flat": "5.0.2", + "gulp": "4.0.2", ++ "gulp-run": "1.7.1", + "i18next-json-sync": "3.1.2", + "ignore": "5.2.4", + "jasmine-core": "5.1.1", + "jasmine-spec-reporter": "7.0.0", + "karma": "6.4.2", + "karma-coverage-istanbul-reporter": "3.0.3", +--- wowup-electron/package.json ++++ wowup-electron/package.json +@@ -15,34 +15,36 @@ + "author": { + "name": "WowUp LLC", + "email": "zakrn@wowup.io" + }, + "main": "app/main.js", + "scripts": { +- "postinstall": "electron-builder install-app-deps", ++ "postinstall": "ow-electron-builder install-app-deps", + "install:prod": "npm ci --only=prod --no-optional", + "ng": "ng", +- "start": "npm-run-all -p electron:serve ng:serve", ++ "start": "npm-run-all -p ow-electron:serve ng:serve", + "build": "npm run electron:serve-tsc && ng build --base-href ./", + "build:dev": "npm run build -- -c dev", + "build:prod": "npm run build -- -c production", + "ng:serve": "ng serve", + "ng:serve:web": "ng serve -c web -o", + "electron:serve-tsc": "tsc -p tsconfig.serve.json", +- "electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve", +- "electron:local": "npm run build:prod && npx electron .", +- "electron:build": "npm run build:prod && electron-builder build", +- "electron:build:local": "npm run build:prod && electron-builder build -c electron-builder-local.json", +- "electron:publish": "npm run lint && npm run build:prod && electron-builder build --publish always", ++ "electron:serve": "wait-on tcp:4200 && npm run ow-electron:serve-tsc && npx ow-electron . --serve", ++ "electron:local": "npm run build:prod && npx ow-electron .", ++ "electron:build": "npm run build:prod && ow-electron-builder build", ++ "electron:build:local": "npm run build:prod && ow-electron-builder build -c electron-builder-local.json", ++ "electron:publish": "npm run lint && npm run build:prod && ow-electron-builder build --publish always", ++ "electron:publish:vanilla": "npm run lint && npm run build:prod && electron-builder build --publish always", + "electron:publish:linux": "docker-compose -f linux-compose.yml up", +- "electron:publish:never": "npm run electron:build && electron-builder --publish never", +- "electron:publish:never:local": "npm run build:dev && npx electron-builder -c electron-builder-local.json --publish never ", ++ "electron:publish:never": "npm run electron:build && ow-electron-builder --publish never", ++ "electron:publish:never:local": "npm run build:dev && npx ow-electron-builder -c electron-builder-local.json --publish never", ++ "electron:publish:never:t": "ow-electron-builder -c ow-electron-builder-local.json --publish never", + "test": "ng test --watch=false", + "test:watch": "ng test", + "test:locales": "ng test --watch=false --include='src/locales.spec.ts'", +- "test:customprotocol:": "npx electron . \"curseforge://install?addonId=3358^^^&fileId=3240590\"", ++ "test:customprotocol:": "npx ow-electron . \"curseforge://install?addonId=14154&fileId=4073235\"", + "e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha --timeout 300000 --require ts-node/register e2e/**/*.e2e.ts", + "version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md", + "lint": "ng lint", + "i18n": "sync-i18n --files ./src/assets/i18n/*.json --primary en --space 2 --finalnewline --lineendings CRLF --languages cs de es fr it nb pt ru zh zh-TW ko pl", + "check-i18n": "npm run i18n -- --check", + "pretty": "npx prettier --write . && ng lint --fix", +@@ -100,12 +102,14 @@ + "@fortawesome/free-regular-svg-icons": "6.4.2", + "@fortawesome/free-solid-svg-icons": "6.4.2", + "@messageformat/core": "3.2.0", + "@microsoft/applicationinsights-web": "3.0.5", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", ++ "@overwolf/ow-electron": "28.2.5", ++ "@overwolf/ow-electron-builder": "24.13.4", + "@types/adm-zip": "0.5.1", + "@types/flat": "5.0.2", + "@types/globrex": "0.1.2", + "@types/jasmine": "4.3.5", + "@types/jasminewd2": "2.0.10", + "@types/lodash": "4.14.198", +@@ -120,23 +124,27 @@ + "@typescript-eslint/parser": "^7.2.0", + "ag-grid-angular": "31.2.1", + "ag-grid-community": "31.2.1", + "chai": "4.3.8", + "core-js": "3.21.1", + "cross-env": "7.0.3", ++ "curseforge-v2": "1.3.0", + "del": "7.1.0", + "dotenv": "16.4.5", + "electron": "30.0.2", + "electron-builder": "24.13.3", + "electron-notarize": "1.2.2", + "electron-reload": "2.0.0-alpha.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "9.1.0", ++ "eslint-plugin-import": "2.28.1", ++ "eslint-plugin-jsdoc": "46.5.1", + "eslint-plugin-prefer-arrow": "1.2.3", + "flat": "5.0.2", + "gulp": "4.0.2", ++ "gulp-run": "1.7.1", + "i18next-json-sync": "3.1.2", + "ignore": "5.2.4", + "jasmine-core": "5.1.1", + "jasmine-spec-reporter": "7.0.0", + "karma": "6.4.2", + "karma-coverage-istanbul-reporter": "3.0.3", +--- wowup-electron/gulpfile.js ++++ wowup-electron/gulpfile.js +@@ -1,8 +1,12 @@ ++require("dotenv").config(); ++ + const gulp = require("gulp"); + const del = require("del"); ++const fs = require("fs/promises"); ++const path = require("path"); + const { spawn } = require("child_process"); + + function defaultTask(cb) { + // place code for your default task here + cb(); + } +@@ -50,18 +54,39 @@ + function npmRun(cmd) { + return function npmRunCmd(cb) { + npmRunTask(cb, cmd); + }; + } + ++async function updateCfKey() { ++ const cfApiKey = process.env.CURSEFORGE_API_KEY; ++ console.log(cfApiKey); ++ if (typeof cfApiKey !== "string" || cfApiKey.length === 0) { ++ throw new Error("CURSEFORGE_API_KEY missing"); ++ } ++ ++ const envPath = "src/environments"; ++ const environments = await fs.readdir(envPath); ++ ++ for (let env of environments) { ++ const filePath = path.join(envPath, env); ++ let envData = await fs.readFile(filePath, { encoding: "utf-8" }); ++ envData = envData.replace("{{CURSEFORGE_API_KEY}}", cfApiKey); ++ ++ await fs.writeFile(filePath, envData); ++ console.log(envData); ++ } ++} ++ + const prePackageTasks = [ + npmRun("lint"), + npmRun("build:prod"), + prePackageTask, + prePackageCopyTask, + packageChDir, + npmRun("install:prod"), + ]; + + exports.default = defaultTask; + exports.package = gulp.series(...prePackageTasks, npmRun("electron:publish")); + exports.packageLocal = gulp.series(...prePackageTasks, npmRun("electron:publish:never:local")); ++exports.packageCfLocal = gulp.series(updateCfKey, npmRun("electron:publish:never:local")); +--- .github/workflows/electron-windows-build.yml ++++ .github/workflows/electron-windows-build.yml +@@ -35,16 +35,23 @@ + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + ++ - name: Inject Token ++ env: ++ CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} ++ run: | ++ cd ./wowup-electron ++ node ./inject-token.js ++ + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: Build Windows App + if: matrix.os == 'windows-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }} + run: cd ./wowup-electron && npm ci && npm run electron:publish +--- .github/workflows/electron-mac-build.yml ++++ .github/workflows/electron-mac-build.yml +@@ -35,12 +35,19 @@ + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + ++ - name: Inject Token ++ env: ++ CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} ++ run: | ++ cd ./wowup-electron ++ node ./inject-token.js ++ + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: dmg-license + run: | + cd ./wowup-electron +@@ -54,7 +61,7 @@ + NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }} + NOTARIZE_APPLE_PASSWORD: ${{ secrets.NOTARIZE_APPLE_PASSWORD }} + NOTARIZE_APPLE_TEAM_ID: ${{ secrets.NOTARIZE_APPLE_TEAM_ID }} + run: | + cd ./wowup-electron + npm i +- npm run electron:publish ++ npm run electron:publish:vanilla +--- .github/workflows/electron-linux-build.yml ++++ .github/workflows/electron-linux-build.yml +@@ -39,17 +39,24 @@ + node-version: ${{ matrix.node-version }} + + - name: Setup Flatpak + if: matrix.os == 'ubuntu-latest' + run: sudo apt install flatpak flatpak-builder + ++ - name: Inject Token ++ env: ++ CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} ++ run: | ++ cd ./wowup-electron ++ node ./inject-token.js ++ + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: Build Linux App + if: matrix.os == 'ubuntu-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + run: | + cd ./wowup-electron + npm i +- npm run electron:publish ++ npm run electron:publish:vanilla diff --git a/WowUp-Unlimited/unlimited-patches/10-IncludeTBCAddons.patch b/WowUp-Unlimited/unlimited-patches/10-IncludeTBCAddons.patch new file mode 100644 index 0000000..2290117 --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/10-IncludeTBCAddons.patch @@ -0,0 +1,47 @@ +--- wowup-electron/src/app/addon-providers/curse-addon-provider.ts ++++ wowup-electron/src/app/addon-providers/curse-addon-provider.ts +@@ -57,22 +57,22 @@ + const GAME_TYPE_LISTS = [ + { + flavor: "wow_classic", +- typeId: 67408, ++ typeId: [67408], + matches: [WowClientType.ClassicEra, WowClientType.ClassicEraPtr], + }, + { + flavor: "wow-wrath-classic", +- typeId: 73713, ++ typeId: [73713, 73246], + matches: [], + }, + { + flavor: "wow_retail", +- typeId: 517, ++ typeId: [517], + matches: [WowClientType.Retail, WowClientType.RetailPtr, WowClientType.Beta, WowClientType.RetailXPtr], + }, + { + flavor: "wow-cataclysm-classic", +- typeId: 77522, ++ typeId: [77522, 73713, 73246], + matches: [WowClientType.Classic, WowClientType.ClassicPtr, WowClientType.ClassicBeta], + }, + ]; +@@ -488,7 +488,7 @@ + throw new Error(`Game type not found: ${clientType}`); + } + +- return gameType.typeId; ++ return gameType.typeId[0]; + } + + private hasSortableGameVersion(file: cfv2.CF2File, typeId: number): boolean { +@@ -802,7 +802,7 @@ + private getValidClientTypes(file: cfv2.CF2File): WowClientType[] { + const gameVersions: WowClientType[] = _.flatten( + GAME_TYPE_LISTS.filter((type) => +- file.sortableGameVersions.find((sgv) => sgv.gameVersionTypeId === type.typeId), ++ file.sortableGameVersions.find((sgv) => type.typeId.includes(sgv.gameVersionTypeId)) + ).map((game) => game.matches), + ); + diff --git a/WowUp-Unlimited/unlimited-patches/11-DisableCurseAds.patch b/WowUp-Unlimited/unlimited-patches/11-DisableCurseAds.patch new file mode 100644 index 0000000..9233d8a --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/11-DisableCurseAds.patch @@ -0,0 +1,17 @@ +--- wowup-electron/src/app/addon-providers/curse-addon-provider.ts ++++ wowup-electron/src/app/addon-providers/curse-addon-provider.ts +@@ -85,13 +85,13 @@ + public readonly forceIgnore = false; + public readonly allowChannelChange = true; + public readonly allowReinstall = true; + public readonly canBatchFetch = true; + public readonly allowEdit = true; + +- public adRequired = true; ++ public adRequired = false; + public enabled = true; + + public constructor( + private _cachingService: CachingService, + private _networkService: NetworkService, + private _tocService: TocService, diff --git a/WowUp-Unlimited/unlimited-patches/12-CustomizableCurseAPIKey.patch b/WowUp-Unlimited/unlimited-patches/12-CustomizableCurseAPIKey.patch new file mode 100644 index 0000000..1ef315b --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/12-CustomizableCurseAPIKey.patch @@ -0,0 +1,333 @@ +--- wowup-electron/src/app/addon-providers/curse-addon-provider.ts ++++ wowup-electron/src/app/addon-providers/curse-addon-provider.ts +@@ -1,5 +1,6 @@ + import * as cfv2 from "curseforge-v2"; + import * as _ from "lodash"; ++import { filter } from "rxjs/operators"; + import { v4 as uuidv4 } from "uuid"; + import { + Addon, +@@ -32,6 +33,7 @@ + ADDON_PROVIDER_CURSEFORGE, + NO_LATEST_SEARCH_RESULT_FILES_ERROR, + NO_SEARCH_RESULTS_ERROR, ++ PREF_CF2_API_KEY, + } from "../../common/constants"; + import { AppConfig } from "../../environments/environment"; + import { CachingService } from "../services/caching/caching-service"; +@@ -39,6 +41,7 @@ + import { TocService } from "../services/toc/toc.service"; + import { strictFilter } from "../utils/array.utils"; + import { TocNotFoundError } from "../errors"; ++import { SensitiveStorageService } from "../services/storage/sensitive-storage.service"; + + interface ProtocolData { + addonId: number; +@@ -79,7 +82,7 @@ + + export class CurseAddonProvider extends AddonProvider { + private readonly _circuitBreaker: CircuitBreakerWrapper; +- private readonly _cf2Client: cfv2.CFV2Client; ++ private _cf2Client: cfv2.CFV2Client; + + public readonly name = ADDON_PROVIDER_CURSEFORGE; + public readonly forceIgnore = false; +@@ -95,6 +98,7 @@ + private _cachingService: CachingService, + private _networkService: NetworkService, + private _tocService: TocService, ++ private _sensitiveStorageService: SensitiveStorageService, + ) { + super(); + +@@ -104,8 +108,9 @@ + AppConfig.curseforge.httpTimeoutMs, + ); + +- this._cf2Client = new cfv2.CFV2Client({ +- apiKey: AppConfig.curseforge.apiKey, ++ // Pick up a CF2 api key change at runtime to force a new client to be created ++ this._sensitiveStorageService.change$.pipe(filter((change) => change.key === PREF_CF2_API_KEY)).subscribe(() => { ++ this._cf2Client = undefined; + }); + } + +@@ -350,13 +355,18 @@ + return; + } + ++ const client = await this.getClient(); ++ if (!client) { ++ return; ++ } ++ + const scanResults = addonFolders + .map((af) => af.cfScanResults) + .filter((sr): sr is AddonScanResult => sr !== undefined); + + const fingerprints = scanResults.map((sr) => sr.fingerprintNum); + +- const result = await this._cf2Client.getFingerprintMatches({ fingerprints }); ++ const result = await client.getFingerprintMatches({ fingerprints }); + const fingerprintData = result.data?.data; + try { + const matchPairs: ScanMatchPair[] = []; +@@ -383,7 +393,7 @@ + } + + const addonIds = matchPairs.map((mp) => mp.match.id); +- const getAddonsResult = await this._cf2Client.getMods({ modIds: addonIds }); ++ const getAddonsResult = await client.getMods({ modIds: addonIds }); + const addonResultData = getAddonsResult.data?.data; + + const potentialChildren: ScanMatchPair[] = []; +@@ -428,12 +438,13 @@ + externalReleaseId: string, + ): Promise { + try { ++ const client = await this.getClient(); + const cacheKey = `${this.name}_changelog_${externalId}_${externalReleaseId}`; + + const response = await this._cachingService.transaction( + cacheKey, + () => { +- return this._cf2Client.getModFileChangelog(parseInt(externalId, 10), parseInt(externalReleaseId, 10)); ++ return client.getModFileChangelog(parseInt(externalId, 10), parseInt(externalReleaseId, 10)); + }, + CHANGELOG_CACHE_TTL_SEC, + ); +@@ -448,11 +459,13 @@ + + public override async getDescription(installation: WowInstallation, externalId: string): Promise { + try { ++ const client = await this.getClient(); ++ + const cacheKey = `${this.name}_description_${externalId}`; + const response = await this._cachingService.transaction( + cacheKey, + () => { +- return this._cf2Client.getModDescription(parseInt(externalId, 10)); ++ return client.getModDescription(parseInt(externalId, 10)); + }, + CHANGELOG_CACHE_TTL_SEC, + ); +@@ -629,8 +642,9 @@ + } + + private async getAddonFileById(addonId: string | number, fileId: string | number): Promise { ++ const client = await this.getClient(); + const response = await this._circuitBreaker.fire(() => +- this._cf2Client.getModFile(parseInt(`${addonId}`, 10), parseInt(`${fileId}`, 10)), ++ client.getModFile(parseInt(`${addonId}`, 10), parseInt(`${fileId}`, 10)), + ); + + return response.data?.data; +@@ -638,7 +652,8 @@ + + private async getByIdBase(addonId: string): Promise { + try { +- const response = await this._circuitBreaker.fire(() => this._cf2Client.getMod(parseInt(addonId, 10))); ++ const client = await this.getClient(); ++ const response = await this._circuitBreaker.fire(() => client.getMod(parseInt(addonId, 10))); + return response.data?.data; + } catch (e) { + // We want to eat things like 400/500 responses +@@ -818,12 +833,19 @@ + modIds: addonIds, + }; + +- const response = await this._circuitBreaker.fire(() => this._cf2Client.getMods(request)); ++ const client = await this.getClient(); ++ ++ const response = await this._circuitBreaker.fire(() => client.getMods(request)); + + return response.data?.data || []; + } + + private async getFeaturedAddonList(wowInstallation: WowInstallation): Promise { ++ const client = await this.getClient(); ++ if (!client) { ++ return []; ++ } ++ + const gameVersionTypeId = this.getGameVersionTypeId(wowInstallation.clientType); + + const request: cfv2.CF2GetFeaturedModsRequest = { +@@ -835,7 +857,7 @@ + const cacheKey = `getFeaturedAddonList-${JSON.stringify(request)}`; + const result = await this._cachingService.transaction( + cacheKey, +- () => this._cf2Client.getFeaturedMods(request), ++ () => client.getFeaturedMods(request), + FEATURED_ADDONS_CACHE_TTL_SEC, + ); + +@@ -868,7 +890,8 @@ + gameVersionTypeId: this.getCFGameVersionType(clientType), + }; + +- const response = await this._circuitBreaker.fire(() => this._cf2Client.searchMods(request)); ++ const client = await this.getClient(); ++ const response = await this._circuitBreaker.fire(() => client.searchMods(request)); + + return response.data?.data || []; + } +@@ -911,10 +934,29 @@ + + const cacheKey = JSON.stringify(request); + ++ const client = await this.getClient(); + const result = await this._cachingService.transaction(cacheKey, () => +- this._circuitBreaker.fire(() => this._cf2Client.searchMods(request)), ++ this._circuitBreaker.fire(() => client.searchMods(request)), + ); + + return result?.data?.data ?? []; + } ++ ++ private async getClient(): Promise { ++ if (this._cf2Client) { ++ return this._cf2Client; ++ } ++ ++ let apiKey = await this._sensitiveStorageService.getAsync(PREF_CF2_API_KEY); ++ if (typeof apiKey !== "string" || apiKey.length === 0) { ++ await this._sensitiveStorageService.setAsync(PREF_CF2_API_KEY, AppConfig.curseforge.apiKey); ++ apiKey = AppConfig.curseforge.apiKey; ++ } ++ ++ this._cf2Client = new cfv2.CFV2Client({ ++ apiKey, ++ }); ++ ++ return this._cf2Client; ++ } + } +--- wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.html ++++ wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.html +@@ -26,12 +26,25 @@ + +
+ + +
+
++
++
++
++ {{ "PAGES.OPTIONS.ADDON.CURSE_FORGE_V2.API_KEY_TITLE" | translate }} ++
++ {{ "PAGES.OPTIONS.ADDON.CURSE_FORGE_V2.API_KEY_DESCRIPTION" | translate }} ++
++ ++ {{ "PAGES.OPTIONS.ADDON.CURSE_FORGE_V2.API_KEY_TITLE" | translate }} ++ ++ ++
++
+
+
+
+ {{ "PAGES.OPTIONS.ADDON.GITHUB_PERSONAL_ACCESS_TOKEN.TITLE" | translate }} +
+ ([]); + + public preferenceForm = new FormGroup({ ++ cfV2ApiKey: new UntypedFormControl(""), + ghPersonalAccessToken: new UntypedFormControl(""), + wagoAccessToken: new UntypedFormControl(""), + }); +@@ -71,6 +74,9 @@ + debounceTime(300), + switchMap((ch) => { + const tasks: Observable[] = []; ++ if (typeof ch?.cfV2ApiKey === "string") { ++ tasks.push(from(this._sensitiveStorageService.setAsync(PREF_CF2_API_KEY, ch.cfV2ApiKey))); ++ } + if (typeof ch?.ghPersonalAccessToken === "string") { + tasks.push( + from(this._sensitiveStorageService.setAsync(PREF_GITHUB_PERSONAL_ACCESS_TOKEN, ch.ghPersonalAccessToken)), +@@ -89,6 +95,10 @@ + .subscribe(); + } + ++ public insertCurseApiKey = (): void => { ++ this.preferenceForm.get("cfV2ApiKey")?.setValue(AppConfig.curseforge.apiKey); ++ }; ++ + public ngOnInit(): void { + this.loadProviderStates(); + this.loadSensitiveData().catch(console.error); +@@ -106,6 +116,9 @@ + public async onProviderStateSelectionChange(event: MatSelectionListChange): Promise { + for (const option of event.options) { + const providerName: AddonProviderType = option.value; ++ if (option.selected && providerName == ADDON_PROVIDER_CURSEFORGE) { ++ this.insertCurseApiKey(); ++ } + if (option.selected && providerName === ADDON_PROVIDER_WAGO) { + this.onWagoEnable(option); + } else { +@@ -145,9 +158,11 @@ + + private async loadSensitiveData() { + try { ++ const cfV2ApiKey = await this._sensitiveStorageService.getAsync(PREF_CF2_API_KEY); + const ghPersonalAccessToken = await this._sensitiveStorageService.getAsync(PREF_GITHUB_PERSONAL_ACCESS_TOKEN); + const wagoAccessToken = await this._sensitiveStorageService.getAsync(PREF_WAGO_ACCESS_KEY); + ++ this.preferenceForm.get("cfV2ApiKey")?.setValue(cfV2ApiKey); + this.preferenceForm.get("ghPersonalAccessToken")?.setValue(ghPersonalAccessToken); + this.preferenceForm.get("wagoAccessToken")?.setValue(wagoAccessToken); + } catch (e) { +--- wowup-electron/src/assets/i18n/en.json ++++ wowup-electron/src/assets/i18n/en.json +@@ -498,6 +498,7 @@ + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "If you have requested a CurseForge API key you can input it here to connect to their API.", + "API_KEY_TITLE": "CurseForge API Key", ++ "INSERT_API_KEY": "Insert Default CurseForge API Key", + "PROVIDER_NOTE": "API Key Required" + }, + "ENABLED_PROVIDERS": { +--- wowup-electron/src/app/services/addons/addon.provider.factory.ts ++++ wowup-electron/src/app/services/addons/addon.provider.factory.ts +@@ -148,7 +148,7 @@ + } + + public createCurseProvider(): CurseAddonProvider { +- return new CurseAddonProvider(this._cachingService, this._networkService, this._tocService); ++ return new CurseAddonProvider(this._cachingService, this._networkService, this._tocService, this._sensitiveStorageService); + } + + public createTukUiAddonProvider(): TukUiAddonProvider { +--- wowup-electron/src/common/constants.ts ++++ wowup-electron/src/common/constants.ts +@@ -148,12 +148,13 @@ + export const CLASSIC_LOCATION_KEY = "wow_classic_location"; // TODO remove, deprecated + export const CLASSIC_PTR_LOCATION_KEY = "wow_classic_ptr_location"; // TODO remove, deprecated + export const BETA_LOCATION_KEY = "wow_beta_location"; // TODO remove, deprecated + export const ACCT_PUSH_ENABLED_KEY = "acct_push_enabled"; + export const WAGO_PROMPT_KEY = "wago_prompt"; + export const PREF_TABS_COLLAPSED = "tabs_collapsed"; ++export const PREF_CF2_API_KEY = "cf2_api_key"; + export const PREF_GITHUB_PERSONAL_ACCESS_TOKEN = "github_personal_access_token"; + export const PREF_WAGO_ACCESS_KEY = "wago_access_key"; + + export const ACCT_FEATURE_KEYS = [ACCT_PUSH_ENABLED_KEY]; + + // THEMES diff --git a/WowUp-Unlimited/unlimited-patches/13-DisableWagoAds.patch b/WowUp-Unlimited/unlimited-patches/13-DisableWagoAds.patch new file mode 100644 index 0000000..67aa283 --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/13-DisableWagoAds.patch @@ -0,0 +1,119 @@ +--- wowup-electron/src/app/addon-providers/wago-addon-provider.ts ++++ wowup-electron/src/app/addon-providers/wago-addon-provider.ts +@@ -165,12 +165,14 @@ + const WAGO_SEARCH_CACHE_TIME_SEC = 60; + const WAGO_DETAILS_CACHE_TIME_SEC = 60; + const WAGO_FEATURED_ADDONS_CACHE_TIME_SEC = 60; ++const WAGO_RELOAD_PERIOD_SEC = 10 * 60; + + export class WagoAddonProvider extends AddonProvider { + private readonly _circuitBreaker: CircuitBreakerWrapper; + + private _apiTokenSrc = new BehaviorSubject(""); + private _wagoSecret = ""; ++ private _getWagoTokenTimer: ReturnType; + + // This is our internal http queue, prevents duplicated requests for some routes + private _requestQueue: Map> = new Map(); +@@ -179,7 +181,7 @@ + public readonly forceIgnore = false; + public enabled = true; + public authRequired = true; +- public adRequired = true; ++ public adRequired = false; + public allowEdit = true; + public allowReinstall = true; + public allowChannelChange = true; +@@ -208,11 +210,13 @@ + .pipe(filter((change) => change.key == PREF_WAGO_ACCESS_KEY)) + .subscribe((change) => { + console.log("[wago] wago secret set", change); ++ if (this._getWagoTokenTimer) clearTimeout(this._getWagoTokenTimer); + if (this.isValidToken(change.value as string)) { + this._wagoSecret = change.value; + this._circuitBreaker.close(); + } else { + this._wagoSecret = ""; ++ void this.getWagoToken(); + } + }); + +@@ -221,12 +225,15 @@ + .pipe( + first(), + tap((accessKey) => { ++ if (this._getWagoTokenTimer) clearTimeout(this._getWagoTokenTimer); + const validToken = this.isValidToken(accessKey); +- this.adRequired = !validToken; + if (validToken) { + this._wagoSecret = accessKey; + console.debug("[wago] secret key set"); + } ++ else { ++ void this.getWagoToken(); ++ } + }), + catchError((e) => { + console.error("[wago] failed to load secret key", e); +@@ -762,6 +769,28 @@ + } + } + ++ private getWagoToken = async () => { ++ const url = new URL(`${WAGO_AD_URL}`); ++ const response = await this._cachingService.transaction( ++ `${url.toString()}`, ++ () => this._circuitBreaker.getText(url, undefined, { "User-Agent": WAGO_AD_USER_AGENT }), ++ WAGO_FEATURED_ADDONS_CACHE_TIME_SEC ++ ); ++ ++ console.debug(`[wago] getWagoToken`, response); ++ ++ if (response) { ++ const token = response.match(/provideApiKey\(atob\('(.*?)'/); ++ if (token && token.length > 1) ++ { ++ console.debug(`[wago] getWagoToken`, token[1]); ++ this.onWagoTokenReceived(null, Buffer.from(token[1], 'base64').toString()); ++ } ++ } ++ ++ this._getWagoTokenTimer = setTimeout(() => { void this.getWagoToken() }, WAGO_RELOAD_PERIOD_SEC * 1000); ++ }; ++ + private onWagoTokenReceived = (evt, token: string) => { + console.log(`[wago] onWagoTokenReceived: ${token.length}`); + if (!this.isValidToken(token)) { +--- wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts ++++ wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts +@@ -193,8 +193,6 @@ + return; + } + +- wago.adRequired = accessToken === undefined || accessToken.length <= 20; +- + await this._addonProviderService.setProviderEnabled(ADDON_PROVIDER_WAGO, wago.enabled); + } + } +--- wowup-electron/src/app/services/network/network.service.ts ++++ wowup-electron/src/app/services/network/network.service.ts +@@ -80,11 +80,17 @@ + ); + } + +- public getText(url: URL | string, timeoutMs?: number): Promise { ++ public getText( ++ url: URL | string, ++ timeoutMs?: number, ++ headers: { ++ [header: string]: string | string[]; ++ } = {} ++ ): Promise { + return this.fire(() => + firstValueFrom( + this._httpClient +- .get(url.toString(), { responseType: "text", headers: { ...CACHE_CONTROL_HEADERS } }) ++ .get(url.toString(), { responseType: "text", headers: { ...CACHE_CONTROL_HEADERS, ...headers } }) + .pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs)) + ) + ); diff --git a/WowUp-Unlimited/unlimited-patches/14-CurseMigrationDialog.patch b/WowUp-Unlimited/unlimited-patches/14-CurseMigrationDialog.patch new file mode 100644 index 0000000..b774fdb --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/14-CurseMigrationDialog.patch @@ -0,0 +1,315 @@ +--- wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.html ++++ wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.html +@@ -0,0 +1,47 @@ ++

++ {{ "DIALOGS.CURSE_MIGRATION.TITLE" | translate }} ++

++
++

++
++ ++

{{ "APP.STATUS_TEXT.ADDON_SCAN_STARTED" | translate }}

++
++
++
++

{{ "DIALOGS.CURSE_MIGRATION.RE_SCAN_SUCCESS" | translate }}

++
++
++

{{ "DIALOGS.CURSE_MIGRATION.RE_SCAN_ERROR" | translate }}

++
++
++ ++ ++ ++
+--- wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.scss ++++ wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.scss +@@ -0,0 +1,4 @@ ++.icon-header { ++ display: flex; ++ align-items: center; ++} +--- wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.ts ++++ wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.ts +@@ -0,0 +1,84 @@ ++import { AfterViewChecked, Component, ElementRef, ViewChild } from "@angular/core"; ++import { MatDialogRef } from "@angular/material/dialog"; ++import { BehaviorSubject, map } from "rxjs"; ++import { ADDON_PROVIDER_CURSEFORGEV2 } from "../../../../common/constants"; ++import { AddonService } from "../../../services/addons/addon.service"; ++import { LinkService } from "../../../services/links/link.service"; ++import { SessionService } from "../../../services/session/session.service"; ++import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; ++import { formatDynamicLinks } from "../../../utils/dom.utils"; ++ ++export interface ConsentDialogResult { ++ telemetry: boolean; ++ wagoProvider: boolean; ++} ++ ++@Component({ ++ selector: "app-curse-migration-dialog", ++ templateUrl: "./curse-migration-dialog.component.html", ++ styleUrls: ["./curse-migration-dialog.component.scss"], ++}) ++export class CurseMigrationDialogComponent implements AfterViewChecked { ++ @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef; ++ ++ public readonly isBusy$ = new BehaviorSubject(false); ++ public readonly autoError$ = new BehaviorSubject(undefined); ++ public readonly autoComplete$ = new BehaviorSubject(false); ++ public readonly autoIncomplete$ = this.autoComplete$.pipe(map((complete) => !complete)); ++ ++ public constructor( ++ public dialogRef: MatDialogRef, ++ private _addonService: AddonService, ++ private _linkService: LinkService, ++ private _sessionService: SessionService, ++ private _warcraftInstallationService: WarcraftInstallationService ++ ) {} ++ ++ public ngAfterViewChecked(): void { ++ const descriptionContainer: HTMLDivElement = this.dialogContent?.nativeElement; ++ formatDynamicLinks(descriptionContainer, this.onOpenLink); ++ } ++ ++ public onNoClick(): void { ++ this.dialogRef.close(); ++ } ++ ++ public async onAutomaticClick(): Promise { ++ this.isBusy$.next(true); ++ ++ try { ++ // Fetch all installations ++ let scanCompleted = false; ++ const wowInstallations = await this._warcraftInstallationService.getWowInstallationsAsync(); ++ for (const wowInstall of wowInstallations) { ++ // If there are any old Curse addons, re-scan that installation ++ let addons = await this._addonService.getAddons(wowInstall); ++ addons = addons.filter( ++ (addon) => ++ addon.isIgnored === false && ++ (addon.providerName === ADDON_PROVIDER_CURSEFORGEV2) ++ ); ++ if (addons.length > 0) { ++ await this._addonService.rescanInstallation(wowInstall); ++ scanCompleted = true; ++ } ++ } ++ ++ if (scanCompleted) { ++ this._sessionService.rescanCompleted(); ++ } ++ } catch (e) { ++ console.error(e); ++ this.autoError$.next(e as Error); ++ } finally { ++ this.isBusy$.next(false); ++ this.autoComplete$.next(true); ++ } ++ } ++ ++ private onOpenLink = (element: HTMLAnchorElement): boolean => { ++ this._linkService.confirmLinkNavigation(element.href).subscribe(); ++ ++ return false; ++ }; ++} +--- wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.html ++++ wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.html +@@ -27,6 +27,15 @@ +
+
+ ++
++
++
++ ++
++
++ + +
+
+--- wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts ++++ wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts +@@ -34,6 +34,8 @@ + import { SensitiveStorageService } from "../../../services/storage/sensitive-storage.service"; + + import { AddonProviderType } from "wowup-lib-core"; ++import { CurseMigrationDialogComponent } from "../../../components/common/curse-migration-dialog/curse-migration-dialog.component"; ++import { MatDialog } from "@angular/material/dialog"; + + interface AddonProviderStateModel extends AddonProviderState { + adRequired: boolean; +@@ -59,6 +61,7 @@ + }); + + public constructor( ++ private _dialog: MatDialog, + private _addonProviderService: AddonProviderFactory, + private _sensitiveStorageService: SensitiveStorageService, + private _translateService: TranslateService, +@@ -104,6 +107,12 @@ + this.loadSensitiveData().catch(console.error); + } + ++ public openCurseMigrationDialog = (): void => { ++ this._dialog.open(CurseMigrationDialogComponent, { ++ disableClose: true, ++ }); ++ }; ++ + public ngAfterViewChecked(): void { + // formatDynamicLinks(descriptionContainer, this.onOpenLink); + } +--- wowup-electron/src/app/modules/common-ui.module.ts ++++ wowup-electron/src/app/modules/common-ui.module.ts +@@ -7,12 +7,13 @@ + import { AnimatedLogoComponent } from "../components/common/animated-logo/animated-logo.component"; + import { CellWrapTextComponent } from "../components/common/cell-wrap-text/cell-wrap-text.component"; + import { CenteredSnackbarComponent } from "../components/common/centered-snackbar/centered-snackbar.component"; + import { ClientSelectorComponent } from "../components/common/client-selector/client-selector.component"; + import { ConfirmDialogComponent } from "../components/common/confirm-dialog/confirm-dialog.component"; + import { ConsentDialogComponent } from "../components/common/consent-dialog/consent-dialog.component"; ++import { CurseMigrationDialogComponent } from "../components/common/curse-migration-dialog/curse-migration-dialog.component"; + import { ExternalUrlConfirmationDialogComponent } from "../components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component"; + import { PatchNotesDialogComponent } from "../components/common/patch-notes-dialog/patch-notes-dialog.component"; + import { ProgressButtonComponent } from "../components/common/progress-button/progress-button.component"; + import { TelemetryDialogComponent } from "../components/common/telemetry-dialog/telemetry-dialog.component"; + import { WebViewComponent } from "../components/common/webview/webview.component"; + import { ProgressSpinnerComponent } from "../components/progress-spinner/progress-spinner.component"; +@@ -32,12 +33,13 @@ + PatchNotesDialogComponent, + TelemetryDialogComponent, + ConsentDialogComponent, + CellWrapTextComponent, + CenteredSnackbarComponent, + ClientSelectorComponent, ++ CurseMigrationDialogComponent, + WebViewComponent, + ], + imports: [CommonModule, FormsModule, TranslateModule, MatModule, PipesModule, ReactiveFormsModule], + exports: [ + ProgressSpinnerComponent, + ProgressButtonComponent, +@@ -49,10 +51,11 @@ + PatchNotesDialogComponent, + TelemetryDialogComponent, + ConsentDialogComponent, + CellWrapTextComponent, + CenteredSnackbarComponent, + ClientSelectorComponent, ++ CurseMigrationDialogComponent, + WebViewComponent, + ], + }) + export class CommonUiModule {} +--- wowup-electron/src/app/services/session/session.service.ts ++++ wowup-electron/src/app/services/session/session.service.ts +@@ -60,12 +60,14 @@ + public readonly wowUpAuthenticated$ = this.wowUpAccount$.pipe(map((account) => account !== undefined)); + + public set myAddonsCompactVersion(val: boolean) { + this._myAddonsCompactVersionSrc.next(val); + } + ++ public didPromptCfMigration = true; ++ + public constructor( + private _warcraftInstallationService: WarcraftInstallationService, + private _preferenceStorageService: PreferenceStorageService, + private _wowUpAccountService: WowUpAccountService, + private _wowUpService: WowUpService, + private _addonService: AddonService, +--- wowup-electron/src/app/app.component.ts ++++ wowup-electron/src/app/app.component.ts +@@ -19,6 +19,7 @@ + import { TranslateService } from "@ngx-translate/core"; + + import { ++ ADDON_PROVIDER_CURSEFORGEV2, + ALLIANCE_LIGHT_THEME, + ALLIANCE_THEME, + CURRENT_THEME_KEY, +@@ -61,6 +62,7 @@ + ConsentDialogResult, + } from "./components/common/consent-dialog/consent-dialog.component"; + import { WowUpProtocolService } from "./services/wowup/wowup-protocol.service"; ++import { CurseMigrationDialogComponent } from "./components/common/curse-migration-dialog/curse-migration-dialog.component"; + import { Addon } from "wowup-lib-core"; + import { LinkService } from "./services/links/link.service"; + +@@ -285,6 +287,15 @@ + } + + this.showPreLoad$.next(false); ++ ++ if (!this.sessionService.didPromptCfMigration) { ++ // If the user has any addons from old Curse that are not ignored prompt them to rescan ++ let cfAddons = await this._addonService.getProviderAddons(ADDON_PROVIDER_CURSEFORGEV2); ++ cfAddons = cfAddons.filter((addon) => addon.isIgnored === false); ++ if (cfAddons.length > 0) { ++ this.openCurseMigrationDialog(); ++ } ++ } + } catch (e) { + console.error(e); + } +@@ -318,6 +329,17 @@ + this.openInstallFromUrlDialog(path); + }; + ++ public openCurseMigrationDialog(): void { ++ const dialogRef = this._dialog.open(CurseMigrationDialogComponent, { ++ disableClose: true, ++ }); ++ ++ dialogRef.afterClosed().subscribe(() => { ++ this.sessionService.didPromptCfMigration = true; ++ this.showRequiredDialogs().catch((e) => console.error(e)); ++ }); ++ } ++ + public openConsentDialog(): void { + const dialogRef = this._dialog.open(ConsentDialogComponent, { + disableClose: true, +--- wowup-electron/src/assets/i18n/en.json ++++ wowup-electron/src/assets/i18n/en.json +@@ -501,6 +501,7 @@ + "INSERT_API_KEY": "Insert Default CurseForge API Key", + "PROVIDER_NOTE": "API Key Required" + }, ++ "CURSE_MIGRATION_BUTTON": "CurseForge Migration", + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Select which providers may be used to search for, and install new addons", + "FIELD_LABEL": "Enabled Addon Providers", diff --git a/WowUp-Unlimited/unlimited-patches/15-DisableCodeSigning.patch b/WowUp-Unlimited/unlimited-patches/15-DisableCodeSigning.patch new file mode 100644 index 0000000..bb25fc3 --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/15-DisableCodeSigning.patch @@ -0,0 +1,63 @@ +--- .github/workflows/electron-mac-build.yml ++++ .github/workflows/electron-mac-build.yml +@@ -53,15 +53,11 @@ + cd ./wowup-electron + npm i dmg-license + + - name: Build Mac App + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token +- CSC_LINK: ${{ secrets.MACOS_CERT }} +- CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} +- NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }} +- NOTARIZE_APPLE_PASSWORD: ${{ secrets.NOTARIZE_APPLE_PASSWORD }} +- NOTARIZE_APPLE_TEAM_ID: ${{ secrets.NOTARIZE_APPLE_TEAM_ID }} ++ CSC_IDENTITY_AUTO_DISCOVERY: 'false' + run: | + cd ./wowup-electron + npm i + npm run electron:publish:vanilla +--- .github/workflows/electron-windows-build.yml ++++ .github/workflows/electron-windows-build.yml +@@ -49,9 +49,7 @@ + run: npm install -g @angular/cli + + - name: Build Windows App + if: matrix.os == 'windows-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token +- CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} +- CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }} + run: cd ./wowup-electron && npm ci && npm run electron:publish +--- wowup-electron/electron-builder.json ++++ wowup-electron/electron-builder.json +@@ -9,13 +9,13 @@ + "publish": ["github"], + "nodeGypRebuild": true, + "files": ["dist/**/*.*", "assets/**/*.*", "app/**/*.js", "src/common/**/*.js", "build/Release/*.node"], + "win": { + "icon": "electron-build/icon.ico", + "target": ["nsis", "portable"], +- "forceCodeSigning": true, ++ "forceCodeSigning": false, + "publisherName": "WowUp LLC" + }, + "nsis": { + "deleteAppDataOnUninstall": true + }, + "mac": { +@@ -24,13 +24,13 @@ + "target": [ + { + "target": "default", + "arch": ["x64", "arm64"] + } + ], +- "hardenedRuntime": true, ++ "hardenedRuntime": false, + "entitlements": "./electron-build/entitlements.mac.plist", + "extendInfo": { + "CFBundleURLTypes": [ + { + "CFBundleTypeRole": "Shell", + "CFBundleURLName": "CurseForge", diff --git a/WowUp-Unlimited/unlimited-patches/16-UpdateRepository.patch b/WowUp-Unlimited/unlimited-patches/16-UpdateRepository.patch new file mode 100644 index 0000000..95a677c --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/16-UpdateRepository.patch @@ -0,0 +1,17 @@ +--- wowup-electron/package.json ++++ wowup-electron/package.json +@@ -7,13 +7,13 @@ + "wow", + "world of warcraft" + ], + "homepage": "https://wowup.io", + "repository": { + "type": "git", +- "url": "https://github.com/WowUp/WowUp.git" ++ "url": "https://github.com/DigitalDJ/WowUp-Unlimited.git" + }, + "author": { + "name": "WowUp LLC", + "email": "zakrn@wowup.io" + }, + "main": "app/main.js", diff --git a/WowUp-Unlimited/unlimited-patches/17-WowUpCFUserAgent.patch b/WowUp-Unlimited/unlimited-patches/17-WowUpCFUserAgent.patch new file mode 100644 index 0000000..5d13d33 --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/17-WowUpCFUserAgent.patch @@ -0,0 +1,12 @@ +--- wowup-electron/app/main.ts ++++ wowup-electron/app/main.ts +@@ -567,8 +567,8 @@ + } + return argv.hidden || loginItems.wasOpenedAsHidden; + } + + function getUserAgent() { + const portableStr = isPortable ? " portable;" : ""; +- return `WowUp-Client/${app.getVersion()} (${osType()}; ${osRelease()}; ${osArch()}; ${portableStr} +https://wowup.io)`; ++ return `WowUp-Client/${app.getVersion()} (${osType()}; ${osRelease()}; ${osArch()}; CF; ${portableStr} +https://wowup.io)`; + } diff --git a/WowUp-Unlimited/unlimited-patches/18-CurseForgeAPIKey.patch b/WowUp-Unlimited/unlimited-patches/18-CurseForgeAPIKey.patch new file mode 100644 index 0000000..8e21af3 --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/18-CurseForgeAPIKey.patch @@ -0,0 +1,29 @@ +--- wowup-electron/inject-token.js ++++ wowup-electron/inject-token.js +@@ -1,7 +1,8 @@ + const fs = require("fs"); + const path = require("path"); + +-const apiToken = process.env.CURSEFORGE_API_KEY || "UNKNOWN"; ++const apiTokenPath = "./curseforge-api-key"; ++const apiToken = process.env.CURSEFORGE_API_KEY || fs.readFileSync(apiTokenPath, { encoding: "utf-8" }) || "UNKNOWN"; + + const environmentsPath = "./src/environments"; + const envFiles = fs.readdirSync(environmentsPath); +--- wowup-electron/gulpfile.js ++++ wowup-electron/gulpfile.js +@@ -58,7 +58,8 @@ + } + + async function updateCfKey() { +- const cfApiKey = process.env.CURSEFORGE_API_KEY; ++ const apiTokenPath = "./curseforge-api-key"; ++ const cfApiKey = process.env.CURSEFORGE_API_KEY || fs.readFileSync(apiTokenPath, { encoding: "utf-8" }); + console.log(cfApiKey); + if (typeof cfApiKey !== "string" || cfApiKey.length === 0) { + throw new Error("CURSEFORGE_API_KEY missing"); +--- wowup-electron/curseforge-api-key ++++ wowup-electron/curseforge-api-key +@@ -0,0 +1 @@ ++$2a$10$kwxGpA0riXrsJLrX/Y9Eh.1K.XamwwwANA95mtZz1NMer/L.r9Xj6 +\ No newline at end of file diff --git a/WowUp-Unlimited/unlimited-patches/19-RevertOverwolfIntegration.patch b/WowUp-Unlimited/unlimited-patches/19-RevertOverwolfIntegration.patch new file mode 100644 index 0000000..d320ed0 --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/19-RevertOverwolfIntegration.patch @@ -0,0 +1,154 @@ +--- wowup-electron/src/app/components/common/webview/webview.component.ts ++++ wowup-electron/src/app/components/common/webview/webview.component.ts +@@ -76,13 +76,13 @@ + + const placeholder = document.createElement("div"); + placeholder.style.width = "400px"; + placeholder.style.height = "300px"; + + /* eslint-disable no-irregular-whitespace */ +- const webview: any = document.createElement("owadview"); ++ const webview: Electron.WebviewTag = document.createElement("webview"); + webview.id = this._id; + webview.src = this.options.pageUrl; + webview.setAttribute("style", "width: 100%; height: 100%;"); + webview.nodeintegration = false; + webview.nodeintegrationinsubframes = false; + webview.plugins = false; +--- wowup-electron/app/ipc-events.ts ++++ wowup-electron/app/ipc-events.ts +@@ -222,14 +222,8 @@ + return true; + }); + +- handle(IPC_OW_IS_CMP_REQUIRED, async (): Promise => { +- // NOTE(twolf): Next version of the ow-electron will fix the types +- try { +- return await (app as any).overwolf.isCMPRequired(); +- } catch (e) { +- console.error("IPC_OW_IS_CMP_REQUIRED failed", e); +- return false; +- } ++ handle(IPC_OW_IS_CMP_REQUIRED, (): boolean => { ++ return false; + }); + + handle(IPC_OW_OPEN_CMP, (evt, cmpTab?: string) => { +@@ -237,8 +231,6 @@ + if (cmpTab) { + options.tab = cmpTab; + } +- +- (app as any).overwolf.openCMPWindow(options); + }); + + handle(IPC_GET_ZOOM_FACTOR, () => { +--- wowup-electron/.vscode/launch.json ++++ wowup-electron/.vscode/launch.json +@@ -29,22 +29,22 @@ + { + "name": "Main", + "type": "node", + "request": "launch", + "protocol": "inspector", + "cwd": "${workspaceFolder}", +- "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ow-electron", ++ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "trace": "verbose", + "runtimeArgs": ["--force-cmp", "--serve", ".", "--remote-debugging-port=9876"], + "windows": { +- "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ow-electron.cmd" ++ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "preLaunchTask": "Build.Main" + } + ], + "compounds": [ + { + "name": "Application Debug", + "configurations": ["Renderer", "Main"] + } + ] + } +--- wowup-electron/package-lock.json ++++ wowup-electron/package-lock.json +@@ -54,14 +54,12 @@ + "@fortawesome/free-regular-svg-icons": "6.4.2", + "@fortawesome/free-solid-svg-icons": "6.4.2", + "@messageformat/core": "3.2.0", + "@microsoft/applicationinsights-web": "3.0.5", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", +- "@overwolf/ow-electron": "28.2.5", +- "@overwolf/ow-electron-builder": "24.13.4", + "@types/adm-zip": "0.5.1", + "@types/flat": "5.0.2", + "@types/globrex": "0.1.2", + "@types/jasmine": "4.3.5", + "@types/jasminewd2": "2.0.10", + "@types/lodash": "4.14.198", +--- wowup-electron/package.json ++++ wowup-electron/package.json +@@ -15,36 +15,36 @@ + "author": { + "name": "WowUp LLC", + "email": "zakrn@wowup.io" + }, + "main": "app/main.js", + "scripts": { +- "postinstall": "ow-electron-builder install-app-deps", ++ "postinstall": "electron-builder install-app-deps", + "install:prod": "npm ci --only=prod --no-optional", + "ng": "ng", +- "start": "npm-run-all -p ow-electron:serve ng:serve", ++ "start": "npm-run-all -p electron:serve ng:serve", + "build": "npm run electron:serve-tsc && ng build --base-href ./", + "build:dev": "npm run build -- -c dev", + "build:prod": "npm run build -- -c production", + "ng:serve": "ng serve", + "ng:serve:web": "ng serve -c web -o", + "electron:serve-tsc": "tsc -p tsconfig.serve.json", +- "electron:serve": "wait-on tcp:4200 && npm run ow-electron:serve-tsc && npx ow-electron . --serve", +- "electron:local": "npm run build:prod && npx ow-electron .", +- "electron:build": "npm run build:prod && ow-electron-builder build", +- "electron:build:local": "npm run build:prod && ow-electron-builder build -c electron-builder-local.json", +- "electron:publish": "npm run lint && npm run build:prod && ow-electron-builder build --publish always", ++ "electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve", ++ "electron:local": "npm run build:prod && npx electron .", ++ "electron:build": "npm run build:prod && electron-builder build", ++ "electron:build:local": "npm run build:prod && electron-builder build -c electron-builder-local.json", ++ "electron:publish": "npm run lint && npm run build:prod && electron-builder build --publish always", + "electron:publish:vanilla": "npm run lint && npm run build:prod && electron-builder build --publish always", + "electron:publish:linux": "docker-compose -f linux-compose.yml up", +- "electron:publish:never": "npm run electron:build && ow-electron-builder --publish never", +- "electron:publish:never:local": "npm run build:dev && npx ow-electron-builder -c electron-builder-local.json --publish never", +- "electron:publish:never:t": "ow-electron-builder -c ow-electron-builder-local.json --publish never", ++ "electron:publish:never": "npm run electron:build && electron-builder --publish never", ++ "electron:publish:never:local": "npm run build:dev && npx electron-builder -c electron-builder-local.json --publish never", ++ "electron:publish:never:t": "electron-builder -c electron-builder-local.json --publish never", + "test": "ng test --watch=false", + "test:watch": "ng test", + "test:locales": "ng test --watch=false --include='src/locales.spec.ts'", +- "test:customprotocol:": "npx ow-electron . \"curseforge://install?addonId=14154&fileId=4073235\"", ++ "test:customprotocol:": "npx electron . \"curseforge://install?addonId=14154&fileId=4073235\"", + "e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha --timeout 300000 --require ts-node/register e2e/**/*.e2e.ts", + "version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md", + "lint": "ng lint", + "i18n": "sync-i18n --files ./src/assets/i18n/*.json --primary en --space 2 --finalnewline --lineendings CRLF --languages cs de es fr it nb pt ru zh zh-TW ko pl", + "check-i18n": "npm run i18n -- --check", + "pretty": "npx prettier --write . && ng lint --fix", +@@ -102,14 +102,12 @@ + "@fortawesome/free-regular-svg-icons": "6.4.2", + "@fortawesome/free-solid-svg-icons": "6.4.2", + "@messageformat/core": "3.2.0", + "@microsoft/applicationinsights-web": "3.0.5", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", +- "@overwolf/ow-electron": "28.2.5", +- "@overwolf/ow-electron-builder": "24.13.4", + "@types/adm-zip": "0.5.1", + "@types/flat": "5.0.2", + "@types/globrex": "0.1.2", + "@types/jasmine": "4.3.5", + "@types/jasminewd2": "2.0.10", + "@types/lodash": "4.14.198", diff --git a/WowUp-Unlimited/unlimited-patches/orphans.lst b/WowUp-Unlimited/unlimited-patches/orphans.lst new file mode 100644 index 0000000..666d251 --- /dev/null +++ b/WowUp-Unlimited/unlimited-patches/orphans.lst @@ -0,0 +1,10 @@ +wowup-electron/app/curse-folder-scanner.ts +wowup-electron/assets/wowup_logo_cf.png +wowup-electron/electron-build/icon-cf.ico +wowup-electron/inject-token.js +wowup-electron/src/app/addon-providers/curse-addon-provider.ts +wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.html +wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.scss +wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.ts +wowup-electron/src/app/utils/markdown.utlils.ts +wowup-electron/src/common/curse/curse-models.ts diff --git a/WowUp-Unlimited/workflow-patches/01-WorkflowCheckouts.patch b/WowUp-Unlimited/workflow-patches/01-WorkflowCheckouts.patch new file mode 100644 index 0000000..f678657 --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/01-WorkflowCheckouts.patch @@ -0,0 +1,45 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -27,11 +27,39 @@ + os: [ubuntu-latest, windows-latest,"macos-11"] + node-version: [20.x] + +- steps: +- - name: Checkout +- uses: actions/checkout@v2 ++ steps: ++ - name: Initialize git config ++ run: | ++ git config --global user.name "GitHub Actions" ++ git config --global user.email noreply@github.com ++ git config --global core.autocrlf false ++ git config --global core.eol lf ++ - name: Checkout WowUp-Unlimited ++ uses: actions/checkout@v3 + with: + fetch-depth: 0 ++ ref: 'main' ++ path: 'WowUp-Unlimited' ++ - name: Checkout WowUp-Unlimited Orphan ++ uses: actions/checkout@v3 ++ with: ++ fetch-depth: 0 ++ ref: '${{ env.release_name }}-Src' ++ path: 'WowUp-Unlimited-Src' ++ - name: Checkout WowUp ++ uses: actions/checkout@v3 ++ with: ++ repository: 'WowUp/WowUp' ++ ref: ${{ env.wowup_branch }} ++ path: 'WowUp' ++ fetch-depth: 0 ++ - name: Checkout WowUp.CF ++ uses: actions/checkout@v3 ++ with: ++ repository: 'WowUp/WowUp.CF' ++ ref: ${{ env.wowupcf_branch }} ++ path: 'WowUp.CF' ++ fetch-depth: 0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 diff --git a/WowUp-Unlimited/workflow-patches/02-CurseForgeMerge.patch b/WowUp-Unlimited/workflow-patches/02-CurseForgeMerge.patch new file mode 100644 index 0000000..21db569 --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/02-CurseForgeMerge.patch @@ -0,0 +1,54 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -63,12 +63,19 @@ + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + ++ - name: Inject Token ++ env: ++ CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} ++ run: | ++ cd ./WowUp/wowup-electron ++ node ./inject-token.js ++ + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: dmg-license + if: matrix.os == 'macos-11' + run: | +@@ -79,13 +86,13 @@ + if: matrix.os == 'ubuntu-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + run: | + cd ./wowup-electron + npm i +- npm run electron:publish ++ npm run electron:publish:vanilla + + - name: Build Mac App + if: matrix.os == 'macos-11' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + CSC_LINK: ${{ secrets.MACOS_CERT }} +@@ -93,15 +100,15 @@ + NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }} + NOTARIZE_APPLE_PASSWORD: ${{ secrets.NOTARIZE_APPLE_PASSWORD }} + NOTARIZE_APPLE_TEAM_ID: ${{ secrets.NOTARIZE_APPLE_TEAM_ID }} + run: | + cd ./wowup-electron + npm i +- npm run electron:publish ++ npm run electron:publish:vanilla + + - name: Build Windows App + if: matrix.os == 'windows-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }} + run: cd ./wowup-electron && npm i && npm run electron:publish diff --git a/WowUp-Unlimited/workflow-patches/03-DefaultShell.patch b/WowUp-Unlimited/workflow-patches/03-DefaultShell.patch new file mode 100644 index 0000000..bb7d95a --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/03-DefaultShell.patch @@ -0,0 +1,12 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -3,6 +3,9 @@ + # - push + workflow_dispatch: + ++defaults: ++ run: ++ shell: bash + jobs: + # create-release: + # name: Create Release diff --git a/WowUp-Unlimited/workflow-patches/04-Trigger.patch b/WowUp-Unlimited/workflow-patches/04-Trigger.patch new file mode 100644 index 0000000..b8a4163 --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/04-Trigger.patch @@ -0,0 +1,12 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -1,7 +1,8 @@ + name: Build WowUp Electron All + on: +- # - push ++ push: + workflow_dispatch: ++ workflow_call: + + jobs: + # create-release: diff --git a/WowUp-Unlimited/workflow-patches/05-Env.patch b/WowUp-Unlimited/workflow-patches/05-Env.patch new file mode 100644 index 0000000..b49b766 --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/05-Env.patch @@ -0,0 +1,13 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -26,6 +26,10 @@ + build: + name: Build + runs-on: ${{ matrix.os }} ++ env: ++ wowup_branch: 'WOWUP_BRANCH' ++ wowupcf_branch: 'WOWUPCF_BRANCH' ++ release_name: 'RELEASE_NAME' + strategy: + matrix: + os: [ubuntu-latest, windows-latest,"macos-11"] diff --git a/WowUp-Unlimited/workflow-patches/06-WorkspaceLocation.patch b/WowUp-Unlimited/workflow-patches/06-WorkspaceLocation.patch new file mode 100644 index 0000000..5d89c8a --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/06-WorkspaceLocation.patch @@ -0,0 +1,35 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -87,7 +87,7 @@ + - name: dmg-license + if: matrix.os == 'macos-11' + run: | +- cd ./wowup-electron ++ cd ./WowUp/wowup-electron + npm i dmg-license + + - name: Build Linux App +@@ -95,7 +95,7 @@ + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + run: | +- cd ./wowup-electron ++ cd ./WowUp/wowup-electron + npm i + npm run electron:publish + +@@ -109,7 +109,7 @@ + NOTARIZE_APPLE_PASSWORD: ${{ secrets.NOTARIZE_APPLE_PASSWORD }} + NOTARIZE_APPLE_TEAM_ID: ${{ secrets.NOTARIZE_APPLE_TEAM_ID }} + run: | +- cd ./wowup-electron ++ cd ./WowUp/wowup-electron + npm i + npm run electron:publish + +@@ -119,4 +119,4 @@ + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }} +- run: cd ./wowup-electron && npm i && npm run electron:publish ++ run: cd ./WowUp/wowup-electron && npm i && npm run electron:publish diff --git a/WowUp-Unlimited/workflow-patches/07-RunMerge.patch b/WowUp-Unlimited/workflow-patches/07-RunMerge.patch new file mode 100644 index 0000000..4f0fb29 --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/07-RunMerge.patch @@ -0,0 +1,32 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -69,6 +69,29 @@ + with: + fetch-depth: 0 + ++ - name: Install GNU patch ++ if: matrix.os == 'macos-11' ++ run: brew install gpatch ++ ++ - name: WowUp Unlimited Merge ++ run: WowUp-Unlimited/merge.sh ++ ++ - name: Commit WowUp Unlimited Patched Source ++ if: matrix.os == 'ubuntu-latest' ++ run: | ++ cd ./WowUp-Unlimited-Src ++ cp -af ../WowUp WowUp ++ cp -af ../WowUp-Unlimited WowUp-Unlimited ++ cp -af .github/workflows/electron-all-build.yml WowUp/.github/workflows/electron-all-build.yml ++ rm -rf WowUp/.git ++ rm -rf WowUp-Unlimited/.git ++ git add WowUp WowUp-Unlimited ++ git commit -a -m "WowUp-Unlimited ${{ env.release_name }}" ++ git tag -d "${{ env.release_name }}" || true ++ git tag "${{ env.release_name }}" ++ git push --force origin "${{ env.release_name }}-Src" ++ git push --tags --force origin "${{ env.release_name }}" ++ + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: diff --git a/WowUp-Unlimited/workflow-patches/08-DisableCodeSigning.patch b/WowUp-Unlimited/workflow-patches/08-DisableCodeSigning.patch new file mode 100644 index 0000000..cdcdca2 --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/08-DisableCodeSigning.patch @@ -0,0 +1,22 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -126,11 +126,7 @@ + if: matrix.os == 'macos-11' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token +- CSC_LINK: ${{ secrets.MACOS_CERT }} +- CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} +- NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }} +- NOTARIZE_APPLE_PASSWORD: ${{ secrets.NOTARIZE_APPLE_PASSWORD }} +- NOTARIZE_APPLE_TEAM_ID: ${{ secrets.NOTARIZE_APPLE_TEAM_ID }} ++ CSC_IDENTITY_AUTO_DISCOVERY: 'false' + run: | + cd ./wowup-electron + npm i +@@ -140,6 +136,4 @@ + if: matrix.os == 'windows-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token +- CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} +- CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }} + run: cd ./WowUp/wowup-electron && npm i && npm run electron:publish diff --git a/WowUp-Unlimited/workflow-patches/09-WorkflowName.patch b/WowUp-Unlimited/workflow-patches/09-WorkflowName.patch new file mode 100644 index 0000000..b593a4c --- /dev/null +++ b/WowUp-Unlimited/workflow-patches/09-WorkflowName.patch @@ -0,0 +1,8 @@ +--- .github/workflows/electron-all-build.yml ++++ .github/workflows/electron-all-build.yml +@@ -1,4 +1,4 @@ +-name: Build WowUp Electron All ++name: Build WowUp Electron All - WowUp:WOWUP_BRANCH WowUp.CF:WOWUPCF_BRANCH - RELEASE_NAME + on: + # - push + workflow_dispatch: diff --git a/WowUp/.github/FUNDING.yml b/WowUp/.github/FUNDING.yml new file mode 100644 index 0000000..63288f4 --- /dev/null +++ b/WowUp/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [jliddev] +patreon: jliddev diff --git a/WowUp/.github/ISSUE_TEMPLATE/bug_report.md b/WowUp/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2fcf1c6 --- /dev/null +++ b/WowUp/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/WowUp/.github/ISSUE_TEMPLATE/feature_request.md b/WowUp/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/WowUp/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/WowUp/.github/dependabot.yml b/WowUp/.github/dependabot.yml new file mode 100644 index 0000000..998798f --- /dev/null +++ b/WowUp/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/wowup-electron" + schedule: + interval: "daily" + target-branch: "develop" \ No newline at end of file diff --git a/WowUp/.github/workflows/electron-all-build.yml b/WowUp/.github/workflows/electron-all-build.yml new file mode 100644 index 0000000..c9ff5b8 --- /dev/null +++ b/WowUp/.github/workflows/electron-all-build.yml @@ -0,0 +1,139 @@ +name: Build WowUp Electron All - WowUp:v2.12.0 WowUp.CF:v2.12.0 - v2.12.0 +on: + push: + workflow_dispatch: + workflow_call: + +defaults: + run: + shell: bash +jobs: + # create-release: + # name: Create Release + # runs-on: ubuntu-latest + # steps: + # - name: Create Release + # id: create_release + # uses: actions/create-release@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + # with: + # tag_name: ${{ github.ref }} + # release_name: Release ${{ github.ref }} + # draft: true + # prerelease: true + + build: + name: Build + runs-on: ${{ matrix.os }} + env: + wowup_branch: 'v2.12.0' + wowupcf_branch: 'v2.12.0' + release_name: 'v2.12.0' + strategy: + matrix: + os: [ubuntu-latest, windows-latest,"macos-11"] + node-version: [20.x] + + steps: + - name: Initialize git config + run: | + git config --global user.name "GitHub Actions" + git config --global user.email noreply@github.com + git config --global core.autocrlf false + git config --global core.eol lf + - name: Checkout WowUp-Unlimited + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: 'main' + path: 'WowUp-Unlimited' + - name: Checkout WowUp-Unlimited Orphan + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: '${{ env.release_name }}-Src' + path: 'WowUp-Unlimited-Src' + - name: Checkout WowUp + uses: actions/checkout@v3 + with: + repository: 'WowUp/WowUp' + ref: ${{ env.wowup_branch }} + path: 'WowUp' + fetch-depth: 0 + - name: Checkout WowUp.CF + uses: actions/checkout@v3 + with: + repository: 'WowUp/WowUp.CF' + ref: ${{ env.wowupcf_branch }} + path: 'WowUp.CF' + fetch-depth: 0 + + - name: Install GNU patch + if: matrix.os == 'macos-11' + run: brew install gpatch + + - name: WowUp Unlimited Merge + run: WowUp-Unlimited/merge.sh + + - name: Commit WowUp Unlimited Patched Source + if: matrix.os == 'ubuntu-latest' + run: | + cd ./WowUp-Unlimited-Src + cp -af ../WowUp WowUp + cp -af ../WowUp-Unlimited WowUp-Unlimited + cp -af .github/workflows/electron-all-build.yml WowUp/.github/workflows/electron-all-build.yml + rm -rf WowUp/.git + rm -rf WowUp-Unlimited/.git + git add WowUp WowUp-Unlimited + git commit -a -m "WowUp-Unlimited ${{ env.release_name }}" + git tag -d "${{ env.release_name }}" || true + git tag "${{ env.release_name }}" + git push --force origin "${{ env.release_name }}-Src" + git push --tags --force origin "${{ env.release_name }}" + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Inject Token + env: + CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} + run: | + cd ./WowUp/wowup-electron + node ./inject-token.js + + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: dmg-license + if: matrix.os == 'macos-11' + run: | + cd ./WowUp/wowup-electron + npm i dmg-license + + - name: Build Linux App + if: matrix.os == 'ubuntu-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + run: | + cd ./WowUp/wowup-electron + npm i + npm run electron:publish:vanilla + + - name: Build Mac App + if: matrix.os == 'macos-11' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + run: | + cd ./WowUp/wowup-electron + npm i + npm run electron:publish:vanilla + + - name: Build Windows App + if: matrix.os == 'windows-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + run: cd ./WowUp/wowup-electron && npm i && npm run electron:publish diff --git a/WowUp/.github/workflows/electron-linux-build.yml b/WowUp/.github/workflows/electron-linux-build.yml new file mode 100644 index 0000000..10d567a --- /dev/null +++ b/WowUp/.github/workflows/electron-linux-build.yml @@ -0,0 +1,62 @@ +name: Build WowUp Electron Linux +on: + # - push + workflow_dispatch: + +jobs: + # create-release: + # name: Create Release + # runs-on: ubuntu-latest + # steps: + # - name: Create Release + # id: create_release + # uses: actions/create-release@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + # with: + # tag_name: ${{ github.ref }} + # release_name: Release ${{ github.ref }} + # draft: true + # prerelease: true + + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: [16.x] + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Setup Flatpak + if: matrix.os == 'ubuntu-latest' + run: sudo apt install flatpak flatpak-builder + + - name: Inject Token + env: + CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} + run: | + cd ./wowup-electron + node ./inject-token.js + + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: Build Linux App + if: matrix.os == 'ubuntu-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + run: | + cd ./wowup-electron + npm i + npm run electron:publish:vanilla diff --git a/WowUp/.github/workflows/electron-mac-build.yml b/WowUp/.github/workflows/electron-mac-build.yml new file mode 100644 index 0000000..e81b7d5 --- /dev/null +++ b/WowUp/.github/workflows/electron-mac-build.yml @@ -0,0 +1,63 @@ +name: Build WowUp Electron Mac +on: + # - push + workflow_dispatch: + +jobs: + # create-release: + # name: Create Release + # runs-on: ubuntu-latest + # steps: + # - name: Create Release + # id: create_release + # uses: actions/create-release@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + # with: + # tag_name: ${{ github.ref }} + # release_name: Release ${{ github.ref }} + # draft: true + # prerelease: true + + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: ["macos-11"] + node-version: [16.x] + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Inject Token + env: + CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} + run: | + cd ./wowup-electron + node ./inject-token.js + + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: dmg-license + run: | + cd ./wowup-electron + npm i dmg-license + + - name: Build Mac App + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + run: | + cd ./wowup-electron + npm i + npm run electron:publish:vanilla diff --git a/WowUp/.github/workflows/electron-windows-build.yml b/WowUp/.github/workflows/electron-windows-build.yml new file mode 100644 index 0000000..e3b3617 --- /dev/null +++ b/WowUp/.github/workflows/electron-windows-build.yml @@ -0,0 +1,55 @@ +name: Build WowUp Electron Windows +on: + # - push + workflow_dispatch: + +jobs: + # create-release: + # name: Create Release + # runs-on: ubuntu-latest + # steps: + # - name: Create Release + # id: create_release + # uses: actions/create-release@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + # with: + # tag_name: ${{ github.ref }} + # release_name: Release ${{ github.ref }} + # draft: true + # prerelease: true + + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest] + node-version: [16.x] + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Inject Token + env: + CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} + run: | + cd ./wowup-electron + node ./inject-token.js + + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: Build Windows App + if: matrix.os == 'windows-latest' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + run: cd ./wowup-electron && npm ci && npm run electron:publish diff --git a/WowUp/.github/workflows/locale-test.yml b/WowUp/.github/workflows/locale-test.yml new file mode 100644 index 0000000..3c31fa0 --- /dev/null +++ b/WowUp/.github/workflows/locale-test.yml @@ -0,0 +1,52 @@ +name: Angular Locale Test + +on: + pull_request: + branches: [ develop ] + +jobs: + + build: + strategy: + matrix: + configuration: [Release] + node-version: [ 16.x ] + + runs-on: ubuntu-latest # For a list of available runner types, refer to + # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Angular CLI + run: npm install -g @angular/cli + + - name: Install Deps + run: | + cd ./wowup-electron + npm i --force + + - name: Lint + run: | + cd ./wowup-electron + ng lint + + - name: Test Locales + run: | + sudo apt-get install xvfb + cd ./wowup-electron + xvfb-run --auto-servernum ng test --watch=false --include='src/locales.spec.ts' + + - name: Run Test Suite + run: | + sudo apt-get install xvfb + cd ./wowup-electron + xvfb-run --auto-servernum ng test --watch=false diff --git a/WowUp/.gitignore b/WowUp/.gitignore new file mode 100644 index 0000000..019d91a --- /dev/null +++ b/WowUp/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +bin/ +obj/ +.vs/ +Packages/ +wowup-electron/package_workspace/ diff --git a/WowUp/CODE_OF_CONDUCT.md b/WowUp/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8caa399 --- /dev/null +++ b/WowUp/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [zakrn@wowup.io](mailto:zakrn@wowup.io). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/WowUp/CONTRIBUTING.md b/WowUp/CONTRIBUTING.md new file mode 100644 index 0000000..b68395e --- /dev/null +++ b/WowUp/CONTRIBUTING.md @@ -0,0 +1,156 @@ +# Contributing to WowUp + +The following is a set of guidelines for contributing to WowUp, which is hosted in the [WowUp Organization](https://github.com/wowup) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +#### Table Of Contents + +[Code of Conduct](#code-of-conduct) + +[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question) + +[What should I know before I get started?](#what-should-i-know-before-i-get-started) + +[How Can I Contribute?](#how-can-i-contribute) + +## Code of Conduct + +This project and everyone participating in it is governed by the [WowUp Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [zakrn@wowup.io](mailto:zakrn@wowup.io). + +## I don't want to read this whole thing I just have a question!!! + +> **Note:** Please don't file an issue to ask a question. You'll get faster results by using the resources below. + +We have an official discord with a detailed FAQ and where the community chimes in with helpful advice if you have questions. + +* [WowUp FAQ](https://wowup.io/faq) + +If chat is more your speed, you can join the WowUp Discord channel: + +* [Join the WowUp Discord channel](https://discord.gg/rk4F5aD) + * Even though Discord is a chat service, sometimes it takes several hours for community members to respond — please be patient! + * Use the `#wowup-support` channel for general questions or help with WowUp + * Use the `#suggestions` channel for feature suggestions + * There are many other channels available, check the channel list + +## What should I know before I get started? + +WowUp is currently split into two code bases, the legacy 1.x C# client and the 2.x Electron/Angular client that we're moving forward with. + +We are no longer adding features to the 1.x client as it will be sunset in favor of 2.x. + +## How Can I Contribute? + +### Reporting Bugs + +This section guides you through submitting a bug report for WowUp. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:. + +When you are creating a bug report, please include as many details as possible. Fill out Template coming soon, the information it asks for helps us resolve issues faster. + +> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. + +#### How Do I Submit A (Good) Bug Report? + +Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue on and provide the following information by filling in [the template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). + +Explain the problem and include additional details to help maintainers reproduce the problem: + +* **Use a clear and descriptive title** for the issue to identify the problem. +* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started WowUp. When listing steps, **don't just say what you did, but explain how you did it**. +* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. +* **Explain which behavior you expected to see instead and why.** +* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. + +Provide more context by answering these questions: + +* **Did the problem start happening recently** (e.g. after updating to a new version of WowUp) or was this always a problem? +* If the problem started happening recently, **can you reproduce the problem in an older version of WowUp?** What's the most recent version in which the problem doesn't happen? You can download older versions of WowUp from [the releases page](https://github.com/wowup/wowup/releases). +* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. + +Include details about your configuration and environment: + +* **Which version of WowUp are you using?** You can get the exact version by looking in the right hand corder of the application. +* **What's the name and version of the OS you're using**? + + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for WowUp, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. + +Before creating enhancement suggestions, please check the existing feature requests as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed. + +#### How Do I Submit A (Good) Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue and provide the following information: + +* **Use a clear and descriptive title** for the issue to identify the suggestion. +* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. +* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. +* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of WowUp which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +* **Explain why this enhancement would be useful** to most WowUp users. + +### Your First Code Contribution + +Unsure where to begin contributing to WowUp? You can start by looking through these `help-wanted` issues: + +* [Help wanted issues][help-wanted] - issues which should be a bit more involved than `beginner` issues. + +#### Local development + +WowUp can be developed locally. For instructions on how to do this, see the following sections in the [README](wowup-electron/README.md): + +### Pull Requests + +The process described here has several goals: + +- Maintain WowUp's quality +- Fix problems that are important to users +- Engage the community in working toward the best possible WowUp +- Enable a sustainable system for WowUp's maintainers to review contributions + +Please follow these steps to have your contribution considered by the maintainers: + +1. Follow all instructions in [the template](PULL_REQUEST_TEMPLATE.md) +2. Follow the [styleguides](#styleguides) +3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing
What if the status checks are failing?If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
+ +While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. + +## Styleguides + +### Git Commit Messages + +* Use the present tense ("Add feature" not "Added feature") +* Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +* Limit the first line to 72 characters or less +* Reference issues and pull requests liberally after the first line + +### TypeScript Styleguide + +All TypeScript code is linted with [Prettier](https://prettier.io/). + +* Prefer the object spread operator (`{...anotherObj}`) to `Object.assign()` +* Place requires in the following order: + * Built in Node Modules (such as `path`) + * Built in Electron Modules (such as `remote`) + * Local Modules (using relative paths) +* Place class properties in the following order: + * Class methods and properties (methods starting with `static`) + * Instance methods and properties + +## Additional Notes + +### Issue and Pull Request Labels + +This section lists the labels we use to help us track and manage issues and pull requests. + +The labels are loosely grouped by their purpose, but it's not required that every issue have a label from every group or that an issue can't have more than one label from the same group. + +Please open an issue if you have suggestions for new labels, and if you notice some labels are missing on some repositories, then please open an issue on that repository. + +## Attribution + +This contributing guide is adapted from the [Contributing to Atom][atomguide]. + +[atomguide]: https://github.com/atom/atom/blob/master/CONTRIBUTING.md#before-submitting-a-bug-report \ No newline at end of file diff --git a/WowUp/LICENSE b/WowUp/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/WowUp/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/WowUp/README.md b/WowUp/README.md new file mode 100644 index 0000000..9703325 --- /dev/null +++ b/WowUp/README.md @@ -0,0 +1,62 @@ +

+ +

+ +# WowUp Client Repository +[![WowUp on Discord](https://img.shields.io/static/v1?label=Discord&message=WowUp&color=7289DA)](https://discord.gg/rk4F5aD) +[![WowUp on Patreon](https://img.shields.io/static/v1?label=Patreon&message=WowUp&color=f96854)](https://www.patreon.com/jliddev) + +This is the repository for all our [WowUp](https://wowup.io) client side code for Windows, Mac, and Linux. + +## WowUp +![image](https://user-images.githubusercontent.com/20467484/150164985-673d02da-e7ec-42aa-b77d-655c8e3117ff.png) + +WowUp is the community centered World of Warcraft addon updater. We attempt to bring the addon community together in an easy to use updater application. We have an ever growing list of supported features. + +- Support for all major addon sources +- Discover or find new addons across addon sources +- Handle all your different World of Warcraft clients +- Auto updates +- [Companion addon](https://github.com/WowUp/WowUp.Addon) + +## Installing + +### Latest Releases +The latest WowUp release is always available on our website [wowup.io](https://wowup.io) + +### Beta Releases +If you feel like helping us test the latest and greatest changes beta builds are available on [GitHub](https://github.com/WowUp/WowUp/releases) + +### Community Support Alternatives +[Chocolatey](https://chocolatey.org) + +You can also install the latest version via Chocolatey package manager: + +```cmd +choco install wowup +``` + +## Contributing +We welcome any and all contributions from translations to feature pull requests. + +Please read our [contribution guide](https://github.com/WowUp/WowUp/blob/master/CONTRIBUTING.md) to get started. + +## Feedback +If you have a question, comment, or request we have several ways you can communicate them. + +- Create a [bug or feature request](https://github.com/WowUp/WowUp/issues) +- Contact us on [Discord](https://discord.gg/rk4F5aD) + +## Related Projects +We have a couple companion projects that are related to WowUp + +- [Companion Addon](https://github.com/WowUp/WowUp.Addon) +- [App Updater](https://github.com/WowUp/WowUpUpdater) (Deprecated) + +## Code of Conduct +Please read and understand our [Code of Conduct](https://github.com/WowUp/WowUp/blob/master/CODE_OF_CONDUCT.md) when submitting a bug or feature request here or on Discord. + +## License +Copyright (c) WowUp LLC. All rights reserved. + +Licensed under the [GNU General Public License v3.0](https://github.com/WowUp/WowUp/blob/master/LICENSE) license. diff --git a/WowUp/SECURITY.md b/WowUp/SECURITY.md new file mode 100644 index 0000000..5186c06 --- /dev/null +++ b/WowUp/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +Currently the only version available is being supported. Once we've released newer versions, this will be updated. + +| Version | Supported | +| ------- | ------------------ | +| 2.x.x | :white_check_mark: | + +## Reporting a Vulnerability + +If you find a security issue, please create an issue and we will get to it ASAP. diff --git a/WowUp/creicns.sh b/WowUp/creicns.sh new file mode 100644 index 0000000..aeba07c --- /dev/null +++ b/WowUp/creicns.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +## Creicns Ver 0.1.0 Written by Unbinilium https://github.com/Unbinilium/Creicns + +[ -z "$(echo "$OSTYPE" | /usr/bin/grep "darwin*")" ] && { echo -e "\033[31;1mError:\033[0m creicns.sh only support macOS"; exit 1; } + +src_image="$1" +[ -z "$src_image" ] && { echo -e "\033[32;1mUseage:\033[0m bash creicns.sh "; exit 1; } + +echo -e "\033[32;1mCreicns:\033[0m loading raw image from '${src_image}'" + +tmp_dir_path="/tmp/creicns-$(/bin/cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1)-$(/bin/date +"%Y-%m-%d")" +[ -e "$tmp_dir_path" ] && /bin/rm -fr "$tmp_dir_path" +echo -e "\033[32;1mCreicns:\033[0m creating tmp folder at '$tmp_dir_path'" +/bin/mkdir "$tmp_dir_path" + +if [ "${src_image:(-3)}" != "png" ]; then + echo -e "\033[32;1mCreicns:\033[0m converting raw image to '.png' format" + /usr/bin/sips -s format png "$src_image" --out "${tmp_dir_path}/${src_image}.png" &> /dev/null || { echo -e "\033[31;1mError:\033[0m raw image could not be converted to PNG format"; exit 1; } + src_image="${tmp_dir_path}/${src_image}.png" +fi + +icns_name="$(/usr/bin/basename -s ".png" "$src_image")" +iconset_path="${tmp_dir_path}/${icns_name}.iconset" +[ -e "$iconset_path" ] && /bin/rm -fr "$iconset_path" +echo -e "\033[32;1mCreicns:\033[0m creating '${icns_name}.iconset' folder at '$iconset_path'" +/bin/mkdir "$iconset_path" + +echo -e "\033[32;1mCreicns:\033[0m creating icon files in '$iconset_path'" +icon_file_list=("icon_16x16.png" "icon_16x16@2x.png" "icon_32x32.png" "icon_32x32@2x.png" "icon_128x128.png" "icon_128x128@2x.png" "icon_256x256.png" "icon_256x256@2x.png" "icon_512x512.png" "icon_512x512@2x.png") +icon_size_list=('16' '32' '32' '64' '128' '256' '256' '512' '512' '1024') +i=0 +for icon_file in ${icon_file_list[@]}; do + icon_path="${iconset_path}/${icon_file}" + /bin/cp -f "$src_image" "$icon_path" + icon_size=${icon_size_list[$((i++))]} + /usr/bin/sips -z "$icon_size" "$icon_size" "$icon_path" &> /dev/null +done + +echo -e "\033[32;1mCreicns:\033[0m creating '${icns_name}.icns' from '$iconset_path'" +/usr/bin/iconutil -c icns "$iconset_path" || { echo -e "\033[31;1mError:\033[0m failed to create the '.icns' file"; exit 1; } + +echo -e "\033[32;1mCreicns:\033[0m moving '${icns_name}.icns' from '${tmp_dir_path}/${icns_name}.icns' to '$(/usr/bin/dirname "$1")/${icns_name}.icns'" +/bin/mv -f "${tmp_dir_path}/${icns_name}.icns" "$(/usr/bin/dirname "$1")/${icns_name}.icns" + +echo -e "\033[32;1mCreicns:\033[0m cleaning up tmp folder '$tmp_dir_path'" +/bin/rm -rf "${tmp_dir_path}" + +echo -e "\033[32;1mCreicns:\033[0m successfully created '$(/usr/bin/dirname "$1")/${icns_name}.icns' from '$1'" +exit 0 diff --git a/WowUp/wowup-electron/.editorconfig b/WowUp/wowup-electron/.editorconfig new file mode 100644 index 0000000..6e87a00 --- /dev/null +++ b/WowUp/wowup-electron/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/WowUp/wowup-electron/.eslintignore b/WowUp/wowup-electron/.eslintignore new file mode 100644 index 0000000..f00c859 --- /dev/null +++ b/WowUp/wowup-electron/.eslintignore @@ -0,0 +1,6 @@ +# don't ever lint node_modules +node_modules +# don't lint build output (make sure it's set to your correct build folder name) +dist +# don't lint nyc coverage output +coverage \ No newline at end of file diff --git a/WowUp/wowup-electron/.eslintrc.json b/WowUp/wowup-electron/.eslintrc.json new file mode 100644 index 0000000..7a08f88 --- /dev/null +++ b/WowUp/wowup-electron/.eslintrc.json @@ -0,0 +1,84 @@ +{ + "env": { + "browser": true, + "node": true, + "es6": true, + "es2017": true + }, + "overrides": [ + { + "files": [ + "*.ts" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 10, + "project": [ + "./tsconfig.serve.json", + "./src/tsconfig.app.json", + "./src/tsconfig.spec.json", + "./e2e/tsconfig.e2e.json" + ], + "sourceType": "module", + "ecmaFeatures": { + "modules": true + } + }, + "plugins": [ + "@typescript-eslint", + "@angular-eslint/eslint-plugin" + ], + "rules": { + "@typescript-eslint/indent": [ + "error", + 2, + { + "SwitchCase": 1, + "CallExpression": { + "arguments": "first" + }, + "FunctionExpression": { + "parameters": "first" + }, + "FunctionDeclaration": { + "parameters": "first" + } + } + ], + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-unsafe-call": 0, + "@typescript-eslint/no-unsafe-member-access": 0, + "@typescript-eslint/no-unsafe-assignment": 0, + "@typescript-eslint/no-unsafe-return": 0, + "@typescript-eslint/explicit-member-accessibility": "error", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "@angular-eslint/use-injectable-provided-in": "error", + "@angular-eslint/no-attribute-decorator": "error", + "no-var": "error" + } + }, + { + "files": [ + "*.component.html" + ], + "parser": "@angular-eslint/template-parser", + "plugins": [ + "@angular-eslint/template" + ], + "rules": { + "@angular-eslint/template/banana-in-box": "error", + "@angular-eslint/template/no-negated-async": "error" + } + } + ] +} diff --git a/WowUp/wowup-electron/.gitignore b/WowUp/wowup-electron/.gitignore new file mode 100644 index 0000000..2777c73 --- /dev/null +++ b/WowUp/wowup-electron/.gitignore @@ -0,0 +1,58 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. +.env + +# compiled output +/dist +/tmp +/out-tsc +/app-builds +/release +/build +/app/utils/*.js +main.js +ipc-events.js +app-updater.js +window-state.js +src/**/*.js +!src/karma.conf.js +*.js.map +app/*.js +!electron-build/*.js + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.angular/cache +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +testem.log +/typings + +# e2e +/e2e/*.js +!/e2e/protractor.conf.js +/e2e/*.map + +# System Files +.DS_Store +Thumbs.db diff --git a/WowUp/wowup-electron/.npmrc b/WowUp/wowup-electron/.npmrc new file mode 100644 index 0000000..f5357d5 --- /dev/null +++ b/WowUp/wowup-electron/.npmrc @@ -0,0 +1,2 @@ +save=true +save-exact=true diff --git a/WowUp/wowup-electron/.prettierignore b/WowUp/wowup-electron/.prettierignore new file mode 100644 index 0000000..83a2619 --- /dev/null +++ b/WowUp/wowup-electron/.prettierignore @@ -0,0 +1,55 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +/app-builds +/release +/build +main.js +ipc-events.js +app-updater.js +window-state.js +src/**/*.js +!src/karma.conf.js +*.js.map +*.hbs + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +testem.log +/typings +package-lock.json + +# e2e +/e2e/*.js +!/e2e/protractor.conf.js +/e2e/*.map + +# System Files +.DS_Store +Thumbs.db diff --git a/WowUp/wowup-electron/.prettierrc.json b/WowUp/wowup-electron/.prettierrc.json new file mode 100644 index 0000000..963354f --- /dev/null +++ b/WowUp/wowup-electron/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} diff --git a/WowUp/wowup-electron/.travis.yml b/WowUp/wowup-electron/.travis.yml new file mode 100644 index 0000000..1289418 --- /dev/null +++ b/WowUp/wowup-electron/.travis.yml @@ -0,0 +1,21 @@ +os: + - linux + - osx +language: node_js +node_js: + - "12" + - "10" +dist: xenial +sudo: required +services: + - xvfb +before_script: + - export DISPLAY=:99.0 +install: + - npm set progress=false + - npm install +script: + - ng lint + - npm run test + - npm run e2e + - npm run build diff --git a/WowUp/wowup-electron/.vscode/launch.json b/WowUp/wowup-electron/.vscode/launch.json new file mode 100644 index 0000000..24ef177 --- /dev/null +++ b/WowUp/wowup-electron/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Run Test Fixer", + "program": "./test-fixer.js", + "request": "launch", + "skipFiles": ["/**"], + "args": ["--find-break"], + "type": "pwa-node" + }, + { + "name": "Renderer", + "type": "chrome", + "request": "attach", + "port": 9876, + "url": "http://localhost:4200", + "sourceMaps": true, + "timeout": 10000, + "trace": "verbose", + "sourceMapPathOverrides": { + "webpack:///./*": "${workspaceFolder}/*" + }, + "preLaunchTask": "Build.Renderer" + }, + { + "name": "Main", + "type": "node", + "request": "launch", + "protocol": "inspector", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "trace": "verbose", + "runtimeArgs": ["--force-cmp", "--serve", ".", "--remote-debugging-port=9876"], + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "preLaunchTask": "Build.Main" + } + ], + "compounds": [ + { + "name": "Application Debug", + "configurations": ["Renderer", "Main"] + } + ] +} diff --git a/WowUp/wowup-electron/.vscode/tasks.json b/WowUp/wowup-electron/.vscode/tasks.json new file mode 100644 index 0000000..aa0691b --- /dev/null +++ b/WowUp/wowup-electron/.vscode/tasks.json @@ -0,0 +1,49 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build.Main", + "type": "shell", + "command": "npm run electron:serve-tsc", + "isBackground": false, + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": { + "owner": "typescript", + "source": "ts", + "applyTo": "closedDocuments", + "fileLocation": ["relative", "${cwd}"], + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": "^.*", + "endsPattern": "^.*Terminal will be reused by tasks, press any key to close it.*" + } + } + }, + { + "label": "Build.Renderer", + "type": "shell", + "command": "npm run ng:serve", + "isBackground": true, + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": { + "owner": "typescript", + "source": "ts", + "applyTo": "closedDocuments", + "fileLocation": ["relative", "${cwd}"], + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": "^.*", + "endsPattern": "^.*Compiled successfully.*" + } + } + } + ] +} \ No newline at end of file diff --git a/WowUp/wowup-electron/CHANGELOG.md b/WowUp/wowup-electron/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/LICENSE.md b/WowUp/wowup-electron/LICENSE.md new file mode 100644 index 0000000..58862f7 --- /dev/null +++ b/WowUp/wowup-electron/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2020 - Maxime GRIS + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/WowUp/wowup-electron/README.md b/WowUp/wowup-electron/README.md new file mode 100644 index 0000000..2d65d7f --- /dev/null +++ b/WowUp/wowup-electron/README.md @@ -0,0 +1,101 @@ +

+ +

+ +# Introduction + +Bootstrap and package your project with Angular 10 and Electron 8 (Typescript + SASS + Hot Reload) for creating Desktop applications. + +Currently runs with: + +- Angular v10.0.3 +- Electron v9.1.0 +- Electron Builder v22.7.0 + +With this sample, you can : + +- Run your app in a local development environment with Electron & Hot reload +- Run your app in a production environment +- Package your app into an executable file for Linux, Windows & Mac + +/!\ Hot reload only pertains to the renderer process. The main electron process is not able to be hot reloaded, only restarted. + +/!\ Angular 10.x CLI needs Node 10.13 or later to work correctly. + +## Getting Started + +Clone this repository locally : + +```bash +git clone https://github.com/maximegris/angular-electron.git +``` + +Install dependencies with npm : + +```bash +npm install +``` + +There is an issue with `yarn` and `node_modules` when the application is built by the packager. Please use `npm` as dependencies manager. + +If you want to generate Angular components with Angular-cli , you **MUST** install `@angular/cli` in npm global context. +Please follow [Angular-cli documentation](https://github.com/angular/angular-cli) if you had installed a previous version of `angular-cli`. + +```bash +npm install -g @angular/cli +``` + +## To build for development + +- **in a terminal window** -> npm start + +Voila! You can use your Angular + Electron app in a local development environment with hot reload ! + +The application code is managed by `main.ts`. In this sample, the app runs with a simple Angular App (http://localhost:4200) and an Electron window. +The Angular component contains an example of Electron and NodeJS native lib import. +You can disable "Developer Tools" by commenting `win.webContents.openDevTools();` in `main.ts`. + +## Included Commands + +| Command | Description | +| ------------------------ | ------------------------------------------------------------------------------------ | +| `npm run ng:serve:web` | Execute the app in the browser | +| `npm run build` | Build the app. Your built files are in the /dist folder. | +| `npm run build:prod` | Build the app with Angular aot. Your built files are in the /dist folder. | +| `npm run electron:local` | Builds your application and start electron | +| `npm run electron:build` | Builds your application and creates an app consumable based on your operating system | + +**Your application is optimised. Only /dist folder and node dependencies are included in the executable.** + +## You want to use a specific lib (like rxjs) in electron main thread ? + +YES! You can do it! Just by importing your library in npm dependencies section (not **devDependencies**) with `npm install --save`. It will be loaded by electron during build phase and added to your final package. Then use your library by importing it in `main.ts` file. Quite simple, isn't it ? + +## Browser mode + +Maybe you want to execute the application in the browser with hot reload ? Just run `npm run ng:serve:web`. +**Note that you can't use Electron or NodeJS native libraries in this case.** Please check `providers/electron.service.ts` to watch how conditional import of electron/Native libraries is done. + +## Branch & Packages version + +- Angular 4 & Electron 1 : Branch [angular4](https://github.com/maximegris/angular-electron/tree/angular4) +- Angular 5 & Electron 1 : Branch [angular5](https://github.com/maximegris/angular-electron/tree/angular5) +- Angular 6 & Electron 3 : Branch [angular6](https://github.com/maximegris/angular-electron/tree/angular6) +- Angular 7 & Electron 3 : Branch [angular7](https://github.com/maximegris/angular-electron/tree/angular7) +- Angular 8 & Electron 7 : Branch [angular8](https://github.com/maximegris/angular-electron/tree/angular8) +- Angular 9 & Electron 7 : Branch [angular9](https://github.com/maximegris/angular-electron/tree/angular9) +- Angular 10 & Electron 9 : (master) + +[build-badge]: https://travis-ci.org/maximegris/angular-electron.svg?branch=master&style=style=flat-square +[build]: https://travis-ci.org/maximegris/angular-electron +[license-badge]: https://img.shields.io/badge/license-Apache2-blue.svg?style=style=flat-square +[license]: https://github.com/maximegris/angular-electron/blob/master/LICENSE.md +[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square +[prs]: http://makeapullrequest.com +[github-watch-badge]: https://img.shields.io/github/watchers/maximegris/angular-electron.svg?style=social +[github-watch]: https://github.com/maximegris/angular-electron/watchers +[github-star-badge]: https://img.shields.io/github/stars/maximegris/angular-electron.svg?style=social +[github-star]: https://github.com/maximegris/angular-electron/stargazers +[twitter]: https://twitter.com/intent/tweet?text=Check%20out%20angular-electron!%20https://github.com/maximegris/angular-electron%20%F0%9F%91%8D +[twitter-badge]: https://img.shields.io/twitter/url/https/github.com/maximegris/angular-electron.svg?style=social +[maintained-badge]: https://img.shields.io/badge/maintained-yes-brightgreen diff --git a/WowUp/wowup-electron/_config.yml b/WowUp/wowup-electron/_config.yml new file mode 100644 index 0000000..b4fb3bc --- /dev/null +++ b/WowUp/wowup-electron/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-architect diff --git a/WowUp/wowup-electron/angular.json b/WowUp/wowup-electron/angular.json new file mode 100644 index 0000000..8907011 --- /dev/null +++ b/WowUp/wowup-electron/angular.json @@ -0,0 +1,170 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "angular-electron": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "architect": { + "build": { + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "main": "src/main.ts", + "tsConfig": "src/tsconfig.app.json", + "polyfills": "src/polyfills.ts", + "assets": [ + "src/assets" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [], + "customWebpackConfig": { + "path": "./angular.webpack.js" + } + }, + "configurations": { + "dev": { + "optimization": false, + "outputHashing": "none", + "sourceMap": true, + "namedChunks": false, + "aot": false, + "extractLicenses": false, + "vendorChunk": false, + "buildOptimizer": false, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.dev.ts" + } + ] + }, + "web": { + "optimization": false, + "outputHashing": "none", + "sourceMap": true, + "namedChunks": false, + "aot": false, + "extractLicenses": false, + "vendorChunk": false, + "buildOptimizer": false, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.web.ts" + } + ] + }, + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] + } + } + }, + "serve": { + "builder": "@angular-builders/custom-webpack:dev-server", + "options": { + "browserTarget": "angular-electron:build" + }, + "configurations": { + "dev": { + "browserTarget": "angular-electron:build:dev" + }, + "web": { + "browserTarget": "angular-electron:build:web" + }, + "production": { + "browserTarget": "angular-electron:build:production" + } + }, + "defaultConfiguration": "dev" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "angular-electron:build" + } + }, + "test": { + "builder": "@angular-builders/custom-webpack:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills-test.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "scripts": [], + "styles": [ + "src/styles.scss" + ], + "assets": [ + "src/assets" + ], + "customWebpackConfig": { + "path": "./angular.webpack.js" + } + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "eslintConfig": ".eslintrc.json", + "lintFilePatterns": [ + "src/**/*.ts", + "app/**.ts" + ] + } + } + } + }, + "angular-electron-e2e": { + "root": "e2e", + "projectType": "application", + "architect": { + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "eslintConfig": ".eslintrc.json", + "lintFilePatterns": [ + "e2e/**/*.ts" + ] + } + } + } + } + }, + "schematics": { + "@schematics/angular:component": { + "prefix": "app", + "style": "scss" + }, + "@schematics/angular:directive": { + "prefix": "app" + }, + "@angular-eslint/schematics:application": { + "setParserOptionsProject": true + }, + "@angular-eslint/schematics:library": { + "setParserOptionsProject": true + } + }, + "cli": { + "analytics": "32a89162-9c82-4828-883e-69877ba4eca3" + } +} diff --git a/WowUp/wowup-electron/angular.webpack.js b/WowUp/wowup-electron/angular.webpack.js new file mode 100644 index 0000000..0f726d6 --- /dev/null +++ b/WowUp/wowup-electron/angular.webpack.js @@ -0,0 +1,23 @@ +/** + * Custom angular webpack configuration + */ + + module.exports = (config, options) => { + config.target = "electron-renderer"; + + if (options.fileReplacements) { + for (let fileReplacement of options.fileReplacements) { + if (fileReplacement.replace !== "src/environments/environment.ts") { + continue; + } + + let fileReplacementParts = fileReplacement["with"].split("."); + if (fileReplacementParts.length > 1 && ["web"].indexOf(fileReplacementParts[1]) >= 0) { + config.target = "web"; + } + break; + } + } + + return config; +}; diff --git a/WowUp/wowup-electron/app/app-menu.ts b/WowUp/wowup-electron/app/app-menu.ts new file mode 100644 index 0000000..2bfdf2e --- /dev/null +++ b/WowUp/wowup-electron/app/app-menu.ts @@ -0,0 +1,192 @@ +import * as platform from "./platform"; +import { app, BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions } from "electron"; +import { + IPC_MENU_ZOOM_IN_CHANNEL, + IPC_MENU_ZOOM_OUT_CHANNEL, + IPC_MENU_ZOOM_RESET_CHANNEL, +} from "../src/common/constants"; +import { MenuConfig } from "../src/common/wowup/models"; + +export function onMenuZoomIn(win: BrowserWindow): void { + win?.webContents.send(IPC_MENU_ZOOM_IN_CHANNEL); +} + +export function onMenuZoomOut(win: BrowserWindow): void { + win?.webContents.send(IPC_MENU_ZOOM_OUT_CHANNEL); +} + +export function onMenuZoomReset(win: BrowserWindow): void { + win?.webContents.send(IPC_MENU_ZOOM_RESET_CHANNEL); +} + +function createMacMenuItems(win: BrowserWindow, config?: MenuConfig): Array { + if (config === undefined) { + return []; + } + + const viewMenu: MenuItemConstructorOptions = { + label: config.viewLabel, + submenu: [ + { label: config.reloadLabel, role: "reload" }, + { label: config.forceReloadLabel, role: "forceReload" }, + { label: config.toggleDevToolsLabel, role: "toggleDevTools", accelerator: "CommandOrControl+Shift+I" }, + { type: "separator" }, + ], + }; + + const viewMenuArr = viewMenu.submenu as MenuItemConstructorOptions[]; + createViewMenu(viewMenuArr, win, config); + + viewMenuArr.push({ type: "separator" }, { label: config.toggleFullScreenLabel, role: "togglefullscreen" }); + + return [ + { + label: app.name, + submenu: [{ label: config.quitLabel, role: "quit" }], + }, + { + label: config.editLabel, + submenu: [ + { label: config.undoLabel, role: "undo" }, + { label: config.redoLabel, role: "redo" }, + { type: "separator" }, + { label: config.cutLabel, role: "cut" }, + { label: config.copyLabel, role: "copy" }, + { label: config.pasteLabel, role: "paste" }, + { label: config.selectAllLabel, role: "selectAll" }, + ], + }, + viewMenu, + { + label: config.windowLabel, + submenu: [ + { + label: config.windowCloseLabel, + role: "close" /*click: () => win?.close(), accelerator: "CommandOrControl+w"*/, + }, + ], + }, + ]; +} + +function createLinuxMenuItems(win: BrowserWindow, config?: MenuConfig): Array { + if (config === undefined) { + return []; + } + + const viewMenu: MenuItemConstructorOptions = { + label: config.viewLabel, + submenu: [ + { label: config.reloadLabel, role: "reload" }, + { label: config.forceReloadLabel, role: "forceReload" }, + { label: config.toggleDevToolsLabel, role: "toggleDevTools", accelerator: "CommandOrControl+Shift+I" }, + ], + }; + + const submenu = viewMenu.submenu as MenuItemConstructorOptions[]; + createViewMenu(submenu, win, config); + + submenu.push({ type: "separator" }, { label: config.toggleFullScreenLabel, role: "togglefullscreen" }); + + return [ + { + label: app.name, + submenu: [{ label: config.quitLabel, role: "quit" }], + }, + { + label: "Edit", + submenu: [ + { label: config.undoLabel, role: "undo" }, + { label: config.redoLabel, role: "redo" }, + { type: "separator" }, + { label: config.cutLabel, role: "cut" }, + { label: config.copyLabel, role: "copy" }, + { label: config.pasteLabel, role: "paste" }, + { label: config.selectAllLabel, role: "selectAll" }, + ], + }, + viewMenu, + ]; +} + +function createWindowsMenuItems(win: BrowserWindow, config?: MenuConfig): Array { + if (config === undefined) { + return []; + } + + const viewMenu: MenuItemConstructorOptions = { + label: config.viewLabel, + submenu: [{ label: config.toggleDevToolsLabel, role: "toggleDevTools", accelerator: "CommandOrControl+Shift+I" }], + }; + + const submenu = viewMenu.submenu as MenuItemConstructorOptions[]; + createViewMenu(submenu, win, config); + + submenu.push({ type: "separator" }, { label: config.toggleFullScreenLabel, role: "togglefullscreen" }); + + return [viewMenu]; +} + +function createViewMenu(submenu: MenuItemConstructorOptions[], win: BrowserWindow, config?: MenuConfig): void { + if (!config) { + return; + } + + submenu.push( + { + label: config.zoomInLabel, + click: () => onMenuZoomIn(win), + accelerator: "CommandOrControl+=", + }, + { + label: config.zoomOutLabel, + click: () => onMenuZoomOut(win), + accelerator: "CommandOrControl+-", + }, + { + label: config.zoomResetLabel, + click: () => onMenuZoomReset(win), + accelerator: "CommandOrControl+0", + }, + { + label: config.zoomInLabel + "num", + visible: false, + click: () => onMenuZoomIn(win), + accelerator: "CommandOrControl+numadd", + }, + { + label: config.zoomOutLabel + "num", + visible: false, + click: () => onMenuZoomOut(win), + accelerator: "CommandOrControl+numsub", + } + ); +} + +function createMenuItems(win: BrowserWindow, config?: MenuConfig): Array { + if (!config) { + return []; + } + + if (platform.isWin) { + return createWindowsMenuItems(win, config); + } else if (platform.isMac) { + return createMacMenuItems(win, config); + } else if (platform.isLinux) { + return createLinuxMenuItems(win, config); + } + + return []; +} + +export function createAppMenu(win: BrowserWindow | null, config?: MenuConfig): boolean { + if (win === null) { + return false; + } + + const menuItems = createMenuItems(win, config); + + Menu.setApplicationMenu(Menu.buildFromTemplate(menuItems)); + + return true; +} diff --git a/WowUp/wowup-electron/app/app-updater.ts b/WowUp/wowup-electron/app/app-updater.ts new file mode 100644 index 0000000..becff58 --- /dev/null +++ b/WowUp/wowup-electron/app/app-updater.ts @@ -0,0 +1,113 @@ +import { app, BrowserWindow, ipcMain } from "electron"; +import * as log from "electron-log/main"; +import { autoUpdater } from "electron-updater"; +import { IPC_APP_CHECK_UPDATE, IPC_APP_INSTALL_UPDATE, IPC_APP_UPDATE_STATE } from "../src/common/constants"; +import { AppUpdateDownloadProgress, AppUpdateEvent, AppUpdateState } from "../src/common/wowup/models"; +import { WowUpReleaseChannelType } from "../src/common/wowup/wowup-release-channel-type"; +import { getWowUpReleaseChannelPreference } from "./preferences"; + +class AppUpdater { + private _win: BrowserWindow; + + public dispose(): void {} + + public init(win: BrowserWindow) { + this._win = win; + this.initUpdater(); + this.initIpcHandlers(); + } + + public async checkForUpdates(): Promise { + try { + const result = await autoUpdater.checkForUpdates(); + log.info(`checkForUpdates`, result); + } catch (e) { + log.error("checkForUpdates", e); + } + } + + private initIpcHandlers() { + ipcMain.on(IPC_APP_CHECK_UPDATE, () => { + this.checkForUpdates().catch((e) => console.error(e)); + }); + + // Used this solution for Mac support + // https://github.com/electron-userland/electron-builder/issues/1604#issuecomment-372091881 + ipcMain.on(IPC_APP_INSTALL_UPDATE, () => { + app.removeAllListeners("window-all-closed"); + const browserWindows = BrowserWindow.getAllWindows(); + browserWindows.forEach(function (browserWindow) { + browserWindow.removeAllListeners("close"); + }); + autoUpdater.quitAndInstall(); + }); + + ipcMain.handle("set-release-channel", (evt, channel: WowUpReleaseChannelType) => { + autoUpdater.allowPrerelease = channel === WowUpReleaseChannelType.Beta; + log.info(`set-release-channel: allowPreRelease = ${autoUpdater.allowPrerelease.toString()}`); + }); + } + + private initUpdater() { + autoUpdater.logger = log; + autoUpdater.autoDownload = true; + autoUpdater.allowPrerelease = getWowUpReleaseChannelPreference() === WowUpReleaseChannelType.Beta; + + autoUpdater.on("checking-for-update", () => { + log.info("autoUpdater checking-for-update"); + const evt: AppUpdateEvent = { + state: AppUpdateState.CheckingForUpdate, + }; + + this._win?.webContents?.send(IPC_APP_UPDATE_STATE, evt); + }); + + autoUpdater.on("update-available", (info) => { + log.info("autoUpdater update-available", info); + const evt: AppUpdateEvent = { + state: AppUpdateState.UpdateAvailable, + }; + + this._win?.webContents?.send(IPC_APP_UPDATE_STATE, evt); + }); + + autoUpdater.on("update-not-available", (info) => { + log.info("autoUpdater update-not-available", info); + const evt: AppUpdateEvent = { + state: AppUpdateState.UpdateNotAvailable, + }; + + this._win?.webContents?.send(IPC_APP_UPDATE_STATE, evt); + }); + + autoUpdater.on("download-progress", (progressObj: AppUpdateDownloadProgress) => { + const evt: AppUpdateEvent = { + state: AppUpdateState.Downloading, + progress: { ...progressObj }, + }; + + this._win?.webContents?.send(IPC_APP_UPDATE_STATE, evt); + }); + + autoUpdater.on("update-downloaded", () => { + log.info("autoUpdater update-downloaded"); + const evt: AppUpdateEvent = { + state: AppUpdateState.Downloaded, + }; + + this._win?.webContents?.send(IPC_APP_UPDATE_STATE, evt); + }); + + autoUpdater.on("error", (e) => { + log.error("autoUpdater error", e); + const evt: AppUpdateEvent = { + state: AppUpdateState.Error, + error: e.toString(), + }; + + this._win?.webContents?.send(IPC_APP_UPDATE_STATE, evt); + }); + } +} + +export const appUpdater = new AppUpdater(); diff --git a/WowUp/wowup-electron/app/curse-folder-scanner.ts b/WowUp/wowup-electron/app/curse-folder-scanner.ts new file mode 100644 index 0000000..914a261 --- /dev/null +++ b/WowUp/wowup-electron/app/curse-folder-scanner.ts @@ -0,0 +1,261 @@ +import * as path from "path"; +import * as _ from "lodash"; +import * as log from "electron-log/main"; +import { exists, readDirRecursive } from "./file.utils"; +import * as fsp from "fs/promises"; +import { firstValueFrom, from, mergeMap, toArray } from "rxjs"; +import { AddonScanResult } from "wowup-lib-core"; + +const nativeAddon = require("../build/Release/addon.node"); + +const INVALID_PATH_CHARS = [ + "|", + "\0", + "\u0001", + "\u0002", + "\u0003", + "\u0004", + "\u0005", + "\u0006", + "\b", + "\t", + "\n", + "\v", + "\f", + "\r", + "\u000e", + "\u000f", + "\u0010", + "\u0011", + "\u0012", + "\u0013", + "\u0014", + "\u0015", + "\u0016", + "\u0017", + "\u0018", + "\u0019", + "\u001a", + "\u001b", + "\u001c", + "\u001d", + "\u001e", + "\u001f", +]; + +export class CurseFolderScanner { + // This map is required for solving for case sensitive mismatches from addon authors on Linux + private _fileMap: { [key: string]: string } = {}; + + private get tocFileCommentsRegex() { + return /\s*#.*$/gim; + } + + private get tocFileIncludesRegex() { + return /^\s*((?:(?/gis; + } + + private get bindingsXmlCommentsRegex() { + return //gims; + } + + public async scanFolder(folderPath: string): Promise { + const fileList = await readDirRecursive(folderPath); + fileList.forEach((fp) => (this._fileMap[fp.toLowerCase()] = fp)); + + let matchingFiles = await this.getMatchingFiles(folderPath, fileList); + matchingFiles = _.orderBy(matchingFiles, [(f) => f.toLowerCase()], ["asc"]); + + const toFileHash = async (path: string) => { + try { + return await this.getFileHash(path); + } catch (e) { + log.error(`Failed to get filehash: ${path}`, e); + return -1; + } + }; + + let individualFingerprints = await firstValueFrom( + from(matchingFiles).pipe( + mergeMap((file) => from(toFileHash(file)), 3), + toArray() + ) + ); + + individualFingerprints = _.filter(individualFingerprints, (fp) => fp >= 0); + + const hashConcat = _.orderBy(individualFingerprints).join(""); + const fingerprint = this.getStringHash(hashConcat); + + return { + source: "curseforge", + path: folderPath, + fileCount: matchingFiles.length, + fingerprint: fingerprint.toString(), + fingerprintNum: fingerprint, + folderName: path.basename(folderPath), + }; + } + + private async getMatchingFiles(folderPath: string, filePaths: string[]): Promise { + const parentDir = path.normalize(path.dirname(folderPath) + path.sep); + const matchingFileList: string[] = []; + const fileInfoList: string[] = []; + + for (const filePath of filePaths) { + const input = filePath.toLowerCase().replace(parentDir.toLowerCase(), ""); + + if (this.tocFileRegex.test(input)) { + fileInfoList.push(filePath); + } else if (this.bindingsXmlRegex.test(input)) { + matchingFileList.push(filePath); + } + } + + // log.debug("fileInfoList", fileInfoList.length); + for (const fileInfo of fileInfoList) { + await this.processIncludeFile(matchingFileList, fileInfo); + } + + return matchingFileList; + } + + private async processIncludeFile(matchingFileList: string[], fileInfo: string) { + let nativePath = ""; + try { + nativePath = this.getRealPath(fileInfo); + } catch (e) { + return; + } + + const pathExists = await exists(nativePath); + if (!pathExists || matchingFileList.indexOf(nativePath) !== -1) { + return; + } + + matchingFileList.push(nativePath); + + let input = await fsp.readFile(nativePath, { encoding: "utf-8" }); + input = this.removeComments(nativePath, input); + + const inclusions = this.getFileInclusionMatches(nativePath, input); + if (!inclusions || !inclusions.length) { + return; + } + + const dirname = path.dirname(nativePath); + for (const include of inclusions) { + if (this.hasInvalidPathChars(include)) { + log.debug(`Invalid include file ${nativePath}`); + break; + } + + const fileName = path.join(dirname, include.replace(/\\/g, path.sep)); + await this.processIncludeFile(matchingFileList, fileName); + } + } + + private hasInvalidPathChars(path: string) { + return INVALID_PATH_CHARS.some((c) => path.indexOf(c) !== -1); + } + + private getFileInclusionMatches(fileInfo: string, fileContent: string): string[] | null { + const ext = path.extname(fileInfo); + switch (ext) { + case ".xml": + return this.ripMatch(fileContent, () => this.bindingsXmlIncludesRegex); + case ".toc": + return this.ripMatch(fileContent, () => this.tocFileIncludesRegex); + default: + return null; + } + } + + private removeComments(fileInfo: string, fileContent: string): string { + const ext = path.extname(fileInfo); + switch (ext) { + case ".xml": + return fileContent.replace(this.bindingsXmlCommentsRegex, ""); + case ".toc": + return fileContent.replace(this.tocFileCommentsRegex, ""); + default: + return fileContent; + } + } + + /** + * Recreate a strange behavior for .net regex regarding how it treats + * lines that end in \r\t vs lines with \r\n\t + */ + private ripMatch(str: string, regex: () => RegExp): string[] { + const splitStrings = str.split("\n"); + const matches = []; + try { + for (const splitStr of splitStrings) { + const trimmedStr = splitStr.trim(); + const match = regex().exec(trimmedStr); + if (match && match.length > 1) { + matches.push(match[1]); + } + } + } catch (e) { + log.error(e); + } + + return matches.map((s) => s.trim()); + } + + private matchAll(str: string, regex: RegExp): string[] { + const matches: string[] = []; + let currentMatch: RegExpExecArray | null; + do { + currentMatch = regex.exec(str); + if (currentMatch) { + matches.push(currentMatch[1]); + } + } while (currentMatch); + + return matches; + } + + private getStringHash(targetString: string, targetStringEncoding?: BufferEncoding): number { + try { + const strBuffer = Buffer.from(targetString, targetStringEncoding || "ascii"); + + const hash = nativeAddon.computeHash(strBuffer, strBuffer.length); + + return hash; + } catch (err) { + log.error(err); + log.info(targetString, targetStringEncoding); + throw err; + } + } + + private async getFileHash(filePath: string): Promise { + const buffer = await fsp.readFile(filePath); + const hash = nativeAddon.computeHash(buffer, buffer.length); + return hash; + } + + private getRealPath(filePath: string) { + const lowerPath = filePath.toLowerCase(); + const matchedPath = this._fileMap[lowerPath]; + if (!matchedPath) { + throw new Error(`Path not found: ${lowerPath}`); + } + return matchedPath; + } +} diff --git a/WowUp/wowup-electron/app/file.utils.ts b/WowUp/wowup-electron/app/file.utils.ts new file mode 100644 index 0000000..55e552a --- /dev/null +++ b/WowUp/wowup-electron/app/file.utils.ts @@ -0,0 +1,257 @@ +import { exec } from "child_process"; +import * as log from "electron-log/main"; +import * as fsp from "fs/promises"; +import { max, sumBy } from "lodash"; +import * as path from "path"; +import * as crypto from "crypto"; +import * as AdmZip from "adm-zip"; +import * as globrex from "globrex"; + +import { TreeNode } from "../src/common/models/ipc-events"; +import { GetDirectoryTreeOptions } from "../src/common/models/ipc-request"; +import { isWin } from "./platform"; +import { ZipEntry } from "../src/common/models/ipc-response"; + +export function zipFile(srcPath: string, outPath: string): Promise { + return new Promise((resolve, reject) => { + const zip = new AdmZip(); + zip.addLocalFolder(srcPath); + + zip.writeZip(outPath, (e) => { + return e ? reject(e) : resolve(true); + }); + }); +} + +export function readFileInZip(zipPath: string, filePath: string): Promise { + return new Promise((resolve, reject) => { + const zip = new AdmZip(zipPath); + zip.readAsTextAsync(filePath, (data, err) => { + return err ? reject(err) : resolve(data); + }); + }); +} + +export function listZipFiles(zipPath: string, filter: string): ZipEntry[] { + const zip = new AdmZip(zipPath); + const entries = zip.getEntries(); + + const globFilter = globrex(filter); + const matches = entries.filter((entry) => globFilter.regex.test(entry.name)); + + return matches.map((entry) => { + return { + isDirectory: entry.isDirectory, + name: entry.name, + path: entry.entryName, + }; + }); +} + +export async function exists(path: string): Promise { + try { + await fsp.access(path); + return true; + } catch (e) { + log.warn(`File does not exist: ${path}`); + log.warn(e.message); + return false; + } +} + +export async function chmodDir(dirPath: string, mode: number | string): Promise { + const entries = await fsp.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(dirPath, entry.name); + + if (entry.isDirectory()) { + await chmodDir(srcPath, mode); + } else { + await fsp.chmod(srcPath, mode); + } + } +} + +export async function copyDir(src: string, dest: string): Promise { + await fsp.mkdir(dest, { recursive: true }); + const entries = await fsp.readdir(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + await copyDir(srcPath, destPath); + } else { + await fsp.copyFile(srcPath, destPath); + } + } +} + +export async function remove(path: string): Promise { + const stat = await fsp.stat(path); + if (stat.isDirectory()) { + await rmdir(path); + } else { + await fsp.unlink(path); + } +} + +/** + * On Windows, users that use the Google Drive sync tool are unable to delete any folders. + * Seems to be a node issue that it cannot delete even empty folders synced by this tool. + * However, if you use CMD to delete the folder it works fine? + */ +async function rmdir(path: string): Promise { + if (isWin) { + await rmdirWin(path); + } else { + await fsp.rm(path, { recursive: true, force: true }); + } +} + +async function rmdirWin(path: string, retryCount = 0, lastError: Error = null): Promise { + if (retryCount > 10) { + throw new Error(lastError?.toString()); + } + + try { + return executeWinRm(path); + } catch (e) { + log.error("rmdirWin", path, retryCount, e); + await delay(retryCount); + return rmdirWin(path, retryCount + 1, e as Error); + } +} + +function delay(retryCount: number, period = 500) { + return new Promise((resolve) => { + setTimeout(resolve, period * retryCount); + }); +} + +function executeWinRm(path: string) { + return new Promise((resolve, reject) => { + exec(`rmdir "${path}" /s /q`, (err, stdout, stderr) => { + if (err || stdout.length || stderr.length) { + log.error("rmdir fallback failed", err, stdout, stderr); + return reject(new Error("rmdir fallback failed")); + } + resolve(undefined); + }); + }); +} + +export async function readDirRecursive(sourcePath: string): Promise { + let hardPath = sourcePath; + + const sourceStats = await fsp.lstat(sourcePath); + if (sourceStats.isSymbolicLink()) { + hardPath = await fsp.readlink(sourcePath); + } + + const dirFiles: string[] = []; + const files = await fsp.readdir(hardPath, { withFileTypes: true }); + + for (const file of files) { + const filePath = path.join(hardPath, file.name); + if (file.isDirectory()) { + const nestedFiles = await readDirRecursive(filePath); + dirFiles.push(...nestedFiles); + } else { + dirFiles.push(filePath); + } + } + + return dirFiles; +} + +export async function getDirTree(sourcePath: string, opts?: GetDirectoryTreeOptions): Promise { + let hardPath = sourcePath; + + // Check if a symlink was passed in, if so get the actual path + let dirStats = await fsp.lstat(sourcePath); + if (dirStats.isSymbolicLink()) { + hardPath = await fsp.readlink(sourcePath); + } + + // Verify that a directory was passed in + dirStats = await fsp.lstat(hardPath); + if (!dirStats.isDirectory()) { + throw new Error(`getDirTree path was not a directory: ${hardPath}`); + } + + const files = await fsp.readdir(hardPath, { withFileTypes: true }); + + const node: TreeNode = { + name: path.basename(hardPath), + path: hardPath, + children: [], + isDirectory: true, + size: 0, + }; + + for (const file of files) { + const filePath = path.join(hardPath, file.name); + if (file.isDirectory()) { + const nestedNode = await getDirTree(filePath, opts); + node.children.push(nestedNode); + node.size = sumBy(node.children, (n) => n.size); + if (opts?.includeHash) { + node.hash = hashString(node.children.map((n) => n.hash).join(""), "sha256"); + } + } else { + let hash = ""; + if (opts?.includeHash) { + hash = await hashFile(filePath, "sha256"); + } + + const stats = await fsp.stat(filePath); + node.size += stats.size; + node.children.push({ + name: file.name, + path: filePath, + children: [], + isDirectory: false, + size: stats.size, + hash, + }); + } + } + + if (opts?.includeHash) { + node.hash = hashString(node.children.map((n) => n.hash).join(""), "sha256"); + } + + return node; +} + +export async function getLastModifiedFileDate(sourcePath: string): Promise { + const dirFiles = await readDirRecursive(sourcePath); + const dates: number[] = []; + for (const file of dirFiles) { + const stat = await fsp.stat(file); + dates.push(stat.mtimeMs); + } + + const latest = max(dates); + return latest; +} + +export function hashString(str: string | crypto.BinaryLike, alg = "md5"): string { + const md5 = crypto.createHash(alg); + md5.update(str); + return md5.digest("hex"); +} + +export async function hashFile(filePath: string, alg = "md5"): Promise { + try { + const text = await fsp.readFile(filePath); + return hashString(text, alg); + } catch (e) { + log.error(`hashFile failed: ${filePath}`); + log.error(e); + throw e; + } +} diff --git a/WowUp/wowup-electron/app/ipc-events.ts b/WowUp/wowup-electron/app/ipc-events.ts new file mode 100644 index 0000000..921a976 --- /dev/null +++ b/WowUp/wowup-electron/app/ipc-events.ts @@ -0,0 +1,813 @@ +import { + app, + BrowserWindow, + clipboard, + dialog, + ipcMain, + IpcMainInvokeEvent, + net, + OpenDialogOptions, + Settings, + shell, + systemPreferences, +} from "electron"; +import * as log from "electron-log/main"; +import * as globrex from "globrex"; +import * as _ from "lodash"; +import { nanoid } from "nanoid"; +import * as nodeDiskInfo from "node-disk-info"; +import * as path from "path"; +import { Transform } from "stream"; +import * as yauzl from "yauzl"; +import * as fs from "fs"; +import * as os from "os"; + +import { + IPC_ADDONS_SAVE_ALL, + IPC_CLOSE_WINDOW, + IPC_COPY_FILE_CHANNEL, + IPC_CREATE_APP_MENU_CHANNEL, + IPC_CREATE_DIRECTORY_CHANNEL, + IPC_CREATE_TRAY_MENU_CHANNEL, + IPC_DELETE_DIRECTORY_CHANNEL, + IPC_DOWNLOAD_FILE_CHANNEL, + IPC_FOCUS_WINDOW, + IPC_GET_APP_VERSION, + IPC_GET_HOME_DIR, + IPC_GET_ASSET_FILE_PATH, + IPC_GET_DIRECTORY_TREE, + IPC_GET_LATEST_DIR_UPDATE_TIME, + IPC_GET_LAUNCH_ARGS, + IPC_GET_LOCALE, + IPC_GET_LOGIN_ITEM_SETTINGS, + IPC_GET_PENDING_OPEN_URLS, + IPC_GET_ZOOM_FACTOR, + IPC_IS_DEFAULT_PROTOCOL_CLIENT, + IPC_LIST_DIR_RECURSIVE, + IPC_LIST_DIRECTORIES_CHANNEL, + IPC_LIST_DISKS_WIN32, + IPC_LIST_ENTRIES, + IPC_LIST_FILES_CHANNEL, + IPC_MAXIMIZE_WINDOW, + IPC_MINIMIZE_WINDOW, + IPC_PATH_EXISTS_CHANNEL, + IPC_QUIT_APP, + IPC_READ_FILE_BUFFER_CHANNEL, + IPC_READ_FILE_CHANNEL, + IPC_READDIR, + IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, + IPC_RESTART_APP, + IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, + IPC_SET_LOGIN_ITEM_SETTINGS, + IPC_SET_ZOOM_FACTOR, + IPC_SET_ZOOM_LIMITS, + IPC_SHOW_DIRECTORY, + IPC_SHOW_OPEN_DIALOG, + IPC_STAT_FILES_CHANNEL, + IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT, + IPC_UNZIP_FILE_CHANNEL, + IPC_UPDATE_APP_BADGE, + IPC_WINDOW_LEAVE_FULLSCREEN, + IPC_WOWUP_GET_SCAN_RESULTS, + IPC_WRITE_FILE_CHANNEL, + DEFAULT_FILE_MODE, + IPC_PUSH_INIT, + IPC_PUSH_REGISTER, + IPC_PUSH_UNREGISTER, + IPC_PUSH_SUBSCRIBE, + IPC_WINDOW_IS_FULLSCREEN, + IPC_WINDOW_IS_MAXIMIZED, + IPC_CURSE_GET_SCAN_RESULTS, + IPC_OW_IS_CMP_REQUIRED, + IPC_OW_OPEN_CMP, + ZOOM_FACTOR_KEY, +} from "../src/common/constants"; +import { CopyFileRequest } from "../src/common/models/copy-file-request"; +import { DownloadRequest } from "../src/common/models/download-request"; +import { DownloadStatus } from "../src/common/models/download-status"; +import { DownloadStatusType } from "../src/common/models/download-status-type"; +import { FsDirent, TreeNode } from "../src/common/models/ipc-events"; +import { UnzipRequest } from "../src/common/models/unzip-request"; +import { RendererChannels } from "../src/common/wowup"; +import { MenuConfig, SystemTrayConfig } from "../src/common/wowup/models"; +import { createAppMenu } from "./app-menu"; +import * as fsp from "fs/promises"; + +import { + chmodDir, + copyDir, + exists, + getDirTree, + getLastModifiedFileDate, + listZipFiles, + readDirRecursive, + readFileInZip, + remove, + zipFile, +} from "./file.utils"; +import { getAddonStore, getPreferenceStore } from "./stores"; +import { createTray } from "./system-tray"; +import { WowUpFolderScanner } from "./wowup-folder-scanner"; +import * as push from "./push"; +import { GetDirectoryTreeRequest } from "../src/common/models/ipc-request"; +import { ProductDb } from "../src/common/wowup/product-db"; +import { restoreWindow } from "./window-state"; +import { firstValueFrom, from, mergeMap, toArray } from "rxjs"; +import { CurseFolderScanner } from "./curse-folder-scanner"; +import { Addon, AddonScanResult, FsStats } from "wowup-lib-core"; + +let PENDING_OPEN_URLS: string[] = []; + +interface SymlinkDir { + original: fs.Dirent; + originalPath: string; + realPath: string; + isDir: boolean; +} + +const _dlMap = new Map void>(); + +async function getSymlinkDirs(basePath: string, files: fs.Dirent[]): Promise { + // Find and resolve symlinks found and return the folder names as + const symlinks = _.filter(files, (file) => file.isSymbolicLink()); + const symlinkDirs: SymlinkDir[] = _.map(symlinks, (sym) => { + return { + original: sym, + originalPath: path.join(basePath, sym.name), + realPath: "", + isDir: false, + }; + }); + + for (const symlinkDir of symlinkDirs) { + const realPath = await fsp.realpath(symlinkDir.originalPath); + const lstat = await fsp.lstat(realPath); + + symlinkDir.realPath = realPath; + symlinkDir.isDir = lstat.isDirectory(); + } + + return _.filter(symlinkDirs, (symDir) => symDir.isDir); +} + +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ +function handle( + channel: RendererChannels, + listener: (event: IpcMainInvokeEvent, ...args: any[]) => Promise | any +) { + ipcMain.handle(channel, listener); +} + +// When doing a cold start on mac, open-url will happen before the app is loaded +export function setPendingOpenUrl(...openUrls: string[]): void { + PENDING_OPEN_URLS = openUrls; +} + +export function initializeIpcHandlers(window: BrowserWindow): void { + log.info("process.versions", process.versions); + + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + ipcMain.on("webview-log", (evt, level, ...data) => { + switch (level) { + case "error": + log.error(...data); + break; + case "warn": + log.warn(...data); + break; + default: + log.info(...data); + break; + } + }); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ + + ipcMain.on("webview-error", (evt, err, msg) => { + log.error("webview-error", err, msg); + }); + + // Remove the pending URLs once read so they are only able to be gotten once + handle(IPC_GET_PENDING_OPEN_URLS, (): string[] => { + const urls = PENDING_OPEN_URLS; + PENDING_OPEN_URLS = []; + return urls; + }); + + handle( + IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT, + ( + _evt, + key: string, + type: "string" | "boolean" | "integer" | "float" | "double" | "url" | "array" | "dictionary" + ) => { + return systemPreferences.getUserDefault(key, type); + } + ); + + handle("clipboard-read-text", () => { + return clipboard.readText(); + }); + + handle(IPC_SHOW_DIRECTORY, async (evt, filePath: string): Promise => { + return await shell.openPath(filePath); + }); + + handle(IPC_GET_ASSET_FILE_PATH, (evt, fileName: string) => { + return path.join(__dirname, "..", "assets", fileName); + }); + + handle(IPC_CREATE_DIRECTORY_CHANNEL, async (evt, directoryPath: string): Promise => { + log.info(`[CreateDirectory] '${directoryPath}'`); + await fsp.mkdir(directoryPath, { recursive: true }); + return true; + }); + + handle(IPC_OW_IS_CMP_REQUIRED, (): boolean => { + return false; + }); + + handle(IPC_OW_OPEN_CMP, (evt, cmpTab?: string) => { + const options = {} as any; + if (cmpTab) { + options.tab = cmpTab; + } + }); + + handle(IPC_GET_ZOOM_FACTOR, () => { + return window?.webContents?.getZoomFactor(); + }); + + handle(IPC_UPDATE_APP_BADGE, (evt, count: number) => { + return app.setBadgeCount(count); + }); + + handle(IPC_SET_ZOOM_LIMITS, (evt, minimumLevel: number, maximumLevel: number) => { + return window.webContents?.setVisualZoomLevelLimits(minimumLevel, maximumLevel); + }); + + handle("show-item-in-folder", (evt, path: string) => { + shell.showItemInFolder(path); + }); + + handle(IPC_SET_ZOOM_FACTOR, (evt, zoomFactor: number) => { + if (window?.webContents) { + window.webContents.zoomFactor = zoomFactor; + } else { + log.warn("could not set zoom factor, no web contents"); + } + getPreferenceStore().set(ZOOM_FACTOR_KEY, zoomFactor); + }); + + handle(IPC_ADDONS_SAVE_ALL, (evt, addons: Addon[]) => { + if (!Array.isArray(addons)) { + return; + } + + const addonStore = getAddonStore(); + if (addonStore === undefined) { + log.warn("IPC_ADDONS_SAVE_ALL failed, addon store undefined"); + return; + } + + for (const addon of addons) { + if (typeof addon.id !== "string") { + log.warn("malformed addon not saved", addon); + continue; + } + + addonStore?.set(addon.id, addon); + } + }); + + handle(IPC_GET_APP_VERSION, () => { + return app.getVersion(); + }); + + handle(IPC_GET_LOCALE, () => { + return `${app.getLocale()}`; + }); + + handle(IPC_GET_LAUNCH_ARGS, () => { + return process.argv; + }); + + handle(IPC_GET_LOGIN_ITEM_SETTINGS, () => { + return app.getLoginItemSettings(); + }); + + handle(IPC_SET_LOGIN_ITEM_SETTINGS, (evt, settings: Settings) => { + console.log("IPC_SET_LOGIN_ITEM_SETTINGS", settings); + return app.setLoginItemSettings(settings); + }); + + handle(IPC_READDIR, async (evt, dirPath: string): Promise => { + return await fsp.readdir(dirPath); + }); + + handle(IPC_IS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => { + return app.isDefaultProtocolClient(protocol); + }); + + handle(IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => { + return app.setAsDefaultProtocolClient(protocol); + }); + + handle(IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => { + return app.removeAsDefaultProtocolClient(protocol); + }); + + handle(IPC_LIST_DIRECTORIES_CHANNEL, async (evt, filePath: string, scanSymlinks: boolean) => { + const files = await fsp.readdir(filePath, { withFileTypes: true }); + let symlinkNames: string[] = []; + if (scanSymlinks === true) { + log.info("Scanning symlinks"); + const symlinkDirs = await getSymlinkDirs(filePath, files); + symlinkNames = _.map(symlinkDirs, (symLink) => symLink.original.name); + } + + const directories = files.filter((file) => file.isDirectory()).map((file) => file.name); + return [...directories, ...symlinkNames]; + }); + + handle(IPC_STAT_FILES_CHANNEL, async (evt, filePaths: string[]) => { + const results: { [path: string]: FsStats } = {}; + + const taskResults = await firstValueFrom( + from(filePaths).pipe( + mergeMap((filePath) => from(statFile(filePath)), 3), + toArray() + ) + ); + + taskResults.forEach((r) => (results[r.path] = r.fsStats)); + + return results; + }); + + handle(IPC_LIST_ENTRIES, async (evt, sourcePath: string, filter: string) => { + const globFilter = globrex(filter); + const results = await fsp.readdir(sourcePath, { withFileTypes: true }); + const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name)); + return _.map(matches, (match) => { + const dirEnt: FsDirent = { + isBlockDevice: match.isBlockDevice(), + isCharacterDevice: match.isCharacterDevice(), + isDirectory: match.isDirectory(), + isFIFO: match.isFIFO(), + isFile: match.isFile(), + isSocket: match.isSocket(), + isSymbolicLink: match.isSymbolicLink(), + name: match.name, + }; + return dirEnt; + }); + }); + + handle(IPC_LIST_FILES_CHANNEL, async (evt, sourcePath: string, filter: string) => { + const pathExists = await exists(sourcePath); + if (!pathExists) { + return []; + } + + const globFilter = globrex(filter); + const results = await fsp.readdir(sourcePath, { withFileTypes: true }); + const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name)); + return _.map(matches, (match) => match.name); + }); + + handle(IPC_PATH_EXISTS_CHANNEL, async (evt, filePath: string) => { + if (!filePath) { + return false; + } + + try { + await fsp.access(filePath); + } catch (e) { + if (e.code !== "ENOENT") { + log.error(e); + } + return false; + } + + return true; + }); + + handle(IPC_CURSE_GET_SCAN_RESULTS, async (evt, filePaths: string[]): Promise => { + // Scan addon folders in parallel for speed!? + try { + const taskResults = await firstValueFrom( + from(filePaths).pipe( + mergeMap((folder) => from(new CurseFolderScanner().scanFolder(folder)), 2), + toArray() + ) + ); + + return taskResults; + } catch (e) { + log.error("Failed during curse scan", e); + throw e; + } + }); + + handle(IPC_WOWUP_GET_SCAN_RESULTS, async (evt, filePaths: string[]): Promise => { + const taskResults = await firstValueFrom( + from(filePaths).pipe( + mergeMap((folder) => from(new WowUpFolderScanner(folder).scanFolder()), 3), + toArray() + ) + ); + + return taskResults; + }); + + handle(IPC_UNZIP_FILE_CHANNEL, async (evt, arg: UnzipRequest) => { + await new Promise((resolve, reject) => { + yauzl.open(arg.zipFilePath, { lazyEntries: true }, (err, zipfile) => { + handleZipFile(err, zipfile, arg.outputFolder).then(resolve).catch(reject); + }); + }); + + await chmodDir(arg.outputFolder, DEFAULT_FILE_MODE); + + return arg.outputFolder; + }); + + handle("zip-file", async (evt, srcPath: string, destPath: string) => { + log.info(`[ZipFile]: '${srcPath} -> ${destPath}`); + return await zipFile(srcPath, destPath); + }); + + handle("zip-read-file", async (evt, zipPath: string, filePath: string) => { + log.info(`[ZipReadFile]: '${zipPath} : ${filePath}`); + return await readFileInZip(zipPath, filePath); + }); + + handle("zip-list-files", (evt, zipPath: string, filter: string) => { + log.info(`[ZipListEntries]: '${zipPath}`); + return listZipFiles(zipPath, filter); + }); + + handle("rename-file", async (evt, srcPath: string, destPath: string) => { + log.info(`[RenameFile]: '${srcPath} -> ${destPath}`); + return await fsp.rename(srcPath, destPath); + }); + + handle("base64-encode", (evt, content: string) => { + const buff = Buffer.from(content); + return buff.toString("base64"); + }); + + handle("base64-decode", (evt, content: string) => { + const buff = Buffer.from(content, "base64"); + return buff.toString("utf-8"); + }); + + handle(IPC_COPY_FILE_CHANNEL, async (evt, arg: CopyFileRequest): Promise => { + log.info(`[FileCopy] '${arg.sourceFilePath}' -> '${arg.destinationFilePath}'`); + const stat = await fsp.lstat(arg.sourceFilePath); + if (stat.isDirectory()) { + await copyDir(arg.sourceFilePath, arg.destinationFilePath); + await chmodDir(arg.destinationFilePath, DEFAULT_FILE_MODE); + } else { + await fsp.copyFile(arg.sourceFilePath, arg.destinationFilePath); + await fsp.chmod(arg.destinationFilePath, DEFAULT_FILE_MODE); + } + return true; + }); + + handle(IPC_DELETE_DIRECTORY_CHANNEL, async (evt, filePath: string) => { + log.info(`[FileRemove] ${filePath}`); + return await remove(filePath); + }); + + handle(IPC_READ_FILE_CHANNEL, async (evt, filePath: string) => { + return await fsp.readFile(filePath, { encoding: "utf-8" }); + }); + + handle(IPC_READ_FILE_BUFFER_CHANNEL, async (evt, filePath: string) => { + return await fsp.readFile(filePath); + }); + + handle("decode-product-db", async (evt, filePath: string) => { + const productDbData = await fsp.readFile(filePath); + const productDb = ProductDb.decode(productDbData); + setTimeout(() => { + console.log("productDb", JSON.stringify(productDb)); + },1); + + return productDb; + }); + + handle(IPC_WRITE_FILE_CHANNEL, async (evt, filePath: string, contents: string) => { + return await fsp.writeFile(filePath, contents, { encoding: "utf-8", mode: DEFAULT_FILE_MODE }); + }); + + handle(IPC_CREATE_TRAY_MENU_CHANNEL, (evt, config: SystemTrayConfig) => { + return createTray(window, config); + }); + + handle(IPC_CREATE_APP_MENU_CHANNEL, (evt, config: MenuConfig) => { + return createAppMenu(window, config); + }); + + handle(IPC_GET_LATEST_DIR_UPDATE_TIME, (evt, dirPath: string) => { + return getLastModifiedFileDate(dirPath); + }); + + handle(IPC_LIST_DIR_RECURSIVE, (evt, dirPath: string): Promise => { + return readDirRecursive(dirPath); + }); + + handle(IPC_GET_DIRECTORY_TREE, (evt, args: GetDirectoryTreeRequest): Promise => { + log.debug(IPC_GET_DIRECTORY_TREE, args); + return getDirTree(args.dirPath, args.opts); + }); + + handle(IPC_GET_HOME_DIR, (): string => { + return os.homedir(); + }); + + handle(IPC_MINIMIZE_WINDOW, () => { + if (window?.minimizable) { + window.minimize(); + } + }); + + handle(IPC_MAXIMIZE_WINDOW, () => { + if (window?.maximizable) { + if (window.isMaximized()) { + window.unmaximize(); + } else { + window.maximize(); + } + } + }); + + ipcMain.handle(IPC_WINDOW_IS_MAXIMIZED, () => { + return window?.isMaximized() ?? false; + }); + + handle(IPC_CLOSE_WINDOW, () => { + window?.close(); + }); + + handle(IPC_FOCUS_WINDOW, () => { + restoreWindow(window); + window?.focus(); + }); + + handle(IPC_RESTART_APP, () => { + log.info(`[RestartApp]`); + app.relaunch(); + app.quit(); + }); + + handle(IPC_QUIT_APP, () => { + log.info(`[QuitApp]`); + app.quit(); + }); + + handle(IPC_LIST_DISKS_WIN32, async () => { + const diskInfos = await nodeDiskInfo.getDiskInfo(); + // Cant pass complex objects over the wire, make them simple + return diskInfos.map((di) => { + return { + mounted: di.mounted, + filesystem: di.filesystem, + }; + }); + }); + + handle(IPC_WINDOW_LEAVE_FULLSCREEN, () => { + window?.setFullScreen(false); + }); + + ipcMain.handle(IPC_WINDOW_IS_FULLSCREEN, () => { + return window?.isFullScreen() ?? false; + }); + + handle(IPC_SHOW_OPEN_DIALOG, async (evt, options: OpenDialogOptions) => { + return await dialog.showOpenDialog(options); + }); + + handle(IPC_PUSH_INIT, () => { + return push.startPushService(); + }); + + handle(IPC_PUSH_REGISTER, async (evt, appId: string) => { + return await push.registerForPush(appId); + }); + + handle(IPC_PUSH_UNREGISTER, async () => { + return await push.unregisterPush(); + }); + + handle(IPC_PUSH_SUBSCRIBE, async (evt, channel: string) => { + return await push.subscribeToChannel(channel); + }); + + handle("get-focus", () => { + return window.isFocused(); + }); + + ipcMain.on(IPC_DOWNLOAD_FILE_CHANNEL, (evt, arg: DownloadRequest) => { + handleDownloadFile(arg).catch((e) => log.error(e.toString())); + }); + + // In order to allow concurrent downloads, we have to get creative with this session handler + window.webContents.session.on("will-download", (evt, item, wc) => { + for (const key of _dlMap.keys()) { + log.info(`will-download: ${key}`); + if (!item.getURLChain().includes(key)) { + continue; + } + + try { + const action = _dlMap.get(key); + if (typeof action === "function") { + action?.call(null, evt, item, wc); + } else { + log.warn("could not call will-download action, undefined"); + } + } catch (e) { + log.error(e); + } finally { + _dlMap.delete(key); + } + } + }); + + async function statFile(filePath: string) { + const stats = await fsp.stat(filePath); + const fsStats: FsStats = { + atime: stats.atime, + atimeMs: stats.atimeMs, + birthtime: stats.birthtime, + birthtimeMs: stats.birthtimeMs, + blksize: stats.blksize, + blocks: stats.blocks, + ctime: stats.ctime, + ctimeMs: stats.ctimeMs, + dev: stats.dev, + gid: stats.gid, + ino: stats.ino, + isBlockDevice: stats.isBlockDevice(), + isCharacterDevice: stats.isCharacterDevice(), + isDirectory: stats.isDirectory(), + isFIFO: stats.isFIFO(), + isFile: stats.isFile(), + isSocket: stats.isSocket(), + isSymbolicLink: stats.isSymbolicLink(), + mode: stats.mode, + mtime: stats.mtime, + mtimeMs: stats.mtimeMs, + nlink: stats.nlink, + rdev: stats.rdev, + size: stats.size, + uid: stats.uid, + }; + return { path: filePath, fsStats }; + } + + async function handleDownloadFile(arg: DownloadRequest) { + const status: DownloadStatus = { + type: DownloadStatusType.Pending, + savePath: "", + }; + + try { + await fsp.mkdir(arg.outputFolder, { recursive: true }); + + const downloadUrl = new URL(arg.url); + if (typeof arg.auth?.queryParams === "object") { + for (const [key, value] of Object.entries(arg.auth.queryParams)) { + downloadUrl.searchParams.set(key, value); + } + } + + const savePath = path.join(arg.outputFolder, `${nanoid()}-${arg.fileName}`); + log.info(`[DownloadFile] '${downloadUrl.toString()}' -> '${savePath}'`); + + const url = downloadUrl.toString(); + const writer = fs.createWriteStream(savePath); + + try { + await new Promise((resolve, reject) => { + let size = 0; + let percentMod = -1; + + const req = net.request({ + url, + redirect: "manual", + }); + + if (typeof arg.auth?.headers === "object") { + for (const [key, value] of Object.entries(arg.auth.headers)) { + log.info(`Setting header: ${key}=${value.substring(0, 3)}***`); + req.setHeader(key, value); + } + } + + req.on("redirect", (status, method, redirectUrl) => { + log.info(`[download] caught redirect`, status, redirectUrl); + req.followRedirect(); + }); + + req.on("response", (response) => { + const fileLength = parseInt((response.headers["content-length"] as string) ?? "0", 10); + + response.on("data", (data) => { + writer.write(data, () => { + size += data.length; + const percent = fileLength <= 0 ? 0 : Math.floor((size / fileLength) * 100); + if (percent % 5 === 0 && percentMod !== percent) { + percentMod = percent; + log.debug(`Write: [${percent}] ${size}`); + } + }); + }); + + response.on("end", () => { + if (response.statusCode < 200 || response.statusCode >= 300) { + return reject(new Error(`Invalid response (${response.statusCode}): ${url}`)); + } + + return resolve(undefined); + }); + response.on("error", (err) => { + return reject(err); + }); + }); + req.end(); + }); + } finally { + // always close stream + writer.end(); + } + + status.type = DownloadStatusType.Complete; + status.savePath = savePath; + + window.webContents.send(arg.responseKey, status); + } catch (err) { + log.error(err); + status.type = DownloadStatusType.Error; + status.error = err; + window.webContents.send(arg.responseKey, status); + } + } +} + +// Adapted from https://github.com/thejoshwolfe/yauzl/blob/96f0eb552c560632a754ae0e1701a7edacbda389/examples/unzip.js#L124 +function handleZipFile(err: Error | null, zipfile: yauzl.ZipFile, targetDir: string): Promise { + return new Promise((resolve, reject) => { + if (err !== null) { + return reject(err); + } + + zipfile.on("close", function () { + resolve(true); + }); + + zipfile.on("error", (error: Error) => { + reject(error); + }); + + zipfile.readEntry(); + zipfile.on("entry", function (entry: yauzl.Entry) { + if (/\/$/.test(entry.fileName)) { + // directory file names end with '/' + const dirPath = path.join(targetDir, entry.fileName); + fs.mkdir(dirPath, { recursive: true }, function () { + if (err) throw err; + zipfile.readEntry(); + }); + } else { + // ensure parent directory exists + const filePath = path.join(targetDir, entry.fileName); + const parentPath = path.join(targetDir, path.dirname(entry.fileName)); + fs.mkdir(parentPath, { recursive: true }, function () { + zipfile.openReadStream(entry, (err, readStream) => { + if (err) { + throw err; + } + + const filter = new Transform(); + filter._transform = function (chunk, encoding, cb) { + cb(null, chunk); + }; + filter._flush = function (cb) { + process.stdout.write("\b \b\b \b\b \b\n"); + cb(); + zipfile.readEntry(); + }; + + // pump file contents + const writeStream = fs.createWriteStream(filePath); + readStream.pipe(filter).pipe(writeStream); + }); + }); + } + }); + }); +} diff --git a/WowUp/wowup-electron/app/main.ts b/WowUp/wowup-electron/app/main.ts new file mode 100644 index 0000000..1e1b8e9 --- /dev/null +++ b/WowUp/wowup-electron/app/main.ts @@ -0,0 +1,574 @@ +import { app, BrowserWindow, BrowserWindowConstructorOptions, dialog, powerMonitor } from "electron"; +import * as log from "electron-log/main"; +import { find } from "lodash"; +import * as minimist from "minimist"; +import { arch as osArch, release as osRelease, type as osType } from "os"; +import { join } from "path"; +import { pathToFileURL } from "url"; +import { inspect } from "util"; + +import { + APP_PROTOCOL_NAME, + APP_USER_MODEL_ID, + COLLAPSE_TO_TRAY_PREFERENCE_KEY, + CURRENT_THEME_KEY, + DEFAULT_BG_COLOR, + DEFAULT_LIGHT_BG_COLOR, + IPC_CUSTOM_PROTOCOL_RECEIVED, + IPC_POWER_MONITOR_LOCK, + IPC_POWER_MONITOR_RESUME, + IPC_POWER_MONITOR_SUSPEND, + IPC_POWER_MONITOR_UNLOCK, + IPC_PUSH_NOTIFICATION, + IPC_WINDOW_ENTER_FULLSCREEN, + IPC_WINDOW_LEAVE_FULLSCREEN, + IPC_WINDOW_MAXIMIZED, + IPC_WINDOW_MINIMIZED, + IPC_WINDOW_UNMAXIMIZED, + START_MINIMIZED_PREFERENCE_KEY, + START_WITH_SYSTEM_PREFERENCE_KEY, + USE_HARDWARE_ACCELERATION_PREFERENCE_KEY, + WINDOW_DEFAULT_HEIGHT, + WINDOW_DEFAULT_WIDTH, + WINDOW_MIN_HEIGHT, + WINDOW_MIN_WIDTH, + WOWUP_LOGO_FILENAME, +} from "../src/common/constants"; +import { AppOptions } from "../src/common/wowup/models"; +import { createAppMenu } from "./app-menu"; +import { appUpdater } from "./app-updater"; +import { initializeIpcHandlers, setPendingOpenUrl } from "./ipc-events"; +import * as platform from "./platform"; +import { initializeDefaultPreferences } from "./preferences"; +import { PUSH_NOTIFICATION_EVENT, pushEvents } from "./push"; +import { getPreferenceStore, initializeStoreIpcHandlers } from "./stores"; +import { wagoHandler } from "./wago-handler"; +import * as windowState from "./window-state"; +import { validateGpuCache } from "./utils/gpu-cache-buster"; + +// LOGGING SETUP +// Override the default log path so they aren't a pain to find on Mac +const LOG_PATH = join(app.getPath("userData"), "logs"); +app.setAppLogsPath(LOG_PATH); +log.initialize(); +log.transports.file.resolvePathFn = (variables) => { + return join(LOG_PATH, variables.fileName ?? "log-file.txt"); +}; +log.info("Main starting"); +log.info(`Electron: ${process.versions.electron}`); +log.info(`BinaryPath: ${app.getPath("exe")}`); +log.info("ExecPath", process.execPath); +log.info("Args", process.argv); +log.info(`Log path: ${LOG_PATH}`); + +// ERROR HANDLING SETUP +process.on("uncaughtException", (error) => { + log.error("uncaughtException", error); +}); + +process.on("unhandledRejection", (error) => { + log.error("unhandledRejection", error); +}); + +// WINDOWS CERTS +if (platform.isWin) { + require("win-ca"); +} + +validateGpuCache(app) + +// VARIABLES +const startedAt = Date.now(); +const argv = minimist(process.argv.slice(1), { + boolean: ["serve", "hidden"], +}) as AppOptions; +log.info("ARGV", argv); +const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR; +const USER_AGENT = getUserAgent(); +log.info("USER_AGENT", USER_AGENT); + +let appIsQuitting = false; +let win: BrowserWindow | null = null; +let loadFailCount = 0; + +initializeDefaultPreferences(); + +// APP MENU SETUP +createAppMenu(win); + +// WowUp Protocol Handler +app.setAsDefaultProtocolClient(APP_PROTOCOL_NAME); + +// Set the app ID so that our notifications work correctly on Windows +app.setAppUserModelId(APP_USER_MODEL_ID); + +// HARDWARE ACCELERATION SETUP +if (getPreferenceStore().get(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY) === "false") { + log.info("Hardware acceleration disabled"); + app.disableHardwareAcceleration(); +} else { + log.info("Hardware acceleration enabled"); +} + +// Some servers don't supply good CORS headers for us, so we ignore them. +app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,OutOfBlinkCors"); + +// Only allow one instance of the app to run at a time, focus running window if user opens a 2nd time +// Adapted from https://github.com/electron/electron/blob/master/docs/api/app.md#apprequestsingleinstancelock +const singleInstanceLock = app.requestSingleInstanceLock(); +if (!singleInstanceLock) { + app.quit(); +} else { + app.on("second-instance", (evt, args) => { + log.info(`Second instance detected`, args); + // Someone tried to run a second instance, we should focus our window. + if (!win) { + log.warn("Second instance launched, but no window found"); + return; + } + + windowState.restoreWindow(win); + + win.focus(); + + // Find the first protocol arg if any exist + const customProtocol = find(args, (arg) => isProtocol(arg)); + if (customProtocol) { + log.info(`Custom protocol detected: ${customProtocol}`); + // If we did get a custom protocol notify the app + win.webContents.send(IPC_CUSTOM_PROTOCOL_RECEIVED, customProtocol); + } else { + log.info(`No custom protocol detected`); + } + }); +} + +function isProtocol(arg: string) { + return getProtocol(arg) != null; +} + +function getProtocol(arg: string) { + const match = /^([a-z][a-z0-9+\-.]*):/.exec(arg); + return match !== null && match.length > 1 ? match[1] : null; +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +// Added 400 ms to fix the black background issue while using transparent window. More details at https://github.com/electron/electron/issues/15947 +app + .whenReady() + .then(() => { + powerMonitor.on("resume", () => { + log.info("powerMonitor resume"); + win?.webContents?.send(IPC_POWER_MONITOR_RESUME); + }); + + powerMonitor.on("suspend", () => { + log.info("powerMonitor suspend"); + win?.webContents?.send(IPC_POWER_MONITOR_SUSPEND); + }); + + powerMonitor.on("lock-screen", () => { + log.info("powerMonitor lock-screen"); + win?.webContents?.send(IPC_POWER_MONITOR_LOCK); + }); + + powerMonitor.on("unlock-screen", () => { + log.info("powerMonitor unlock-screen"); + win?.webContents?.send(IPC_POWER_MONITOR_UNLOCK); + }); + + log.info(`App ready: ${Date.now() - startedAt}ms`); + // setTimeout(() => { + createWindow(); + // }, 400); + }) + .catch((e) => { + log.error("whenready failed", e); + }); + +app.on("before-quit", () => { + windowState.saveWindowConfig(win); + win = null; + appIsQuitting = true; + appUpdater?.dispose(); +}); + +// Quit when all windows are closed. +app.on("window-all-closed", () => { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + // if (process.platform !== "darwin") { + app.quit(); + // } +}); + +app.on("activate", () => { + void onActivate(); +}); + +app.on("child-process-gone", (e, details) => { + log.warn("child-process-gone", inspect(details)); + if (details.reason === "crashed") { + onChildProcessCrashed(details); + } else if (details.reason === "killed") { + app.quit(); + } +}); + +// See https://www.electronjs.org/docs/api/app#event-open-url-macos +if (platform.isMac) { + app.on("open-url", (evt, url) => { + log.info(`Open url received ${url}`); + + // If we did get a custom protocol notify the app + if (isProtocol(url)) { + evt.preventDefault(); + log.info(`Custom protocol detected: ${url}`); + win?.webContents.send(IPC_CUSTOM_PROTOCOL_RECEIVED, url); + setPendingOpenUrl(url); + } + }); +} + +let lastCrash = 0; + +const crashMap = new Map(); +const exitOnCrashServices = ["network.mojom.NetworkService"]; +/** If a particular child process crashes too many times, notify the user and exit the app to attempt preventing softlock of system */ +function onChildProcessCrashed(details: Electron.Details) { + if (typeof details.serviceName !== "string") { + return; + } + if (!exitOnCrashServices.includes(details.serviceName)) { + return; + } + + let ct = crashMap.get(details.serviceName) ?? 0; + ct += 1; + crashMap.set(details.serviceName, ct); + + if (ct >= 3) { + dialog.showErrorBox( + "Child Process Failure", + `Child process ${details.serviceName} has crashed too many times, app will now exit`, + ); + app.quit(); + } +} + +function createWindow(): BrowserWindow { + if (win) { + win.destroy(); + } + + // Main object for managing window state + // Initialize with a window name and default size + + const windowOptions: BrowserWindowConstructorOptions = { + width: WINDOW_DEFAULT_WIDTH, + height: WINDOW_DEFAULT_HEIGHT, + minWidth: WINDOW_MIN_WIDTH, + minHeight: WINDOW_MIN_HEIGHT, + transparent: false, + resizable: true, + backgroundColor: getBackgroundColor(), + title: "WowUp", + titleBarStyle: "hidden", + webPreferences: { + preload: join(__dirname, "preload.js"), + nodeIntegration: true, + contextIsolation: false, + allowRunningInsecureContent: argv.serve, + webSecurity: false, + additionalArguments: [ + `--log-path=${LOG_PATH}`, + `--user-data-path=${app.getPath("userData")}`, + `--base-bg-color=${getBackgroundColor()}`, + ], + webviewTag: true, + }, + show: false, + }; + + if (platform.isWin || platform.isLinux) { + windowOptions.frame = false; + } + + // Attempt to fix the missing icon issue on Ubuntu + if (platform.isLinux) { + windowOptions.icon = join(__dirname, "assets", WOWUP_LOGO_FILENAME); + } + + windowState.applyWindowBoundsToConfig(windowOptions); + + // Create the browser window. + win = new BrowserWindow(windowOptions); + + windowState.restoreMainWindowBounds(win); + + if (windowState.wasMaximized()) { + win.maximize(); + } + + appUpdater.init(win); + + initializeIpcHandlers(win); + initializeStoreIpcHandlers(); + wagoHandler.initialize(win); + + pushEvents.on(PUSH_NOTIFICATION_EVENT, (data) => { + win?.webContents.send(IPC_PUSH_NOTIFICATION, data); + }); + + win.on("blur", () => { + win?.webContents.send("blur"); + }); + + win.on("focus", () => { + win?.webContents.send("focus"); + }); + + win.webContents.userAgent = USER_AGENT; + win.webContents.setAudioMuted(true); + + win.webContents.on("will-attach-webview", (evt, webPreferences) => { + log.debug("will-attach-webview"); + + webPreferences.additionalArguments = [`--log-path=${LOG_PATH}`]; + webPreferences.contextIsolation = true; + webPreferences.plugins = false; + webPreferences.webgl = false; + }); + + win.webContents.on("did-attach-webview", (evt, webContents) => { + webContents.session.setUserAgent(webContents.userAgent); + + webContents.on("preload-error", (evt, path, e) => { + log.error("[webview] preload-error", e.message); + }); + + webContents.on("did-fail-provisional-load", (evt) => { + log.error("[webview] did-fail-provisional-load", evt); + }); + + webContents.session.setPermissionRequestHandler((contents, permission, callback) => { + log.warn("[webview] setPermissionRequestHandler", permission); + return callback(false); + }); + + webContents.session.setPermissionCheckHandler((contents, permission, origin) => { + if (["background-sync"].includes(permission)) { + return true; + } + + log.warn("[webview] setPermissionCheckHandler", permission, origin); + return false; + }); + + // webview allowpopups must be enabled for any link to work + // https://www.electronjs.org/docs/latest/api/webview-tag#allowpopups + webContents.setWindowOpenHandler((details) => { + log.debug("[webview] setWindowOpenHandler"); + win?.webContents.send("webview-new-window", details); // forward this new window to the app for processing + return { action: "deny" }; + }); + + webContents.on("did-start-navigation", (evt, url) => { + if (url === "https://addons.wago.io/wowup_ad") { + log.debug("[webview] did-start-navigation", url); + wagoHandler.initializeWebContents(webContents); + } + }); + }); + + win.webContents.on("zoom-changed", (zoomDirection) => { + win?.webContents?.send("zoom-changed", zoomDirection); + }); + + // See https://www.electronjs.org/docs/api/web-contents#event-render-process-gone + win.webContents.on("render-process-gone", (evt, details) => { + log.error("webContents render-process-gone"); + log.error(details); + + // If something killed the process, quit + if (details.reason === "killed") { + win?.destroy(); + app.quit(); + return; + } + + // If process crashes too quickly, kill the app + const crashTime = Date.now(); + if (crashTime - lastCrash < 5000) { + log.error("Crash loop detected"); + win?.destroy(); + app.quit(); + return; + } + + lastCrash = crashTime; + log.info("Restarting main window"); + }); + + // See https://www.electronjs.org/docs/api/web-contents#event-unresponsive + win.webContents.on("unresponsive", () => { + log.error("webContents unresponsive"); + }); + + // See https://www.electronjs.org/docs/api/web-contents#event-responsive + win.webContents.on("responsive", () => { + log.error("webContents responsive"); + }); + + win.once("ready-to-show", () => { + if (canStartHidden()) { + return; + } + + win?.webContents.session.setPermissionRequestHandler((contents, permission, callback) => { + log.warn("win setPermissionRequestHandler", permission); + return callback(false); + }); + + win?.webContents.session.setPermissionCheckHandler((contents, permission, origin) => { + log.warn("win setPermissionCheckHandler", permission, origin); + return false; + }); + + win?.show(); + }); + + win.once("show", () => { + // win.webContents.openDevTools(); + + if (windowState.wasFullScreen()) { + win?.setFullScreen(true); + } + + appUpdater.checkForUpdates().catch((e) => console.error(e)); + }); + + if (platform.isLinux || platform.isWin) { + win.on("close", () => { + if (win === null) { + return; + } + + windowState.saveWindowConfig(win); + }); + } + + win.on("close", (e) => { + if (appIsQuitting || getPreferenceStore().get(COLLAPSE_TO_TRAY_PREFERENCE_KEY) !== "true") { + pushEvents.removeAllListeners(PUSH_NOTIFICATION_EVENT); + return; + } + e.preventDefault(); + win?.hide(); + win?.setSkipTaskbar(true); + + if (platform.isMac) { + app.setBadgeCount(0); + app.dock.hide(); + } + }); + + win.once("closed", () => { + win = null; + }); + + win.on("maximize", () => { + windowState.saveWindowConfig(win); + win?.webContents?.send(IPC_WINDOW_MAXIMIZED); + }); + + win.on("unmaximize", () => { + win?.webContents?.send(IPC_WINDOW_UNMAXIMIZED); + }); + + win.on("minimize", () => { + win?.webContents?.send(IPC_WINDOW_MINIMIZED); + }); + + win.on("enter-full-screen", () => { + win?.webContents?.send(IPC_WINDOW_ENTER_FULLSCREEN); + }); + + win.on("leave-full-screen", () => { + win?.webContents?.send(IPC_WINDOW_LEAVE_FULLSCREEN); + }); + + win.webContents.on("did-fail-load", () => { + log.info("did-fail-load"); + if (loadFailCount < 5) { + loadFailCount += 1; + loadMainUrl(win).catch((e) => log.error(e)); + } else { + log.error(`Failed to load too many times, exiting`); + app.quit(); + } + }); + + log.info(`Loading app URL: ${Date.now() - startedAt}ms`); + if (argv.serve) { + require("electron-reload")(__dirname, { + electron: require(join(__dirname, "..", "node_modules", "electron")), + }); + win.loadURL("http://localhost:4200").catch((e) => log.error(e)); + } else { + loadMainUrl(win).catch((e) => log.error(e)); + } + + return win; +} + +async function loadMainUrl(window: BrowserWindow | null): Promise { + if (window === null) { + return; + } + + const url = pathToFileURL(join(__dirname, "..", "dist", "index.html")); + return await window?.loadURL(url.toString()); +} + +async function onActivate() { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (platform.isMac) { + await app.dock.show(); + win?.show(); + } + + if (win === null) { + createWindow(); + } +} + +function getBackgroundColor() { + const savedTheme = getPreferenceStore().get(CURRENT_THEME_KEY) as string | undefined; + return savedTheme && savedTheme.indexOf("light") !== -1 ? DEFAULT_LIGHT_BG_COLOR : DEFAULT_BG_COLOR; +} + +function canStartHidden() { + const prefStore = getPreferenceStore(); + const systemStart = prefStore?.get(START_WITH_SYSTEM_PREFERENCE_KEY) as string | undefined; + const startMin = prefStore?.get(START_MINIMIZED_PREFERENCE_KEY) as string | undefined; + + console.log(`START_WITH_SYSTEM_PREFERENCE_KEY: ${systemStart}`); + console.log(`START_MINIMIZED_PREFERENCE_KEY: ${startMin}`); + + const loginItems = app.getLoginItemSettings(); + if (Array.isArray(loginItems?.launchItems)) { + loginItems?.launchItems.forEach((li) => { + console.log(`launchItem: ${li.name} args -> ${li.args.join(",")}`); + }); + } + return argv.hidden || loginItems.wasOpenedAsHidden; +} + +function getUserAgent() { + const portableStr = isPortable ? " portable;" : ""; + return `WowUp-Client/${app.getVersion()} (${osType()}; ${osRelease()}; ${osArch()}; CF; ${portableStr} +https://wowup.io)`; +} diff --git a/WowUp/wowup-electron/app/platform.ts b/WowUp/wowup-electron/app/platform.ts new file mode 100644 index 0000000..6c87cd9 --- /dev/null +++ b/WowUp/wowup-electron/app/platform.ts @@ -0,0 +1,3 @@ +export const isMac = process.platform === "darwin"; +export const isWin = process.platform === "win32"; +export const isLinux = process.platform === "linux"; diff --git a/WowUp/wowup-electron/app/preferences.ts b/WowUp/wowup-electron/app/preferences.ts new file mode 100644 index 0000000..02737d4 --- /dev/null +++ b/WowUp/wowup-electron/app/preferences.ts @@ -0,0 +1,52 @@ +import { app } from "electron"; + +import { + ACCT_PUSH_ENABLED_KEY, + COLLAPSE_TO_TRAY_PREFERENCE_KEY, + CURRENT_THEME_KEY, + DEFAULT_THEME, + DEFAULT_TRUSTED_DOMAINS, + ENABLE_APP_BADGE_KEY, + ENABLE_SYSTEM_NOTIFICATIONS_PREFERENCE_KEY, + TRUSTED_DOMAINS_KEY, + USE_HARDWARE_ACCELERATION_PREFERENCE_KEY, + USE_SYMLINK_MODE_PREFERENCE_KEY, + WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY, +} from "../src/common/constants"; +import { WowUpReleaseChannelType } from "../src/common/wowup/wowup-release-channel-type"; +import { getPreferenceStore } from "./stores"; +import * as log from "electron-log/main"; + +export function initializeDefaultPreferences() { + const isBetaBuild = app.getVersion().toLowerCase().indexOf("beta") != -1; + const defaultReleaseChannel = isBetaBuild ? WowUpReleaseChannelType.Beta : WowUpReleaseChannelType.Stable; + + setDefaultPreference(ENABLE_SYSTEM_NOTIFICATIONS_PREFERENCE_KEY, true); + setDefaultPreference(COLLAPSE_TO_TRAY_PREFERENCE_KEY, true); + setDefaultPreference(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY, true); + setDefaultPreference(CURRENT_THEME_KEY, DEFAULT_THEME); + setDefaultPreference(WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY, defaultReleaseChannel); + setDefaultPreference(USE_SYMLINK_MODE_PREFERENCE_KEY, false); + setDefaultPreference(ENABLE_APP_BADGE_KEY, true); + setDefaultPreference(TRUSTED_DOMAINS_KEY, DEFAULT_TRUSTED_DOMAINS); + setDefaultPreference(ACCT_PUSH_ENABLED_KEY, false); +} + +export function getWowUpReleaseChannelPreference(): WowUpReleaseChannelType { + const val = getPreferenceStore().get(WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY) as string; + return parseInt(val, 10) as WowUpReleaseChannelType; +} + +function setDefaultPreference(key: string, defaultValue: any) { + const prefStore = getPreferenceStore(); + const pref = prefStore.get(key); + if (pref === null || pref === undefined) { + const valStr: string = defaultValue.toString(); + log.info(`Setting default preference: ${key} -> ${valStr}`); + if (Array.isArray(defaultValue)) { + prefStore.set(key, defaultValue); + } else { + prefStore.set(key, defaultValue.toString()); + } + } +} diff --git a/WowUp/wowup-electron/app/preload.ts b/WowUp/wowup-electron/app/preload.ts new file mode 100644 index 0000000..d99b72f --- /dev/null +++ b/WowUp/wowup-electron/app/preload.ts @@ -0,0 +1,113 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +import { ipcRenderer, IpcRendererEvent, shell, OpenExternalOptions } from "electron"; +import * as log from "electron-log/renderer"; +import * as platform from "./platform"; + +if (!process.isMainFrame) { + throw new Error("Preload scripts should not be running in a subframe"); +} + +if (platform.isWin) { + const ca = require("win-ca"); + const list: any[] = []; + ca({ + async: true, + format: ca.der2.txt, + ondata: list, + onend: () => { + log.info("win-ca loaded"); + }, + }); +} + +function getArg(argKey: string): string { + for (const arg of window.process.argv) { + const [key, val] = arg.split("="); + if (key === `--${argKey}`) { + return val; + } + } + + throw new Error(`Arg not found: ${argKey}`); +} + +const LOG_PATH = getArg("log-path"); +const USER_DATA_PATH = getArg("user-data-path"); +const BASE_BG_COLOR = getArg("base-bg-color"); + +function onRendererEvent(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) { + ipcRenderer.on(channel, listener); +} + +function onceRendererEvent(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) { + ipcRenderer.once(channel, listener); +} + +function rendererSend(channel: string, ...args: any[]) { + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + ipcRenderer.send(channel, ...args); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ +} + +function rendererSendSync(channel: string, ...args: any[]): any { + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + return ipcRenderer.sendSync(channel, ...args); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ +} + +function rendererInvoke(channel: string, ...args: any[]): Promise { + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + return ipcRenderer.invoke(channel, ...args); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ +} + +function rendererOff(channel: string, listener: (...args: any[]) => void) { + ipcRenderer.off(channel, listener); +} + +function rendererOn(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) { + ipcRenderer.on(channel, listener); +} + +function openExternal(url: string, options?: OpenExternalOptions): Promise { + return shell.openExternal(url, options); +} + +function openPath(path: string): Promise { + return shell.openPath(path); +} + +window.addEventListener("auxclick", (event) => { + event.preventDefault(); +}); + +try { + if (window.opener === null) { + window.log = log; + window.baseBgColor = BASE_BG_COLOR; + window.libs = { + handlebars: require("handlebars"), + autoLaunch: require("auto-launch"), + }; + window.userDataPath = USER_DATA_PATH; + window.logPath = LOG_PATH; + window.platform = process.platform; + window.wowup = { + onRendererEvent, + onceRendererEvent, + rendererSend, + rendererSendSync, + rendererInvoke, + rendererOff, + rendererOn, + openExternal, + openPath, + }; + } else { + console.log("HAS OPENER"); + } +} catch (e) { + log.error(e); +} diff --git a/WowUp/wowup-electron/app/push.ts b/WowUp/wowup-electron/app/push.ts new file mode 100644 index 0000000..b82814d --- /dev/null +++ b/WowUp/wowup-electron/app/push.ts @@ -0,0 +1,64 @@ +import * as log from "electron-log/main"; +import { EventEmitter } from "events"; + +export const PUSH_NOTIFICATION_EVENT = "push-notification"; +export const pushEvents = new EventEmitter(); + +const channelSubscriptions = new Map(); + +let Pushy: any; +export function startPushService(): boolean { + if (!Pushy) { + Pushy = require("pushy-electron"); + } + + // Listen for push notifications + Pushy.setNotificationListener((data) => { + pushEvents.emit(PUSH_NOTIFICATION_EVENT, data); + }); + + Pushy.listen(); + return true; +} + +export async function registerForPush(appId: string): Promise { + if (!Pushy) { + throw new Error("Push not started"); + } + if (!appId) { + throw new Error("Invalid push app id"); + } + + return await Pushy.register({ appId }); +} + +export async function unregisterPush(): Promise { + for (const [key] of channelSubscriptions) { + try { + await Pushy.unsubscribe(key); + } catch (e) { + console.error(e); + } + } + + channelSubscriptions.clear(); + + Pushy.disconnect(); +} + +export async function subscribeToChannel(channel: string): Promise { + // Make sure the user is registered + if (!Pushy.isRegistered()) { + throw new Error("Push services not registered"); + } + + if (channelSubscriptions.has(channel)) { + log.warn(`Already listening: ${channel}`); + return; + } + + // Subscribe the user to a topic + await Pushy.subscribe(channel); + channelSubscriptions.set(channel, true); + log.debug(`Subscribed: ${channel}`); +} diff --git a/WowUp/wowup-electron/app/stores.ts b/WowUp/wowup-electron/app/stores.ts new file mode 100644 index 0000000..c33c490 --- /dev/null +++ b/WowUp/wowup-electron/app/stores.ts @@ -0,0 +1,76 @@ +import { ipcMain, IpcMainInvokeEvent } from "electron"; +import * as Store from "electron-store"; +import { + ADDON_STORE_NAME, + IPC_STORE_GET_ALL, + IPC_STORE_GET_OBJECT, + IPC_STORE_GET_OBJECT_SYNC, + IPC_STORE_REMOVE_OBJECT, + IPC_STORE_SET_OBJECT, + PREFERENCE_STORE_NAME, + SENSITIVE_STORE_NAME, +} from "../src/common/constants"; + +const addonStore = new Store({ name: ADDON_STORE_NAME }); +const preferenceStore = new Store({ name: PREFERENCE_STORE_NAME }); +const sensitiveStore = new Store({ name: SENSITIVE_STORE_NAME }); + +const stores: { [storeName: string]: Store } = { + [ADDON_STORE_NAME]: addonStore, + [PREFERENCE_STORE_NAME]: preferenceStore, + [SENSITIVE_STORE_NAME]: sensitiveStore, +}; + +export function getPreferenceStore(): Store { + return preferenceStore; +} + +export function getAddonStore(): Store { + return addonStore; +} + +export function initializeStoreIpcHandlers(): void { + // Return the store value for a specific key + ipcMain.handle(IPC_STORE_GET_ALL, (evt: IpcMainInvokeEvent, storeName: string): any[] => { + const store = stores[storeName]; + + const items: any[] = []; + for (const result of store) { + const item = result[1]; + items.push(item as any); + } + + return items; + }); + + // Return the store value for a specific key + ipcMain.handle(IPC_STORE_GET_OBJECT, (evt: IpcMainInvokeEvent, storeName: string, key: string): any => { + const store = stores[storeName]; + return store ? store.get(key, undefined) : undefined; + }); + + ipcMain.on(IPC_STORE_GET_OBJECT_SYNC, (evt, storeName: string, key: string) => { + const store = stores[storeName]; + evt.returnValue = store ? store.get(key, undefined) : undefined; + }); + + // Set the store value for a specific key + ipcMain.handle(IPC_STORE_SET_OBJECT, (evt: IpcMainInvokeEvent, storeName: string, key: string, value: any): void => { + const store = stores[storeName]; + + let storedVal = value.toString(); + if (typeof value === "object" || Array.isArray(value)) { + storedVal = value; + } + + store?.set(key, storedVal); + + return storedVal; + }); + + // Remove the store value for a specific key + ipcMain.handle(IPC_STORE_REMOVE_OBJECT, (evt: IpcMainInvokeEvent, storeName: string, key: string): void => { + const store = stores[storeName]; + store?.delete(key); + }); +} diff --git a/WowUp/wowup-electron/app/system-tray.ts b/WowUp/wowup-electron/app/system-tray.ts new file mode 100644 index 0000000..be73a18 --- /dev/null +++ b/WowUp/wowup-electron/app/system-tray.ts @@ -0,0 +1,54 @@ +import { app, BrowserWindow, Menu, Tray } from "electron"; +import * as path from "path"; + +import { WOWUP_LOGO_FILENAME, WOWUP_LOGO_MAC_SYSTEM_TRAY } from "../src/common/constants"; +import { SystemTrayConfig } from "../src/common/wowup/models"; +import * as platform from "./platform"; +import { restoreWindow } from "./window-state"; + +let _trayRef: Tray; + +export function createTray(window: BrowserWindow, config: SystemTrayConfig): boolean { + _trayRef?.destroy(); + + console.log("Creating tray"); + const trayIconFile = platform.isMac ? WOWUP_LOGO_MAC_SYSTEM_TRAY : WOWUP_LOGO_FILENAME; + const trayIconPath = path.join(__dirname, "..", "assets", trayIconFile); + + _trayRef = new Tray(trayIconPath); + const contextMenu = Menu.buildFromTemplate([ + { + label: app.name, + type: "normal", + enabled: false, + }, + { + label: config.showLabel || "Show", + click: () => { + restoreWindow(window); + }, + }, + // Removing this for now per discussion with zak + // { + // label: config.showLabel || "Check for Updates...", + // click: () => { + // checkForUpdates(window); + // }, + // }, + { + label: config.quitLabel || "Quit", + role: "quit", + }, + ]); + + if (platform.isWin) { + _trayRef.on("click", () => { + restoreWindow(window); + }); + } + + _trayRef.setToolTip("WowUp"); + _trayRef.setContextMenu(contextMenu); + + return true; +} diff --git a/WowUp/wowup-electron/app/utils/gpu-cache-buster.ts b/WowUp/wowup-electron/app/utils/gpu-cache-buster.ts new file mode 100644 index 0000000..995992d --- /dev/null +++ b/WowUp/wowup-electron/app/utils/gpu-cache-buster.ts @@ -0,0 +1,81 @@ +import * as fs from "fs"; +import { join } from "path"; +import * as log from "electron-log/main"; + +/** + * There appears to be some issue with Ubuntu and the GPUCache system of electron + * https://github.com/WowUp/WowUp.CF/issues/63 + * + * User from an app with similar issue discovered deleting the folder and letting the app rebuild it appears to fix it. + * https://github.com/ferdium/ferdium-app/issues/1265 + * + * Added in a version file to the GPU cache folder that should get checked and delete the folder before the initial + * cache is generated hopefully fixing this issue + */ + +const GPU_CACHE_FOLDER = "GPUCache"; +const GPU_VERSION_FILE = ".wuversion"; + +interface VersionFileData { + version: string; +} + +export function validateGpuCache(app: Electron.App) { + try { + const cacheDir = join(app.getPath("userData"), GPU_CACHE_FOLDER); + const cacheExists = fileExists(cacheDir); + if (cacheExists) { + const files = fs.readdirSync(cacheDir); + const verFile = files.find((f) => f === GPU_VERSION_FILE); + if (verFile === undefined) { + removeDir(cacheDir); + } else { + const verFile = readVersionFile(cacheDir); + if (verFile.version !== app.getVersion()) { + removeDir(cacheDir); + } else { + return; + } + } + } + + fs.mkdirSync(cacheDir); + createVersionFile(app.getVersion(), cacheDir); + } catch (e) { + log.error("failed to validate GPU Cache", e); + } + // app.relaunch(); +} + +function removeDir(path: string) { + fs.rmSync(path, { force: true, recursive: true }); +} + +function fileExists(path: string) { + try { + fs.accessSync(path); + return true; + } catch (e) { + log.warn(`File does not exist: ${path}`); + log.warn(e.message); + return false; + } +} + +function readVersionFile(cacheDir: string): VersionFileData { + const filePath = join(cacheDir, GPU_VERSION_FILE); + const fileData = fs.readFileSync(filePath, { encoding: "utf-8" }); + const versionData: VersionFileData = JSON.parse(fileData); + return versionData; +} + +function createVersionFile(version: string, cacheDir: string) { + const versionData: VersionFileData = { + version, + }; + + const filePath = join(cacheDir, GPU_VERSION_FILE); + const verFileData = JSON.stringify(versionData); + + fs.writeFileSync(filePath, verFileData, { encoding: "utf-8" }); +} diff --git a/WowUp/wowup-electron/app/wago-handler.ts b/WowUp/wowup-electron/app/wago-handler.ts new file mode 100644 index 0000000..ad0344c --- /dev/null +++ b/WowUp/wowup-electron/app/wago-handler.ts @@ -0,0 +1,120 @@ +import { BrowserWindow, ipcMain, powerMonitor, WebContents } from "electron"; +import * as log from "electron-log"; + +class WagoHandler { + private _initialized = false; + private _window: BrowserWindow | undefined = undefined; + private _tokenTimer: ReturnType | undefined = undefined; + private _webContents: WebContents | undefined = undefined; + private _tokenMap = new Map(); + + public constructor() { + powerMonitor.on("resume", () => { + log.info("[wago-handler] powerMonitor resume"); + this._tokenMap.clear(); + this._webContents?.reload(); + }); + } + + public initialize(window: BrowserWindow): void { + if (this._initialized) { + return; + } + + this._window = window; + + // Just forward the token event out to the window + // this is not a handler, just a passive listener + ipcMain.on("wago-token-received", (evt, token: string) => { + if (typeof token !== "string" || token.length < 20) { + log.warn(`[wago-handler] malformed token detected: ${token.length}`); + return; + } + + log.warn("[wago-handler] clearing reload timer"); + this._tokenMap.set(this._webContents?.id ?? 0, true); + this.stopTimeout(); + this._window?.webContents?.send("wago-token-received", token); + }); + } + + public initializeWebContents(webContents: WebContents) { + if (this._webContents !== undefined) { + this.removeListeners(this._webContents); + } + + this._webContents = webContents; + + webContents.on("did-fail-provisional-load", this.onDidFailProvisionalLoad); + webContents.on("did-fail-load", this.onDidFail); + webContents.on("will-navigate", this.onWillNavigate); + webContents.on("did-finish-load", () => { + // log.debug("[wago-handler] did-finish-load"); + if (this._tokenMap.has(webContents.id)) { + this.stopTimeout(); + } + }); + + // webview allowpopups must be enabled for any link to work + // https://www.electronjs.org/docs/latest/api/webview-tag#allowpopups + webContents.setWindowOpenHandler(this.onWindowOpenHandler); + } + + private stopTimeout() { + clearTimeout(this._tokenTimer); + this._tokenTimer = undefined; + } + + private readonly onDidFailProvisionalLoad = (evt: Electron.Event, code: number, description: string) => { + log.error("[webview] did-fail-provisional-load", code, description); + if (this._webContents !== undefined) { + this.setReloadTime(this._webContents); + } + }; + + private readonly onDidFail = (evt: Electron.Event, code: number, description: string, url: string) => { + log.error("[wago-handler] did-fail-load", code, description, url); + if (this._webContents !== undefined) { + this.setReloadTime(this._webContents); + } + }; + + private readonly onWillNavigate = (evt: Electron.Event, url: string) => { + log.debug("[wago-handler] will-navigate", url); + if (this._webContents !== undefined && this._webContents.getURL() === url) { + log.debug(`[wago-handler] reload detected`); + } else { + evt.preventDefault(); // block the webview from navigating at all + } + }; + + private readonly onWindowOpenHandler = (details: Electron.HandlerDetails): { action: "deny" } => { + log.debug("[webview] setWindowOpenHandler"); + this._window?.webContents.send("webview-new-window", details); // forward this new window to the app for processing + return { action: "deny" }; + }; + + private removeListeners(webContents: WebContents) { + this.stopTimeout(); + webContents.off("did-fail-provisional-load", this.onDidFailProvisionalLoad); + webContents.off("did-fail-load", this.onDidFail); + webContents.off("will-navigate", this.onWillNavigate); + webContents.setWindowOpenHandler(() => ({ action: "allow" })); + } + + private setReloadTime(webContents: WebContents) { + if (this._tokenMap.has(webContents.id)) { + return; + } + + if (this._tokenTimer === undefined) { + log.warn("[wago-handler] setting reload timer"); + this._tokenTimer = setTimeout(() => { + log.error("[wago-handler] reload"); + webContents.reload(); + }, 5000); + } + } +} + +export const wagoHandler = new WagoHandler(); diff --git a/WowUp/wowup-electron/app/window-state.ts b/WowUp/wowup-electron/app/window-state.ts new file mode 100644 index 0000000..1cc0b61 --- /dev/null +++ b/WowUp/wowup-electron/app/window-state.ts @@ -0,0 +1,131 @@ +import { app, BrowserWindow, BrowserWindowConstructorOptions, Display, Rectangle, screen } from "electron"; +import * as log from "electron-log/main"; + +import { IPC_WINDOW_RESUME, MIN_VISIBLE_ON_SCREEN, WINDOW_MIN_HEIGHT, WINDOW_MIN_WIDTH } from "../src/common/constants"; +import * as platform from "./platform"; +import { getPreferenceStore } from "./stores"; + +export function wasMaximized() { + return getPreferenceStore().get(`main-window-is-maximized`) as boolean; +} + +export function wasFullScreen() { + return getPreferenceStore().get(`main-window-is-fullscreen`) as boolean; +} + +export function saveWindowConfig(window: BrowserWindow | null): void { + if (window === null) { + return; + } + + try { + const prefStore = getPreferenceStore(); + prefStore.set(`main-window-is-maximized`, window.isMaximized()); + prefStore.set(`main-window-is-minimized`, window.isMinimized()); + prefStore.set(`main-window-is-fullscreen`, window.isFullScreen()); + + if (!window.isMinimized() && !window.isMaximized()) { + prefStore.set(`main-window-state`, window.getBounds()); + } else if (window.isMaximized()) { + // Attempt to reduce the placement so its remember but in bounds + const bounds = window.getBounds(); + bounds.x += 50; + bounds.y += 50; + bounds.width -= 100; + bounds.height -= 100; + prefStore.set(`main-window-state`, bounds); + } + + log.info("window config saved"); + } catch (e) { + log.error(e); + } +} + +export function applyWindowBoundsToConfig(config: BrowserWindowConstructorOptions) { + const state = getWindowConfig(); + + if (state == null) { + config.center = true; + return; + } + + config.width = state.width; + config.height = state.height; + config.x = state.x; + config.y = state.y; +} + +export function restoreMainWindowBounds(mainWindow: BrowserWindow) { + const savedWindowBounds = getWindowConfig(); + const currentBounds = mainWindow.getBounds(); + + if ( + savedWindowBounds != null && + (currentBounds.height !== savedWindowBounds.height || currentBounds.width !== savedWindowBounds.width) + ) { + mainWindow.setBounds(savedWindowBounds); + } +} + +export function getWindowConfig() { + let state: Rectangle = getPreferenceStore().get(`main-window-state`) as Rectangle; + if (!state) { + state = { + height: 0, + width: 0, + x: 0, + y: 0, + }; + } + + state.width = Math.max(WINDOW_MIN_WIDTH, state.width); + state.height = Math.max(WINDOW_MIN_HEIGHT, state.height); + + const displays = screen.getAllDisplays(); + const display = getDisplayForBounds(displays, state); + return display != null ? state : null; +} + +export function restoreWindow(window: BrowserWindow): void { + window?.show(); + window?.setSkipTaskbar(false); + + if (platform.isMac) { + app.dock.show().catch((e) => log.error(`Failed to show on Mac dock`, e)); + } + + window?.webContents?.send(IPC_WINDOW_RESUME); +} + +function getDisplayForBounds(displays: Display[], bounds: Rectangle) { + return displays.find((display) => { + const displayBound = display.workArea; + displayBound.x += MIN_VISIBLE_ON_SCREEN; + displayBound.y += MIN_VISIBLE_ON_SCREEN; + displayBound.width -= 2 * MIN_VISIBLE_ON_SCREEN; + displayBound.height -= 2 * MIN_VISIBLE_ON_SCREEN; + return doRectanglesOverlap(bounds, displayBound); + }); +} + +function doRectanglesOverlap(a: Rectangle, b: Rectangle) { + const ax1 = a.x + a.width; + const bx1 = b.x + b.width; + const ay1 = a.y + a.height; + const by1 = b.y + b.height; // clamp a to b, see if it is non-empty + + const cx0 = a.x < b.x ? b.x : a.x; + const cx1 = ax1 < bx1 ? ax1 : bx1; + + if (cx1 - cx0 > 0) { + const cy0 = a.y < b.y ? b.y : a.y; + const cy1 = ay1 < by1 ? ay1 : by1; + + if (cy1 - cy0 > 0) { + return true; + } + } + + return false; +} diff --git a/WowUp/wowup-electron/app/wowup-folder-scanner.ts b/WowUp/wowup-electron/app/wowup-folder-scanner.ts new file mode 100644 index 0000000..f230b11 --- /dev/null +++ b/WowUp/wowup-electron/app/wowup-folder-scanner.ts @@ -0,0 +1,219 @@ +import * as _ from "lodash"; +import * as path from "path"; +import * as log from "electron-log"; +import { exists, readDirRecursive, hashFile, hashString } from "./file.utils"; +import * as fsp from "fs/promises"; +import { firstValueFrom, from, mergeMap, toArray } from "rxjs"; +import { AddonScanResult } from "wowup-lib-core"; + +const INVALID_PATH_CHARS = [ + "|", + "\0", + "\u0001", + "\u0002", + "\u0003", + "\u0004", + "\u0005", + "\u0006", + "\b", + "\t", + "\n", + "\v", + "\f", + "\r", + "\u000e", + "\u000f", + "\u0010", + "\u0011", + "\u0012", + "\u0013", + "\u0014", + "\u0015", + "\u0016", + "\u0017", + "\u0018", + "\u0019", + "\u001a", + "\u001b", + "\u001c", + "\u001d", + "\u001e", + "\u001f", +]; + +export class WowUpFolderScanner { + private _folderPath = ""; + + // This map is required for solving for case sensitive mismatches from addon authors on Linux + private _fileMap: { [key: string]: string } = {}; + + public constructor(folderPath: string) { + this._folderPath = folderPath; + } + + private get tocFileCommentsRegex() { + return /\s*#.*$/gim; + } + + private get tocFileIncludesRegex() { + return /^\s*((?:(?/gi; + } + + private get bindingsXmlCommentsRegex() { + return //gis; + } + + public async scanFolder(): Promise { + const files = await readDirRecursive(this._folderPath); + files.forEach((fp) => (this._fileMap[fp.toLowerCase()] = fp)); + + let matchingFiles = await this.getMatchingFiles(this._folderPath, files); + matchingFiles = _.orderBy(matchingFiles, [(f) => f.toLowerCase()], ["asc"]); + + async function toFileHash(file: string) { + return { hash: await hashFile(file), file }; + } + + const fileFingerprints = await firstValueFrom( + from(matchingFiles).pipe( + mergeMap((file) => from(toFileHash(file)), 3), + toArray() + ) + ); + + const fingerprintList = _.map(fileFingerprints, (ff) => ff.hash); + const hashConcat = _.orderBy(fingerprintList).join(""); + const fingerprint = hashString(hashConcat); + + const result: AddonScanResult = { + source: 'wowup', + fileFingerprints: fingerprintList, + fingerprint, + fingerprintNum: 0, + path: this._folderPath, + folderName: path.basename(this._folderPath), + fileCount: matchingFiles.length, + }; + + return result; + } + + private async getMatchingFiles(folderPath: string, filePaths: string[]): Promise { + const parentDir = path.normalize(path.dirname(folderPath) + path.sep); + const matchingFileList: string[] = []; + const fileInfoList: string[] = []; + + for (const filePath of filePaths) { + const input = filePath.toLowerCase().replace(parentDir.toLowerCase(), ""); + + if (this.tocFileRegex.test(input)) { + fileInfoList.push(filePath); + } else if (this.bindingsXmlRegex.test(input)) { + matchingFileList.push(filePath); + } + } + + for (const fileInfo of fileInfoList) { + await this.processIncludeFile(matchingFileList, fileInfo); + } + + return matchingFileList; + } + + private async processIncludeFile(matchingFileList: string[], fileInfo: string) { + let nativePath = ""; + try { + nativePath = this.getRealPath(fileInfo); + } catch (e) { + return; + } + + const pathExists = await exists(nativePath); + if (!pathExists || matchingFileList.indexOf(nativePath) !== -1) { + return; + } + + matchingFileList.push(nativePath); + + let input = await fsp.readFile(nativePath, { encoding: "utf-8" }); + input = this.removeComments(nativePath, input); + + const inclusions = this.getFileInclusionMatches(nativePath, input); + if (!inclusions || !inclusions.length) { + return; + } + + const dirname = path.dirname(nativePath); + for (const include of inclusions) { + if (this.hasInvalidPathChars(include)) { + log.debug(`Invalid include file ${nativePath}`); + break; + } + + const fileName = path.join(dirname, include.replace(/\\/g, path.sep)); + await this.processIncludeFile(matchingFileList, fileName); + } + } + + private hasInvalidPathChars(path: string) { + return INVALID_PATH_CHARS.some((c) => path.indexOf(c) !== -1); + } + + private removeComments(fileInfo: string, fileContent: string): string { + const ext = path.extname(fileInfo); + switch (ext) { + case ".xml": + return fileContent.replace(this.bindingsXmlCommentsRegex, ""); + case ".toc": + return fileContent.replace(this.tocFileCommentsRegex, ""); + default: + return fileContent; + } + } + + private getFileInclusionMatches(fileInfo: string, fileContent: string): string[] | null { + const ext = path.extname(fileInfo); + switch (ext) { + case ".xml": + return this.matchAll(fileContent, this.bindingsXmlIncludesRegex); + case ".toc": + return this.matchAll(fileContent, this.tocFileIncludesRegex); + default: + return null; + } + } + + private matchAll(str: string, regex: RegExp): string[] { + const matches: string[] = []; + let currentMatch: RegExpExecArray; + do { + currentMatch = regex.exec(str); + if (currentMatch) { + matches.push(currentMatch[1]); + } + } while (currentMatch); + + return matches; + } + + private getRealPath(filePath: string) { + const lowerPath = filePath.toLowerCase(); + const matchedPath = this._fileMap[lowerPath]; + if (!matchedPath) { + throw new Error(`Path not found: ${lowerPath}`); + } + return matchedPath; + } +} diff --git a/WowUp/wowup-electron/assets/WowUpAddon/data.lua.hbs b/WowUp/wowup-electron/assets/WowUpAddon/data.lua.hbs new file mode 100644 index 0000000..0f1f6a7 --- /dev/null +++ b/WowUp/wowup-electron/assets/WowUpAddon/data.lua.hbs @@ -0,0 +1,11 @@ +-- this file is autogenerated by WowUp (app) at {{ generatedAt }} +WOWUP_DATA = {} +WOWUP_DATA.icon = "Interface/AddOns/wowup_data_addon/ldbicon" +WOWUP_DATA.addonManagerName = "WowUp" +WOWUP_DATA.addonManagerColorRGB = {232/255,205/255,134/255} +WOWUP_DATA.addonManagerNameSlashCommand = "/wowup" +WOWUP_DATA.updateAddonsList = { +{{#each updatesAvailable}} + ["{{ name }}"] = {"{{ currentVersion }}", "{{ newVersion }}"}, +{{/each}} +} diff --git a/WowUp/wowup-electron/assets/WowUpAddon/ldbicon.tga b/WowUp/wowup-electron/assets/WowUpAddon/ldbicon.tga new file mode 100644 index 0000000..f552960 Binary files /dev/null and b/WowUp/wowup-electron/assets/WowUpAddon/ldbicon.tga differ diff --git a/WowUp/wowup-electron/assets/WowUpAddon/wowup_data_addon.toc.hbs b/WowUp/wowup-electron/assets/WowUpAddon/wowup_data_addon.toc.hbs new file mode 100644 index 0000000..e9dce46 --- /dev/null +++ b/WowUp/wowup-electron/assets/WowUpAddon/wowup_data_addon.toc.hbs @@ -0,0 +1,11 @@ +## Interface: {{ interfaceVersion }} +## Title: Addon Update Notifications (WowUp Data) +## Notes: Contains the data for addon updates available in WowUp. This addon is automatically installed through WowUp when the Addon Update Notifications addon is installed. +## Author: WowUp +## Version: {{ wowUpAddonVersion }} +## DefaultState: enabled +## RequiredDeps: {{ wowUpAddonName }} +## X-Website: https://wowup.io +## X-AddonProvider: wowup-app + +data.lua diff --git a/WowUp/wowup-electron/assets/preload/wago.js b/WowUp/wowup-electron/assets/preload/wago.js new file mode 100644 index 0000000..2811a9b --- /dev/null +++ b/WowUp/wowup-electron/assets/preload/wago.js @@ -0,0 +1,101 @@ +const { contextBridge, ipcRenderer } = require("electron"); +// const log = require("electron-log"); +// const { join } = require("path"); +// const { inspect } = require("util"); + +// const LOG_PATH = getArg("log-path"); + +const BACKOFF_KEY = "wago-backoff"; +const BACKOFF_SET_KEY = "wago-backoff-set"; +const BACKOFF_RESET_AGE = 5 * 60000; +const BACKOFF_MAX_WAIT = 2 * 60000; + +let keyExpectedTimeout = undefined; + +// function getArg(argKey) { +// for (const arg of window.process.argv) { +// const [key, val] = arg.split("="); +// if (key === `--${argKey}`) { +// return val; +// } +// } + +// throw new Error(`Arg not found: ${argKey}`); +// } + +// log.transports.file.resolvePath = (variables) => { +// return join(LOG_PATH, variables.fileName); +// }; + +// /* eslint-disable @typescript-eslint/no-unsafe-argument */ +console.log = function (...data) { + ipcRenderer.send("webview-log", "info", ...data); +}; + +console.warn = function (...data) { + ipcRenderer.send("webview-log", "warn", ...data); +}; + +console.error = function (...data) { + ipcRenderer.send("webview-log", "error", ...data); +}; +// /* eslint-enable @typescript-eslint/no-unsafe-argument */ + +window.addEventListener( + "error", + function (e) { + const errMsg = e.error?.toString() || "unknown error on " + window.location; + console.error(`[wago-preload] error listener:`, e.message, errMsg); + ipcRenderer.send("webview-error", e.error, e.message); + + if (keyExpectedTimeout != undefined) { + backoffReload(); + } + }, + true +); + +window.onerror = function (msg, url, lineNo, columnNo, error) { + console.error(`[wago-preload] error:`, msg, url, lineNo, columnNo, error); + return false; +}; + +contextBridge.exposeInMainWorld("wago", { + provideApiKey: (key) => { + window.clearTimeout(keyExpectedTimeout); + keyExpectedTimeout = undefined; + console.debug(`[wago-preload] got key`); + ipcRenderer.send("wago-token-received", key); + }, +}); + +// If the api key does not get populated after a certain time, reload +// Can happen if the page returns bad responses (500 etc) +keyExpectedTimeout = window.setTimeout(() => { + console.log("[wago-preload] failed to get key in time, reloading"); + backoffReload(); +}, 30000); + +console.log(`[wago-preload] init`, window.location.href); + +function backoffReload() { + let backoffSet = window.sessionStorage.getItem("wago-backoff-set"); + backoffSet = backoffSet ? parseInt(backoffSet, 10) : 0; + + let backoff = window.sessionStorage.getItem(BACKOFF_KEY); + backoff = Math.min(backoff ? parseInt(backoff, 10) * 2 : 2000, BACKOFF_MAX_WAIT); + + // If the backoff time is old, reset the backoff + if (Date.now() - backoffSet > BACKOFF_RESET_AGE) { + backoff = 2000; + } + + console.log("[wago] setting reload backoff", backoff); + window.sessionStorage.setItem(BACKOFF_KEY, backoff); + window.sessionStorage.setItem(BACKOFF_SET_KEY, Date.now().toString()); + + // Wait the calculated time + window.setTimeout(() => { + window.location.reload(); + }, backoff); +} diff --git a/WowUp/wowup-electron/assets/preload/wago_test.html b/WowUp/wowup-electron/assets/preload/wago_test.html new file mode 100644 index 0000000..5a19945 --- /dev/null +++ b/WowUp/wowup-electron/assets/preload/wago_test.html @@ -0,0 +1,8 @@ + + + + +
Test
+ Test Link + + diff --git a/WowUp/wowup-electron/assets/wowupBlackLgNopadTemplate copy.png b/WowUp/wowup-electron/assets/wowupBlackLgNopadTemplate copy.png new file mode 100644 index 0000000..5789e26 Binary files /dev/null and b/WowUp/wowup-electron/assets/wowupBlackLgNopadTemplate copy.png differ diff --git a/WowUp/wowup-electron/assets/wowupBlackLgNopadTemplate@2x.png b/WowUp/wowup-electron/assets/wowupBlackLgNopadTemplate@2x.png new file mode 100644 index 0000000..cd1c515 Binary files /dev/null and b/WowUp/wowup-electron/assets/wowupBlackLgNopadTemplate@2x.png differ diff --git a/WowUp/wowup-electron/assets/wowup_logo_512np.png b/WowUp/wowup-electron/assets/wowup_logo_512np.png new file mode 100644 index 0000000..05521e1 Binary files /dev/null and b/WowUp/wowup-electron/assets/wowup_logo_512np.png differ diff --git a/WowUp/wowup-electron/assets/wowup_logo_cf.png b/WowUp/wowup-electron/assets/wowup_logo_cf.png new file mode 100644 index 0000000..3d9caba Binary files /dev/null and b/WowUp/wowup-electron/assets/wowup_logo_cf.png differ diff --git a/WowUp/wowup-electron/assets/wowup_logo_purple.png b/WowUp/wowup-electron/assets/wowup_logo_purple.png new file mode 100644 index 0000000..473a9df Binary files /dev/null and b/WowUp/wowup-electron/assets/wowup_logo_purple.png differ diff --git a/WowUp/wowup-electron/binding.gyp b/WowUp/wowup-electron/binding.gyp new file mode 100644 index 0000000..f2d26b8 --- /dev/null +++ b/WowUp/wowup-electron/binding.gyp @@ -0,0 +1,21 @@ +{ + "targets": [ + { + "target_name": "addon", + "cflags!": [ "-fno-exceptions" ], + "cflags_cc!": [ "-fno-exceptions" ], + "sources": [ + "native/hello.cc", + "native/curse.cc" + ], + 'include_dirs': [ + " + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + \ No newline at end of file diff --git a/WowUp/wowup-electron/electron-build/flatpak/icon_128x128x32.png b/WowUp/wowup-electron/electron-build/flatpak/icon_128x128x32.png new file mode 100644 index 0000000..47ea8f3 Binary files /dev/null and b/WowUp/wowup-electron/electron-build/flatpak/icon_128x128x32.png differ diff --git a/WowUp/wowup-electron/electron-build/flatpak/icon_16x16x32.png b/WowUp/wowup-electron/electron-build/flatpak/icon_16x16x32.png new file mode 100644 index 0000000..297cb01 Binary files /dev/null and b/WowUp/wowup-electron/electron-build/flatpak/icon_16x16x32.png differ diff --git a/WowUp/wowup-electron/electron-build/flatpak/icon_256x256x32.png b/WowUp/wowup-electron/electron-build/flatpak/icon_256x256x32.png new file mode 100644 index 0000000..a43ceaf Binary files /dev/null and b/WowUp/wowup-electron/electron-build/flatpak/icon_256x256x32.png differ diff --git a/WowUp/wowup-electron/electron-build/flatpak/icon_32x32x32.png b/WowUp/wowup-electron/electron-build/flatpak/icon_32x32x32.png new file mode 100644 index 0000000..0123e9b Binary files /dev/null and b/WowUp/wowup-electron/electron-build/flatpak/icon_32x32x32.png differ diff --git a/WowUp/wowup-electron/electron-build/flatpak/icon_48x48x32.png b/WowUp/wowup-electron/electron-build/flatpak/icon_48x48x32.png new file mode 100644 index 0000000..37ad18e Binary files /dev/null and b/WowUp/wowup-electron/electron-build/flatpak/icon_48x48x32.png differ diff --git a/WowUp/wowup-electron/electron-build/flatpak/icon_512x512x32.png b/WowUp/wowup-electron/electron-build/flatpak/icon_512x512x32.png new file mode 100644 index 0000000..f184295 Binary files /dev/null and b/WowUp/wowup-electron/electron-build/flatpak/icon_512x512x32.png differ diff --git a/WowUp/wowup-electron/electron-build/icon-cf.ico b/WowUp/wowup-electron/electron-build/icon-cf.ico new file mode 100644 index 0000000..f975432 Binary files /dev/null and b/WowUp/wowup-electron/electron-build/icon-cf.ico differ diff --git a/WowUp/wowup-electron/electron-build/icon.icns b/WowUp/wowup-electron/electron-build/icon.icns new file mode 100644 index 0000000..ae04638 Binary files /dev/null and b/WowUp/wowup-electron/electron-build/icon.icns differ diff --git a/WowUp/wowup-electron/electron-build/icon.ico b/WowUp/wowup-electron/electron-build/icon.ico new file mode 100644 index 0000000..29b3493 Binary files /dev/null and b/WowUp/wowup-electron/electron-build/icon.ico differ diff --git a/WowUp/wowup-electron/electron-build/uninstaller.nsh b/WowUp/wowup-electron/electron-build/uninstaller.nsh new file mode 100644 index 0000000..141b370 --- /dev/null +++ b/WowUp/wowup-electron/electron-build/uninstaller.nsh @@ -0,0 +1,3 @@ +!macro customUnInstall + RMDir /r "$APPDATA\WowUp" +!macroend diff --git a/WowUp/wowup-electron/electron-builder-local.json b/WowUp/wowup-electron/electron-builder-local.json new file mode 100644 index 0000000..c8b9311 --- /dev/null +++ b/WowUp/wowup-electron/electron-builder-local.json @@ -0,0 +1,61 @@ +{ + "productName": "WowUp", + "appId": "io.wowup.jliddev", + "directories": { + "output": "release/" + }, + "afterSign": "./electron-build/after-sign.js", + "generateUpdatesFilesForAllChannels": true, + "publish": ["github"], + "nodeGypRebuild": true, + "files": ["dist/**/*.*", "assets/**/*.*", "app/**/*.js", "src/common/**/*.js", "build/Release/*.node"], + "win": { + "icon": "electron-build/icon.ico", + "target": ["nsis", "portable"], + "forceCodeSigning": false, + "publisherName": "WowUp LLC" + }, + "nsis": { + "deleteAppDataOnUninstall": true, + "include": "./electron-build/uninstaller.nsh" + }, + "mac": { + "icon": "electron-build/icon.icns", + "category": "public.app-category.games", + "target": [ + { + "target": "default", + "arch": ["x64", "arm64"] + } + ] + }, + "linux": { + "icon": "electron-build/flatpak/", + "target": ["AppImage"] + }, + "flatpak": { + "base": "org.electronjs.Electron2.BaseApp", + "runtime": "org.freedesktop.Platform", + "runtimeVersion": "21.08", + "sdk": "org.freedesktop.Sdk", + "baseVersion": "21.08", + "finishArgs": [ + "--socket=wayland", + "--socket=x11", + "--socket=pulseaudio", + "--socket=system-bus", + "--socket=session-bus", + "--share=ipc", + "--share=network", + "--device=dri", + "--filesystem=xdg-config/WoWUp", + "--filesystem=~/Games", + "--filesystem=~/.wine/drive_c", + "--talk-name=org.freedesktop.Notifications", + "--talk-name=org.gtk.Notifications", + "--talk-name=org.kde.StatusNotifierWatcher", + "--env=APPIMAGE=true" + ], + "useWaylandFlags": false + } +} diff --git a/WowUp/wowup-electron/electron-builder.json b/WowUp/wowup-electron/electron-builder.json new file mode 100644 index 0000000..c56d4d4 --- /dev/null +++ b/WowUp/wowup-electron/electron-builder.json @@ -0,0 +1,72 @@ +{ + "productName": "WowUp", + "appId": "io.wowup.jliddev", + "directories": { + "output": "release/" + }, + "afterSign": "./electron-build/after-sign.js", + "generateUpdatesFilesForAllChannels": true, + "publish": ["github"], + "nodeGypRebuild": true, + "files": ["dist/**/*.*", "assets/**/*.*", "app/**/*.js", "src/common/**/*.js", "build/Release/*.node"], + "win": { + "icon": "electron-build/icon.ico", + "target": ["nsis", "portable"], + "forceCodeSigning": false, + "publisherName": "WowUp LLC" + }, + "nsis": { + "deleteAppDataOnUninstall": true + }, + "mac": { + "icon": "electron-build/icon.icns", + "category": "public.app-category.games", + "target": [ + { + "target": "default", + "arch": ["x64", "arm64"] + } + ], + "hardenedRuntime": false, + "entitlements": "./electron-build/entitlements.mac.plist", + "extendInfo": { + "CFBundleURLTypes": [ + { + "CFBundleTypeRole": "Shell", + "CFBundleURLName": "CurseForge", + "CFBundleURLSchemes": "curseforge" + } + ] + } + }, + "linux": { + "icon": "electron-build/flatpak/", + "target": ["AppImage"], + "asarUnpack": "**/*.node" + }, + "flatpak": { + "base": "org.electronjs.Electron2.BaseApp", + "runtime": "org.freedesktop.Platform", + "runtimeVersion": "21.08", + "sdk": "org.freedesktop.Sdk", + "baseVersion": "21.08", + "finishArgs": [ + "--socket=wayland", + "--socket=x11", + "--socket=pulseaudio", + "--socket=system-bus", + "--socket=session-bus", + "--share=ipc", + "--share=network", + "--device=dri", + "--filesystem=xdg-config/WoWUp", + "--filesystem=~/Games", + "--filesystem=~/.wine/drive_c", + "--talk-name=org.freedesktop.Notifications", + "--talk-name=org.gtk.Notifications", + "--talk-name=org.kde.StatusNotifierWatcher", + "--env=APPIMAGE=true" + ], + "useWaylandFlags": false + } +} diff --git a/WowUp/wowup-electron/gulpfile.js b/WowUp/wowup-electron/gulpfile.js new file mode 100644 index 0000000..d8b8557 --- /dev/null +++ b/WowUp/wowup-electron/gulpfile.js @@ -0,0 +1,93 @@ +require("dotenv").config(); + +const gulp = require("gulp"); +const del = require("del"); +const fs = require("fs/promises"); +const path = require("path"); +const { spawn } = require("child_process"); + +function defaultTask(cb) { + // place code for your default task here + cb(); +} + +function prePackageTask(cb) { + return del("package_workspace/**", { force: true }); +} + +function prePackageCopyTask(cb) { + return gulp + .src(["./**/*.*", "!node_modules/**/*.*", "!package_workspace/**/*.*", "!release/**/*.*"], { base: "./" }) + .pipe(gulp.dest("package_workspace/")); +} + +function packageChDir(cb) { + process.chdir("./package_workspace"); + cb(); +} + +function npmRunTask(cb, buildJob) { + console.log("packageTask", buildJob); + + const ls = spawn("npm", ["run", buildJob], { + shell: true, + }); + + ls.stdout.on("data", (data) => { + console.log(`stdout: ${data}`); + }); + + ls.stderr.on("data", (data) => { + console.log(`stderr: ${data}`); + }); + + ls.on("error", (error) => { + console.log(`error: ${error.message}`); + }); + + ls.on("close", (code) => { + console.log(`child process exited with code ${code}`); + cb(); + }); +} + +function npmRun(cmd) { + return function npmRunCmd(cb) { + npmRunTask(cb, cmd); + }; +} + +async function updateCfKey() { + const apiTokenPath = "./curseforge-api-key"; + const cfApiKey = process.env.CURSEFORGE_API_KEY || fs.readFileSync(apiTokenPath, { encoding: "utf-8" }); + console.log(cfApiKey); + if (typeof cfApiKey !== "string" || cfApiKey.length === 0) { + throw new Error("CURSEFORGE_API_KEY missing"); + } + + const envPath = "src/environments"; + const environments = await fs.readdir(envPath); + + for (let env of environments) { + const filePath = path.join(envPath, env); + let envData = await fs.readFile(filePath, { encoding: "utf-8" }); + envData = envData.replace("{{CURSEFORGE_API_KEY}}", cfApiKey); + + await fs.writeFile(filePath, envData); + console.log(envData); + } +} + +const prePackageTasks = [ + npmRun("lint"), + npmRun("build:prod"), + prePackageTask, + prePackageCopyTask, + packageChDir, + npmRun("install:prod"), +]; + +exports.default = defaultTask; +exports.package = gulp.series(...prePackageTasks, npmRun("electron:publish")); +exports.packageLocal = gulp.series(...prePackageTasks, npmRun("electron:publish:never:local")); +exports.packageCfLocal = gulp.series(updateCfKey, npmRun("electron:publish:never:local")); diff --git a/WowUp/wowup-electron/inject-token.js b/WowUp/wowup-electron/inject-token.js new file mode 100644 index 0000000..7810739 --- /dev/null +++ b/WowUp/wowup-electron/inject-token.js @@ -0,0 +1,17 @@ +const fs = require("fs"); +const path = require("path"); + +const apiTokenPath = "./curseforge-api-key"; +const apiToken = process.env.CURSEFORGE_API_KEY || fs.readFileSync(apiTokenPath, { encoding: "utf-8" }) || "UNKNOWN"; + +const environmentsPath = "./src/environments"; +const envFiles = fs.readdirSync(environmentsPath); + +for (const envFile of envFiles) { + const p = path.join(environmentsPath, envFile); + + let fileData = fs.readFileSync(p, { encoding: "utf-8" }); + fileData = fileData.replace("{{CURSEFORGE_API_KEY}}", apiToken); + + fs.writeFileSync(p, fileData); +} diff --git a/WowUp/wowup-electron/linux-compose.yml b/WowUp/wowup-electron/linux-compose.yml new file mode 100644 index 0000000..4f2539a --- /dev/null +++ b/WowUp/wowup-electron/linux-compose.yml @@ -0,0 +1,13 @@ +version: "2" +services: + electron-builder: + image: node:10 + working_dir: /home/node/app + environment: + - "GH_TOKEN=${GH_TOKEN}" + volumes: + - ./:/home/node/app + command: > + sh -c "npm i -g node-gyp && + npm i && + npm run electron:publish" diff --git a/WowUp/wowup-electron/native/curse.cc b/WowUp/wowup-electron/native/curse.cc new file mode 100644 index 0000000..99b2a8c --- /dev/null +++ b/WowUp/wowup-electron/native/curse.cc @@ -0,0 +1,126 @@ +#include "curse.h" +#include + +std::string curse::hello() +{ + return "Hello World"; +} + +uint32_t curse::add(int a, int b) +{ + return (uint32_t)1767121699 * (uint32_t)1540483477; + // return a + b; +} + +Napi::String curse::HelloWrapped(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + Napi::String returnValue = Napi::String::New(env, curse::hello()); + return returnValue; +} + +Napi::Number curse::AddWrapped(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) + { + Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); + } + + Napi::Number first = info[0].As(); + Napi::Number second = info[1].As(); + + uint32_t returnValue = curse::add(first.Int32Value(), second.Int32Value()); + + return Napi::Number::New(env, returnValue); +} + +bool curse::isWhitespaceCharacter(char b) +{ + return b == 9 || b == 10 || b == 13 || b == 32; +} + +uint32_t curse::computeNormalizedLength(char *input, int length) +{ + int32_t num1 = 0; + + for (int32_t index = 0; index < length; ++index) + { + if (!curse::isWhitespaceCharacter(input[index])) + { + ++num1; + } + } + return num1; +} + +uint32_t curse::computeHash(char *buffer, int length) +{ + // std::ofstream myfile("hash.txt", std::ios::out | std::ios::binary); + + uint32_t multiplex = 1540483477; + uint32_t num1 = length; + + if (true) + { + num1 = curse::computeNormalizedLength(buffer, length); + } + + uint32_t num2 = (uint32_t)1 ^ num1; + uint32_t num3 = 0; + uint32_t num4 = 0; + + for (int index = 0; index < length; ++index) + { + unsigned char b = buffer[index]; + + if (!curse::isWhitespaceCharacter(b)) + { + num3 |= (uint32_t)b << num4; + + num4 += 8; + if (num4 == 32) + { + uint32_t num6 = num3 * multiplex; + uint32_t num7 = (num6 ^ num6 >> 24) * multiplex; + + num2 = num2 * multiplex ^ num7; + num3 = 0; + num4 = 0; + } + } + } + + if (num4 > 0) + { + num2 = (num2 ^ num3) * multiplex; + } + + uint32_t num6 = (num2 ^ num2 >> 13) * multiplex; + + return num6 ^ num6 >> 15; +} + +Napi::Number curse::ComputeHashWrapped(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + if (info.Length() < 2 || !info[0].IsBuffer() || !info[1].IsNumber()) + { + Napi::TypeError::New(env, "Buffer, Number expected").ThrowAsJavaScriptException(); + } + + Napi::Buffer buffer = info[0].As>(); + Napi::Number length = info[1].As(); + + uint32_t returnValue = curse::computeHash(buffer.Data(), length.Int32Value()); + + return Napi::Number::New(env, returnValue); +} + +Napi::Object curse::Init(Napi::Env env, Napi::Object exports) +{ + exports.Set("computeHash", Napi::Function::New(env, curse::ComputeHashWrapped)); + exports.Set("hello", Napi::Function::New(env, curse::HelloWrapped)); + exports.Set("add", Napi::Function::New(env, curse::AddWrapped)); + return exports; +} \ No newline at end of file diff --git a/WowUp/wowup-electron/native/curse.h b/WowUp/wowup-electron/native/curse.h new file mode 100644 index 0000000..605e0ae --- /dev/null +++ b/WowUp/wowup-electron/native/curse.h @@ -0,0 +1,17 @@ +#include + +namespace curse +{ + std::string hello(); + Napi::String HelloWrapped(const Napi::CallbackInfo &info); + + uint32_t add(int a, int b); + Napi::Number AddWrapped(const Napi::CallbackInfo &info); + + bool isWhitespaceCharacter(char b); + uint32_t computeNormalizedLength(char *input, int length); + uint32_t computeHash(char *buffer, int length); + Napi::Number ComputeHashWrapped(const Napi::CallbackInfo &info); + + Napi::Object Init(Napi::Env env, Napi::Object exports); +} // namespace curse \ No newline at end of file diff --git a/WowUp/wowup-electron/native/hello.cc b/WowUp/wowup-electron/native/hello.cc new file mode 100644 index 0000000..16a05e6 --- /dev/null +++ b/WowUp/wowup-electron/native/hello.cc @@ -0,0 +1,10 @@ + +#include +#include "curse.h" + +Napi::Object InitAll(Napi::Env env, Napi::Object exports) { + curse::Init(env, exports); + return exports; +} + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll) diff --git a/WowUp/wowup-electron/package-lock.json b/WowUp/wowup-electron/package-lock.json new file mode 100644 index 0000000..d4fed38 --- /dev/null +++ b/WowUp/wowup-electron/package-lock.json @@ -0,0 +1,27868 @@ +{ + "name": "wowup", + "version": "2.12.0-beta.5", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wowup", + "version": "2.12.0-beta.5", + "hasInstallScript": true, + "dependencies": { + "adm-zip": "0.5.12", + "auto-launch": "5.0.6", + "electron-log": "5.1.2", + "electron-store": "8.2.0", + "electron-updater": "6.1.8", + "globrex": "0.1.2", + "handlebars": "4.7.8", + "lodash": "4.17.21", + "minimist": "1.2.8", + "nanoid": "3.3.4", + "node-disk-info": "1.3.0", + "protobufjs": "7.2.6", + "pushy-electron": "1.0.11", + "rxjs": "7.8.1", + "win-ca": "3.5.1", + "wowup-lib-core": "1.0.6", + "yauzl": "2.10.0" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "17.0.2", + "@angular-devkit/build-angular": "17.3.4", + "@angular-eslint/builder": "17.3.0", + "@angular-eslint/eslint-plugin": "17.3.0", + "@angular-eslint/eslint-plugin-template": "17.3.0", + "@angular-eslint/schematics": "17.3.0", + "@angular-eslint/template-parser": "17.3.0", + "@angular/animations": "17.3.4", + "@angular/cdk": "17.3.4", + "@angular/cli": "17.3.4", + "@angular/common": "17.3.4", + "@angular/compiler": "17.3.4", + "@angular/compiler-cli": "17.3.4", + "@angular/core": "17.3.4", + "@angular/forms": "17.3.4", + "@angular/material": "17.3.4", + "@angular/platform-browser": "17.3.4", + "@angular/platform-browser-dynamic": "17.3.4", + "@angular/router": "17.3.4", + "@electron/notarize": "2.3.0", + "@fortawesome/angular-fontawesome": "0.14.0", + "@fortawesome/fontawesome-svg-core": "6.4.2", + "@fortawesome/free-brands-svg-icons": "6.4.2", + "@fortawesome/free-regular-svg-icons": "6.4.2", + "@fortawesome/free-solid-svg-icons": "6.4.2", + "@messageformat/core": "3.2.0", + "@microsoft/applicationinsights-web": "3.0.5", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", + "@types/adm-zip": "0.5.1", + "@types/flat": "5.0.2", + "@types/globrex": "0.1.2", + "@types/jasmine": "4.3.5", + "@types/jasminewd2": "2.0.10", + "@types/lodash": "4.14.198", + "@types/mocha": "10.0.1", + "@types/node": "18.14.6", + "@types/object-hash": "3.0.4", + "@types/opossum": "6.2.2", + "@types/slug": "5.0.4", + "@types/uuid": "9.0.3", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/eslint-plugin-tslint": "7.0.2", + "@typescript-eslint/parser": "^7.2.0", + "ag-grid-angular": "31.2.1", + "ag-grid-community": "31.2.1", + "chai": "4.3.8", + "core-js": "3.21.1", + "cross-env": "7.0.3", + "curseforge-v2": "1.3.0", + "del": "7.1.0", + "dotenv": "16.4.5", + "electron": "30.0.2", + "electron-builder": "24.13.3", + "electron-notarize": "1.2.2", + "electron-reload": "2.0.0-alpha.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-import": "2.28.1", + "eslint-plugin-jsdoc": "46.5.1", + "eslint-plugin-prefer-arrow": "1.2.3", + "flat": "5.0.2", + "gulp": "4.0.2", + "gulp-run": "1.7.1", + "i18next-json-sync": "3.1.2", + "ignore": "5.2.4", + "jasmine-core": "5.1.1", + "jasmine-spec-reporter": "7.0.0", + "karma": "6.4.2", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-electron": "7.3.0", + "karma-jasmine": "5.1.0", + "karma-jasmine-html-reporter": "2.1.0", + "markdown-it": "13.0.1", + "messageformat": "2.3.0", + "mocha": "10.2.0", + "ng-gallery": "11.0.0", + "ngx-translate-messageformat-compiler": "7.0.0", + "node-addon-api": "8.0.0", + "node-cache": "5.1.2", + "node-gyp": "10.1.0", + "npm-run-all": "4.1.5", + "object-hash": "3.0.0", + "opossum": "7.1.0", + "prettier": "3.2.5", + "slug": "8.2.3", + "spectron": "19.0.0", + "ts-custom-error": "3.3.1", + "ts-node": "10.9.1", + "tslib": "2.3.0", + "typescript": "5.2.2", + "uuid": "9.0.0", + "wait-on": "7.2.0", + "zone.js": "0.14.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-builders/common": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-1.0.2.tgz", + "integrity": "sha512-lUusRq6jN1It5LcUTLS6Q+AYAYGTo/EEN8hV0M6Ek9qXzweAouJaSEnwv7p04/pD7yJTl0YOCbN79u+wGm3x4g==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "^17.1.0", + "ts-node": "^10.0.0", + "tsconfig-paths": "^4.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + } + }, + "node_modules/@angular-builders/custom-webpack": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-17.0.2.tgz", + "integrity": "sha512-K0jqdW5UdVIeKiZXO4nLiiiVt0g6PKJELdxgjsBGMtyRk+RLEY+pIp1061oy/Yf09nGYseZ7Mdx3XASYHQjNwA==", + "dev": true, + "dependencies": { + "@angular-builders/common": "1.0.2", + "@angular-devkit/architect": ">=0.1700.0 < 0.1800.0", + "@angular-devkit/build-angular": "^17.0.0", + "@angular-devkit/core": "^17.0.0", + "lodash": "^4.17.15", + "webpack-merge": "^5.7.3" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1703.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.4.tgz", + "integrity": "sha512-o+XCMOiMh8tmQGEwcxjAj2/lmUVT7CGSUAM31ydDomVOFFw4CnBvsoyKqQNRC+/AUXvovb2dCegQl/lTAnrwOg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.4", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.4.tgz", + "integrity": "sha512-8KieoPrsJcFPoza0gLQ6yebtIb3WdH3j/V1TnAihk4tVpgtdch8tOBE3FP1TnSW3RF+iCsA0I5NO9/4YbEsWtw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1703.4", + "@angular-devkit/build-webpack": "0.1703.4", + "@angular-devkit/core": "17.3.4", + "@babel/core": "7.24.0", + "@babel/generator": "7.23.6", + "@babel/helper-annotate-as-pure": "7.22.5", + "@babel/helper-split-export-declaration": "7.22.6", + "@babel/plugin-transform-async-generator-functions": "7.23.9", + "@babel/plugin-transform-async-to-generator": "7.23.3", + "@babel/plugin-transform-runtime": "7.24.0", + "@babel/preset-env": "7.24.0", + "@babel/runtime": "7.24.0", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "17.3.4", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.18", + "babel-loader": "9.1.3", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.22", + "css-loader": "6.10.0", + "esbuild-wasm": "0.20.1", + "fast-glob": "3.3.2", + "http-proxy-middleware": "2.0.6", + "https-proxy-agent": "7.0.4", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.8", + "mini-css-extract-plugin": "2.8.1", + "mrmime": "2.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.1", + "piscina": "4.4.0", + "postcss": "8.4.35", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.71.1", + "sass-loader": "14.1.1", + "semver": "7.6.0", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.29.1", + "tree-kill": "1.2.2", + "tslib": "2.6.2", + "undici": "6.11.1", + "vite": "5.1.7", + "watchpack": "2.4.0", + "webpack": "5.90.3", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.15.1", + "webpack-merge": "5.10.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.20.1" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "@angular/localize": "^17.0.0", + "@angular/platform-server": "^17.0.0", + "@angular/service-worker": "^17.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^17.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.2 <5.5" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1703.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.4.tgz", + "integrity": "sha512-9Vsl6rfIH8kF02W7i3tW/aMOT2Ld1zpcok7n7JdL3Pb7oW0SOjt73FN6Ykm/hVig12gsOGJtEsDfQRsnCddmfQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1703.4", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.4.tgz", + "integrity": "sha512-vE69/Db555NTRPh+LUFO3rAQBbv7QGrK59F7chRggDZKamtCq/FfhEg2O+0BXQnUitOQN6WgQ79+payFYWyCCg==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.4.tgz", + "integrity": "sha512-Z6801QhIwrMTcKPzdo9si+ZtJkPz8fys0ftOTfTM66+tDECasU7pvk8Dr54WkDY29mdSHzPxpSxAsooEwfxvQQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.4", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-eslint/builder": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.3.0.tgz", + "integrity": "sha512-JXSZE7+KA3UGU6jwc0v9lwOIMptosrvLIOXGlXqrhHWEXfkfu3ENPq1Lm3K8jLndQ57XueEhC+Nab/AuUiWA/Q==", + "dev": true, + "dependencies": { + "@nx/devkit": "^17.2.8 || ^18.0.0", + "nx": "^17.2.8 || ^18.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.3.0.tgz", + "integrity": "sha512-ejfNzRuBeHUV8m2fkgs+M809rj5STuCuQo4fdfc6ccQpzXDI6Ha7BKpTznWfg5g529q/wrkoGSGgFxU9Yc2/dQ==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.3.0.tgz", + "integrity": "sha512-81cQbOEPoQupFX8WmpqZn+y8VA7JdVRGBtt+uJNKBXcJknTpPWdLBZRFlgVakmC24iEZ0Fint/N3NBBQI3mz2A==", + "dev": true, + "dependencies": { + "@angular-eslint/utils": "17.3.0", + "@typescript-eslint/utils": "7.2.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.3.0.tgz", + "integrity": "sha512-9l/aRfpE9MCRVDWRb+rSB9Zei0paep1vqV6M/87VUnzBnzqeMRnVuPvQowilh2zweVSGKBF25Vp4HkwOL6ExDQ==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.3.0", + "@angular-eslint/utils": "17.3.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "aria-query": "5.3.0", + "axobject-query": "4.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.3.0.tgz", + "integrity": "sha512-5yssd5EOomxlKt9vN/OXXCTCuI3Pmfj16pkjBDoW0wzC8/M2l5zlXIEfoKumHYv2wtF553LhaMXVYVU35e0lTw==", + "dev": true, + "dependencies": { + "@angular-eslint/eslint-plugin": "17.3.0", + "@angular-eslint/eslint-plugin-template": "17.3.0", + "@nx/devkit": "^17.2.8 || ^18.0.0", + "ignore": "5.3.1", + "nx": "^17.2.8 || ^18.0.0", + "strip-json-comments": "3.1.1", + "tmp": "0.2.3" + }, + "peerDependencies": { + "@angular/cli": ">= 17.0.0 < 18.0.0" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.3.0.tgz", + "integrity": "sha512-m+UzAnWgtjeS0x6skSmR0eXltD/p7HZA+c8pPyAkiHQzkxE7ohhfyZc03yWGuYJvWQUqQAKKdO/nQop14TP0bg==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.3.0", + "eslint-scope": "^8.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/template-parser/node_modules/eslint-scope": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.3.0.tgz", + "integrity": "sha512-PJT9pxWqpvI9OXO+7L5SIVhvMW+RFjeafC7PYjtvSbNFpz+kF644BiAcfMJ0YqBnkrw3JXt+RAX25CT4mXIoXw==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.3.0", + "@typescript-eslint/utils": "7.2.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/animations": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.4.tgz", + "integrity": "sha512-2nBgXRdTSVPZMueV6ZJjajDRucwJBLxwiVhGafk/nI5MJF0Yss/Jfp2Kfzk5Xw2AqGhz0rd00IyNNUQIzO2mlw==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.4" + } + }, + "node_modules/@angular/cdk": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.4.tgz", + "integrity": "sha512-/wbKUbc0YC3HGE2TCgW7D07Q99PZ/5uoRvMyWw0/wHa8VLNavXZPecbvtyLs//3HnqoCMSUFE7E2Mrd7jAWfcA==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.4.tgz", + "integrity": "sha512-o4oIA2stUwXOur/T/kP3Zr8ZUCB4VYmvjACbsQ3tpzVCFYPeaW9psQagBNJfaBVVDSYL+EacVYBYJR9ZImvcGw==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1703.4", + "@angular-devkit/core": "17.3.4", + "@angular-devkit/schematics": "17.3.4", + "@schematics/angular": "17.3.4", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.1.2", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "npm-package-arg": "11.0.1", + "npm-pick-manifest": "9.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "17.0.6", + "resolve": "1.22.8", + "semver": "7.6.0", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@angular/common": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.4.tgz", + "integrity": "sha512-rEsmtwUMJaNvaimh9hwaHdDLXaOIrjEnYdhmJUvDaKPQaFfSbH3CGGVz9brUyzVJyiWJYkYM0ssxavczeiEe8g==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.4", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.4.tgz", + "integrity": "sha512-YrDClIzgj6nQwiYHrfV6AkT1C5LCDgJh+LICus/2EY1w80j1Qf48Zh4asictReePdVE2Tarq6dnpDh4RW6LenQ==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.4" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.4.tgz", + "integrity": "sha512-TVWjpZSI/GIXTYsmVgEKYjBckcW8Aj62DcxLNehRFR+c7UB95OY3ZFjU8U4jL0XvWPgTkkVWQVq+P6N4KCBsyw==", + "dev": true, + "dependencies": { + "@babel/core": "7.23.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.3.4", + "typescript": ">=5.2 <5.5" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/core": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.4.tgz", + "integrity": "sha512-fvhBkfa/DDBzp1UcNzSxHj+Z9DebSS/o9pZpZlbu/0uEiu9hScmScnhaty5E0EbutzHB0SVUCz7zZuDeAywvWg==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" + } + }, + "node_modules/@angular/forms": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.4.tgz", + "integrity": "sha512-XWA/FAs0r7VRdztMIfGU9EE0Chj+1U/sDnzJK3ZPO0n8F8oDAEWGJyiw8GIyWTLs+mz43thVIED3DhbRNsXbWw==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.4", + "@angular/core": "17.3.4", + "@angular/platform-browser": "17.3.4", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.4.tgz", + "integrity": "sha512-SgCroIlHKt3s9pTEYlhW4ww6Gm1sIzJKuk0wlputPZvQS5PTJ8YY8vDg4QohpQcltlaXCbutt4qw+CBNU9W9iA==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/auto-init": "15.0.0-canary.7f224ddd4.0", + "@material/banner": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/card": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/chips": "15.0.0-canary.7f224ddd4.0", + "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", + "@material/data-table": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dialog": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/drawer": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/fab": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/form-field": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/image-list": "15.0.0-canary.7f224ddd4.0", + "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/radio": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/slider": "15.0.0-canary.7f224ddd4.0", + "@material/snackbar": "15.0.0-canary.7f224ddd4.0", + "@material/switch": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/textfield": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tooltip": "15.0.0-canary.7f224ddd4.0", + "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^17.0.0 || ^18.0.0", + "@angular/cdk": "17.3.4", + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/forms": "^17.0.0 || ^18.0.0", + "@angular/platform-browser": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.4.tgz", + "integrity": "sha512-W2nH9WSQJfdNG4HH9B1Cvj5CTmy9gF3321I+65Tnb8jFmpeljYDBC/VVUhTZUCRpg8udMWeMHEQHuSb8CbozmQ==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "17.3.4", + "@angular/common": "17.3.4", + "@angular/core": "17.3.4" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.4.tgz", + "integrity": "sha512-S53jPyQtInVYkjdGEFt4dxM1NrHNkWCvXGRsCO7Uh+laDf1OpIDp9YHf49OZohYLajJradN6y4QfdZL6IUwXKA==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.4", + "@angular/compiler": "17.3.4", + "@angular/core": "17.3.4", + "@angular/platform-browser": "17.3.4" + } + }, + "node_modules/@angular/router": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.4.tgz", + "integrity": "sha512-B1zjUYyhN66dp47zdF96NRwo0dEdM5In4Ob8HN64PAbnaK3y1EPp31aN6EGernPvKum1ibgwSZw+Uwnbkuv7Ww==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.4", + "@angular/core": "17.3.4", + "@angular/platform-browser": "17.3.4", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.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" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz", + "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", + "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", + "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz", + "integrity": "sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.4", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz", + "integrity": "sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz", + "integrity": "sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz", + "integrity": "sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz", + "integrity": "sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz", + "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz", + "integrity": "sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", + "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz", + "integrity": "sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", + "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.24.0", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@develar/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@develar/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@develar/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.9.tgz", + "integrity": "sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==", + "dev": true, + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/notarize": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.3.0.tgz", + "integrity": "sha512-EiTBU0BwE7HZZjAG1fFWQaiQpCuPrVGn7jPss1kUjD6eTTdXXd29RiZqEqkgN7xqt/Pgn4g3I7Saqovanrfj3w==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/remote": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@electron/remote/-/remote-2.0.4.tgz", + "integrity": "sha512-8m2P/d2RH986PmMW5lKygbPEjEYJ7RgCe37Y8DQ1wujKMH6VjmLIB+Y+DP2SA611svCZc58TRSd8FraGvcfGZw==", + "dev": true, + "peerDependencies": { + "electron": ">= 13.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fortawesome/angular-fontawesome": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.14.0.tgz", + "integrity": "sha512-nB7an9t66nY0m/1MIBOIvi+vKyZaTskhtGtQwGTiMyte3Bmy9080pFpXguyox68/vxGVmLxZkRxYIgjMCvm7QQ==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular/core": "^17.0.0", + "@fortawesome/fontawesome-svg-core": "~1.2.27 || ~1.3.0-beta2 || ^6.1.0" + } + }, + "node_modules/@fortawesome/angular-fontawesome/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", + "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", + "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz", + "integrity": "sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz", + "integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", + "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@ljharb/through": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-1GSJaPKef+7HRuV+HusVZHps64cmZuOItDbt40tjJVaikcaZvwmHlcTxRIqzcRoCdt5ZKHh3NoO7GB9Khg4Jnw==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-t7ZGpRJ3ec0QDUO0nJu/SMgLW7qcuG2KqIsEYD1Ej8qhI2xpdR2ydSDQOkVEitXmKoGol1oq4nYSBjTlB65GqA==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-g9wBUZzYBizyBcBQXTIafnRUUPi7efU9gPJfzeGgkynXiccP/vh5XMmH+PBxl5v+4MlP/d4cZ2NUYoAN7UTqSA==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-I9KQOKXpLfJkP8MqZyr8wZIzdPHrwPjFvGd9zSK91/vPyE4hzHRJc/0njsh9g8Lm9PRYLbifXX+719uTbHxx+A==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BHB7iyHgRVH+JF16+iscR+Qaic+p7LU1FOLgP8KucRlpF9tTwIxQA6mJwGRi5gUtcG+vyCmzVS+hIQ6DqT/7BA==", + "dev": true, + "dependencies": { + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kt7y9/IWOtJTr3Z/AoWJT3ZLN7CLlzXhx2udCLP9ootZU2bfGK0lzNwmo80bv/pJfrY9ihQKCtuGTtNxUy+vIw==", + "dev": true, + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-rURcrL5O1u6hzWR+dNgiQ/n89vk6tdmdP3mZgnxJx61q4I/k1yijKqNJSLrkXH7Rto3bM5NRKMOlgvMvVd7UMQ==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AYAivV3GSk/T/nRIpH27sOHFPaSMrE3L0WYbnb5Wa93FgY8a0fbsFYtSH2QmtwnzXveg+B1zGTt7/xIIcynKdQ==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DJrqCKb+LuGtjNvKl8XigvyK02y36GRkfhMUYTcJEi3PrOE00bwXtyj7ilhzEVshQiXg6AHGWXtf5UqwNrx3Ow==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-/2WZsuBIq9z9RWYF5Jo6b7P6u0fwit+29/mN7rmAZ6akqUR54nXyNfoSNiyydMkzPlZZsep5KrSHododDhBZbA==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-o9EXmGKVpiQ6mHhyV3oDDzc78Ow3E7v8dlaOhgaDSXgmqaE8v5sIlLNa/LKSyUga83/fpGk3QViSGXotpQx0jA==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-u0XpTlv1JqWC/bQ3DavJ1JguofTelLT2wloj59l3/1b60jv42JQ6Am7jU3I8/SIUB1MKaW7dYocXjDWtWJakLA==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-mQ1HT186GPQSkRg5S18i70typ5ZytfjL09R0gJ2Qg5/G+MLCGi7TAjZZSH65tuD/QGOjel4rDdWOTmYbPYV6HA==", + "dev": true, + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-qyO0W0KBftfH8dlLR0gVAgv7ZHNvU8ae11Ao6zJif/YxcvK4+gph1z8AO4H410YmC2kZiwpSKyxM1iQCCzbb4g==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-tV6s4/pUBECedaI36Yj18KmRCk1vfue/JP/5yYRlFNnLMRVISePbZaKkn/BHXVf+26I3W879+XqIGlDVdmOoMA==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-4h76QrzfZTcPdd+awDPZ4Q0YdSqsXQnS540TPtyXUJ/5G99V6VwGpjMPIxAsW0y+pmI9UkLL/srrMaJec+7r4Q==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SAjtxYh6YlKZriU83diDEQ7jNSP2MnxKsER0TvFeyG1vX/DWsUyYDOIJTOEa9K1N+fgJEBkNK8hY55QhQaspew==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-0KMo5ijjYaEHPiZ2pCVIcbaTS2LycvH9zEhEMKwPPGssBCX7iz5ffYQFk7e5yrQand1r3jnQQgYfHAwtykArnQ==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Jmg1nltq4J6S6A10EGMZnvufrvU3YTi+8R8ZD9lkSbun0Fm2TVdICQt/Auyi6An9zP66oQN6c31eqO6KfIPsDg==", + "dev": true, + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-fEPWgDQEPJ6WF7hNnIStxucHR9LE4DoDSMqCsGWS2Yu+NLZYLuCEecgR0UqQsl1EQdNRaFh8VH93KuxGd2hiPg==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DcK7IL4ICY/DW+48YQZZs9g0U1kRaW0Wb0BxhvppDMYziHo/CTpFdle4gjyuTyRxPOdHQz5a97ru48Z9O4muTw==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-voMjG2p80XbjL1B2lmF65zO5gEgJOVKClLdqh4wbYzYfwY/SR9c8eLvlYG7DLdFaFBl/7gGxD8TvvZ329HUFPw==", + "dev": true, + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-veDABLxMn2RmvfnUO2RUmC1OFfWr4cU+MrxKPoDD2hl3l3eDYv5fxws6r5T1JoSyXoaN+oEZpheS0+M9Ure8Pg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-f60hVJhIU6I3/17Tqqzch1emUKEcfVVgHVqADbU14JD+oEIz429ZX9ksZ3VChoU3+eejFl+jVdZMLE/LrAuwpg==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-pRDEwPQielDiC9Sc5XhCXrGxP8wWOnAO8sQlMebfBYHYqy5hhiIzibezS8CSaW4MFQFyXmCmpmqWlbqGYRmiyg==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Is0NV91sJlXF5pOebYAtWLF4wU2MJDbYqztML/zQNENkQxDOvEXu3nWNb3YScMIYJJXvARO0Liur5K4yPagS1Q==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-D11QU1dXqLbh5X1zKlEhS3QWh0b5BPNXlafc5MXfkdJHhOiieb7LC9hMJhbrHtj24FadJ7evaFW/T2ugJbJNnQ==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-7RZHvw0gbwppaAJ/Oh5SWmfAKJ62aw1IMB3+3MRwsb5PLoV666wInYa+zJfE4i7qBeOn904xqT2Nko5hY0ssrg==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Yg2usuKB2DKlKIBISbie9BFsOVuffF71xjbxPbybvqemxqUBd+bD5/t6H1fLE+F8/NCu5JMigho4ewUU+0RCiw==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UPbDjE5CqT+SqTs0mNFG6uFEw7wBlgYmh+noSkQ6ty/EURm8lF125dmi4dv4kW0+octonMXqkGtAoZwLIHKf/w==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-wR1X0Sr0KmQLu6+YOFKAI84G3L6psqd7Kys5kfb8WKBM36zxO5HQXC5nJm/Y0rdn22ixzsIz2GBo0MNU4V4k1A==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-JqOsWM1f4aGdotP0rh1vZlPZTg6lZgh39FIYHFMfOwfhR+LAikUJ+37ciqZuewgzXB6iiRO6a8aUH6HR5SJYPg==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UVf14qAtmPiaaZjuJtmN36HETyoKWmsZM/qn1L5ciR2URb8O035dFWnz4ZWFMmAYBno/L7JiZaCkPurv2ZNrGA==", + "dev": true, + "dependencies": { + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LCnVRUSAhELTKI/9hSvyvIvQIpPpqF29BV+O9yM4WoNNmNWqTulvuiv7grHZl6Z+kJuxSg4BGbsPxxb9dXozPg==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-WioZtQEXRpglum0cMSzSqocnhsGRr+ZIhvKb3FlaNrTaK8H3Y4QA7rVjv3emRtrLOOjaT6/RiIaUMTo9AGzWQQ==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8z8l1W3+cymObunJoRhwFPKZ+FyECfJ4MJykNiaZq7XJFZkV6xNmqAVrrbQj93FtLsECn9g4PjjIomguVn/OEw==", + "dev": true, + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-QU/WSaSWlLKQRqOhJrPgm29wqvvzRusMqwAcrCh1JTrCl+xwJ43q5WLDfjYhubeKtrEEgGu9tekkAiYfMG7EBw==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-sm7EbVKddaXpT/aXAYBdPoN0k8yeg9+dprgBUkrdqGzWJAeCkxb4fv2B3He88YiCtvkTz2KLY4CThPQBSEsMFQ==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-lEDJfRvkVyyeHWIBfoxYjJVl+WlEAE2kZ/+6OqB1FW0OV8ftTODZGhHRSzjVBA1/p4FPuhAtKtoK9jTpa4AZjA==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-E1xGACImyCLurhnizyOTCgOiVezce4HlBFAI6YhJo/AyVwjN2Dtas4ZLQMvvWWqpyhITNkeYdOchwCC1mrz3AQ==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-p1Asb2NzrcECvAQU3b2SYrpyJGyJLQWR+nXTYzDKE8WOpLIRCXap2audNqD7fvN/A20UJ1J8U01ptrvCkwJ4eA==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-h9Td3MPqbs33spcPS7ecByRHraYgU4tNCZpZzZXw31RypjKvISDv/PS5wcA4RmWqNGih78T7xg4QIGsZg4Pk4w==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LFeYNjQpdXecwECd8UaqHYbhscDCwhGln5Yh+3ctvcEgvmDPNjhKn/DL3sWprWvG8NAhP6sHMrsGhQFVdCWtTg==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AExmFvgE5nNF0UA4l2cSzPghtxSUQeeoyRjFLHLy+oAaE4eKZFrSy0zEpqPeWPQpEMDZk+6Y+6T3cOFYBeSvsw==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-hs45hJoE9yVnoVOcsN1jklyOa51U4lzWsEnQEuJTPOk2+0HqCQ0yv/q0InpSnm2i69fNSyZC60+8HADZGF8ugQ==", + "dev": true, + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-r9TDoicmcT7FhUXC4eYMFnt9TZsz0G8T3wXvkKncLppYvZ517gPyD/1+yhuGfGOxAzxTrM66S/oEc1fFE2q4hw==", + "dev": true, + "dependencies": { + "@material/elevation": "15.0.0-canary.7f224ddd4.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8qNk3pmPLTnam3XYC1sZuplQXW9xLn4Z4MI3D+U17Q7pfNZfoOugGr+d2cLA9yWAEjVJYB0mj8Yu86+udo4N9w==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SARR5/ClYT4CLe9qAXakbr0i0cMY0V3V4pe3ElIJPfL2Z2c4wGR1mTR8m2LxU1MfGKK8aRoUdtfKaxWejp+eNA==", + "dev": true, + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BJo/wFKHPYLGsRaIpd7vsQwKr02LtO2e89Psv0on/p0OephlNIgeB9dD9W+bQmaeZsZ6liKSKRl6wJWDiK71PA==", + "dev": true, + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kBaZeCGD50iq1DeRRH5OM5Jl7Gdk+/NOfKArkY4ksBZvJiStJ7ACAhpvb8MEGm4s3jvDInQFLsDq3hL+SA79sQ==", + "dev": true, + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@messageformat/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.2.0.tgz", + "integrity": "sha512-ppbb/7OYqg/t4WdFk8VAfZEV2sNUq3+7VeBAo5sKFhmF786sh6gB7fUeXa2qLTDIcTHS49HivTBN7QNOU5OFTg==", + "dev": true, + "dependencies": { + "@messageformat/date-skeleton": "^1.0.0", + "@messageformat/number-skeleton": "^1.0.0", + "@messageformat/parser": "^5.1.0", + "@messageformat/runtime": "^3.0.1", + "make-plural": "^7.0.0", + "safe-identifier": "^0.4.1" + } + }, + "node_modules/@messageformat/date-skeleton": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.0.1.tgz", + "integrity": "sha512-jPXy8fg+WMPIgmGjxSlnGJn68h/2InfT0TNSkVx0IGXgp4ynnvYkbZ51dGWmGySEK+pBiYUttbQdu5XEqX5CRg==", + "dev": true + }, + "node_modules/@messageformat/number-skeleton": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", + "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==", + "dev": true + }, + "node_modules/@messageformat/parser": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.0.tgz", + "integrity": "sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ==", + "dev": true, + "dependencies": { + "moo": "^0.5.1" + } + }, + "node_modules/@messageformat/runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz", + "integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==", + "dev": true, + "dependencies": { + "make-plural": "^7.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-analytics-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-3.0.5.tgz", + "integrity": "sha512-SUcBfC0zPT1Pm0PyWTbqZPYNYap6C4Rnub/umrpk62UvBz/X78o3gxng2zm5QMGMf9MJtzwb63K8pl+lYpknTQ==", + "dev": true, + "dependencies": { + "@microsoft/applicationinsights-common": "3.0.5", + "@microsoft/applicationinsights-core-js": "3.0.5", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-cfgsync-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-cfgsync-js/-/applicationinsights-cfgsync-js-3.0.5.tgz", + "integrity": "sha512-YQUWlw7vGj6JKmWLfSRtiiHQ0A3rIUmCO2K3i+ufGmH6oPPS5jrnQ8F0R9tJd82W0RxqeMmROW6KGcF7zyY1MQ==", + "dev": true, + "dependencies": { + "@microsoft/applicationinsights-common": "3.0.5", + "@microsoft/applicationinsights-core-js": "3.0.5", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.3.0 < 2.x", + "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.0.5.tgz", + "integrity": "sha512-KfTYY0uZmrQgrz8ErBh1q08eiYfzjUIVzJZHETgEkqv3l2RTndQgpmywDbVNf9wVTB7Mp89ZrFeCciVJFf5geg==", + "dev": true, + "dependencies": { + "@microsoft/applicationinsights-common": "3.0.5", + "@microsoft/applicationinsights-core-js": "3.0.5", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.3.0 < 2.x", + "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-common": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.5.tgz", + "integrity": "sha512-ahph1fMqyLcZ1twzDKMzpHRgR9zEIyqNhMQxDgQ45ieVD641bZiYVwSlbntSXhGCtr5G5HE02zlEzwSxbx95ng==", + "dev": true, + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.0.5", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.5.tgz", + "integrity": "sha512-/x+tkxsVALNWSvwGMyaLwFPdD3p156Pef9WHftXrzrKkJ+685nhrwm9MqHIyEHHpSW09ElOdpJ3rfFVqpKRQyQ==", + "dev": true, + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.3.0 < 2.x", + "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-dependencies-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-3.0.5.tgz", + "integrity": "sha512-awF9Qx6y9mP27Bc/W5NiMMBX12ujKfaDqAw/k0YISyvhHU66lxoaI/1Lnby5hRHYYil+jMtH6FHsstnB+DRS3Q==", + "dev": true, + "dependencies": { + "@microsoft/applicationinsights-common": "3.0.5", + "@microsoft/applicationinsights-core-js": "3.0.5", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.3.0 < 2.x", + "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-properties-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-3.0.5.tgz", + "integrity": "sha512-e0Lo408vN5FjaW/6SIIWbeC5H4qTX1sd1qRKVpe2rnZICDNUk6Rane8tJ47yBsO2eBIn8vXPx8JKRK5249yYNg==", + "dev": true, + "dependencies": { + "@microsoft/applicationinsights-common": "3.0.5", + "@microsoft/applicationinsights-core-js": "3.0.5", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dev": true, + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-3.0.5.tgz", + "integrity": "sha512-+jyOGj+pMxTxLBv9pU5zhOqSaYfn7CTN86y9rzZu8CoE3eibNIZFvliICTpLX++1/lYvJgUd1OP8ZGp+xb4JYQ==", + "dev": true, + "dependencies": { + "@microsoft/applicationinsights-analytics-js": "3.0.5", + "@microsoft/applicationinsights-cfgsync-js": "3.0.5", + "@microsoft/applicationinsights-channel-js": "3.0.5", + "@microsoft/applicationinsights-common": "3.0.5", + "@microsoft/applicationinsights-core-js": "3.0.5", + "@microsoft/applicationinsights-dependencies-js": "3.0.5", + "@microsoft/applicationinsights-properties-js": "3.0.5", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.3.0 < 2.x", + "@nevware21/ts-utils": ">= 0.10.1 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.2.tgz", + "integrity": "sha512-MB8trWaFREpmb037k/d0bB7T2BP7Ai24w1e1tbz3ASLB0/lwphsq3Nq8S9I5AsI5vs4zAQT+SB5nC5/dLYTiOg==", + "dev": true, + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "dev": true, + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==", + "dev": true + }, + "node_modules/@ngtools/webpack": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.4.tgz", + "integrity": "sha512-3uNX4tRTKPm91mSQcnmQtqDMMKLGDevJERSPJU7hlOXZZ05QrT4et1mwvXNYYMpXqi2OkC7D4ryIS2YxAiItBA==", + "dev": true, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "typescript": ">=5.2 <5.5", + "webpack": "^5.54.0" + } + }, + "node_modules/@ngx-translate/core": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-15.0.0.tgz", + "integrity": "sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA==", + "dev": true, + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-8.0.0.tgz", + "integrity": "sha512-SFMsdUcmHF5OdZkL1CHEoSAwbP5EbAOPTLLboOCRRoOg21P4GJx+51jxGdJeGve6LSKLf4Pay7BkTwmE6vxYlg==", + "dev": true, + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@ngx-translate/core": ">=15.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.0.tgz", + "integrity": "sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.5.tgz", + "integrity": "sha512-x8hXItC8OFOwdgERzRIxg0ic1lQqW6kSZFFQtZTCNYOeGb9UqzVcod02TYljI9UBl4RtfcyQ0A7ygmcGFvEqWw==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", + "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", + "dev": true, + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.2.tgz", + "integrity": "sha512-LmW+tueGSK+FCM3OpcKtwKKo3igpefh6HHiw23sGd8OdJ8l0GrfGfVdGOFVtJRMaXVnvI1RUdEPlB9VUln5Wbw==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", + "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", + "dev": true, + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", + "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", + "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@nrwl/devkit": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-18.2.4.tgz", + "integrity": "sha512-dLK8MMb3eEFWlhtI1kNDNbWIT1Xbrgg3eAQ+Ix/N5JDbxJkJhE28WsIJgQb1NTwe/N87O5JtOpxz4/TsSLJCsQ==", + "dev": true, + "dependencies": { + "@nx/devkit": "18.2.4" + } + }, + "node_modules/@nrwl/tao": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-18.2.4.tgz", + "integrity": "sha512-kgJwZ26F+AzvFXaW5eh1g4HLntPcJ6+EE7JyEvrdRzpw7KxTqWy6Ql7dYys6zGlpP4c3PbsXwdc7tGM3Df2PNg==", + "dev": true, + "dependencies": { + "nx": "18.2.4", + "tslib": "^2.3.0" + }, + "bin": { + "tao": "index.js" + } + }, + "node_modules/@nx/devkit": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-18.2.4.tgz", + "integrity": "sha512-Ws3BcA/aeXuwsCQ5e7PYy2H7DswareTOEfgs7izxNyGugpydktVH9DZZTOFNDsc06yzgvyTucDbDQ+JsrJ9PcQ==", + "dev": true, + "dependencies": { + "@nrwl/devkit": "18.2.4", + "ejs": "^3.1.7", + "enquirer": "~2.3.6", + "ignore": "^5.0.4", + "semver": "^7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0", + "yargs-parser": "21.1.1" + }, + "peerDependencies": { + "nx": ">= 16 <= 18" + } + }, + "node_modules/@nx/devkit/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@nx/nx-darwin-arm64": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-18.2.4.tgz", + "integrity": "sha512-RYhMImghdyHmwnbNoR2CkLz4Opj9EmuHY3lMfsorg+T4wIOql/iXACrqjnreN7Hy9myJDo1EIbYZ4x8VSxFWtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-darwin-x64": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-18.2.4.tgz", + "integrity": "sha512-2mXMslSRD/ZoI/oaX+0Mh9J/hucXtNgdwC4YFbp1u8UKquAaQ6hf4uo0s4i+AfLX0F7roMtkFPaG/+MQUJE1Rw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-freebsd-x64": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-18.2.4.tgz", + "integrity": "sha512-QUiYLvyUT0PS7D8erf49xa1Jyw4Gfev5gtYfME34Twmn/JPx/99ZkBG4wHbzLqRGwlO5K6m6P4qs30Pzfwtw7A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-18.2.4.tgz", + "integrity": "sha512-+fjFciSUhvDV8dPa97Brwb83k3Xa4gHPI2Un8wlpp28Cv4horeGruRZrrifR1VmD2wp2UBIMl5n7YsDP8KvYhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-18.2.4.tgz", + "integrity": "sha512-lfaTc+AvV56Uv5mXROiRwh2REiI/7IsqeRDfL+prcuuvJ5Oxi2wYVgnmqcHL+ryQnk0Qn7/d+j/BmYHX5Ve5jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-18.2.4.tgz", + "integrity": "sha512-U6eoLTQmbxUWU9kZxx6hsYN4zmmOrsDDeW+i3aj5aeahfYlmyz6TsT0V3FSB70WGJC5aMVgEi4RkntQMKkm5vQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-18.2.4.tgz", + "integrity": "sha512-q8WcJhmcRNORkKjax6WcUwMJe/1mQs+RYlUkGqmi7tD7lfcLSqdLPJVjqVmQAwmy1Wh/MHPsbqRwSerUnCxB1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-musl": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-18.2.4.tgz", + "integrity": "sha512-0MDuoPgHa6kkBrjg7hwZ2qQivhJbh3lk7r3q4osDrqZcGxq5XVJqeAmYFyChQy4dbQfUm4hhYkEfzpU8M2lnvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-18.2.4.tgz", + "integrity": "sha512-uLhSRtfnXzN000Qf27GOjEPXzd4/jBWqv2x419IMh+AEtKHuCEpQNBUAyLvBbQ79SMr+FmCXHB8AeeJ7bEUiRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-18.2.4.tgz", + "integrity": "sha512-Y52Afz02Ub1kRZXd6NUTwPMjKQqBKZ35e5dUEpl14na2fWvdgdMz4bYOBPUcmQrovlxBGhmFXtFzxkdW3zyRbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.2.tgz", + "integrity": "sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.2.tgz", + "integrity": "sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.2.tgz", + "integrity": "sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.2.tgz", + "integrity": "sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.2.tgz", + "integrity": "sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.2.tgz", + "integrity": "sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.2.tgz", + "integrity": "sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.2.tgz", + "integrity": "sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.2.tgz", + "integrity": "sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.2.tgz", + "integrity": "sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.2.tgz", + "integrity": "sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.2.tgz", + "integrity": "sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.2.tgz", + "integrity": "sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.2.tgz", + "integrity": "sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.2.tgz", + "integrity": "sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.4.tgz", + "integrity": "sha512-Rqhp5l76Ej6BOZCHPrvHlA2SBkjv1aHFWAfW9gREke826j46D+fuA0eDAdgeVTz0Fx9e7XM3LdtWsz7CBlV4Ug==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.4", + "@angular-devkit/schematics": "17.3.4", + "jsonc-parser": "3.2.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.1.tgz", + "integrity": "sha512-eqV17lO3EIFqCWK3969Rz+J8MYrRZKw9IBHpSo6DEcEX2c+uzDFOgHE9f2MnyDpfs48LFO4hXmk9KhQ74JzU1g==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.1.tgz", + "integrity": "sha512-aIL8Z9NsMr3C64jyQzE0XlkEyBLpgEJJFDHLVVStkFV5Q3Il/r/YtY6NJWKQ4cy4AE7spP1IX5Jq7VCAxHHMfQ==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.0.tgz", + "integrity": "sha512-tsAyV6FC3R3pHmKS880IXcDJuiFJiKITO1jxR1qbplcsBkZLBmjrEw5GbC7ikD6f5RU1hr7WnmxB/2kKc1qUWQ==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.0", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.1", + "make-fetch-happen": "^13.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.2.tgz", + "integrity": "sha512-mwbY1VrEGU4CO55t+Kl6I7WZzIl+ysSzEYdA1Nv/FTrl2bkeaPXo5PnWZAVfcY2zSdhOpsUTJW67/M2zHXGn5w==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.0", + "tuf-js": "^2.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.0.tgz", + "integrity": "sha512-hQF60nc9yab+Csi4AyoAmilGNfpXT+EXdBgFkP9OgPwIBPwyqVf7JAWPtmqrrrneTmAT6ojv7OlH1f6Ix5BG4Q==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.1", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz", + "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/adm-zip": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.1.tgz", + "integrity": "sha512-3+psmbh60N5JXM2LMkujFqnjMf3KB0LZoIQO73NJvkv57q+384nK/A7pP0v+ZkB/Zrfqn+5xtAyt5OsY+GiYLQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.9.tgz", + "integrity": "sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-W+8iW69LnEae5H09PxjpmI3CdJyKUTBAoDyqWx6T5ewhX5unBHYYQsGYXlXQP8l3HE/Y71vswPAvxkJC92FQhQ==", + "dev": true + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.5.tgz", + "integrity": "sha512-9YHUdvuNDDRJYXZwHqSsO72Ok0vmqoJbNn73ttyITQp/VA60SarnZ+MPLD37rJAhVoKp+9BWOvJP5tHIRfZylQ==", + "dev": true + }, + "node_modules/@types/jasminewd2": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.10.tgz", + "integrity": "sha512-J7mDz7ovjwjc+Y9rR9rY53hFWKATcIkrr9DwQWmOas4/pnIPJTXawnzjwpHm3RSxz/e3ZVUvQ7cRbd5UQLo10g==", + "dev": true, + "dependencies": { + "@types/jasmine": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.14.198", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz", + "integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.14.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz", + "integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==" + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/object-hash": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-3.0.4.tgz", + "integrity": "sha512-w4fEy2suq1bepUxHoJRCBHJz0vS5DPAYpSbcgNwOahljxwyJsiKmi8qyes2/TJc+4Avd7fsgP+ZgUuXZjPvdug==", + "dev": true + }, + "node_modules/@types/opossum": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@types/opossum/-/opossum-6.2.2.tgz", + "integrity": "sha512-VYrqkucrT4nbQZpPxk522+HnhAC5f0EEZNMZATCRQeJwchNwx/GqBscZrHCQvNRx7mlhbgMvZ0DG4NE6BgiVAw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/qs": { + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", + "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/slug": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/slug/-/slug-5.0.4.tgz", + "integrity": "sha512-6zl8VXHEDtDsfdSlRsLW3uatVpwPJ17MKIzOy3m2lObF1dtA/dHl71pKvqiJJL1YKhdsB4gJ4WXENSLQ9TjcPQ==", + "dev": true + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ua-parser-js": { + "version": "0.7.39", + "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz", + "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz", + "integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==", + "dev": true + }, + "node_modules/@types/verror": { + "version": "1.10.10", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", + "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", + "dev": true, + "optional": true + }, + "node_modules/@types/which": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-1.3.2.tgz", + "integrity": "sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz", + "integrity": "sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.6.0", + "@typescript-eslint/type-utils": "7.6.0", + "@typescript-eslint/utils": "7.6.0", + "@typescript-eslint/visitor-keys": "7.6.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin-tslint": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-7.0.2.tgz", + "integrity": "sha512-Os20XlgmnXPlfqcvO5I6asARarEXZ/BQ2WEHaphfN+d8CUq8H3lGM2ep3SGcwaF1PXpAxfNBDN8U4EYhliFfSQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "7.0.2" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0", + "tslint": "^5.0.0 || ^6.0.0", + "typescript": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin-tslint/node_modules/@typescript-eslint/scope-manager": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", + "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin-tslint/node_modules/@typescript-eslint/types": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", + "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin-tslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", + "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin-tslint/node_modules/@typescript-eslint/utils": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin-tslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", + "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.0.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin-tslint/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz", + "integrity": "sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.6.0", + "@typescript-eslint/utils": "7.6.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.6.0.tgz", + "integrity": "sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.6.0", + "@typescript-eslint/types": "7.6.0", + "@typescript-eslint/typescript-estree": "7.6.0", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.6.0.tgz", + "integrity": "sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.6.0", + "@typescript-eslint/types": "7.6.0", + "@typescript-eslint/typescript-estree": "7.6.0", + "@typescript-eslint/visitor-keys": "7.6.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz", + "integrity": "sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.6.0", + "@typescript-eslint/visitor-keys": "7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.6.0.tgz", + "integrity": "sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz", + "integrity": "sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.6.0", + "@typescript-eslint/visitor-keys": "7.6.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz", + "integrity": "sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.6.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@wdio/config": { + "version": "7.16.13", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-7.16.13.tgz", + "integrity": "sha512-LSGoa83tWQIBppB+LeHjY40B9tuuvmDV1qdBLVXR1ROcOUWWz/oQP3NFLtLm3266LXoJUbwebzGcRIK1EcNk3Q==", + "dev": true, + "dependencies": { + "@wdio/logger": "7.16.0", + "@wdio/types": "7.16.13", + "deepmerge": "^4.0.0", + "glob": "^7.1.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wdio/logger": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-7.16.0.tgz", + "integrity": "sha512-/6lOGb2Iow5eSsy7RJOl1kCwsP4eMlG+/QKro5zUJsuyNJSQXf2ejhpkzyKWLgQbHu83WX6cM1014AZuLkzoQg==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wdio/logger/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@wdio/logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@wdio/logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wdio/logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wdio/protocols": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-7.16.7.tgz", + "integrity": "sha512-Wv40pNQcLiPzQ3o98Mv4A8T1EBQ6k4khglz/e2r16CTm+F3DDYh8eLMAsU5cgnmuwwDKX1EyOiFwieykBn5MCg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wdio/repl": { + "version": "7.16.13", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-7.16.13.tgz", + "integrity": "sha512-XWh3dzp6U8LLL4cNGWFra+quVyXZ25Ym38zpsBVtV0/z5NCHJmjRS4ytyvvkzbQ8SyqQ7Y3G8MjfGNi2sBNkIQ==", + "dev": true, + "dependencies": { + "@wdio/utils": "7.16.13" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wdio/types": { + "version": "7.16.13", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.16.13.tgz", + "integrity": "sha512-HIeXKCL+mUjyJxvnHSoaIo3NRgZLbeekyRIwo6USfd9qGlQ8dQ6fyCR3ZU9VqNz9j4+JIn+LRQ7imbz5SdnGbw==", + "dev": true, + "dependencies": { + "@types/node": "^17.0.4", + "got": "^11.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wdio/types/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + }, + "node_modules/@wdio/utils": { + "version": "7.16.13", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-7.16.13.tgz", + "integrity": "sha512-O6D89Ghtm5XtTv4DPKvCBKZOZYNONIcBM5/hmdr3V9mzVrTFq8Q3uE8pmmq303Oh91KcoN8Em5zoAG7Zpc5tRg==", + "dev": true, + "dependencies": { + "@wdio/logger": "7.16.0", + "@wdio/types": "7.16.13", + "p-iteration": "^1.1.8" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/@yarnpkg/parsers": { + "version": "3.0.0-rc.46", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz", + "integrity": "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.10.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@zkochan/js-yaml": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", + "integrity": "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@zkochan/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz", + "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ag-grid-angular": { + "version": "31.2.1", + "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-31.2.1.tgz", + "integrity": "sha512-8PhyQGsUNeLKBg++bQHlDv+ktpwv+hrFWA5hMk8HBUiCgGx/93N6sLBj+FQzAX5a2FuEWu0FF6ZLPBhmqYP7zQ==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">= 14.0.0", + "@angular/core": ">= 14.0.0", + "ag-grid-community": "31.2.1" + } + }, + "node_modules/ag-grid-community": { + "version": "31.2.1", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.2.1.tgz", + "integrity": "sha512-D+gnUQ4dHZ/EQJmupQnDqcEKiCEeuK5ZxlsIpdPKgHg/23dmW+aEdivtB9nLpSc2IEK0RUpchcSxeUT37Boo5A==", + "dev": true + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", + "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", + "dev": true, + "dependencies": { + "clean-stack": "^4.0.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==", + "dev": true, + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true + }, + "node_modules/app-builder-lib": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", + "dev": true, + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.2.1", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.5.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.13.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/app-builder-lib/node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", + "dev": true, + "dependencies": { + "buffer-equal": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/applescript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/applescript/-/applescript-1.0.0.tgz", + "integrity": "sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ==" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA==", + "dev": true, + "dependencies": { + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw==", + "dev": true, + "dependencies": { + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/array-initial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", + "integrity": "sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==", + "dev": true, + "dependencies": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-initial/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-last": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", + "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "dev": true, + "dependencies": { + "is-number": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-last/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-sort": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", + "dev": true, + "dependencies": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/async-each": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==", + "dev": true, + "dependencies": { + "async-done": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/auto-launch": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/auto-launch/-/auto-launch-5.0.6.tgz", + "integrity": "sha512-OgxiAm4q9EBf9EeXdPBiVNENaWE3jUZofwrhAkWjHDYGezu1k3FRZHU8V2FBxGuSJOHzKmTJEd0G7L7/0xDGFA==", + "dependencies": { + "applescript": "^1.0.0", + "mkdirp": "^0.5.1", + "path-is-absolute": "^1.0.0", + "untildify": "^3.0.2", + "winreg": "1.2.4" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz", + "integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.1", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.5.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==", + "dev": true, + "dependencies": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true, + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builder-util": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", + "dev": true, + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", + "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/builder-util/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/builder-util/node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/builder-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/builder-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/builder-util/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/builder-util/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/builder-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.0.tgz", + "integrity": "sha512-I7mVOPl3PUCeRub1U8YoGz2Lqv9WOBpobZ8RyWFXmReuILz+3OAyTa5oH3QPdtKZD7N0Yk00aLfzn0qvp8dZ1w==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001609", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001609.tgz", + "integrity": "sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", + "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chrome-launcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clean-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", + "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clean-stack/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA==", + "dev": true, + "dependencies": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha512-UlxQ9Vw0b/Bt/KYwCFqdEwsQ1eL8d1gibiFb7lxQJFdvTgc2hIZi6ugsg+kyhzhPV+QEpUiEIwInIAIrgoEkrg==", + "dev": true, + "dependencies": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + } + }, + "node_modules/combine-source-map/node_modules/convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg==", + "dev": true + }, + "node_modules/combine-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "optional": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "optional": true + }, + "node_modules/config-file-ts": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", + "dev": true, + "dependencies": { + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-props": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", + "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", + "dev": true, + "dependencies": { + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", + "integrity": "sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz", + "integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/critters": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", + "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-shorthand-properties": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz", + "integrity": "sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==", + "dev": true + }, + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "dev": true + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "dev": true, + "dependencies": { + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-7.1.0.tgz", + "integrity": "sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==", + "dev": true, + "dependencies": { + "globby": "^13.1.2", + "graceful-fs": "^4.2.10", + "is-glob": "^4.0.3", + "is-path-cwd": "^3.0.0", + "is-path-inside": "^4.0.0", + "p-map": "^5.5.0", + "rimraf": "^3.0.2", + "slash": "^4.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dev-null": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", + "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==", + "dev": true + }, + "node_modules/devtools": { + "version": "7.16.13", + "resolved": "https://registry.npmjs.org/devtools/-/devtools-7.16.13.tgz", + "integrity": "sha512-jm/DL5tlOUUMe0pUgahDqixw3z+NANLN6DYDeZPFv7z0CBtmnaTyOe2zbT0apLxCBpi800VeXaISVZwmKE2NiQ==", + "dev": true, + "dependencies": { + "@types/node": "^17.0.4", + "@types/ua-parser-js": "^0.7.33", + "@wdio/config": "7.16.13", + "@wdio/logger": "7.16.0", + "@wdio/protocols": "7.16.7", + "@wdio/types": "7.16.13", + "@wdio/utils": "7.16.13", + "chrome-launcher": "^0.15.0", + "edge-paths": "^2.1.0", + "puppeteer-core": "^13.0.0", + "query-selector-shadow-dom": "^1.0.0", + "ua-parser-js": "^1.0.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.953906", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.953906.tgz", + "integrity": "sha512-Z2vAafCNnl0Iw/u7TUjqOXW1sOhAMDOviflmUoUIxfq2rgfsoCO3qruB/LUJCdqF9aTJ32DUjXyMsX3+if6kDQ==", + "dev": true + }, + "node_modules/devtools/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + }, + "node_modules/devtools/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", + "dev": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/dmg-builder/node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-license/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/dmg-license/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "optional": true + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "dev": true + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "node_modules/each-props/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/edge-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-2.2.1.tgz", + "integrity": "sha512-AI5fC7dfDmCdKo3m5y7PkYE8m6bMqR6pvVpgtrZkkhcJXFLelUgkjrhk3kXXx8Kbw2cRaTT4LkOR7hqf39KJdw==", + "dev": true, + "dependencies": { + "@types/which": "^1.3.2", + "which": "^2.0.2" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-30.0.2.tgz", + "integrity": "sha512-zv7T+GG89J/hyWVkQsLH4Y/rVEfqJG5M/wOBIGNaDdqd8UV9/YZPdS7CuFeaIj0H9LhCt95xkIQNpYB/3svOkQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", + "dev": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "dmg-builder": "24.13.3", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", + "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", + "dev": true, + "peer": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/electron-builder/node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-builder/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/electron-builder/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/electron-builder/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-builder/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-chromedriver": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-17.0.0.tgz", + "integrity": "sha512-ccBACsMgbGd3HcQsD9vwT2pnWbCFT+P5h2ICMy77JU4kNrR5pN1uTBjQ1Q9Fl5Cpg0FHjhVREZCmBxt4e+ekHQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@electron/get": "^1.12.4", + "extract-zip": "^2.0.0" + }, + "bin": { + "chromedriver": "chromedriver.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/electron-chromedriver/node_modules/@electron/get": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.14.1.tgz", + "integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^9.6.0", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=8.6" + }, + "optionalDependencies": { + "global-agent": "^3.0.0", + "global-tunnel-ng": "^2.7.1" + } + }, + "node_modules/electron-chromedriver/node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/electron-chromedriver/node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/electron-chromedriver/node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-chromedriver/node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/electron-chromedriver/node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "node_modules/electron-chromedriver/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-chromedriver/node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/electron-chromedriver/node_modules/got/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/electron-chromedriver/node_modules/got/node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-chromedriver/node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true + }, + "node_modules/electron-chromedriver/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-chromedriver/node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/electron-chromedriver/node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-chromedriver/node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/electron-chromedriver/node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dev": true, + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/electron-chromedriver/node_modules/responselike/node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-chromedriver/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/electron-log": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.1.2.tgz", + "integrity": "sha512-Cpg4hAZ27yM9wzE77c4TvgzxzavZ+dVltCczParXN+Vb3jocojCSAuSMCVOI9fhFuuOR+iuu3tZLX1cu0y0kgQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/electron-notarize": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz", + "integrity": "sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw==", + "deprecated": "Please use @electron/notarize moving forward. There is no API change, just a package name change", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/electron-publish/node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-publish/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/electron-publish/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/electron-publish/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-publish/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-reload": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz", + "integrity": "sha512-hTde7gv0TEqxbxlB3pj2CwoyCQ9sdiQrcP8GkpzhosxyVeYM3mZbMEVKCZK3L0fED7Mz5A9IWmK7zEvi4H3P1g==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2" + } + }, + "node_modules/electron-store": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.2.0.tgz", + "integrity": "sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==", + "dependencies": { + "conf": "^10.2.0", + "type-fest": "^2.17.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.735", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.735.tgz", + "integrity": "sha512-pkYpvwg8VyOTQAeBqZ7jsmpCjko1Qc6We1ZtZCjRyYbT5v4AIUKDy5cQTRotQlSSZmMr8jqpEt6JtOj5k7lR7A==", + "dev": true + }, + "node_modules/electron-updater": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.8.tgz", + "integrity": "sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==", + "dependencies": { + "builder-util-runtime": "9.2.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.8", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", + "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", + "dev": true + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", + "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prefer-arrow": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.3.tgz", + "integrity": "sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==", + "dev": true, + "peerDependencies": { + "eslint": ">=2.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "optional": true + }, + "node_modules/fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "dev": true, + "dependencies": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/findup-sync/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fined/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/glob-stream/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-stream/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob-watcher": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", + "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "normalize-path": "^3.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/glob-watcher/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/glob-watcher/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/glob-watcher/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/glob-watcher/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-watcher/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/glob-watcher/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/global-tunnel-ng": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", + "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", + "dev": true, + "optional": true, + "dependencies": { + "encodeurl": "^1.0.2", + "lodash": "^4.17.10", + "npm-conf": "^1.1.3", + "tunnel": "^0.0.6" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, + "node_modules/glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "dev": true, + "dependencies": { + "sparkles": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "dev": true, + "dependencies": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-cli/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "dev": true, + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/gulp-cli/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "node_modules/gulp-cli/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "dev": true, + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true + }, + "node_modules/gulp-cli/node_modules/yargs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", + "dev": true, + "dependencies": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.1" + } + }, + "node_modules/gulp-cli/node_modules/yargs-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", + "dev": true, + "dependencies": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" + } + }, + "node_modules/gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", + "dev": true, + "dependencies": { + "glogg": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/i18next-json-sync": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/i18next-json-sync/-/i18next-json-sync-3.1.2.tgz", + "integrity": "sha512-svJXk7Zt40ddLZPJjRpDx2acEmvbyua9/ijiBc3LoR5urmOQpPf/vYJEBxbCoZKRXRVaj4S+Jqv1yPQ5pk3Zlg==", + "dev": true, + "dependencies": { + "glob": "^7.2.0", + "json-stable-stringify": "^1.0.1", + "yargs": "^17.0.0" + }, + "bin": { + "sync-i18n": "dist/cli.js" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-corefoundation/node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "optional": true + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz", + "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", + "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha512-0mVWSSbNDvedDWIN4wxLsdPM4a7cIPcpyMxj3QZ406QRwQ6ePGB1YIHxVPjqpcUGbWQ5C+nHTwGNWAGvt7ggVA==", + "dev": true, + "dependencies": { + "source-map": "~0.5.3" + } + }, + "node_modules/inline-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", + "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", + "dev": true, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jasmine-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.1.tgz", + "integrity": "sha512-UrzO3fL7nnxlQXlvTynNAenL+21oUQRlzqQFsA2U11ryb4+NLOCOePZ70PTojEaUKhiFugh7dG0Q+I58xlPdWg==", + "dev": true + }, + "node_modules/jasmine-spec-reporter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz", + "integrity": "sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg==", + "dev": true, + "dependencies": { + "colors": "1.4.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.12.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.3.tgz", + "integrity": "sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, + "node_modules/json-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", + "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/just-debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", + "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==", + "dev": true + }, + "node_modules/karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-coverage-istanbul-reporter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", + "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^3.0.2", + "minimatch": "^3.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/mattlewis92" + } + }, + "node_modules/karma-coverage-istanbul-reporter/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage-istanbul-reporter/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-electron": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/karma-electron/-/karma-electron-7.3.0.tgz", + "integrity": "sha512-JCwAZxtzLo+Qk6HD8MqlU+c6mB7A5jZYNb+ftbMNxutnmi1hzb8/wIqJzpw087R7jV5ZzNHujMq8mStI5n4Q6Q==", + "dev": true, + "dependencies": { + "async": "~3.2.2", + "combine-source-map": "~0.8.0", + "commander": "~2.9.0", + "convert-source-map": "~1.2.0", + "js-string-escape": "~1.0.0", + "minstache": "~1.2.0", + "xtend": "~4.0.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/karma-electron/node_modules/commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "dev": true, + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/karma-electron/node_modules/convert-source-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.2.0.tgz", + "integrity": "sha512-S8g9WfpATYd4Qajgygr9CsIRSvd+omrKmaqLE0Lz4dSWprOFpWSqYTrXBTNrYqM+x4OaLlFWhJSOJHU8vuU1wA==", + "dev": true + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", + "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", + "dev": true + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/karma/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/karma/node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/karma/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/karma/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/karma/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/node_modules/ua-parser-js": { + "version": "0.7.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", + "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/keypress": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.1.0.tgz", + "integrity": "sha512-x0yf9PL/nx9Nw9oLL8ZVErFAk85/lslwEP7Vz7s5SI1ODXZIgit3C5qyWjw4DxOuO/3Hb4866SQh28a1V1d+WA==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ky": { + "version": "0.28.7", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.28.7.tgz", + "integrity": "sha512-a23i6qSr/ep15vdtw/zyEQIDLoUaKDg9Jf04CYl/0ns/wXNYna26zJpI+MeIFaPeDvkrjLPrKtKOiiI3IE53RQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", + "dev": true, + "dependencies": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "dev": true, + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", + "dev": true, + "dependencies": { + "flush-write-stream": "^1.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/liftoff/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "dev": true, + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz", + "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==", + "dev": true, + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/make-iterator/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/make-plural": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.3.0.tgz", + "integrity": "sha512-/K3BC0KIsO+WK2i94LkMPv3wslMrazrQhfi5We9fMbLlLjzoOSJWr7TAdupLlDWaJcWxwoNosBkhFDejiu5VDw==", + "dev": true + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "dev": true + }, + "node_modules/matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", + "dev": true, + "dependencies": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/matchdep/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/matchdep/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/messageformat": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz", + "integrity": "sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==", + "deprecated": "Package renamed as '@messageformat/core', see messageformat.github.io for more details. 'messageformat@4' will eventually provide a polyfill for Intl.MessageFormat, once it's been defined by Unicode & ECMA.", + "dev": true, + "dependencies": { + "make-plural": "^4.3.0", + "messageformat-formatters": "^2.0.1", + "messageformat-parser": "^4.1.2" + } + }, + "node_modules/messageformat-formatters": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz", + "integrity": "sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg==", + "dev": true + }, + "node_modules/messageformat-parser": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.3.tgz", + "integrity": "sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg==", + "dev": true + }, + "node_modules/messageformat/node_modules/make-plural": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz", + "integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==", + "dev": true, + "bin": { + "make-plural": "bin/make-plural" + }, + "optionalDependencies": { + "minimist": "^1.2.0" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-fetch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", + "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minstache": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minstache/-/minstache-1.2.0.tgz", + "integrity": "sha512-VSAeaiKXHIKifdNHCalWmFvChtLrNirwhDZd0yeEO57WXCT+uJYN3RPAusvLi3z7VlwFBBtDX80bG7aHkcMAmg==", + "dev": true, + "dependencies": { + "commander": "1.0.4" + }, + "bin": { + "minstache": "bin/minstache" + } + }, + "node_modules/minstache/node_modules/commander": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/commander/-/commander-1.0.4.tgz", + "integrity": "sha512-Xz0JOF7NqSubDnWmw7qvX1FuIpCsV62ci/gkpa2NFlm+roeMniBtbxK8QePjs762ZGsuhKaGgcb83eaBiSJ16A==", + "dev": true, + "dependencies": { + "keypress": "0.1.x" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mocha/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/mocha/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "dev": true, + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, + "node_modules/ng-gallery": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/ng-gallery/-/ng-gallery-11.0.0.tgz", + "integrity": "sha512-xG0WD4Hd8FrKAinrH+dseH3dpbthhgc+n6EKUCv1WuwdUJC7YiEMuPOl1aV2t8DxSblVUokR2w5Y6pAFRo9Ixw==", + "dev": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "peerDependencies": { + "@angular/cdk": ">=16.0.0", + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/ng-gallery/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/ngx-translate-messageformat-compiler": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ngx-translate-messageformat-compiler/-/ngx-translate-messageformat-compiler-7.0.0.tgz", + "integrity": "sha512-nWklMQ9kzRsCCFemfmzGHsvuD+MnAx1/bCLUCyEun0Xrgvph+TpMh59QczslO47swSzWJt/iF8QLVn3Vori/KA==", + "dev": true, + "dependencies": { + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@messageformat/core": "^3.2.0", + "@ngx-translate/core": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/ngx-translate-messageformat-compiler/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/nice-napi/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/node-addon-api": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz", + "integrity": "sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==", + "dev": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dev": true, + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/node-disk-info": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-disk-info/-/node-disk-info-1.3.0.tgz", + "integrity": "sha512-NEx858vJZ0AoBtmD/ChBIHLjFTF28xCsDIgmFl4jtGKsvlUx9DU/OrMDjvj3qp/E4hzLN0HvTg7eJx5XFQvbeg==", + "dependencies": { + "iconv-lite": "^0.6.2" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", + "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/nopt": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", + "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", + "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "dependencies": { + "once": "^1.3.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", + "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "optional": true, + "dependencies": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", + "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", + "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.0.tgz", + "integrity": "sha512-zVH+G0q1O2hqgQBUvQ2LWp6ujr6VJAeDnmWxqiMlCguvLexEzBnuQIwC70r04vcvCMAcYEIpA/rO9YyVi+fmJQ==", + "dev": true, + "dependencies": { + "@npmcli/redact": "^1.1.0", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nx": { + "version": "18.2.4", + "resolved": "https://registry.npmjs.org/nx/-/nx-18.2.4.tgz", + "integrity": "sha512-GxqJcDOhfLa9jsPmip0jG73CZKA96wCryss2DhixCiCU66I3GLYF4+585ObO8Tx7Z1GqhT92RaNGjCxjMIwaPg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@nrwl/tao": "18.2.4", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "3.0.0-rc.46", + "@zkochan/js-yaml": "0.0.6", + "axios": "^1.6.0", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^8.0.1", + "dotenv": "~16.3.1", + "dotenv-expand": "~10.0.0", + "enquirer": "~2.3.6", + "figures": "3.2.0", + "flat": "^5.0.2", + "fs-extra": "^11.1.0", + "ignore": "^5.0.4", + "jest-diff": "^29.4.1", + "js-yaml": "4.1.0", + "jsonc-parser": "3.2.0", + "lines-and-columns": "~2.0.3", + "minimatch": "9.0.3", + "node-machine-id": "1.1.12", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "ora": "5.3.0", + "semver": "^7.5.3", + "string-width": "^4.2.3", + "strong-log-transformer": "^2.1.0", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "bin/nx.js", + "nx-cloud": "bin/nx-cloud.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "18.2.4", + "@nx/nx-darwin-x64": "18.2.4", + "@nx/nx-freebsd-x64": "18.2.4", + "@nx/nx-linux-arm-gnueabihf": "18.2.4", + "@nx/nx-linux-arm64-gnu": "18.2.4", + "@nx/nx-linux-arm64-musl": "18.2.4", + "@nx/nx-linux-x64-gnu": "18.2.4", + "@nx/nx-linux-x64-musl": "18.2.4", + "@nx/nx-win32-arm64-msvc": "18.2.4", + "@nx/nx-win32-x64-msvc": "18.2.4" + }, + "peerDependencies": { + "@swc-node/register": "^1.8.0", + "@swc/core": "^1.3.85" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/nx/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nx/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/nx/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/nx/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nx/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/nx/node_modules/dotenv": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", + "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/nx/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/nx/node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/nx/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nx/node_modules/ora": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", + "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "log-symbols": "^4.0.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opossum": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-7.1.0.tgz", + "integrity": "sha512-u3KZa2JTVsewFILe2NIebLMii/zzszTTBxRnM9USxVNcq2R2me40uP38/B39GueeOABXgWGtClPZPg4NAwHU4g==", + "dev": true, + "engines": { + "node": "^19 || ^18 || ^16 || ^14" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", + "dev": true, + "dependencies": { + "lcid": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-iteration": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/p-iteration/-/p-iteration-1.1.8.tgz", + "integrity": "sha512-IMFBSDIYcPNnW7uWYGrBqmvTiq7W0uB0fJn6shQZs7dlF3OvrHOre+JT9ikSZ7gZS3vWqclVgoQSvToJrns7uQ==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz", + "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==", + "dev": true, + "dependencies": { + "aggregate-error": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.6.tgz", + "integrity": "sha512-cJKrW21VRE8vVTRskJo78c/RCvwJCn1f4qgfxL4w77SOWrTCRcmfkYHlHtS0gqpgjv3zhXflRtgsrUCX5xwNnQ==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^7.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^7.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/parse-json/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/piscina": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz", + "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==", + "dev": true, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "optional": true + }, + "node_modules/protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-13.7.0.tgz", + "integrity": "sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q==", + "dev": true, + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.981744", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.5.0" + }, + "engines": { + "node": ">=10.18.1" + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.981744", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.981744.tgz", + "integrity": "sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==", + "dev": true + }, + "node_modules/puppeteer-core/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/puppeteer-core/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/pushy-electron": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pushy-electron/-/pushy-electron-1.0.11.tgz", + "integrity": "sha512-kxuKS96NOMeoEm3yXRrff3Kr/IY7HoIlPJc2n83kz4nFnrBC13BwAfQmL/7LhrBDq9HAgjzvMvEDOeYpLxtTjQ==", + "hasShrinkwrap": true, + "dependencies": { + "electron-store": "^5.1.0", + "mqtt": "^4.3.7", + "node-fetch": "^2.6.7", + "packpath": "^0.1.0" + } + }, + "node_modules/pushy-electron/node_modules/@electron/get": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.14.1.tgz", + "integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==", + "extraneous": true, + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^9.6.0", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "optionalDependencies": { + "global-agent": "^3.0.0", + "global-tunnel-ng": "^2.7.1" + } + }, + "node_modules/pushy-electron/node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "extraneous": true, + "dependencies": { + "defer-to-connect": "^1.0.1" + } + }, + "node_modules/pushy-electron/node_modules/@types/node": { + "version": "16.18.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz", + "integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/pushy-electron/node_modules/ajv/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/pushy-electron/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/pushy-electron/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "node_modules/pushy-electron/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/pushy-electron/node_modules/bl/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "node_modules/pushy-electron/node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/pushy-electron/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/pushy-electron/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/pushy-electron/node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "extraneous": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + } + }, + "node_modules/pushy-electron/node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "extraneous": true, + "dependencies": { + "pump": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "extraneous": true, + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/pushy-electron/node_modules/commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "dependencies": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, + "node_modules/pushy-electron/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/pushy-electron/node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "extraneous": true, + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/pushy-electron/node_modules/conf": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-6.2.0.tgz", + "integrity": "sha512-fvl40R6YemHrFsNiyP7TD0tzOe3pQD2dfT2s20WvCaq57A1oV+RImbhn2Y4sQGDz1lB0wNSb7dPcPIvQB69YNA==", + "dependencies": { + "ajv": "^6.10.2", + "debounce-fn": "^3.0.1", + "dot-prop": "^5.0.0", + "env-paths": "^2.2.0", + "json-schema-typed": "^7.0.1", + "make-dir": "^3.0.0", + "onetime": "^5.1.0", + "pkg-up": "^3.0.1", + "semver": "^6.2.0", + "write-file-atomic": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/conf/node_modules/write-file-atomic": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", + "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/pushy-electron/node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "extraneous": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/pushy-electron/node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/debounce-fn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-3.0.1.tgz", + "integrity": "sha512-aBoJh5AhpqlRoHZjHmOzZlRx+wz2xVwGL9rjs+Kj0EWUrL4/h4K7OD176thl2Tdoqui/AaA4xhHrNArGLAaI3Q==", + "dependencies": { + "mimic-fn": "^2.1.0" + } + }, + "node_modules/pushy-electron/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + } + }, + "node_modules/pushy-electron/node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "extraneous": true, + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/pushy-electron/node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "extraneous": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "node_modules/pushy-electron/node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "dependencies": { + "is-obj": "^2.0.0" + } + }, + "node_modules/pushy-electron/node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/pushy-electron/node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "node_modules/pushy-electron/node_modules/electron": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-18.3.7.tgz", + "integrity": "sha512-SDvX0VYejR1xw9PrJyvnyiDcuIhdzFVaA1NaRN2LEWXr5R6mEFl8NVTM+i5dtxMm2SHP/FPnkvmsWZs6MHijqg==", + "extraneous": true, + "dependencies": { + "@electron/get": "^1.13.0", + "@types/node": "^16.11.26", + "extract-zip": "^1.0.3" + } + }, + "node_modules/pushy-electron/node_modules/electron-store": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-5.1.0.tgz", + "integrity": "sha512-uhAF/4+zDb+y0hWqlBirEPEAR4ciCZDp4fRWGFNV62bG+ArdQPpXk7jS0MEVj3CfcG5V7hx7Dpq5oD+1j6GD8Q==", + "dependencies": { + "conf": "^6.2.0", + "type-fest": "^0.7.1" + } + }, + "node_modules/pushy-electron/node_modules/electron-store/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==" + }, + "node_modules/pushy-electron/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/pushy-electron/node_modules/env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" + }, + "node_modules/pushy-electron/node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "extraneous": true, + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + } + }, + "node_modules/pushy-electron/node_modules/extract-zip/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "extraneous": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/pushy-electron/node_modules/extract-zip/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "node_modules/pushy-electron/node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "extraneous": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/pushy-electron/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "extraneous": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/pushy-electron/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/pushy-electron/node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "extraneous": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "node_modules/pushy-electron/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "extraneous": true, + "dependencies": { + "pump": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "node_modules/pushy-electron/node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "extraneous": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + } + }, + "node_modules/pushy-electron/node_modules/global-agent/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "extraneous": true, + "dependencies": { + "lru-cache": "^6.0.0" + } + }, + "node_modules/pushy-electron/node_modules/global-tunnel-ng": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", + "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", + "extraneous": true, + "dependencies": { + "encodeurl": "^1.0.2", + "lodash": "^4.17.10", + "npm-conf": "^1.1.3", + "tunnel": "^0.0.6" + } + }, + "node_modules/pushy-electron/node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "extraneous": true, + "dependencies": { + "define-properties": "^1.1.3" + } + }, + "node_modules/pushy-electron/node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "extraneous": true, + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "extraneous": true, + "dependencies": { + "function-bind": "^1.1.1" + } + }, + "node_modules/pushy-electron/node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "extraneous": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + } + }, + "node_modules/pushy-electron/node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "dependencies": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, + "node_modules/pushy-electron/node_modules/help-me/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "node_modules/pushy-electron/node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "node_modules/pushy-electron/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "node_modules/pushy-electron/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/pushy-electron/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/pushy-electron/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "node_modules/pushy-electron/node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/pushy-electron/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/js-sdsl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-2.1.4.tgz", + "integrity": "sha512-/Ew+CJWHNddr7sjwgxaVeIORIH4AMVC9dy0hPf540ZGMVgS9d3ajwuVdyhDt6/QUvT8ATjR3yuYBKsS79F+H4A==" + }, + "node_modules/pushy-electron/node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/pushy-electron/node_modules/json-schema-typed": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.2.tgz", + "integrity": "sha512-40FRIcBSz4y0Ego3gMpbkhtIgebpxKRgW/7i1FfDNL4/xEPQKBM12tKSiCZFNQvad5K4IS3I5Sc8cxza/KSwog==" + }, + "node_modules/pushy-electron/node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "extraneous": true, + "dependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/pushy-electron/node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "extraneous": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "node_modules/pushy-electron/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + } + }, + "node_modules/pushy-electron/node_modules/make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dependencies": { + "semver": "^6.0.0" + } + }, + "node_modules/pushy-electron/node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "extraneous": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + } + }, + "node_modules/pushy-electron/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "node_modules/pushy-electron/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + } + }, + "node_modules/pushy-electron/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/pushy-electron/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "extraneous": true, + "dependencies": { + "minimist": "^1.2.6" + } + }, + "node_modules/pushy-electron/node_modules/mqtt": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", + "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "dependencies": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + } + }, + "node_modules/pushy-electron/node_modules/mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/pushy-electron/node_modules/mqtt/node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/pushy-electron/node_modules/mqtt/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "node_modules/pushy-electron/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/pushy-electron/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + } + }, + "node_modules/pushy-electron/node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "extraneous": true, + "dependencies": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/number-allocator": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.10.tgz", + "integrity": "sha512-K4AvNGKo9lP6HqsZyfSr9KDaqnwFzW203inhQEOwFrmFaYevpdX4VNwdOLk197aHujzbT//z6pCBrCOUYSM5iw==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "^2.1.2" + } + }, + "node_modules/pushy-electron/node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pushy-electron/node_modules/onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dependencies": { + "mimic-fn": "^2.1.0" + } + }, + "node_modules/pushy-electron/node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dependencies": { + "p-try": "^2.0.0" + } + }, + "node_modules/pushy-electron/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + } + }, + "node_modules/pushy-electron/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "node_modules/pushy-electron/node_modules/packpath": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/packpath/-/packpath-0.1.0.tgz", + "integrity": "sha512-6E+ncOyZ83bDW/3TFriKpz8XENvpG56Yt3di3KjyYaJV5deZ4o0OmwvFThHrscugBfGQqTWOGnf2IoEecJewuA==" + }, + "node_modules/pushy-electron/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "node_modules/pushy-electron/node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "node_modules/pushy-electron/node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/pushy-electron/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pushy-electron/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "node_modules/pushy-electron/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "extraneous": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/pushy-electron/node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, + "node_modules/pushy-electron/node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "extraneous": true, + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/pushy-electron/node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, + "node_modules/pushy-electron/node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "extraneous": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + } + }, + "node_modules/pushy-electron/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/pushy-electron/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "node_modules/pushy-electron/node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "extraneous": true, + "dependencies": { + "type-fest": "^0.13.1" + } + }, + "node_modules/pushy-electron/node_modules/signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "node_modules/pushy-electron/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/split2/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "node_modules/pushy-electron/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "node_modules/pushy-electron/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/pushy-electron/node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "extraneous": true, + "dependencies": { + "debug": "^4.1.0" + } + }, + "node_modules/pushy-electron/node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "node_modules/pushy-electron/node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "node_modules/pushy-electron/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/pushy-electron/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "extraneous": true + }, + "node_modules/pushy-electron/node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/pushy-electron/node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "extraneous": true, + "dependencies": { + "prepend-http": "^2.0.0" + } + }, + "node_modules/pushy-electron/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/pushy-electron/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/pushy-electron/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/pushy-electron/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/pushy-electron/node_modules/ws": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", + "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==" + }, + "node_modules/pushy-electron/node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "node_modules/pushy-electron/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/pushy-electron/node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "extraneous": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/read-config-file": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", + "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", + "dev": true, + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/read-config-file/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/read-config-file/node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/read-config-file/node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/read-config-file/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/read-package-json": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.0.tgz", + "integrity": "sha512-uL4Z10OKV4p6vbdvIXB+OzhInYtIozl/VxUBPgNkBuUi2DeRonnuspmaVAMcrkmfjKGNmRndyQAbE7/AmzGwFg==", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg-up/node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg-up/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg-up/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", + "dev": true, + "dependencies": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", + "dev": true, + "dependencies": { + "value-or-function": "^3.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/roarr/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "optional": true + }, + "node_modules/rollup": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.2.tgz", + "integrity": "sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.14.2", + "@rollup/rollup-android-arm64": "4.14.2", + "@rollup/rollup-darwin-arm64": "4.14.2", + "@rollup/rollup-darwin-x64": "4.14.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.14.2", + "@rollup/rollup-linux-arm64-gnu": "4.14.2", + "@rollup/rollup-linux-arm64-musl": "4.14.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.14.2", + "@rollup/rollup-linux-riscv64-gnu": "4.14.2", + "@rollup/rollup-linux-s390x-gnu": "4.14.2", + "@rollup/rollup-linux-x64-gnu": "4.14.2", + "@rollup/rollup-linux-x64-musl": "4.14.2", + "@rollup/rollup-win32-arm64-msvc": "4.14.2", + "@rollup/rollup-win32-ia32-msvc": "4.14.2", + "@rollup/rollup-win32-x64-msvc": "4.14.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", + "dev": true + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==", + "dev": true + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sass": { + "version": "1.71.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", + "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", + "dev": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "optional": true + }, + "node_modules/semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==", + "dev": true, + "dependencies": { + "sver-compat": "^1.5.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sigstore": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.0.tgz", + "integrity": "sha512-q+o8L2ebiWD1AxD17eglf1pFrl9jtW7FHa0ygqY6EKvibK8JHyq9Z26v9MZXeDiw+RbfOJ9j2v70M10Hd6E06A==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.1", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.1", + "@sigstore/sign": "^2.3.0", + "@sigstore/tuf": "^2.3.1", + "@sigstore/verify": "^1.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, + "node_modules/slug": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/slug/-/slug-8.2.3.tgz", + "integrity": "sha512-fXjhAZszNecz855GUNIwW0+sFPi9WV4bMiEKDOCA4wcq1ts1UnUVNy/F78B0Aat7/W3rA+se//33ILKNMrbeYQ==", + "dev": true, + "bin": { + "slug": "cli.js" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/spectron": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/spectron/-/spectron-19.0.0.tgz", + "integrity": "sha512-p8xWLCDjZstCTG+IzXURINZcvcwhKtnjN0JCEUuv6r6UzAi+pwfRye8Ww4xImknoN7gHhlgjvJthIbs3DuAnVg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@electron/remote": "2.0.4", + "dev-null": "^0.1.1", + "electron-chromedriver": "17.0.0", + "got": "^11.8.0", + "split": "^1.0.1", + "webdriverio": "7.16.13" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", + "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-similarity": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", + "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info." + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", + "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strong-log-transformer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", + "integrity": "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "minimist": "^1.2.0", + "through": "^2.3.4" + }, + "bin": { + "sl-log-transformer": "bin/sl-log-transformer.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sver-compat": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", + "integrity": "sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg==", + "dev": true, + "dependencies": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/terser": { + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==" + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", + "dev": true, + "dependencies": { + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "node_modules/tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" + } + }, + "node_modules/tslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/tslint/node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslint/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "peer": true + }, + "node_modules/tslint/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tslint/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tslint/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "peer": true + }, + "node_modules/tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "peer": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "peer": true + }, + "node_modules/tuf-js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz", + "integrity": "sha512-ZSDngmP1z6zw+FIkIBjvOp/II/mIub/O7Pp12j1WNsiCpg5R5wAc//i555bBQsE44O94btLt0xM/Zr2LQjwdCg==", + "dev": true, + "dependencies": { + "@tufjs/models": "2.0.0", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undertaker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", + "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "fast-levenshtein": "^1.0.0", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", + "integrity": "sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw==", + "dev": true + }, + "node_modules/undici": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", + "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", + "dev": true, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "dev": true, + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/untildify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz", + "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "dev": true, + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "optional": true + }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "dev": true, + "dependencies": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-sourcemap/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.7.tgz", + "integrity": "sha512-sgnEEFTZYMui/sTlH1/XEnVNHMujOahPLGMxn1+5sIT45Xjng1Ec1K78jRP15dSmVgg5WBin9yO81j3o9OxofA==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wait-on": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "dev": true, + "dependencies": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webdriver": { + "version": "7.16.13", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-7.16.13.tgz", + "integrity": "sha512-Vfr952W1uIgDeWHPGzqH43dYLeRSZshh3TzA9ICUkvnC+Q7YziQdv/8xI8tuuyvb7lSr3VsuB2cGzyCRoC/NWw==", + "dev": true, + "dependencies": { + "@types/node": "^17.0.4", + "@wdio/config": "7.16.13", + "@wdio/logger": "7.16.0", + "@wdio/protocols": "7.16.7", + "@wdio/types": "7.16.13", + "@wdio/utils": "7.16.13", + "got": "^11.0.2", + "ky": "^0.28.5", + "lodash.merge": "^4.6.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/webdriver/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + }, + "node_modules/webdriverio": { + "version": "7.16.13", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-7.16.13.tgz", + "integrity": "sha512-jl1VRZYL1+cPeG6klskKX7mCEBWNQWDFaNtaIl5pwWgtKWPau6fCzKntSARzfNV8+hKJKwJ2mZn5Nsxfw28Oeg==", + "dev": true, + "dependencies": { + "@types/aria-query": "^5.0.0", + "@types/node": "^17.0.4", + "@wdio/config": "7.16.13", + "@wdio/logger": "7.16.0", + "@wdio/protocols": "7.16.7", + "@wdio/repl": "7.16.13", + "@wdio/types": "7.16.13", + "@wdio/utils": "7.16.13", + "archiver": "^5.0.0", + "aria-query": "^5.0.0", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "devtools": "7.16.13", + "devtools-protocol": "^0.0.953906", + "fs-extra": "^10.0.0", + "get-port": "^5.1.1", + "grapheme-splitter": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "lodash.isobject": "^3.0.2", + "lodash.isplainobject": "^4.0.6", + "lodash.zip": "^4.2.0", + "minimatch": "^3.0.4", + "puppeteer-core": "^13.0.0", + "query-selector-shadow-dom": "^1.0.0", + "resq": "^1.9.1", + "rgb2hex": "0.2.5", + "serialize-error": "^8.0.0", + "webdriver": "7.16.13" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/webdriverio/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + }, + "node_modules/webdriverio/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/webdriverio/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/webdriverio/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/webdriverio/node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriverio/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webdriverio/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/webpack": { + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", + "dev": true + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/win-ca": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/win-ca/-/win-ca-3.5.1.tgz", + "integrity": "sha512-RNy9gpBS6cxWHjfbqwBA7odaHyT+YQNhtdpJZwYCFoxB/Dq22oeOZ9YCXMwjhLytKpo7JJMnKdJ/ve7N12zzfQ==", + "hasInstallScript": true, + "dependencies": { + "is-electron": "^2.2.0", + "make-dir": "^1.3.0", + "node-forge": "^1.2.1", + "split": "^1.0.1" + } + }, + "node_modules/win-ca/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==" + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wowup-lib-core": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/wowup-lib-core/-/wowup-lib-core-1.0.6.tgz", + "integrity": "sha512-Hzm24TcWJPPp9oXc8Ooqv8g2fYimMHRE/UedNNMRt9DfuPwo83apgy2si5T5tBR858QS66kidqfDhwW03U8CtA==", + "dependencies": { + "lodash": "^4.17.21", + "markdown-it": "^13.0.1", + "string-similarity": "^4.0.4", + "ts-custom-error": "^3.3.1", + "uuid": "^9.0.1" + } + }, + "node_modules/wowup-lib-core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/zone.js": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.2.tgz", + "integrity": "sha512-X4U7J1isDhoOmHmFWiLhloWc2lzMkdnumtfQ1LXzf/IOZp5NQYuMUTaviVzG/q1ugMBIXzin2AqeVJUoSEkNyQ==", + "dev": true, + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/WowUp/wowup-electron/package.json b/WowUp/wowup-electron/package.json new file mode 100644 index 0000000..aa0ba73 --- /dev/null +++ b/WowUp/wowup-electron/package.json @@ -0,0 +1,179 @@ +{ + "name": "wowup", + "version": "2.12.0", + "private": true, + "description": "World of Warcraft addon updater", + "keywords": [ + "wow", + "world of warcraft" + ], + "homepage": "https://wowup.io", + "repository": { + "type": "git", + "url": "https://github.com/DigitalDJ/WowUp-Unlimited.git" + }, + "author": { + "name": "WowUp LLC", + "email": "zakrn@wowup.io" + }, + "main": "app/main.js", + "scripts": { + "postinstall": "electron-builder install-app-deps", + "install:prod": "npm ci --only=prod --no-optional", + "ng": "ng", + "start": "npm-run-all -p electron:serve ng:serve", + "build": "npm run electron:serve-tsc && ng build --base-href ./", + "build:dev": "npm run build -- -c dev", + "build:prod": "npm run build -- -c production", + "ng:serve": "ng serve", + "ng:serve:web": "ng serve -c web -o", + "electron:serve-tsc": "tsc -p tsconfig.serve.json", + "electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve", + "electron:local": "npm run build:prod && npx electron .", + "electron:build": "npm run build:prod && electron-builder build", + "electron:build:local": "npm run build:prod && electron-builder build -c electron-builder-local.json", + "electron:publish": "npm run lint && npm run build:prod && electron-builder build --publish always", + "electron:publish:vanilla": "npm run lint && npm run build:prod && electron-builder build --publish always", + "electron:publish:linux": "docker-compose -f linux-compose.yml up", + "electron:publish:never": "npm run electron:build && electron-builder --publish never", + "electron:publish:never:local": "npm run build:dev && npx electron-builder -c electron-builder-local.json --publish never", + "electron:publish:never:t": "electron-builder -c electron-builder-local.json --publish never", + "test": "ng test --watch=false", + "test:watch": "ng test", + "test:locales": "ng test --watch=false --include='src/locales.spec.ts'", + "test:customprotocol:": "npx electron . \"curseforge://install?addonId=14154&fileId=4073235\"", + "e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha --timeout 300000 --require ts-node/register e2e/**/*.e2e.ts", + "version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md", + "lint": "ng lint", + "i18n": "sync-i18n --files ./src/assets/i18n/*.json --primary en --space 2 --finalnewline --lineendings CRLF --languages cs de es fr it nb pt ru zh zh-TW ko pl", + "check-i18n": "npm run i18n -- --check", + "pretty": "npx prettier --write . && ng lint --fix", + "find-broken-test": "node ./test-fixer.js --find-break", + "restore-tests": "node ./test-fixer.js --repair", + "package:prod": "npx gulp package", + "package:local": "npx gulp packageLocal" + }, + "browserslist": [ + "chrome 98" + ], + "dependencies": { + "adm-zip": "0.5.12", + "auto-launch": "5.0.6", + "electron-log": "5.1.2", + "electron-store": "8.2.0", + "electron-updater": "6.1.8", + "globrex": "0.1.2", + "handlebars": "4.7.8", + "lodash": "4.17.21", + "minimist": "1.2.8", + "nanoid": "3.3.4", + "node-disk-info": "1.3.0", + "protobufjs": "7.2.6", + "pushy-electron": "1.0.11", + "rxjs": "7.8.1", + "win-ca": "3.5.1", + "wowup-lib-core": "1.0.6", + "yauzl": "2.10.0" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "17.0.2", + "@angular-devkit/build-angular": "17.3.4", + "@angular-eslint/builder": "17.3.0", + "@angular-eslint/eslint-plugin": "17.3.0", + "@angular-eslint/eslint-plugin-template": "17.3.0", + "@angular-eslint/schematics": "17.3.0", + "@angular-eslint/template-parser": "17.3.0", + "@angular/animations": "17.3.4", + "@angular/cdk": "17.3.4", + "@angular/cli": "17.3.4", + "@angular/common": "17.3.4", + "@angular/compiler": "17.3.4", + "@angular/compiler-cli": "17.3.4", + "@angular/core": "17.3.4", + "@angular/forms": "17.3.4", + "@angular/material": "17.3.4", + "@angular/platform-browser": "17.3.4", + "@angular/platform-browser-dynamic": "17.3.4", + "@angular/router": "17.3.4", + "@electron/notarize": "2.3.0", + "@fortawesome/angular-fontawesome": "0.14.0", + "@fortawesome/fontawesome-svg-core": "6.4.2", + "@fortawesome/free-brands-svg-icons": "6.4.2", + "@fortawesome/free-regular-svg-icons": "6.4.2", + "@fortawesome/free-solid-svg-icons": "6.4.2", + "@messageformat/core": "3.2.0", + "@microsoft/applicationinsights-web": "3.0.5", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", + "@types/adm-zip": "0.5.1", + "@types/flat": "5.0.2", + "@types/globrex": "0.1.2", + "@types/jasmine": "4.3.5", + "@types/jasminewd2": "2.0.10", + "@types/lodash": "4.14.198", + "@types/mocha": "10.0.1", + "@types/node": "18.14.6", + "@types/object-hash": "3.0.4", + "@types/opossum": "6.2.2", + "@types/slug": "5.0.4", + "@types/uuid": "9.0.3", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/eslint-plugin-tslint": "7.0.2", + "@typescript-eslint/parser": "^7.2.0", + "ag-grid-angular": "31.2.1", + "ag-grid-community": "31.2.1", + "chai": "4.3.8", + "core-js": "3.21.1", + "cross-env": "7.0.3", + "curseforge-v2": "1.3.0", + "del": "7.1.0", + "dotenv": "16.4.5", + "electron": "30.0.2", + "electron-builder": "24.13.3", + "electron-notarize": "1.2.2", + "electron-reload": "2.0.0-alpha.1", + "eslint": "^8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-import": "2.28.1", + "eslint-plugin-jsdoc": "46.5.1", + "eslint-plugin-prefer-arrow": "1.2.3", + "flat": "5.0.2", + "gulp": "4.0.2", + "gulp-run": "1.7.1", + "i18next-json-sync": "3.1.2", + "ignore": "5.2.4", + "jasmine-core": "5.1.1", + "jasmine-spec-reporter": "7.0.0", + "karma": "6.4.2", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-electron": "7.3.0", + "karma-jasmine": "5.1.0", + "karma-jasmine-html-reporter": "2.1.0", + "markdown-it": "13.0.1", + "messageformat": "2.3.0", + "mocha": "10.2.0", + "ng-gallery": "11.0.0", + "ngx-translate-messageformat-compiler": "7.0.0", + "node-addon-api": "8.0.0", + "node-cache": "5.1.2", + "node-gyp": "10.1.0", + "npm-run-all": "4.1.5", + "object-hash": "3.0.0", + "opossum": "7.1.0", + "prettier": "3.2.5", + "slug": "8.2.3", + "spectron": "19.0.0", + "ts-custom-error": "3.3.1", + "ts-node": "10.9.1", + "tslib": "2.3.0", + "typescript": "5.2.2", + "uuid": "9.0.0", + "wait-on": "7.2.0", + "zone.js": "0.14.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "gypfile": true, + "productName": "WowUp" +} diff --git a/WowUp/wowup-electron/scripts/core_to_local.bat b/WowUp/wowup-electron/scripts/core_to_local.bat new file mode 100644 index 0000000..b46ff66 --- /dev/null +++ b/WowUp/wowup-electron/scripts/core_to_local.bat @@ -0,0 +1,3 @@ +call npm uninstall wowup-lib-core + +call npm install ../../../WowUp.Lib/Core \ No newline at end of file diff --git a/WowUp/wowup-electron/scripts/core_to_npm.bat b/WowUp/wowup-electron/scripts/core_to_npm.bat new file mode 100644 index 0000000..362e815 --- /dev/null +++ b/WowUp/wowup-electron/scripts/core_to_npm.bat @@ -0,0 +1,3 @@ +call npm uninstall wowup-lib-core + +call npm install wowup-lib-core \ No newline at end of file diff --git a/WowUp/wowup-electron/src/app/addon-providers/curse-addon-provider.ts b/WowUp/wowup-electron/src/app/addon-providers/curse-addon-provider.ts new file mode 100644 index 0000000..b5aa30b --- /dev/null +++ b/WowUp/wowup-electron/src/app/addon-providers/curse-addon-provider.ts @@ -0,0 +1,962 @@ +import * as cfv2 from "curseforge-v2"; +import * as _ from "lodash"; +import { filter } from "rxjs/operators"; +import { v4 as uuidv4 } from "uuid"; +import { + Addon, + AddonCategory, + AddonChannelType, + AddonDependencyType, + AddonFolder, + AddonProvider, + AddonScanResult, + AddonSearchResult, + AddonSearchResultDependency, + AddonSearchResultFile, + AddonWarningType, + AdPageOptions, + GetAllBatchResult, + GetAllResult, + getEnumName, + getGameVersion, + getGameVersionList, + getWowClientGroupForType, + ProtocolSearchResult, + SearchByUrlResult, + SourceRemovedAddonError, + WowClientGroup, + WowClientType, + WowInstallation, +} from "wowup-lib-core"; + +import { + ADDON_PROVIDER_CURSEFORGE, + NO_LATEST_SEARCH_RESULT_FILES_ERROR, + NO_SEARCH_RESULTS_ERROR, + PREF_CF2_API_KEY, +} from "../../common/constants"; +import { AppConfig } from "../../environments/environment"; +import { CachingService } from "../services/caching/caching-service"; +import { CircuitBreakerWrapper, NetworkService } from "../services/network/network.service"; +import { TocService } from "../services/toc/toc.service"; +import { strictFilter } from "../utils/array.utils"; +import { TocNotFoundError } from "../errors"; +import { SensitiveStorageService } from "../services/storage/sensitive-storage.service"; + +interface ProtocolData { + addonId: number; + fileId: number; +} + +interface ScanMatchPair { + addonFolder: AddonFolder; + match: cfv2.CF2FingerprintMatch; + addon?: cfv2.CF2Addon; +} + +const CHANGELOG_CACHE_TTL_SEC = 30 * 60; +const FEATURED_ADDONS_CACHE_TTL_SEC = AppConfig.featuredAddonsCacheTimeSec; + +const GAME_TYPE_LISTS = [ + { + flavor: "wow_classic", + typeId: [67408], + matches: [WowClientType.ClassicEra, WowClientType.ClassicEraPtr], + }, + { + flavor: "wow-wrath-classic", + typeId: [73713, 73246], + matches: [], + }, + { + flavor: "wow_retail", + typeId: [517], + matches: [WowClientType.Retail, WowClientType.RetailPtr, WowClientType.Beta, WowClientType.RetailXPtr], + }, + { + flavor: "wow-cataclysm-classic", + typeId: [77522, 73713, 73246], + matches: [WowClientType.Classic, WowClientType.ClassicPtr, WowClientType.ClassicBeta], + }, +]; + +export class CurseAddonProvider extends AddonProvider { + private readonly _circuitBreaker: CircuitBreakerWrapper; + private _cf2Client: cfv2.CFV2Client; + + public readonly name = ADDON_PROVIDER_CURSEFORGE; + public readonly forceIgnore = false; + public readonly allowChannelChange = true; + public readonly allowReinstall = true; + public readonly canBatchFetch = true; + public readonly allowEdit = true; + + public adRequired = false; + public enabled = true; + + public constructor( + private _cachingService: CachingService, + private _networkService: NetworkService, + private _tocService: TocService, + private _sensitiveStorageService: SensitiveStorageService, + ) { + super(); + + this._circuitBreaker = this._networkService.getCircuitBreaker( + `${this.name}_main`, + undefined, + AppConfig.curseforge.httpTimeoutMs, + ); + + // Pick up a CF2 api key change at runtime to force a new client to be created + this._sensitiveStorageService.change$.pipe(filter((change) => change.key === PREF_CF2_API_KEY)).subscribe(() => { + this._cf2Client = undefined; + }); + } + + public override async getAllBatch(installations: WowInstallation[], addonIds: string[]): Promise { + const batchResult: GetAllBatchResult = { + errors: {}, + installationResults: {}, + }; + + if (!addonIds.length) { + return batchResult; + } + + const searchResults = await this.getAllIds(addonIds.map((id) => parseInt(id, 10))); + + for (const installation of installations) { + const addonResults: AddonSearchResult[] = []; + for (const result of searchResults) { + const latestFiles = this.getLatestFiles(result, installation.clientType); + if (!latestFiles.length) { + continue; + } + + const addonSearchResult = this.getAddonSearchResult(result, latestFiles); + if (addonSearchResult) { + addonResults.push(addonSearchResult); + } + } + + const missingAddonIds = _.filter( + addonIds, + (addonId) => _.find(searchResults, (sr) => sr.id.toString() === addonId) === undefined, + ); + + batchResult.errors[installation.id] = _.map( + missingAddonIds, + (addonId) => new SourceRemovedAddonError(addonId, undefined), + ); + + batchResult.installationResults[installation.id] = addonResults; + } + + return batchResult; + } + + public override async getAll(installation: WowInstallation, addonIds: string[]): Promise { + if (!addonIds.length) { + return { + searchResults: [], + errors: [], + }; + } + + const addonResults: AddonSearchResult[] = []; + const searchResults = await this.getAllIds(addonIds.map((id) => parseInt(id, 10))); + + for (const result of searchResults) { + const latestFiles = this.getLatestFiles(result, installation.clientType); + if (!latestFiles.length) { + continue; + } + + const addonSearchResult = this.getAddonSearchResult(result, latestFiles); + if (addonSearchResult) { + addonResults.push(addonSearchResult); + } + } + + const missingAddonIds = _.filter( + addonIds, + (addonId) => _.find(searchResults, (sr) => sr.id.toString() === addonId) === undefined, + ); + + const deletedErrors = _.map(missingAddonIds, (addonId) => new SourceRemovedAddonError(addonId, undefined)); + + return { + errors: [...deletedErrors], + searchResults: addonResults, + }; + } + + public override async getFeaturedAddons(installation: WowInstallation): Promise { + const addons = await this.getFeaturedAddonList(installation); + const filteredAddons = this.filterFeaturedAddons(addons, installation.clientType); + + const mapped = filteredAddons.map((addon) => { + const latestFiles = this.getLatestFiles(addon, installation.clientType); + return this.getAddonSearchResult(addon, latestFiles); + }); + + return strictFilter(mapped); + } + + public override shouldMigrate(addon: Addon): boolean { + return !addon.installedExternalReleaseId; + } + + public override async searchByQuery( + query: string, + installation: WowInstallation, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + channelType?: AddonChannelType, + ): Promise { + const searchResults: AddonSearchResult[] = []; + + const response = await this.getSearchResults(query, installation.clientType); + + for (const result of response) { + const latestFiles = this.getLatestFiles(result, installation.clientType); + if (!latestFiles.length) { + continue; + } + + const searchResult = this.getAddonSearchResult(result, latestFiles); + if (searchResult) { + searchResults.push(searchResult); + } + } + + return searchResults; + } + + public override async searchByUrl( + addonUri: URL, + installation: WowInstallation, + ): Promise { + const slugRegex = /\/addons\/(.*?)(\/|$)/gi; + const slugMatch = slugRegex.exec(addonUri.pathname); + if (!slugMatch || slugMatch.length < 2) { + return undefined; + } + const result = await this.searchBySlug(slugMatch[1], installation.clientType); + + return { + errors: [], + searchResult: result, + }; + } + + public override async searchProtocol(protocol: string): Promise { + const protocolData = this.parseProtocol(protocol); + if (!protocolData.addonId || !protocolData.fileId) { + throw new Error("Invalid protocol data"); + } + + const addonResult = await this.getByIdBase(protocolData.addonId.toString()); + if (!addonResult) { + throw new Error(`Failed to get addon data: ${protocolData.addonId}`); + } + + console.debug("addonResult", addonResult); + + const addonFileResponse = await this.getAddonFileById(protocolData.addonId, protocolData.fileId); + console.debug("targetFile", addonFileResponse); + + if (!addonFileResponse) { + throw new Error("Failed to get target file"); + } + + const addonSearchResult = this.getAddonSearchResult(addonResult, [addonFileResponse]); + if (!addonSearchResult) { + throw new Error("Addon search result not created"); + } + + const searchResult: ProtocolSearchResult = { + protocol, + protocolAddonId: protocolData.addonId.toString(), + protocolReleaseId: protocolData.fileId.toString(), + validClientTypes: this.getValidClientTypes(addonFileResponse), + ...addonSearchResult, + }; + console.debug("searchResult", searchResult); + + return searchResult; + } + + public override async getCategory( + category: AddonCategory, + installation: WowInstallation, + ): Promise { + const curseCategories = this.mapAddonCategory(category); + const gameVersionTypeId = this.getGameVersionTypeId(installation.clientType); + + const response = await this.getCategoryAddons(curseCategories[0], gameVersionTypeId, 50, 0); + + const searchResults: AddonSearchResult[] = []; + for (const responseItem of response) { + const latestFiles = this.getLatestFiles(responseItem, installation.clientType); + if (!latestFiles.length) { + continue; + } + + const searchResult = this.getAddonSearchResult(responseItem, latestFiles); + if (searchResult !== undefined) { + searchResults.push(searchResult); + } + } + + return searchResults; + } + + public override async getById( + addonId: string, + installation: WowInstallation, + ): Promise { + const result = await this.getByIdBase(addonId); + + if (!result) { + return undefined; + } + + const latestFiles = this.getLatestFiles(result, installation.clientType); + if (!latestFiles?.length) { + return undefined; + } + + return this.getAddonSearchResult(result, latestFiles); + } + + public override isValidAddonUri(addonUri: URL): boolean { + return ( + addonUri.host !== undefined && + addonUri.host.endsWith("curseforge.com") && + addonUri.pathname.startsWith("/wow/addons") + ); + } + + public override isValidAddonId(addonId: string): boolean { + return !!addonId && !isNaN(parseInt(addonId, 10)); + } + + public override isValidProtocol(protocol: string): boolean { + return protocol.toLowerCase().startsWith("curseforge://"); + } + + public override async scan( + installation: WowInstallation, + addonChannelType: AddonChannelType, + addonFolders: AddonFolder[], + ): Promise { + if (!addonFolders.length) { + return; + } + + const client = await this.getClient(); + if (!client) { + return; + } + + const scanResults = addonFolders + .map((af) => af.cfScanResults) + .filter((sr): sr is AddonScanResult => sr !== undefined); + + const fingerprints = scanResults.map((sr) => sr.fingerprintNum); + + const result = await client.getFingerprintMatches({ fingerprints }); + const fingerprintData = result.data?.data; + try { + const matchPairs: ScanMatchPair[] = []; + for (const af of addonFolders) { + let exactMatch = fingerprintData?.exactMatches.find( + (em) => + this.isCfFileCompatible(installation.clientType, em.file) && + em.file.modules.some((m) => m.fingerprint == af.cfScanResults?.fingerprintNum), + ); + + // If the addon does not have an exact match, check the partial matches. + if (!exactMatch && Array.isArray(fingerprintData?.partialMatches) && fingerprintData !== undefined) { + exactMatch = fingerprintData.partialMatches.find((partialMatch) => + partialMatch.file?.modules?.some((module) => module.fingerprint === af.cfScanResults?.fingerprintNum), + ); + } + + if (exactMatch) { + matchPairs.push({ + addonFolder: af, + match: exactMatch, + }); + } + } + + const addonIds = matchPairs.map((mp) => mp.match.id); + const getAddonsResult = await client.getMods({ modIds: addonIds }); + const addonResultData = getAddonsResult.data?.data; + + const potentialChildren: ScanMatchPair[] = []; + matchPairs.forEach((mp) => { + const cfAddon = addonResultData?.find((ar) => ar.id === mp.match.id); + if (!cfAddon) { + return; + } + + try { + mp.addonFolder.matchingAddon = this.createAddon(installation, mp.addonFolder, mp.match.file, cfAddon); + } catch (e) { + if (e instanceof TocNotFoundError) { + potentialChildren.push(mp); + } else { + console.error(e); + } + } + }); + + potentialChildren.forEach((pc) => { + const parent = matchPairs.find( + (mp) => mp.addonFolder.matchingAddon !== undefined && this.isChildAddon(mp.match.file, pc.addonFolder.name), + ); + pc.addonFolder.matchingAddon = parent?.addonFolder.matchingAddon; + }); + } catch (e) { + console.error("failed to process fingerprint response"); + console.error(e); + console.log(result); + throw e; + } + } + + private isChildAddon(cfAddon: cfv2.CF2File, addonName: string) { + return cfAddon.modules.some((m) => m.name == addonName); + } + + public override async getChangelog( + installation: WowInstallation, + externalId: string, + externalReleaseId: string, + ): Promise { + try { + const client = await this.getClient(); + const cacheKey = `${this.name}_changelog_${externalId}_${externalReleaseId}`; + + const response = await this._cachingService.transaction( + cacheKey, + () => { + return client.getModFileChangelog(parseInt(externalId, 10), parseInt(externalReleaseId, 10)); + }, + CHANGELOG_CACHE_TTL_SEC, + ); + + return response.data?.data || ""; + } catch (e) { + console.error("Failed to get changelog", e); + } + + return ""; + } + + public override async getDescription(installation: WowInstallation, externalId: string): Promise { + try { + const client = await this.getClient(); + + const cacheKey = `${this.name}_description_${externalId}`; + const response = await this._cachingService.transaction( + cacheKey, + () => { + return client.getModDescription(parseInt(externalId, 10)); + }, + CHANGELOG_CACHE_TTL_SEC, + ); + + if (response.data) { + return this.standardizeDescription(response.data.data); + } + } catch (e) { + console.error("Failed to get changelog", e); + } + + return ""; + } + + public override getAdPageParams(): AdPageOptions { + return { + pageUrl: "", + }; + } + + private isCfFileCompatible(clientType: WowClientType, file: cfv2.CF2File): boolean { + if (Array.isArray(file.sortableGameVersions) && file.sortableGameVersions.length > 0) { + const gameVersionTypeId = this.getGameVersionTypeId(clientType); + return this.hasSortableGameVersion(file, gameVersionTypeId); + } + + return false; + } + + private getGameVersionTypeId(clientType: WowClientType): number { + const gameType = GAME_TYPE_LISTS.find((gtl) => gtl.matches.includes(clientType)); + if (!gameType) { + throw new Error(`Game type not found: ${clientType}`); + } + + return gameType.typeId[0]; + } + + private hasSortableGameVersion(file: cfv2.CF2File, typeId: number): boolean { + if (!file?.sortableGameVersions) { + console.debug("sortableGameVersions missing", file); + } + return file.sortableGameVersions.some((sgv) => sgv.gameVersionTypeId === typeId); + } + + private getThumbnailUrl(result: cfv2.CF2Addon): string { + return result.logo?.thumbnailUrl ?? ""; + } + + private getScreenshotUrls(result: cfv2.CF2Addon): string[] { + return result.screenshots.map((f) => f.url).filter(Boolean); + } + + private createAddon( + installation: WowInstallation, + addonFolder: AddonFolder, + cfFile: cfv2.CF2File, + cfAddon: cfv2.CF2Addon, + ): Addon { + const authors = cfAddon.authors.map((author) => author.name).join(", "); + const folders = cfFile.modules.map((module) => module.name); + const folderList = folders.join(","); + const latestFiles = this.getLatestFiles(cfAddon, installation.clientType); + + const channelType = this.getChannelType(cfFile.releaseType); + const latestVersion = latestFiles.find((lf) => this.getChannelType(lf.releaseType) <= channelType); + + const targetToc = this._tocService.getTocForGameType2(addonFolder.name, addonFolder.tocs, installation.clientType); + if (!targetToc) { + console.error('targetToc undefined', cfAddon.name, addonFolder.tocs); + throw new TocNotFoundError("Target toc not found"); + } + + const gameVersions = getGameVersionList(targetToc.interface); + + const addon: Addon = { + id: uuidv4(), + author: authors, + name: cfAddon?.name ?? "unknown", + channelType, + autoUpdateEnabled: false, + autoUpdateNotificationsEnabled: false, + clientType: installation.clientType, + downloadUrl: latestVersion?.downloadUrl ?? cfFile.downloadUrl ?? "", + externalUrl: cfAddon?.links?.websiteUrl ?? "", + externalId: cfAddon?.id.toString() ?? "", + gameVersion: gameVersions, + installedAt: new Date(addonFolder?.fileStats?.birthtimeMs ?? 0), + installedFolders: folderList, + installedFolderList: folders, + installedVersion: cfFile.displayName, + installedExternalReleaseId: cfFile.id.toString(), + isIgnored: false, + latestVersion: latestVersion?.displayName ?? cfFile.displayName ?? "", + providerName: this.name, + thumbnailUrl: cfAddon ? this.getThumbnailUrl(cfAddon) : "", + screenshotUrls: cfAddon ? this.getScreenshotUrls(cfAddon) : [], + downloadCount: cfAddon?.downloadCount ?? 0, + summary: cfAddon?.summary ?? "", + releasedAt: new Date(latestVersion?.fileDate ?? cfFile.fileDate ?? ""), + isLoadOnDemand: false, + externalLatestReleaseId: (latestVersion?.id ?? cfFile.id ?? "").toString(), + updatedAt: addonFolder?.fileStats?.birthtime ?? new Date(0), + externalChannel: getEnumName(AddonChannelType, channelType), + installationId: installation.id, + }; + + if (!latestFiles.length) { + addon.warningType = AddonWarningType.NoProviderFiles; + } + + return addon; + } + + private getLatestFiles(result: cfv2.CF2Addon, clientType: WowClientType): cfv2.CF2File[] { + const filtered = result.latestFiles.filter( + (latestFile) => latestFile.exposeAsAlternative !== true && this.isClientType(latestFile, clientType), + ); + return _.sortBy(filtered, (latestFile) => latestFile.id).reverse(); + } + + private isClientType(file: cfv2.CF2File, clientType: WowClientType) { + return this.isCfFileCompatible(clientType, file); + } + + private getChannelType(releaseType: cfv2.CF2FileReleaseType): AddonChannelType { + switch (releaseType) { + case cfv2.CF2FileReleaseType.Alpha: + return AddonChannelType.Alpha; + case cfv2.CF2FileReleaseType.Beta: + return AddonChannelType.Beta; + case cfv2.CF2FileReleaseType.Release: + default: + return AddonChannelType.Stable; + } + } + + /** We want to pull all the A tags and fix what we can */ + private standardizeDescription(description: string): string { + let descriptionCpy = `${description}`; + const hrefRegex = /]*?\s+)?href=(["'])(.*?)\1/g; + const results = descriptionCpy.matchAll(hrefRegex); + const resultArr = [...results]; + for (const result of resultArr) { + try { + const href = result[2]; + if (!href) { + continue; + } + + if (href.toLowerCase().indexOf("/linkout") === 0) { + descriptionCpy = this.rebuildLinkOut(descriptionCpy, href); + } + } catch (e) { + console.error(e); + } + } + return descriptionCpy; + } + + private rebuildLinkOut(description: string, href: string) { + const url = new URL(`https://www.curseforge.com${href}`); + const remoteUrl = url.searchParams.get("remoteUrl") ?? ""; + const destination = window.decodeURIComponent(remoteUrl); + return description.replace(href, destination); + } + + private parseProtocol(protocol: string): ProtocolData { + const url = new URL(protocol); + return { + addonId: +(url.searchParams.get("addonId") ?? ""), + fileId: +(url.searchParams.get("fileId") ?? ""), + }; + } + + private async getAddonFileById(addonId: string | number, fileId: string | number): Promise { + const client = await this.getClient(); + const response = await this._circuitBreaker.fire(() => + client.getModFile(parseInt(`${addonId}`, 10), parseInt(`${fileId}`, 10)), + ); + + return response.data?.data; + } + + private async getByIdBase(addonId: string): Promise { + try { + const client = await this.getClient(); + const response = await this._circuitBreaker.fire(() => client.getMod(parseInt(addonId, 10))); + return response.data?.data; + } catch (e) { + // We want to eat things like 400/500 responses + console.error(e); + } + } + + private getAuthor(result: cfv2.CF2Addon): string { + const authorNames = result.authors.map((a) => a.name).filter((lf) => !lf.toLowerCase().startsWith("_forgeuser")); + return authorNames.join(", "); + } + + private getFolderNames(file: cfv2.CF2File): string[] { + return file.modules.map((m) => m.name); + } + + private getGameVersion(file: cfv2.CF2File): string { + return _.first(file.gameVersions) ?? ""; + } + + private createAddonSearchResultDependency = (dependency: cfv2.CF2FileDependency): AddonSearchResultDependency => { + return { + externalAddonId: dependency.modId.toString(), + type: this.toAddonDependencyType(dependency.relationType), + }; + }; + + private toAddonDependencyType(curseDependencyType: cfv2.CF2FileRelationType): AddonDependencyType { + switch (curseDependencyType) { + case cfv2.CF2FileRelationType.EmbeddedLibrary: + return AddonDependencyType.Embedded; + case cfv2.CF2FileRelationType.OptionalDependency: + return AddonDependencyType.Optional; + case cfv2.CF2FileRelationType.RequiredDependency: + return AddonDependencyType.Required; + case cfv2.CF2FileRelationType.Include: + case cfv2.CF2FileRelationType.Incompatible: + case cfv2.CF2FileRelationType.Tool: + default: + return AddonDependencyType.Other; + } + } + + private getCFGameVersionType(clientType: WowClientType): cfv2.CF2WowGameVersionType { + const clientGroup = getWowClientGroupForType(clientType); + + switch (clientGroup) { + case WowClientGroup.Cata: + return cfv2.CF2WowGameVersionType.Cata; + case WowClientGroup.WOTLK: + return cfv2.CF2WowGameVersionType.WOTLK; + case WowClientGroup.BurningCrusade: + return cfv2.CF2WowGameVersionType.BurningCrusade; + case WowClientGroup.Classic: + return cfv2.CF2WowGameVersionType.Classic; + case WowClientGroup.Retail: + return cfv2.CF2WowGameVersionType.Retail; + default: + throw new Error(`invalid game type: ${clientGroup as string}`); + } + } + + private mapAddonCategory(category: AddonCategory): cfv2.CF2WowAddonCategory[] { + switch (category) { + case AddonCategory.Achievements: + return [cfv2.CF2WowAddonCategory.Achievements]; + case AddonCategory.ActionBars: + return [cfv2.CF2WowAddonCategory.ActionBars]; + case AddonCategory.AuctionEconomy: + return [cfv2.CF2WowAddonCategory.AuctionEconomy]; + case AddonCategory.BagsInventory: + return [cfv2.CF2WowAddonCategory.BagsInventory]; + case AddonCategory.BossEncounters: + return [cfv2.CF2WowAddonCategory.BossEncounters]; + case AddonCategory.BuffsDebuffs: + return [cfv2.CF2WowAddonCategory.BuffsDebuffs]; + case AddonCategory.ChatCommunication: + return [cfv2.CF2WowAddonCategory.ChatCommunication]; + case AddonCategory.Class: + return [cfv2.CF2WowAddonCategory.Class]; + case AddonCategory.Combat: + return [cfv2.CF2WowAddonCategory.Combat]; + case AddonCategory.Companions: + return [cfv2.CF2WowAddonCategory.Companions]; + case AddonCategory.DataExport: + return [cfv2.CF2WowAddonCategory.DataExport]; + case AddonCategory.DevelopmentTools: + return [cfv2.CF2WowAddonCategory.DevelopmentTools]; + case AddonCategory.Guild: + return [cfv2.CF2WowAddonCategory.Guild]; + case AddonCategory.Libraries: + return [cfv2.CF2WowAddonCategory.Libraries]; + case AddonCategory.Mail: + return [cfv2.CF2WowAddonCategory.Mail]; + case AddonCategory.MapMinimap: + return [cfv2.CF2WowAddonCategory.MapMinimap]; + case AddonCategory.Miscellaneous: + return [cfv2.CF2WowAddonCategory.Miscellaneous]; + case AddonCategory.Missions: + return [cfv2.CF2WowAddonCategory.Garrison]; + case AddonCategory.Plugins: + return [cfv2.CF2WowAddonCategory.Plugins]; + case AddonCategory.Professions: + return [cfv2.CF2WowAddonCategory.Professions]; + case AddonCategory.PVP: + return [cfv2.CF2WowAddonCategory.PvP]; + case AddonCategory.QuestsLeveling: + return [cfv2.CF2WowAddonCategory.QuestsLeveling]; + case AddonCategory.Roleplay: + return [cfv2.CF2WowAddonCategory.Roleplay]; + case AddonCategory.Tooltips: + return [cfv2.CF2WowAddonCategory.Tooltip]; + case AddonCategory.UnitFrames: + return [cfv2.CF2WowAddonCategory.UnitFrames]; + default: + throw new Error("Unhandled addon category"); + } + } + + private getAddonSearchResult(result: cfv2.CF2Addon, latestFiles: cfv2.CF2File[] = []): AddonSearchResult | undefined { + try { + const thumbnailUrl = this.getThumbnailUrl(result); + const id = result.id; + const name = result.name; + const author = this.getAuthor(result); + + const searchResultFiles: AddonSearchResultFile[] = latestFiles.map((lf) => { + return { + channelType: this.getChannelType(lf.releaseType), + version: lf.displayName, + downloadUrl: lf.downloadUrl, + folders: this.getFolderNames(lf), + gameVersion: getGameVersion(this.getGameVersion(lf)), + releaseDate: new Date(lf.fileDate), + dependencies: lf.dependencies.map(this.createAddonSearchResultDependency), + externalId: lf.id.toString(), + }; + }); + + const searchResult: AddonSearchResult = { + author, + externalId: id.toString(), + name, + thumbnailUrl, + externalUrl: result.links?.websiteUrl, + providerName: this.name, + files: _.orderBy(searchResultFiles, (f) => f.channelType).reverse(), + downloadCount: result.downloadCount, + summary: result.summary, + screenshotUrls: this.getScreenshotUrls(result), + externallyBlocked: false, + }; + + return searchResult; + } catch (e) { + console.error(e); + return undefined; + } + } + + private getValidClientTypes(file: cfv2.CF2File): WowClientType[] { + const gameVersions: WowClientType[] = _.flatten( + GAME_TYPE_LISTS.filter((type) => + file.sortableGameVersions.find((sgv) => type.typeId.includes(sgv.gameVersionTypeId)) + ).map((game) => game.matches), + ); + + return _.uniq(gameVersions); + } + + private async getAllIds(addonIds: number[]): Promise { + if (!addonIds?.length) { + return []; + } + + const request: cfv2.CF2GetModsRequest = { + modIds: addonIds, + }; + + const client = await this.getClient(); + + const response = await this._circuitBreaker.fire(() => client.getMods(request)); + + return response.data?.data || []; + } + + private async getFeaturedAddonList(wowInstallation: WowInstallation): Promise { + const client = await this.getClient(); + if (!client) { + return []; + } + + const gameVersionTypeId = this.getGameVersionTypeId(wowInstallation.clientType); + + const request: cfv2.CF2GetFeaturedModsRequest = { + gameId: 1, + gameVersionTypeId, + excludedModIds: [], + }; + + const cacheKey = `getFeaturedAddonList-${JSON.stringify(request)}`; + const result = await this._cachingService.transaction( + cacheKey, + () => client.getFeaturedMods(request), + FEATURED_ADDONS_CACHE_TTL_SEC, + ); + + if (!result || result.statusCode !== 200) { + return []; + } + + const body = result.data?.data; + if (body) { + // Remove duplicate addons that are already in the popular list from the recents list + const uniqueRecent = body.recentlyUpdated.filter((ru) => !body.popular.some((p) => p.id === ru.id)); + + return [...body.popular, ...uniqueRecent]; + } + return []; + } + + private filterFeaturedAddons(results: cfv2.CF2Addon[], clientType: WowClientType): cfv2.CF2Addon[] { + return results.filter((r) => r.latestFiles.some((lf) => this.isClientType(lf, clientType))); + } + + private async getSearchResults(query: string, clientType: WowClientType): Promise { + const request: cfv2.CF2SearchModsParams = { + gameId: 1, + categoryId: 0, + searchFilter: query, + sortField: 2, + sortOrder: "desc", + index: 0, + gameVersionTypeId: this.getCFGameVersionType(clientType), + }; + + const client = await this.getClient(); + const response = await this._circuitBreaker.fire(() => client.searchMods(request)); + + return response.data?.data || []; + } + + private async searchBySlug(slug: string, clientType: WowClientType): Promise { + const searchWord = _.first(slug.split("-")); + if (!searchWord) { + throw new Error("Invalid slug"); + } + + const response = await this.getSearchResults(searchWord, clientType); + + const match = _.find(response, (res) => res.slug === slug); + if (!match) { + throw new Error(NO_SEARCH_RESULTS_ERROR); + } + + const latestFiles = this.getLatestFiles(match, clientType); + if (!latestFiles?.length) { + throw new Error(NO_LATEST_SEARCH_RESULT_FILES_ERROR); + } + + return this.getAddonSearchResult(match, latestFiles); + } + + private async getCategoryAddons( + category: cfv2.CF2WowAddonCategory, + gameVersionFlavor: cfv2.CF2WowGameVersionType, + pageSize: number, + pageNumber: number, + ): Promise { + const request: cfv2.CF2SearchModsParams = { + gameId: 1, + categoryId: category, + pageSize: pageSize, + index: pageNumber, + sortOrder: "desc", + gameVersionTypeId: gameVersionFlavor, + }; + + const cacheKey = JSON.stringify(request); + + const client = await this.getClient(); + const result = await this._cachingService.transaction(cacheKey, () => + this._circuitBreaker.fire(() => client.searchMods(request)), + ); + + return result?.data?.data ?? []; + } + + private async getClient(): Promise { + if (this._cf2Client) { + return this._cf2Client; + } + + let apiKey = await this._sensitiveStorageService.getAsync(PREF_CF2_API_KEY); + if (typeof apiKey !== "string" || apiKey.length === 0) { + await this._sensitiveStorageService.setAsync(PREF_CF2_API_KEY, AppConfig.curseforge.apiKey); + apiKey = AppConfig.curseforge.apiKey; + } + + this._cf2Client = new cfv2.CFV2Client({ + apiKey, + }); + + return this._cf2Client; + } +} diff --git a/WowUp/wowup-electron/src/app/addon-providers/github-addon-provider.ts b/WowUp/wowup-electron/src/app/addon-providers/github-addon-provider.ts new file mode 100644 index 0000000..9134fc9 --- /dev/null +++ b/WowUp/wowup-electron/src/app/addon-providers/github-addon-provider.ts @@ -0,0 +1,586 @@ +import * as _ from "lodash"; +import { firstValueFrom, from, mergeMap, toArray } from "rxjs"; + +import { HttpClient, HttpErrorResponse, HttpHeaders } from "@angular/common/http"; + +import { ADDON_PROVIDER_GITHUB, PREF_GITHUB_PERSONAL_ACCESS_TOKEN } from "../../common/constants"; +import { + AssetMissingError, + GitHubError, + GitHubFetchReleasesError, + GitHubFetchRepositoryError, + GitHubLimitError, +} from "../errors"; +import { convertMarkdown, getWowClientGroupForType } from "wowup-lib-core"; +import { strictFilterBy } from "../utils/array.utils"; +import { SensitiveStorageService } from "../services/storage/sensitive-storage.service"; +import { + AddonChannelType, + AddonProvider, + AddonSearchResult, + AddonSearchResultFile, + DownloadAuth, + GetAllResult, + SearchByUrlResult, + WowClientType, +} from "wowup-lib-core"; +import { GitHubAsset, GitHubRelease, GitHubRepository, WowInstallation } from "wowup-lib-core"; +import { SourceRemovedAddonError } from "wowup-lib-core"; + +type MetadataFlavor = "bcc" | "classic" | "mainline" | "wrath" | "cata"; + +interface LatestValidAsset { + matchedAsset: GitHubAsset | undefined; + release: GitHubRelease | undefined; + latestAsset: GitHubAsset | undefined; +} + +interface GitHubRepoParts { + repository: string; + owner: string; +} + +interface ReleaseMeta { + releases: ReleaseMetaItem[]; +} + +interface ReleaseMetaItem { + filename: string; + nolib: boolean; + metadata: ReleaseMetaItemMetadata[]; +} + +interface ReleaseMetaItemMetadata { + flavor: MetadataFlavor; + interface: number; +} + +const API_URL = "https://api.github.com/repos"; +const RELEASE_CONTENT_TYPES = { + XZIP: "application/x-zip-compressed", + ZIP: "application/zip", + OCTET_STREAM: "application/octet-stream", +}; +const HEADER_RATE_LIMIT_MAX = "x-ratelimit-limit"; +const HEADER_RATE_LIMIT_REMAINING = "x-ratelimit-remaining"; +const HEADER_RATE_LIMIT_RESET = "x-ratelimit-reset"; +const HEADER_RATE_LIMIT_USED = "x-ratelimit-used"; + +export class GitHubAddonProvider extends AddonProvider { + public readonly name = ADDON_PROVIDER_GITHUB; + public readonly forceIgnore = false; + public readonly allowReinstall = true; + public readonly allowChannelChange = false; + public readonly allowEdit = false; + public readonly allowReScan = false; + public enabled = true; + + public constructor( + private _httpClient: HttpClient, + private _sensitiveStorageService: SensitiveStorageService, + ) { + super(); + } + + public async getDownloadAuth(): Promise { + const hasPat = await this.hasPersonalAccessKey(); + if (hasPat) { + const headers = await this.getAuthorizationHeader(); + headers.Accept = "application/octet-stream"; + + return { + headers, + }; + } else { + return undefined; + } + } + + public async getAll(installation: WowInstallation, addonIds: string[]): Promise { + const taskResults = await firstValueFrom( + from(addonIds).pipe( + mergeMap((addonId) => from(this.handleGetAllItem(addonId, installation)), 3), + toArray(), + ), + ); + + const result: GetAllResult = { + errors: _.concat(taskResults.map((tr) => tr.error).filter((e): e is Error => e !== undefined)), + searchResults: _.concat( + taskResults.map((tr) => tr.searchResult).filter((sr): sr is AddonSearchResult => sr !== undefined), + ), + }; + + return result; + } + + private async handleGetAllItem( + addonId: string, + installation: WowInstallation, + ): Promise<{ searchResult: AddonSearchResult | undefined; error: Error | undefined }> { + const result: { searchResult: AddonSearchResult | undefined; error: Error | undefined } = { + searchResult: undefined, + error: undefined, + }; + + try { + result.searchResult = await this.getByIdAsync(addonId, installation); + } catch (e) { + // If we're at the limit, just give up the loop + if (e instanceof GitHubLimitError) { + throw e; + } + + if (e instanceof SourceRemovedAddonError) { + e.addonId = addonId; + } + + result.error = e as Error; + } + + return result; + } + + public async searchByUrl(addonUri: URL, installation: WowInstallation): Promise { + const repoPath = addonUri.pathname; + if (!repoPath) { + throw new Error(`Invalid URL: ${addonUri.toString()}`); + } + + const searchByUrlResult: SearchByUrlResult = { + errors: [], + searchResult: undefined, + }; + + const clientGroup = getWowClientGroupForType(installation.clientType); + + try { + const results = await this.getReleases(repoPath); + + const prereleaseRes = results.filter((res) => res.prerelease); + const stableRes = results.filter((res) => !res.prerelease); + let checkRes = stableRes.length === 0 ? prereleaseRes : stableRes; + if (installation.defaultAddonChannelType !== AddonChannelType.Stable) { + checkRes = results; + } + + const result = await this.getLatestValidAsset(checkRes, installation.clientType); + console.log("searchByUrl result", result); + if (!result.matchedAsset && !result.latestAsset) { + throw new AssetMissingError(addonUri.toString(), clientGroup); + } + + const hasPat = await this.hasPersonalAccessKey(); + const repository = await this.getRepository(repoPath); + const author = repository.owner.login; + const authorImageUrl = repository.owner.avatar_url; + + const asset = result.matchedAsset || result.latestAsset; + const potentialAddon: AddonSearchResult = { + author: author, + downloadCount: asset?.download_count ?? 0, + externalId: this.createExternalId(addonUri), + externalUrl: repository.html_url, + name: repository.name, + providerName: this.name, + thumbnailUrl: authorImageUrl, + files: [ + { + channelType: result.release?.prerelease ? AddonChannelType.Beta : AddonChannelType.Stable, + downloadUrl: (hasPat ? asset?.url : asset?.browser_download_url) ?? "", + folders: [], + gameVersion: "", + releaseDate: new Date(result.release?.published_at ?? ""), + version: asset?.name ?? "", + }, + ], + }; + + // If there was not an exact match, throw an error with the addon we created as the metadata + if (!result.matchedAsset) { + searchByUrlResult.errors?.push(new AssetMissingError(addonUri.toString())); + } + + searchByUrlResult.searchResult = potentialAddon; + } catch (e) { + console.error("searchByUrl failed", e); + throw e; + } + + return searchByUrlResult; + } + + private createExternalId(addonUri: URL) { + const parsed = this.parseRepoPath(addonUri.pathname); + return `${parsed.owner}/${parsed.repository}`; + } + + public override async getById( + addonId: string, + installation: WowInstallation, + ): Promise { + return await this.getByIdAsync(addonId, installation); + } + + private async getByIdAsync(addonId: string, installation: WowInstallation) { + const repository = await this.getRepository(addonId); + const releases = await this.getReleases(addonId); + if (!releases?.length) { + return undefined; + } + + const prereleaseRes = releases.filter((res) => res.prerelease); + const stableRes = releases.filter((res) => !res.prerelease); + let checkRes = stableRes.length === 0 ? prereleaseRes : stableRes; + if (installation.defaultAddonChannelType !== AddonChannelType.Stable) { + checkRes = releases; + } + + const assetResult = await this.getLatestValidAsset(checkRes, installation.clientType); + if (!assetResult.matchedAsset && !assetResult.latestAsset) { + return undefined; + } + console.debug("assetResult", assetResult); + + const author = repository.owner.login; + const authorImageUrl = repository.owner.avatar_url; + const addonName = this.getAddonName(addonId); + const asset = assetResult.matchedAsset || assetResult.latestAsset; + console.debug("asset", asset); + + const hasPat = await this.hasPersonalAccessKey(); + + const searchResultFile: AddonSearchResultFile = { + channelType: AddonChannelType.Stable, + downloadUrl: (hasPat ? asset?.url : asset?.browser_download_url) ?? "", + folders: [addonName], + gameVersion: "", + version: asset?.name ?? "", + releaseDate: new Date(asset?.created_at ?? ""), + changelog: convertMarkdown(assetResult?.release?.body ?? ""), + }; + + const searchResult: AddonSearchResult = { + author: author, + externalId: addonId, + externalUrl: repository.html_url, + files: [searchResultFile], + name: addonName, + providerName: this.name, + thumbnailUrl: authorImageUrl, + summary: repository.description, + }; + + return searchResult; + } + + public isValidAddonUri(addonUri: URL): boolean { + return !!addonUri.host && addonUri.host.endsWith("com"); + } + + public isValidAddonId(addonId: string): boolean { + return addonId.indexOf("/") !== -1; + } + + private async getLatestValidAsset(releases: GitHubRelease[], clientType: WowClientType): Promise { + let sortedReleases = releases.filter((r) => !r.draft); + sortedReleases = _.sortBy(sortedReleases, (release) => new Date(release.published_at)).reverse(); + sortedReleases = _.take(sortedReleases, 5); + + let validAsset: GitHubAsset | undefined = undefined; + let latestRelease: GitHubRelease | undefined = _.first(sortedReleases); + let latestAsset = this.getValidAssetForAny(latestRelease); + + for (const release of sortedReleases) { + let iAsset: GitHubAsset | undefined = undefined; + if (this.hasReleaseMetadata(release)) { + console.log(`Checking release metadata: ${release.name}`); + const metadata = await this.getReleaseMetadata(release); + iAsset = this.getValidAssetFromMetadata(release, clientType, metadata); + } + + // If we didn't find an asset with metadata, try the old way + if (!iAsset) { + iAsset = this.getValidAsset(release, clientType); + } + + if (iAsset) { + validAsset = iAsset; + latestRelease = release; + latestAsset = this.getValidAssetForAny(latestRelease); + break; + } + } + + return { + matchedAsset: validAsset, + release: latestRelease, + latestAsset: latestAsset, + }; + } + + private getLatestRelease(releases: GitHubRelease[]): GitHubRelease { + let sortedReleases = strictFilterBy(releases, (r) => !r.draft); + sortedReleases = _.sortBy(sortedReleases, (release) => new Date(release.published_at)).reverse(); + const firstItem = _.first(sortedReleases); + if (firstItem === undefined) { + throw new Error("No releases found"); + } + + return firstItem; + } + + /** Fetch the json object for the BigWigs metadata json file */ + private async getReleaseMetadata(release: GitHubRelease): Promise { + const metadataAsset = release.assets.find((asset) => asset.name === "release.json"); + if (!metadataAsset) { + throw new Error("No metadata asset found"); + } + + const hasPat = await this.hasPersonalAccessKey(); + const url = hasPat ? metadataAsset.url : metadataAsset.browser_download_url; + + return await this.getWithRateLimit(url, hasPat); + } + + /** Check if any of the assets are the BigWigs metadata json file */ + private hasReleaseMetadata(release: GitHubRelease): boolean { + return release.assets.findIndex((asset) => asset.name === "release.json") !== -1; + } + + /** Return the valid zip file asset for a given client type combined with the BigWigs metadata */ + private getValidAssetFromMetadata( + release: GitHubRelease, + clientType: WowClientType, + releaseMeta: ReleaseMeta, + ): GitHubAsset | undefined { + // map the client type to the flavor we want + const targetFlavor = this.getMetadataTargetFlavor(clientType); + console.log(`Target metadata flavor: ${targetFlavor}`); + + // see if we can find that flavor in the metadata + const targetMetaRelease = releaseMeta.releases.find( + (release) => release.nolib === false && release.metadata.findIndex((m) => m.flavor === targetFlavor) !== -1, + ); + if (!targetMetaRelease) { + console.log(`No matching metadata file found for target`); + return undefined; + } + + console.log(`Target metadata release: ${targetMetaRelease.filename}`); + + // return any matching valid asset with the metadata file name and content type + return release.assets.find((asset) => this.isValidContentType(asset) && asset.name === targetMetaRelease.filename); + } + + /** Return the BigWigs metadata flavor for a given client type */ + private getMetadataTargetFlavor(clientType: WowClientType): MetadataFlavor { + switch (clientType) { + case WowClientType.ClassicBeta: + case WowClientType.Classic: + case WowClientType.ClassicPtr: + return "cata"; + case WowClientType.ClassicEra: + case WowClientType.ClassicEraPtr: + return "classic"; + case WowClientType.Beta: + case WowClientType.Retail: + case WowClientType.RetailPtr: + case WowClientType.RetailXPtr: + return "mainline"; + default: + throw new Error("Unknown client type for metadata"); + } + } + + private getValidAsset(release: GitHubRelease, clientType: WowClientType): GitHubAsset | undefined { + const sortedAssets = _.filter( + release.assets, + (asset) => this.isNotNoLib(asset) && this.isValidContentType(asset) && this.isValidClientType(clientType, asset), + ); + + return _.first(sortedAssets); + } + + private getValidAssetForAny(release: GitHubRelease | undefined): GitHubAsset | undefined { + if (release === undefined) { + return undefined; + } + + const sortedAssets = _.filter(release.assets, (asset) => this.isNotNoLib(asset) && this.isValidContentType(asset)); + return _.first(sortedAssets); + } + + private getSortedAssets(release: GitHubRelease): GitHubAsset[] { + return release.assets.filter((asset) => this.isNotNoLib(asset) && this.isValidContentType(asset)); + } + + private isNotNoLib(asset: GitHubAsset): boolean { + return asset.name.toLowerCase().indexOf("-nolib") === -1; + } + + private isValidContentType(asset: GitHubAsset): boolean { + if ([RELEASE_CONTENT_TYPES.ZIP, RELEASE_CONTENT_TYPES.XZIP].includes(asset.content_type)) { + return true; + } + + if (RELEASE_CONTENT_TYPES.OCTET_STREAM === asset.content_type && asset.browser_download_url.endsWith(".zip")) { + return true; + } + + return false; + } + + private isValidClientType(clientType: WowClientType, asset: GitHubAsset): boolean { + const isClassic = this.isClassicAsset(asset); + const isBurningCrusade = this.isBurningCrusadeAsset(asset); + const isWotlk = this.isWotlk(asset); + const isCataclysm = this.isCataclysm(asset); + + switch (clientType) { + case WowClientType.Retail: + case WowClientType.RetailPtr: + case WowClientType.RetailXPtr: + case WowClientType.Beta: + return !isClassic && !isBurningCrusade && !isWotlk && !isCataclysm; + case WowClientType.ClassicEra: + case WowClientType.ClassicEraPtr: + return isClassic; + case WowClientType.Classic: + case WowClientType.ClassicPtr: + case WowClientType.ClassicBeta: + return isCataclysm; + default: + return false; + } + } + + private isClassicAsset(asset: GitHubAsset): boolean { + return /[-_](classic|vanilla)\.zip$/i.test(asset.name); + } + + private isBurningCrusadeAsset(asset: GitHubAsset): boolean { + return /[-_](bc|bcc|tbc)\.zip$/i.test(asset.name); + } + + private isWotlk(asset: GitHubAsset): boolean { + return /[-_](wrath|wotlkc)\.zip$/i.test(asset.name); + } + + private isCataclysm(asset: GitHubAsset): boolean { + return /[-_](cata)\.zip$/i.test(asset.name); + } + + private getAddonName(addonId: string): string { + return addonId.split("/").filter((str) => !!str)[1]; + } + + private async getReleases(repositoryPath: string): Promise { + const parsed = this.parseRepoPath(repositoryPath); + try { + return await this.getReleasesByParts(parsed); + } catch (e) { + console.error(`Failed to get GitHub releases`, e); + // If some other internal handler already handled this, use that error + if (e instanceof GitHubError) { + throw e; + } + + throw new GitHubFetchReleasesError(repositoryPath, e as Error); + } + } + + private getReleasesByParts(repoParts: GitHubRepoParts): Promise { + const url = `${API_URL}/${repoParts.owner}/${repoParts.repository}/releases`; + return this.getWithRateLimit(url); + } + + private async getRepository(repositoryPath: string): Promise { + const parsed = this.parseRepoPath(repositoryPath); + try { + return await this.getRepositoryByParts(parsed); + } catch (e) { + console.error(`Failed to get GitHub repository`, e); + // If some other internal handler already handled this, use that error + if (e instanceof GitHubError || e instanceof SourceRemovedAddonError) { + throw e; + } + + throw new GitHubFetchRepositoryError(repositoryPath, e as Error); + } + } + + private getRepositoryByParts(repoParts: GitHubRepoParts): Promise { + const url = `${API_URL}/${repoParts.owner}/${repoParts.repository}`; + return this.getWithRateLimit(url); + } + + private handleRateLimitError(response: HttpErrorResponse) { + if (response.status === 403) { + const rateLimitMax = this.getIntHeader(response.headers, HEADER_RATE_LIMIT_MAX); + const rateLimitUsed = this.getIntHeader(response.headers, HEADER_RATE_LIMIT_USED); + const rateLimitRemaining = this.getIntHeader(response.headers, HEADER_RATE_LIMIT_REMAINING); + const rateLimitReset = this.getIntHeader(response.headers, HEADER_RATE_LIMIT_RESET); + + if (rateLimitRemaining === 0) { + throw new GitHubLimitError(rateLimitMax, rateLimitUsed, rateLimitRemaining, rateLimitReset); + } + } + } + + private handleNotFoundError(response: HttpErrorResponse) { + if (response.status === 404) { + throw new SourceRemovedAddonError("", response); + } + } + + private getIntHeader(headers: HttpHeaders, key: string) { + return parseInt(headers.get(key) ?? "", 10); + } + + private async hasPersonalAccessKey(): Promise { + const pat = await this._sensitiveStorageService.getAsync(PREF_GITHUB_PERSONAL_ACCESS_TOKEN); + return typeof pat === "string" && pat.length > 0; + } + + private async getAuthorizationHeader(): Promise<{ [param: string]: string }> { + const personalAccessToken = await this._sensitiveStorageService.getAsync(PREF_GITHUB_PERSONAL_ACCESS_TOKEN); + const headers: { [param: string]: string } = {}; + if (typeof personalAccessToken === "string" && personalAccessToken.length > 0) { + headers.Authorization = `token ${personalAccessToken}`; + } + + return headers; + } + + private async getWithRateLimit(url: URL | string, expectBinary = false): Promise { + try { + const headers = await this.getAuthorizationHeader(); + + if (expectBinary) { + headers.Accept = "application/octet-stream"; + } + + return await firstValueFrom(this._httpClient.get(url.toString(), { headers })); + } catch (e) { + if (e instanceof HttpErrorResponse) { + this.handleRateLimitError(e); + this.handleNotFoundError(e); + } + throw e; + } + } + + private parseRepoPath(repositoryPath: string): GitHubRepoParts { + const regex = /\/?(.*?)\/(.*?)(\/.*|\.git.*)?$/; + const matches = regex.exec(repositoryPath); + if (!matches) { + throw new Error("No matches found"); + } + + return { + owner: matches[1], + repository: matches[2], + }; + } +} diff --git a/WowUp/wowup-electron/src/app/addon-providers/raiderio-provider.ts b/WowUp/wowup-electron/src/app/addon-providers/raiderio-provider.ts new file mode 100644 index 0000000..4669cb8 --- /dev/null +++ b/WowUp/wowup-electron/src/app/addon-providers/raiderio-provider.ts @@ -0,0 +1,101 @@ +import * as _ from "lodash"; +import { v4 as uuidv4 } from "uuid"; + +import { ADDON_PROVIDER_RAIDERIO } from "../../common/constants"; +import { TocService } from "../services/toc/toc.service"; +import { AddonChannelType, AddonFolder, AddonProvider, getGameVersionList } from "wowup-lib-core"; +import { getEnumName } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +export class RaiderIoAddonProvider extends AddonProvider { + private readonly _scanWebsite = "https://raider.io"; + private readonly _scanAddonProvider = "raiderio-client"; + private readonly _scanFolderName = "RaiderIO"; + + public readonly name = ADDON_PROVIDER_RAIDERIO; + public readonly forceIgnore = true; + public readonly allowReinstall = false; + public readonly allowChannelChange = false; + public readonly allowEdit = false; + public enabled = true; + + public constructor(private _tocService: TocService) { + super(); + } + + public scan( + installation: WowInstallation, + addonChannelType: AddonChannelType, + addonFolders: AddonFolder[], + ): Promise { + console.debug("RAIDER IO CLIENT SCAN"); + const raiderIo = _.find(addonFolders, (addonFolder) => this.isRaiderIo(addonFolder)); + if (!raiderIo) { + return Promise.resolve(undefined); + } + + const targetToc = this._tocService.getTocForGameType2(raiderIo.name, raiderIo.tocs, installation.clientType); + const dependencies = _.filter(addonFolders, (addonFolder) => this.isRaiderIoDependant(addonFolder)); + console.debug("RAIDER IO CLIENT FOUND", dependencies); + + const rioAddonFolders = [raiderIo, ...dependencies]; + const installedFolderList = rioAddonFolders.map((addonFolder) => addonFolder.name); + const installedFolders = installedFolderList.join(","); + + for (const rioAddonFolder of rioAddonFolders) { + const subTargetToc = this._tocService.getTocForGameType2( + rioAddonFolder.name, + rioAddonFolder.tocs, + installation.clientType, + ); + if (subTargetToc === undefined) { + throw new Error("subTargetToc was undefined"); + } + + rioAddonFolder.matchingAddon = { + autoUpdateEnabled: false, + autoUpdateNotificationsEnabled: false, + channelType: AddonChannelType.Stable, + clientType: installation.clientType, + id: uuidv4(), + isIgnored: true, + name: targetToc.title ?? "unknown", + author: subTargetToc.author, + downloadUrl: "", + externalId: this.name, + externalUrl: this._scanWebsite, + gameVersion: getGameVersionList(subTargetToc.interface), + installedAt: new Date(), + installedFolders: installedFolders, + installedFolderList: installedFolderList, + installedVersion: subTargetToc.version || targetToc.version, + latestVersion: subTargetToc.version, + providerName: this.name, + thumbnailUrl: "http://cdnassets.raider.io/images/fb_app_image.jpg?2019-11-18", + updatedAt: new Date(), + summary: subTargetToc.notes, + downloadCount: 0, + screenshotUrls: [], + releasedAt: new Date(), + isLoadOnDemand: subTargetToc.loadOnDemand === "1", + externalChannel: getEnumName(AddonChannelType, AddonChannelType.Stable), + installationId: installation.id, + }; + } + + return Promise.resolve(undefined); + } + + private isRaiderIo(addonFolder: AddonFolder) { + return ( + addonFolder.name === this._scanFolderName && + addonFolder.tocs.some((toc) => toc.website === this._scanWebsite && toc.addonProvider === this._scanAddonProvider) + ); + } + + private isRaiderIoDependant(addonFolder: AddonFolder) { + return addonFolder.tocs.some( + (toc) => toc.dependencies !== undefined && toc.dependencies.indexOf(this._scanFolderName) !== -1, + ); + } +} diff --git a/WowUp/wowup-electron/src/app/addon-providers/wago-addon-provider.ts b/WowUp/wowup-electron/src/app/addon-providers/wago-addon-provider.ts new file mode 100644 index 0000000..73b377c --- /dev/null +++ b/WowUp/wowup-electron/src/app/addon-providers/wago-addon-provider.ts @@ -0,0 +1,842 @@ +import { BehaviorSubject, firstValueFrom, from, Observable, of } from "rxjs"; +import { catchError, filter, first, tap, timeout } from "rxjs/operators"; +import { v4 as uuidv4 } from "uuid"; +import _ from "lodash"; + +import { ADDON_PROVIDER_WAGO, IPC_POWER_MONITOR_RESUME, PREF_WAGO_ACCESS_KEY } from "../../common/constants"; +import { AppConfig } from "../../environments/environment"; +import { ElectronService } from "../services"; +import { CachingService } from "../services/caching/caching-service"; +import { CircuitBreakerWrapper, NetworkService } from "../services/network/network.service"; +import { TocService } from "../services/toc/toc.service"; +import { WarcraftService } from "../services/warcraft/warcraft.service"; +import { getEnumName, getGameVersion, getWowClientGroupForType } from "wowup-lib-core"; +import { convertMarkdown } from "wowup-lib-core"; +import { HttpErrorResponse } from "@angular/common/http"; +import { UiMessageService } from "../services/ui-message/ui-message.service"; +import { SensitiveStorageService } from "../services/storage/sensitive-storage.service"; +import { + Addon, + AddonChannelType, + AddonFolder, + AddonProvider, + AddonScanResult, + AddonSearchResult, + AddonSearchResultFile, + AdPageOptions, + DownloadAuth, + GetAllResult, + WowClientGroup, + WowClientType, +} from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; +import { SourceRemovedAddonError } from "wowup-lib-core"; + +declare type WagoGameVersion = "retail" | "classic" | "bc" | "wotlk" | "cata"; +declare type WagoStability = "stable" | "beta" | "alpha"; + +interface WagoFingerprintAddon { + name: string; // the folder name + hash: string; // hash fingerprint of the folder + cf?: string; // curseforge toc id + wowi?: string; // wow interface toc id + wago?: string; // wago interface toc id +} + +interface WagoFingerprintRequest { + game_version: WagoGameVersion; + addons: WagoFingerprintAddon[]; +} + +interface WagoSearchResponse { + data: WagoSearchResponseItem[]; +} + +interface WagoSearchResponseItem { + display_name: string; + id: string; + releases: WagoReleasesResponse; + summary: string; + thumbnail_image: string; + authors: string[]; + download_count: number; + website_url: string; +} + +interface WagoSearchResponseRelease { + download_link: string; + label: string; + created_at: string; + logical_timestamp: number; // download link expiration time +} + +interface WagoReleasesResponse { + [key: string]: T | undefined; + alpha?: T; + beta?: T; + stable?: T; +} + +interface WagoAddon { + authors: string[]; + description: string; + display_name: string; + download_count: number; + gallery: string[]; + id: string; + recent_release?: WagoReleasesResponse; // probably a typo on the wago side, shows up in details route + recent_releases: WagoReleasesResponse; + slug: string; + summary: string; + thumbnail_image: string; + website: string; + website_url: string; +} + +interface WagoRelease { + label: string; + supported_retail_patch: string; + supported_classic_patch: string; + supported_bc_patch: string; + changelog: string; + stability: WagoStability; + download_link: string; + created_at: string; +} + +interface WagoScanRelease { + id: string; + created_at: string; + label: string; + patch: string; + link?: string; +} + +interface WagoScanReleaseSortable extends WagoScanRelease { + stability: string; + addonChannelType: AddonChannelType; +} + +interface WagoScanModule { + hash?: string; +} + +interface WagoScanAddon { + id: string; + name: string; + thumbnail: string; + website_url: string; + authors?: string[]; + cf?: string; + wago?: string; + wowi?: string; + matched_release?: WagoScanRelease; + modules?: { [folder: string]: WagoScanModule }; + recent_releases: { + stable?: WagoScanRelease; + beta?: WagoScanRelease; + alpha?: WagoScanRelease; + }; +} + +interface WagoScanResponse { + addons: WagoScanAddon[]; +} + +interface WagoPopularAddonsResponse { + data: WagoSearchResponseItem[]; +} + +interface WagoRecentsRequest { + game_version: WagoGameVersion; + addons: string[]; +} + +interface WagoRecentsResponse { + addons: { [addonId: string]: WagoScanAddon }; +} + +const WAGO_BASE_URL = "https://addons.wago.io/api/external"; +const WAGO_AD_URL = "https://addons.wago.io/wowup_ad"; +const WAGO_AD_REFERRER = "https://wago.io"; +const WAGO_AD_USER_AGENT = + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"; // the ad requires a normal looking user agent +const WAGO_AD_PRELOAD = "preload/wago.js"; +const WAGO_SEARCH_CACHE_TIME_SEC = 60; +const WAGO_DETAILS_CACHE_TIME_SEC = 60; +const WAGO_FEATURED_ADDONS_CACHE_TIME_SEC = 60; +const WAGO_RELOAD_PERIOD_SEC = 10 * 60; + +export class WagoAddonProvider extends AddonProvider { + private readonly _circuitBreaker: CircuitBreakerWrapper; + + private _apiTokenSrc = new BehaviorSubject(""); + private _wagoSecret = ""; + private _getWagoTokenTimer: ReturnType; + + // This is our internal http queue, prevents duplicated requests for some routes + private _requestQueue: Map> = new Map(); + + public readonly name = ADDON_PROVIDER_WAGO; + public readonly forceIgnore = false; + public enabled = true; + public authRequired = true; + public adRequired = false; + public allowEdit = true; + public allowReinstall = true; + public allowChannelChange = true; + + public constructor( + private _electronService: ElectronService, + private _cachingService: CachingService, + private _warcraftService: WarcraftService, + private _tocService: TocService, + private _uiMessageService: UiMessageService, + private _sensitiveStorageService: SensitiveStorageService, + _networkService: NetworkService, + ) { + super(); + + this._circuitBreaker = _networkService.getCircuitBreaker( + `${this.name}_main`, + AppConfig.defaultHttpResetTimeoutMs, + AppConfig.wagoHttpTimeoutMs, + ); + + this._electronService.on("wago-token-received", this.onWagoTokenReceived); + + // Watch for the change of the wago secret in the store + this._sensitiveStorageService.change$ + .pipe(filter((change) => change.key == PREF_WAGO_ACCESS_KEY)) + .subscribe((change) => { + console.log("[wago] wago secret set", change); + if (this._getWagoTokenTimer) clearTimeout(this._getWagoTokenTimer); + if (this.isValidToken(change.value as string)) { + this._wagoSecret = change.value; + this._circuitBreaker.close(); + } else { + this._wagoSecret = ""; + void this.getWagoToken(); + } + }); + + // Initial load of the wago secret + from(this._sensitiveStorageService.getAsync(PREF_WAGO_ACCESS_KEY)) + .pipe( + first(), + tap((accessKey) => { + if (this._getWagoTokenTimer) clearTimeout(this._getWagoTokenTimer); + const validToken = this.isValidToken(accessKey); + if (validToken) { + this._wagoSecret = accessKey; + console.debug("[wago] secret key set"); + } + else { + void this.getWagoToken(); + } + }), + catchError((e) => { + console.error("[wago] failed to load secret key", e); + return of(undefined); + }), + ) + .subscribe(); + + // clear the token on resume so we wait for a new one + this._electronService.powerMonitor$.pipe(filter((state) => state === IPC_POWER_MONITOR_RESUME)).subscribe(() => { + this._apiTokenSrc.next(""); + }); + } + + public isValidAddonId(addonId: string): boolean { + return typeof addonId === "string" && addonId.length >= 8 && addonId.length <= 10; + } + + public override async scan( + installation: WowInstallation, + addonChannelType: AddonChannelType, + addonFolders: AddonFolder[], + ): Promise { + if (!_.some(addonFolders)) { + return; + } + + const gameVersion = this.getGameVersion(installation.clientType); + const scanResults = addonFolders + .map((af) => af.wowUpScanResults) + .filter((sr): sr is AddonScanResult => sr !== undefined); + + const request: WagoFingerprintRequest = { + game_version: gameVersion, + addons: [], + }; + + scanResults.forEach((res) => { + const addonFolder = addonFolders.find((af) => af.name === res.folderName); + if (addonFolder === undefined) { + console.warn("[wago]: addon folder not found: " + res.folderName); + return; + } + + const toc = this._tocService.getTocForGameType2(addonFolder.name, addonFolder.tocs, installation.clientType); + if (toc === undefined) { + console.warn("[wago]: getTocForGameType2 returned undefined, " + addonFolder.name); + return; + } + + const waddon: WagoFingerprintAddon = { + name: res.folderName, + hash: res.fingerprint, + }; + + if (toc.wagoAddonId) { + waddon.wago = toc.wagoAddonId; + } + + request.addons.push(waddon); + }); + + console.debug(`[wago] scan`, request); + console.debug(JSON.stringify(request)); + + const matchResult = await this.sendMatchesRequest(request); + console.debug(`[wago] matchResult`, matchResult); + + const scanResultMap: { [folder: string]: WagoScanAddon } = {}; + + for (const scanResult of scanResults) { + try { + const fingerprintMatches = matchResult.addons.filter((addon) => { + // Sometimes the API can return an array with null elements + if (typeof addon !== "object" || addon === null) { + return false; + } + + const mods = Object.values(addon.modules ?? {}).filter((mod) => typeof mod.hash === "string"); + return mods.findIndex((mod) => mod.hash === scanResult.fingerprint) !== -1; + }); + + if (fingerprintMatches.length > 0) { + if (fingerprintMatches.length > 1) { + console.warn(`[wago] found multiple fingerprintMatches: ${scanResult.folderName}`); + } + + scanResultMap[scanResult.folderName] = fingerprintMatches[0]; + } + // console.debug(`[wago] fingerprintMatches`, fingerprintMatches); + } catch (e) { + console.error(`fingerprint scan failed`, matchResult); + throw e; + } + } + + for (const addonFolder of addonFolders) { + const scanResult = scanResults.find((sr) => sr.folderName === addonFolder.name); + if (scanResult === undefined || !scanResultMap[scanResult.folderName]) { + continue; + } + + try { + const match = scanResultMap[scanResult.folderName]; + const newAddon = this.toAddon(installation, addonChannelType, match); + + addonFolder.matchingAddon = newAddon; + } catch (e) { + console.error(`[wago] scan result`, scanResult); + console.error(`[wago] scan error`, e); + } + } + + console.debug(`[wago] delta`, addonFolders); + } + + public async getFeaturedAddons(installation: WowInstallation): Promise { + const url = new URL(`${WAGO_BASE_URL}/addons/popular`); + url.searchParams.set("game_version", this.getGameVersion(installation.clientType)); + + await firstValueFrom(this.ensureToken()); + + const response = await this.sendRequest(() => + this._cachingService.transaction( + `${installation.id}|${url.toString()}`, + () => this._circuitBreaker.getJson(url, this.getRequestHeaders()), + WAGO_FEATURED_ADDONS_CACHE_TIME_SEC, + ), + ); + + console.debug(`[wago] getFeaturedAddons`, response); + + const searchResults = response.data?.map((item) => this.toSearchResult(item)) ?? []; + return searchResults; + } + + public async searchByQuery( + query: string, + installation: WowInstallation, + channelType?: AddonChannelType, + ): Promise { + try { + await firstValueFrom(this.ensureToken()); + } catch (e) { + console.error("[wago]", e); + return []; + } + + const url = new URL(`${WAGO_BASE_URL}/addons/_search`); + url.searchParams.set("query", query); + url.searchParams.set("game_version", this.getGameVersion(installation.clientType)); + url.searchParams.set("stability", this.getStability(channelType)); + + const response = await this.sendRequest(() => + this._cachingService.transaction( + `${installation.id}|${query}|${url.toString()}`, + () => this._circuitBreaker.getJson(url, this.getRequestHeaders()), + WAGO_SEARCH_CACHE_TIME_SEC, + ), + ); + + const searchResults = response.data?.map((item) => this.toSearchResult(item)) ?? []; + + console.debug(`[wago] searchByQuery`, response, searchResults); + + return searchResults; + } + + public override async getById(addonId: string): Promise { + const response = await this.getAddonById(addonId); + return this.toSearchResultFromDetails(response); + } + + public async getDescription(installation: WowInstallation, externalId: string): Promise { + try { + const response = await this.getAddonById(externalId); + return convertMarkdown(response?.description ?? ""); + } catch (e) { + console.error(`[wago] failed to get description`, e); + return ""; + } + } + + public async getChangelog(installation: WowInstallation, externalId: string): Promise { + console.debug("[wago] getChangelog"); + try { + const response = await this.getAddonById(externalId); + console.debug("[wago] getChangelog", response); + + let release: WagoRelease | undefined = response.recent_release?.stable; + switch (installation.defaultAddonChannelType) { + case AddonChannelType.Alpha: + release = response.recent_release?.alpha || release; + break; + case AddonChannelType.Beta: + release = response.recent_release?.beta || release; + break; + default: + break; + } + + // if there is not a matching apparent release, return empty + return release ? convertMarkdown(release.changelog) : ""; + } catch (e) { + console.error("[wago] Failed to get changelog", e); + return ""; + } + } + + public getAdPageParams(): AdPageOptions { + return { + pageUrl: WAGO_AD_URL, + referrer: WAGO_AD_REFERRER, + userAgent: WAGO_AD_USER_AGENT, + preloadFilePath: WAGO_AD_PRELOAD, + }; + } + + private async sendRequest(action: () => Promise): Promise { + try { + return await action.call(this, null); + } catch (err) { + if (err instanceof HttpErrorResponse) { + console.error("HttpErr", err); + this._uiMessageService.sendMessage("ad-frame-reload"); + } + throw err; + } + } + + // used when checking for new addon updates + public override async getAll(installation: WowInstallation, addonIds: string[]): Promise { + await firstValueFrom(this.ensureToken()); + + const url = new URL(`${WAGO_BASE_URL}/addons/_recents`).toString(); + const request: WagoRecentsRequest = { + game_version: this.getGameVersion(installation.clientType), + addons: [...addonIds], + }; + + const response = await this.sendRequest(() => + this._cachingService.transaction( + `${installation.id}|${url.toString()}`, + () => this._circuitBreaker.postJson(url, request, this.getRequestHeaders()), + WAGO_DETAILS_CACHE_TIME_SEC, + ), + ); + + const searchResults: AddonSearchResult[] = []; + for (const [, addon] of Object.entries(response.addons)) { + searchResults.push(this.toSearchResultFromScan(addon)); + } + + const missingAddonIds = _.filter( + addonIds, + (addonId) => _.find(searchResults, (sr) => sr.externalId === addonId) === undefined, + ); + + const deletedErrors = _.map(missingAddonIds, (addonId) => new SourceRemovedAddonError(addonId, undefined)); + + return Promise.resolve({ + errors: [...deletedErrors], + searchResults, + }); + } + + public getDownloadAuth(): Promise { + return Promise.resolve({ + queryParams: { + token: this.getToken(), + }, + }); + } + + private getToken(): string { + return this.isValidToken(this._wagoSecret) ? this._wagoSecret : this._apiTokenSrc.value; + } + + private async getAddonById(addonId: string): Promise { + const url = new URL(`${WAGO_BASE_URL}/addons/${addonId}`).toString(); + + if (this._requestQueue.has(url)) { + return this._requestQueue.get(url); + } + + await firstValueFrom(this.ensureToken()); + + const prom = this.sendRequest(() => + this._cachingService + .transaction( + url, + () => this._circuitBreaker.getJson(url, this.getRequestHeaders()), + WAGO_DETAILS_CACHE_TIME_SEC, + ) + .finally(() => { + this._requestQueue.delete(url); + }), + ); + + this._requestQueue.set(url, prom); + + return await prom; + } + + private async sendMatchesRequest(request: WagoFingerprintRequest) { + const url = new URL(`${WAGO_BASE_URL}/addons/_match`); + await firstValueFrom(this.ensureToken()); + + return await this.sendRequest(() => + this._circuitBreaker.postJson(url, request, this.getRequestHeaders()), + ); + } + + private toSearchResultFromScan(item: WagoScanAddon): AddonSearchResult { + const releaseObj = item.recent_releases; + const releaseTypes = Object.keys(releaseObj) as WagoStability[]; + const searchResultFiles: AddonSearchResultFile[] = []; + for (const type of releaseTypes) { + const channel = releaseObj[type]; + if (channel !== undefined) { + searchResultFiles.push(this.toSearchResultFile(channel, type)); + } + } + + return { + author: item.authors?.join(", ") ?? "", + externalId: item.id, + externalUrl: item.website_url, + name: item.name, + providerName: this.name, + thumbnailUrl: item.thumbnail, + downloadCount: 0, + files: searchResultFiles, + releasedAt: new Date(), + summary: "", + }; + } + + private toSearchResultFromDetails(item: WagoAddon): AddonSearchResult { + const releaseObj = item.recent_releases ?? item.recent_release; + const releaseTypes = Object.keys(releaseObj) as WagoStability[]; + const searchResultFiles: AddonSearchResultFile[] = []; + for (const type of releaseTypes) { + const channel = releaseObj[type]; + if (channel !== undefined) { + searchResultFiles.push(this.toSearchResultFile(channel, type)); + } + } + + return { + author: item.authors.join(", "), + externalId: item.id, + externalUrl: item.website_url, + name: item.display_name, + providerName: this.name, + thumbnailUrl: item.thumbnail_image, + downloadCount: item.download_count, + files: searchResultFiles, + releasedAt: new Date(), + summary: item.summary, + }; + } + + private toSearchResult(item: WagoSearchResponseItem): AddonSearchResult { + const releaseTypes = Object.keys(item.releases); + const searchResultFiles: AddonSearchResultFile[] = []; + for (const type of releaseTypes) { + const release = item.releases[type]; + if (release !== undefined) { + searchResultFiles.push(this.toSearchResultFile(release, type as WagoStability)); + } + } + + return { + author: item.authors.join(", "), + externalId: item.id, + externalUrl: item.website_url, + name: item.display_name, + providerName: this.name, + thumbnailUrl: item.thumbnail_image, + downloadCount: item.download_count, + files: searchResultFiles, + releasedAt: new Date(), + summary: item.summary, + }; + } + + private toSearchResultFile( + release: WagoSearchResponseRelease | WagoRelease | WagoScanRelease, + stability: WagoStability, + ): AddonSearchResultFile { + if (release === undefined) { + throw new Error("toSearchResultFile failed, undefined release"); + } + + return { + channelType: this.getAddonChannelType(stability), + downloadUrl: (release as any).download_link || (release as any).link, + folders: [], + gameVersion: "", + releaseDate: new Date(release.created_at), + version: release.label, + dependencies: [], + }; + } + + private toAddon( + installation: WowInstallation, + addonChannelType: AddonChannelType, + wagoScanAddon: WagoScanAddon, + ): Addon { + // Grab a ref to the recent release matching the id of the matched release, the objects do not appear to match. + // const recentRelease = Object.values(wagoScanAddon.recent_releases).find( + // (rr) => rr.id === wagoScanAddon.matched_release.id + // ); + + const authors = wagoScanAddon?.authors?.join(", ") ?? ""; + const name = wagoScanAddon?.name ?? ""; + const externalUrl = wagoScanAddon?.website_url ?? ""; + const externalId = wagoScanAddon?.id ?? ""; + const gameVersion = getGameVersion(wagoScanAddon?.matched_release?.patch); + const thumbnailUrl = wagoScanAddon?.thumbnail ?? ""; + const releasedAt = wagoScanAddon?.matched_release?.created_at + ? new Date(wagoScanAddon?.matched_release?.created_at) + : undefined; + + const installedVersion = wagoScanAddon?.matched_release?.label ?? ""; + const installedExternalReleaseId = wagoScanAddon?.matched_release?.id ?? ""; + const installedFolders: string[] = []; + + for (const [key, val] of Object.entries(wagoScanAddon.modules ?? {})) { + if (typeof val.hash !== "string") { + continue; + } + + installedFolders.push(key); + } + + // Sort the releases by release date, then find the first one that has a valid channel type + const releaseList: WagoScanReleaseSortable[] = this.getSortedReleaseList(wagoScanAddon); + const validVersion = releaseList.find((rel) => rel.addonChannelType <= addonChannelType); + if (validVersion === undefined) { + throw new Error("toAddon failed valid version not found"); + } + + const latestVersion = validVersion.label; + const externalLatestReleaseId = validVersion.id; + const externalChannel = getEnumName(AddonChannelType, validVersion.addonChannelType); + const downloadUrl = validVersion.link ?? ""; + + return { + id: uuidv4(), + author: authors, + name, + channelType: validVersion.addonChannelType, + autoUpdateEnabled: false, + autoUpdateNotificationsEnabled: false, + clientType: installation.clientType, + downloadUrl, + externalUrl, + externalId, + gameVersion: [gameVersion], + installedAt: new Date(), + installedFolders: installedFolders.join(","), + installedFolderList: installedFolders, + installedVersion, + installedExternalReleaseId, + isIgnored: false, + latestVersion, + providerName: this.name, + thumbnailUrl, + isLoadOnDemand: false, + releasedAt, + externalChannel, + externalLatestReleaseId, + installationId: installation.id, + }; + } + + /** Convert a stability map of addons into a sorted list of addons */ + private getSortedReleaseList(wagoScanAddon: WagoScanAddon): WagoScanReleaseSortable[] { + let releaseList: WagoScanReleaseSortable[] = []; + for (const [key, value] of Object.entries(wagoScanAddon.recent_releases)) { + releaseList.push({ ...value, stability: key, addonChannelType: this.getAddonChannelType(key as WagoStability) }); + } + + releaseList = _.sortBy(releaseList, (rel) => new Date(rel.created_at).getTime()).reverse(); + + return releaseList; + } + + private getAddonChannelType(stability: WagoStability): AddonChannelType { + switch (stability) { + case "alpha": + return AddonChannelType.Alpha; + case "beta": + return AddonChannelType.Beta; + case "stable": + default: + return AddonChannelType.Stable; + } + } + + // Get the wago friendly name for our addon channel + private getStability(channelType: AddonChannelType | undefined): WagoStability { + switch (channelType) { + case AddonChannelType.Alpha: + return "alpha"; + case AddonChannelType.Beta: + return "beta"; + case AddonChannelType.Stable: + default: + return "stable"; + } + } + + // The wago name for the client type + private getGameVersion(clientType: WowClientType): WagoGameVersion { + const clientGroup = getWowClientGroupForType(clientType); + switch (clientGroup) { + case WowClientGroup.BurningCrusade: + return "bc"; + case WowClientGroup.Classic: + return "classic"; + case WowClientGroup.Retail: + return "retail"; + case WowClientGroup.WOTLK: + return "wotlk"; + case WowClientGroup.Cata: + return "cata"; + default: + throw new Error(`[wago] Un-handled client type: ${clientType}`); + } + } + + private getWagoToken = async () => { + const url = new URL(`${WAGO_AD_URL}`); + const response = await this._cachingService.transaction( + `${url.toString()}`, + () => this._circuitBreaker.getText(url, undefined, { "User-Agent": WAGO_AD_USER_AGENT }), + WAGO_FEATURED_ADDONS_CACHE_TIME_SEC + ); + + console.debug(`[wago] getWagoToken`, response); + + if (response) { + const token = response.match(/provideApiKey\(atob\('(.*?)'/); + if (token && token.length > 1) + { + console.debug(`[wago] getWagoToken`, token[1]); + this.onWagoTokenReceived(null, Buffer.from(token[1], 'base64').toString()); + } + } + + this._getWagoTokenTimer = setTimeout(() => { void this.getWagoToken() }, WAGO_RELOAD_PERIOD_SEC * 1000); + }; + + private onWagoTokenReceived = (evt, token: string) => { + console.log(`[wago] onWagoTokenReceived: ${token.length}`); + if (!this.isValidToken(token)) { + console.warn("[wagp] malformed token detected"); + return; + } + + // Whenever we get a new token, manually re-enable the circuit breaker + this._circuitBreaker.close(); + + this._apiTokenSrc.next(token); + }; + + private getRequestHeaders(): { + [header: string]: string; + } { + return { + Authorization: `Bearer ${this.getToken()}`, + }; + } + + private isValidToken(token: string) { + return typeof token === "string" && token.length > 20; + } + + // + private ensureToken(timeoutMs = 10000): Observable { + if (this._circuitBreaker.isOpen()) { + throw new Error("[wago] circuit breaker is open"); + } + + // if we have a secret set, use that + if (this.isValidToken(this._wagoSecret)) { + console.log("[wago] using secret", this._wagoSecret.length); + return of(this._wagoSecret); + } + + // wait for a public token from the ad frame + return this._apiTokenSrc.pipe( + timeout(timeoutMs), + first((token) => this.isValidToken(token)), + tap((token) => console.log(`[wago] token ready`, token.length)), + catchError(() => { + console.error("[wago] no token received after timeout"); + return of(""); + }), + ); + } +} diff --git a/WowUp/wowup-electron/src/app/addon-providers/wowup-companion-addon-provider.ts b/WowUp/wowup-electron/src/app/addon-providers/wowup-companion-addon-provider.ts new file mode 100644 index 0000000..f656aae --- /dev/null +++ b/WowUp/wowup-electron/src/app/addon-providers/wowup-companion-addon-provider.ts @@ -0,0 +1,84 @@ +import * as _ from "lodash"; +import { v4 as uuidv4 } from "uuid"; + +import { ADDON_PROVIDER_WOWUP_COMPANION, WOWUP_DATA_ADDON_FOLDER_NAME } from "../../common/constants"; +import { FileService } from "../services/files/file.service"; +import { TocService } from "../services/toc/toc.service"; +import { getEnumName, getGameVersionList } from "wowup-lib-core"; +import { AddonChannelType, AddonFolder, AddonProvider } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +export const X_WOWUP_ADDON_PROVIDER = "wowup-app"; +export const X_WEBSITE = "https://wowup.io"; + +export class WowUpCompanionAddonProvider extends AddonProvider { + public readonly name = ADDON_PROVIDER_WOWUP_COMPANION; + public readonly forceIgnore = true; + public readonly allowReinstall = false; + public readonly allowChannelChange = false; + public readonly allowEdit = false; + public enabled = true; + + public constructor( + private _fileService: FileService, + private _tocService: TocService, + ) { + super(); + } + + public async scan( + installation: WowInstallation, + addonChannelType: AddonChannelType, + addonFolders: AddonFolder[], + ): Promise { + const companion = _.find(addonFolders, (addonFolder) => this.isWowUpCompanion(addonFolder)); + if (!companion) { + return; + } + + const targetToc = this._tocService.getTocForGameType2(companion.name, companion.tocs, installation.clientType); + if (targetToc === undefined) { + throw new Error("target toc was not found"); + } + + const lastUpdatedAt = await this._fileService.getLatestDirUpdateTime(companion.path); + + companion.matchingAddon = { + autoUpdateEnabled: false, + autoUpdateNotificationsEnabled: false, + channelType: AddonChannelType.Stable, + clientType: installation.clientType, + id: uuidv4(), + isIgnored: true, + name: targetToc.title ?? "unknown", + author: targetToc.author ?? "unknown", + downloadUrl: "", + externalId: this.name, + externalUrl: X_WEBSITE, + gameVersion: getGameVersionList(targetToc.interface), + installedAt: new Date(lastUpdatedAt), + installedFolders: companion.name, + installedFolderList: [companion.name], + installedVersion: targetToc.version, + latestVersion: targetToc.version, + providerName: this.name, + thumbnailUrl: "https://avatars.githubusercontent.com/u/74023737?s=400&v=4", + updatedAt: new Date(lastUpdatedAt), + summary: targetToc.notes, + downloadCount: 0, + screenshotUrls: [], + releasedAt: new Date(lastUpdatedAt), + isLoadOnDemand: targetToc.loadOnDemand === "1", + externalChannel: getEnumName(AddonChannelType, AddonChannelType.Stable), + installationId: installation.id, + }; + } + + private isWowUpCompanion(addonFolder: AddonFolder) { + return ( + addonFolder.name === WOWUP_DATA_ADDON_FOLDER_NAME && + addonFolder.tocs?.some((toc) => toc.website === X_WEBSITE) && + addonFolder.tocs?.some((toc) => toc.addonProvider === X_WOWUP_ADDON_PROVIDER) + ); + } +} diff --git a/WowUp/wowup-electron/src/app/addon-providers/zip-provider.ts b/WowUp/wowup-electron/src/app/addon-providers/zip-provider.ts new file mode 100644 index 0000000..02e89f3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/addon-providers/zip-provider.ts @@ -0,0 +1,188 @@ +import * as _ from "lodash"; +import { basename, join } from "path"; + +import { HttpClient } from "@angular/common/http"; + +import { ADDON_PROVIDER_ZIP } from "../../common/constants"; +import { FileService } from "../services/files/file.service"; +import { TocService } from "../services/toc/toc.service"; +import { WarcraftService } from "../services/warcraft/warcraft.service"; +import { + Addon, + AddonChannelType, + AddonProvider, + AddonSearchResult, + AddonSearchResultFile, + SearchByUrlResult, + Toc, +} from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +const VALID_ZIP_CONTENT_TYPES = ["application/zip", "application/x-zip-compressed", "application/octet-stream"]; + +export class ZipAddonProvider extends AddonProvider { + public readonly name = ADDON_PROVIDER_ZIP; + public readonly forceIgnore = true; + public readonly allowReinstall = false; + public readonly allowChannelChange = false; + public readonly allowEdit = false; + public readonly allowReScan = false; + public readonly canShowChangelog = false; + public enabled = true; + + public constructor( + private _httpClient: HttpClient, + private _fileService: FileService, + private _tocService: TocService, + private _warcraftService: WarcraftService + ) { + super(); + } + + public isValidAddonUri(addonUri: URL): boolean { + return addonUri.pathname?.toLowerCase()?.endsWith(".zip"); + } + + public isValidAddonId(): boolean { + return false; + } + + public async getDescription(installation: WowInstallation, externalId: string, addon?: Addon): Promise { + if (!addon) { + return ""; + } + + const folders = addon?.installedFolderList ?? []; + const clientAddonFolderPath = this._warcraftService.getAddonFolderPath(installation); + const allTocs = await this.getAllTocs(clientAddonFolderPath, folders); + + const primaryToc = this.getPrimaryToc(allTocs); + if (!primaryToc) { + console.warn("No primary toc found"); + return ""; + } + + const lines = _.map(Object.entries(primaryToc), ([key, value]) => { + if (typeof value === "string" && !!value) { + return `${key}: ${value}`; + } + return ""; + }) + .filter((str) => !!str) + .map((str) => `

${str}

`) + .join(""); + + return lines; + } + + private getPrimaryToc(tocs: Toc[]) { + return _.maxBy(tocs, (toc) => Object.values(toc).join("").length); + } + + private async getAllTocs(baseDir: string, installedFolders: string[]) { + const tocs: Toc[] = []; + + for (const dir of installedFolders) { + const dirPath = join(baseDir, dir); + + const tocFiles = await this._fileService.listFiles(dirPath, "*.toc"); + const tocFile = _.first(tocFiles); + if (!tocFile) { + continue; + } + + const tocPath = join(dirPath, tocFile); + const toc = await this._tocService.parse(tocPath); + if (!toc.interface) { + continue; + } + + tocs.push(toc); + } + + return tocs; + } + + public async searchByUrl(addonUri: URL): Promise { + if (!addonUri.pathname.toLowerCase().endsWith(".zip")) { + throw new Error(`Invalid zip URL ${addonUri.toString()}`); + } + + await this.validateUrlContentType(addonUri); + + const fileName = _.last(addonUri.pathname.split("/")) ?? "unknown"; + const fileNameNoExt = basename(fileName, ".zip"); + + const potentialFile: AddonSearchResultFile = { + channelType: AddonChannelType.Stable, + downloadUrl: addonUri.toString(), + folders: [fileNameNoExt], + gameVersion: "", + version: fileNameNoExt, + releaseDate: new Date(), + changelog: "", + }; + + const potentialAddon: AddonSearchResult = { + author: addonUri.hostname, + downloadCount: 1, + externalId: addonUri.toString(), + externalUrl: addonUri.origin, + name: fileName, + providerName: this.name, + thumbnailUrl: "", + files: [potentialFile], + }; + + return { + errors: [], + searchResult: potentialAddon, + }; + } + + public override async getById(addonId: string): Promise { + const addonUri = new URL(addonId); + + if (!addonUri.pathname.toLowerCase().endsWith(".zip")) { + throw new Error(`Invalid zip URL ${addonUri.toString()}`); + } + + await this.validateUrlContentType(addonUri); + + const fileName = _.last(addonUri.pathname.split("/")) ?? "unknown"; + + const searchResultFile: AddonSearchResultFile = { + channelType: AddonChannelType.Stable, + downloadUrl: addonUri.toString(), + folders: [], + gameVersion: "", + version: fileName, + releaseDate: new Date(), + }; + + const potentialAddon: AddonSearchResult = { + author: "", + downloadCount: 1, + externalId: addonUri.toString(), + externalUrl: addonUri.origin, + name: fileName, + providerName: this.name, + thumbnailUrl: "", + files: [searchResultFile], + }; + + return potentialAddon; + } + + private async validateUrlContentType(addonUri: URL) { + const response = await this.getUrlInfo(addonUri); + const contentType = response.headers.get("content-type") ?? ""; + if (!VALID_ZIP_CONTENT_TYPES.includes(contentType)) { + throw new Error(`Invalid zip content type ${contentType}`); + } + } + + private getUrlInfo(addonUri: URL) { + return this._httpClient.head(addonUri.toString(), { observe: "response", responseType: "text" }).toPromise(); + } +} diff --git a/WowUp/wowup-electron/src/app/app-routing.module.ts b/WowUp/wowup-electron/src/app/app-routing.module.ts new file mode 100644 index 0000000..b6a9767 --- /dev/null +++ b/WowUp/wowup-electron/src/app/app-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { HomeRoutingModule } from "./pages/home/home-routing.module"; + +const routes: Routes = [ + { + path: "", + redirectTo: "home", + pathMatch: "full", + }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes), HomeRoutingModule], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/WowUp/wowup-electron/src/app/app.component.html b/WowUp/wowup-electron/src/app/app.component.html new file mode 100644 index 0000000..db836ae --- /dev/null +++ b/WowUp/wowup-electron/src/app/app.component.html @@ -0,0 +1,23 @@ +
+ +
+ + + +
+ +
+ +
+
+ +
+ + diff --git a/WowUp/wowup-electron/src/app/app.component.scss b/WowUp/wowup-electron/src/app/app.component.scss new file mode 100644 index 0000000..a9ab41b --- /dev/null +++ b/WowUp/wowup-electron/src/app/app.component.scss @@ -0,0 +1,46 @@ +.app { + height: 100vh; + overflow: hidden; + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto 1fr auto; + grid-template-areas: + "title title" + "tabs main" + "tabs footer"; + + .gtitle { + grid-area: title; + } + + .gtabs { + grid-area: tabs; + } + + .gmain { + grid-area: main; + } + + .gfooter { + grid-area: footer; + } + + .content { + flex-grow: 1; + overflow: auto; + display: flex; + flex-direction: row; + + // border-radius: 4px; + + .tabs { + flex-shrink: 0; + } + .main { + flex-grow: 1; + } + .extra { + flex-shrink: 0; + } + } +} diff --git a/WowUp/wowup-electron/src/app/app.component.spec.ts b/WowUp/wowup-electron/src/app/app.component.spec.ts new file mode 100644 index 0000000..fea9650 --- /dev/null +++ b/WowUp/wowup-electron/src/app/app.component.spec.ts @@ -0,0 +1,139 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { BehaviorSubject, Observable, Subject } from "rxjs"; + +import { OverlayContainer, OverlayModule } from "@angular/cdk/overlay"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TestBed } from "@angular/core/testing"; +import { MatDialog } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { RouterTestingModule } from "@angular/router/testing"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { AppComponent } from "./app.component"; +import { httpLoaderFactory } from "./app.module"; +import { AnimatedLogoComponent } from "./components/common/animated-logo/animated-logo.component"; +import { MatModule } from "./modules/mat-module"; +import { PreferenceChange } from "./models/wowup/preference-change"; +import { ElectronService } from "./services"; +import { AddonService } from "./services/addons/addon.service"; +import { AnalyticsService } from "./services/analytics/analytics.service"; +import { FileService } from "./services/files/file.service"; +import { SessionService } from "./services/session/session.service"; +import { PreferenceStorageService } from "./services/storage/preference-storage.service"; +import { WarcraftInstallationService } from "./services/warcraft/warcraft-installation.service"; +import { WowUpAddonService } from "./services/wowup/wowup-addon.service"; +import { WowUpService } from "./services/wowup/wowup.service"; +import { ZoomService } from "./services/zoom/zoom.service"; +import { AddonProviderFactory } from "./services/addons/addon.provider.factory"; +import { mockPreload } from "./tests/test-helpers"; + +describe("AppComponent", () => { + let addonServiceSpy: AddonService; + let electronServiceSpy: ElectronService; + let wowUpServiceSpy: WowUpService; + let sessionServiceSpy: SessionService; + let fileServiceSpy: FileService; + let analyticsServiceSpy: AnalyticsService; + let preferenceStorageSpy: PreferenceStorageService; + let wowUpAddonServiceSpy: WowUpAddonService; + let warcraftInstallationService: WarcraftInstallationService; + let zoomService: ZoomService; + let addonProviderService: any; + + beforeEach(async () => { + mockPreload(); + + wowUpAddonServiceSpy = jasmine.createSpyObj( + "WowUpAddonService", + ["updateForClientType", "updateForAllClientTypes"], + { + persistUpdateInformationToWowUpAddon: () => {}, + }, + ); + + addonServiceSpy = jasmine.createSpyObj("AddonService", ["processAutoUpdates", "syncAllClients"], { + syncError$: new Subject(), + }); + + addonProviderService = jasmine.createSpyObj( + "AddonProviderFactory", + { + getAdRequiredProviders: () => [], + }, + {}, + ); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], { + wowInstallations$: new Subject(), + }); + + zoomService = jasmine.createSpyObj("ZoomService", [""], {}); + + electronServiceSpy = jasmine.createSpyObj("ElectronService", ["invoke", "on", "off"], { + appOptions: { quit: null }, + getAppOptions: () => Promise.resolve({}), + powerMonitor$: new Observable(), + appUpdate$: new Observable(), + ipcRenderer: { + on: () => {}, + }, + }); + wowUpServiceSpy = jasmine.createSpyObj("WowUpService", [""], { + preferenceChange$: new Subject().asObservable(), + }); + sessionServiceSpy = jasmine.createSpyObj("SessionService", ["autoUpdateComplete"], { + adSpace$: new BehaviorSubject(false), + }); + fileServiceSpy = jasmine.createSpyObj("FileService", [""]); + analyticsServiceSpy = jasmine.createSpyObj("AnalyticsService", ["trackStartup"]); + preferenceStorageSpy = jasmine.createSpyObj("PreferenceStorageService", ["get"], {}); + + await TestBed.configureTestingModule({ + declarations: [AppComponent, AnimatedLogoComponent], + providers: [MatDialog, ElectronService], + imports: [ + OverlayModule, + RouterTestingModule, + HttpClientModule, + MatModule, + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(AppComponent, { + set: { + providers: [ + MatDialog, + OverlayContainer, + { provide: AddonService, useValue: addonServiceSpy }, + { provide: ElectronService, useValue: electronServiceSpy }, + { provide: WowUpService, useValue: wowUpServiceSpy }, + { provide: SessionService, useValue: sessionServiceSpy }, + { provide: FileService, useValue: fileServiceSpy }, + { provide: AnalyticsService, useValue: analyticsServiceSpy }, + { provide: PreferenceStorageService, useValue: preferenceStorageSpy }, + { provide: WowUpAddonService, useValue: wowUpAddonServiceSpy }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + { provide: ZoomService, useValue: zoomService }, + { provide: AddonProviderFactory, useValue: addonProviderService }, + ], + }, + }) + .compileComponents(); + }); + + it("should create", () => { + const fixture = TestBed.createComponent(AppComponent); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/app.component.ts b/WowUp/wowup-electron/src/app/app.component.ts new file mode 100644 index 0000000..2bda52f --- /dev/null +++ b/WowUp/wowup-electron/src/app/app.component.ts @@ -0,0 +1,635 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +import * as _ from "lodash"; +import { BehaviorSubject, from, of } from "rxjs"; +import { catchError, delay, filter, first, map, switchMap } from "rxjs/operators"; + +import { OverlayContainer } from "@angular/cdk/overlay"; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + HostListener, + OnDestroy, + OnInit, +} from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { TranslateService } from "@ngx-translate/core"; + +import { + ADDON_PROVIDER_CURSEFORGEV2, + ALLIANCE_LIGHT_THEME, + ALLIANCE_THEME, + CURRENT_THEME_KEY, + DEFAULT_LIGHT_THEME, + DEFAULT_THEME, + HORDE_LIGHT_THEME, + HORDE_THEME, + IPC_CREATE_APP_MENU_CHANNEL, + IPC_CREATE_TRAY_MENU_CHANNEL, + IPC_MENU_ZOOM_IN_CHANNEL, + IPC_MENU_ZOOM_OUT_CHANNEL, + IPC_MENU_ZOOM_RESET_CHANNEL, + IPC_POWER_MONITOR_RESUME, + IPC_POWER_MONITOR_UNLOCK, + IPC_REQUEST_INSTALL_FROM_URL, + WOWUP_LOGO_FILENAME, + ZOOM_FACTOR_KEY, +} from "../common/constants"; +import { AppUpdateState, MenuConfig, SystemTrayConfig } from "../common/wowup/models"; +import { AppConfig } from "../environments/environment"; +import { InstallFromUrlDialogComponent } from "./components/addons/install-from-url-dialog/install-from-url-dialog.component"; +import { AlertDialogComponent } from "./components/common/alert-dialog/alert-dialog.component"; +import { AddonSyncError, GitHubFetchReleasesError, GitHubFetchRepositoryError, GitHubLimitError } from "./errors"; +import { AddonInstallState } from "./models/wowup/addon-install-state"; +import { ElectronService } from "./services"; +import { AddonService } from "./services/addons/addon.service"; +import { AnalyticsService } from "./services/analytics/analytics.service"; +import { FileService } from "./services/files/file.service"; +import { SessionService } from "./services/session/session.service"; +import { SnackbarService } from "./services/snackbar/snackbar.service"; +import { PreferenceStorageService } from "./services/storage/preference-storage.service"; +import { WarcraftInstallationService } from "./services/warcraft/warcraft-installation.service"; +import { WowUpAddonService } from "./services/wowup/wowup-addon.service"; +import { WowUpService } from "./services/wowup/wowup.service"; +import { ZoomService } from "./services/zoom/zoom.service"; +import { ZoomDirection } from "./utils/zoom.utils"; +import { AddonProviderFactory } from "./services/addons/addon.provider.factory"; +import { + ConsentDialogComponent, + ConsentDialogResult, +} from "./components/common/consent-dialog/consent-dialog.component"; +import { WowUpProtocolService } from "./services/wowup/wowup-protocol.service"; +import { CurseMigrationDialogComponent } from "./components/common/curse-migration-dialog/curse-migration-dialog.component"; +import { Addon } from "wowup-lib-core"; +import { LinkService } from "./services/links/link.service"; + +@Component({ + selector: "app-root", + templateUrl: "./app.component.html", + styleUrls: ["./app.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent implements OnInit, OnDestroy, AfterViewInit { + private _autoUpdateInterval?: number; + + @HostListener("document:click", ["$event"]) + public onDocumentClick(event: MouseEvent) { + let currentElem: HTMLElement | null = event.target as HTMLElement; + while (currentElem !== null) { + if (currentElem?.tagName === "A") { + const link = currentElem as HTMLAnchorElement; + if (link.href === undefined || link.href === "") { + break; + } + + console.log("link clicked", link.href); + event.preventDefault(); + event.stopPropagation(); + this._linkService.confirmLinkNavigation(link.href).subscribe(); + break; + } + currentElem = currentElem.parentElement; + } + } + + @HostListener("mousewheel", ["$event"]) + public async handleMouseWheelEvent(event: WheelEvent): Promise { + if (!event.ctrlKey) { + return; + } + + try { + if ((event as any).wheelDelta > 0) { + await this._zoomService.applyZoom(ZoomDirection.ZoomIn); + } else { + await this._zoomService.applyZoom(ZoomDirection.ZoomOut); + } + } catch (e) { + console.error(e); + } + } + + public quitEnabled?: boolean; + public showPreLoad$ = new BehaviorSubject(true); + + public constructor( + private _addonService: AddonService, + private _addonProviderService: AddonProviderFactory, + private _analyticsService: AnalyticsService, + private _cdRef: ChangeDetectorRef, + private _dialog: MatDialog, + private _fileService: FileService, + private _preferenceStore: PreferenceStorageService, + private _snackbarService: SnackbarService, + private _translateService: TranslateService, + private _warcraftInstallationService: WarcraftInstallationService, + private _wowupAddonService: WowUpAddonService, + private _zoomService: ZoomService, + private _wowUpProtocolService: WowUpProtocolService, + private _linkService: LinkService, + public electronService: ElectronService, + public overlayContainer: OverlayContainer, + public sessionService: SessionService, + public wowUpService: WowUpService, + ) { + this._warcraftInstallationService.wowInstallations$ + .pipe( + first((installations) => installations.length > 0), + switchMap(() => from(this.initializeAutoUpdate())), + ) + .subscribe(); + + this.electronService.appUpdate$.subscribe((evt) => { + if (evt.state === AppUpdateState.Error) { + if (evt.error.indexOf("dev-app-update.yml") === -1) { + this._snackbarService.showErrorSnackbar("APP.WOWUP_UPDATE.UPDATE_ERROR"); + } + } else if (evt.state === AppUpdateState.Downloaded) { + // Force the user to update when one is ready + const dialogRef = this._dialog.open(AlertDialogComponent, { + minWidth: 250, + disableClose: true, + data: { + title: this._translateService.instant("APP.WOWUP_UPDATE.INSTALL_TITLE"), + message: this._translateService.instant("APP.WOWUP_UPDATE.SNACKBAR_TEXT"), + positiveButton: "APP.WOWUP_UPDATE.DOWNLOADED_TOOLTIP", + positiveButtonColor: "primary", + positiveButtonStyle: "raised", + }, + }); + + dialogRef + .afterClosed() + .pipe(first()) + .subscribe(() => { + this.wowUpService.installUpdate(); + }); + } + }); + + this._wowUpProtocolService.initialize(); + } + + public ngOnInit(): void { + this.loadZoom().catch(console.error); + + this.wowUpService + .getCurrentTheme() + .then((theme) => { + this.overlayContainer.getContainerElement().classList.add(theme); + }) + .catch(console.error); + + this.overlayContainer.getContainerElement().classList.add(this.electronService.platform); + + this.wowUpService.preferenceChange$.pipe(filter((pref) => pref.key === CURRENT_THEME_KEY)).subscribe((pref) => { + this.overlayContainer + .getContainerElement() + .classList.remove( + HORDE_THEME, + HORDE_LIGHT_THEME, + ALLIANCE_THEME, + ALLIANCE_LIGHT_THEME, + DEFAULT_THEME, + DEFAULT_LIGHT_THEME, + ); + this.overlayContainer.getContainerElement().classList.add(pref.value); + }); + + this._addonService.syncError$.subscribe(this.onAddonSyncError); + + this._addonService.addonAction$ + .pipe( + filter((action) => action.type === "sync" || action.type === "scan"), + switchMap(() => from(this.updateBadgeCount())), + ) + .subscribe(); + + //If the window is restored update the badge number + this.electronService.windowResumed$ + .pipe( + delay(1000), // If you dont delay this on Mac, it will sometimes not show up + switchMap(() => from(this.updateBadgeCount())), + ) + .subscribe(); + + // If an addon is installed/updated check the badge number + this._addonService.addonInstalled$ + .pipe( + filter((evt) => evt.installState === AddonInstallState.Complete), + switchMap(() => from(this.updateBadgeCount())), + ) + .subscribe(); + + // If user removes an addon, update the badge count + this._addonService.addonRemoved$.pipe(switchMap(() => from(this.updateBadgeCount()))).subscribe(); + + this.electronService.on(IPC_MENU_ZOOM_IN_CHANNEL, this.onMenuZoomIn); + this.electronService.on(IPC_MENU_ZOOM_OUT_CHANNEL, this.onMenuZoomOut); + this.electronService.on(IPC_MENU_ZOOM_RESET_CHANNEL, this.onMenuZoomReset); + this.electronService.on(IPC_REQUEST_INSTALL_FROM_URL, this.onRequestInstallFromUrl); + + from(this.electronService.getAppOptions()) + .pipe( + first(), + map((appOptions) => { + this.quitEnabled = appOptions.quit; + this._cdRef.detectChanges(); + }), + switchMap(() => from(this.electronService.processPendingOpenUrls())), + catchError((err) => { + console.error(err); + return of(undefined); + }), + ) + .subscribe(); + + this.electronService.powerMonitor$.pipe(filter((evt) => !!evt)).subscribe((evt) => { + console.log("Stopping auto update..."); + window.clearInterval(this._autoUpdateInterval); + this._autoUpdateInterval = undefined; + + if (evt === IPC_POWER_MONITOR_RESUME || evt === IPC_POWER_MONITOR_UNLOCK) { + this.initializeAutoUpdate().catch((e) => console.error(e)); + } + }); + } + + private async loadZoom() { + const zoomPref = await this._preferenceStore.getAsync(ZOOM_FACTOR_KEY); + console.log("zoomPref", zoomPref); + const zoomFactor = parseFloat(zoomPref); + if (!isNaN(zoomFactor) && isFinite(zoomFactor)) { + this._zoomService.setZoomFactor(zoomFactor).catch((e) => console.error(e)); + } + } + + public ngAfterViewInit(): void { + this.onNgAfterViewInit().catch((e) => console.error(e)); + } + + private async onNgAfterViewInit(): Promise { + await this.createAppMenu(); + await this.createSystemTray(); + await this._analyticsService.trackStartup(); + await this.showRequiredDialogs(); + } + + private async showRequiredDialogs(): Promise { + try { + const shouldShowConsent = await this.shouldShowConsentDialog(); + if (shouldShowConsent) { + this.openConsentDialog(); + return; + } + + this.showPreLoad$.next(false); + + if (!this.sessionService.didPromptCfMigration) { + // If the user has any addons from old Curse that are not ignored prompt them to rescan + let cfAddons = await this._addonService.getProviderAddons(ADDON_PROVIDER_CURSEFORGEV2); + cfAddons = cfAddons.filter((addon) => addon.isIgnored === false); + if (cfAddons.length > 0) { + this.openCurseMigrationDialog(); + } + } + } catch (e) { + console.error(e); + } + } + + private async shouldShowConsentDialog(): Promise { + const shouldPromptTelemetry = await this._analyticsService.shouldPromptTelemetry(); + const shouldShowConsentDialog = await this._addonProviderService.shouldShowConsentDialog(); + return shouldPromptTelemetry || shouldShowConsentDialog; + } + + public ngOnDestroy(): void { + this.electronService.off(IPC_MENU_ZOOM_IN_CHANNEL, this.onMenuZoomIn); + this.electronService.off(IPC_MENU_ZOOM_OUT_CHANNEL, this.onMenuZoomOut); + this.electronService.off(IPC_MENU_ZOOM_RESET_CHANNEL, this.onMenuZoomReset); + } + + public onMenuZoomIn = (): void => { + this._zoomService.applyZoom(ZoomDirection.ZoomIn).catch((e) => console.error(e)); + }; + + public onMenuZoomOut = (): void => { + this._zoomService.applyZoom(ZoomDirection.ZoomOut).catch((e) => console.error(e)); + }; + + public onMenuZoomReset = (): void => { + this._zoomService.applyZoom(ZoomDirection.ZoomReset).catch((e) => console.error(e)); + }; + + public onRequestInstallFromUrl = (evt: unknown, path?: string): void => { + this.openInstallFromUrlDialog(path); + }; + + public openCurseMigrationDialog(): void { + const dialogRef = this._dialog.open(CurseMigrationDialogComponent, { + disableClose: true, + }); + + dialogRef.afterClosed().subscribe(() => { + this.sessionService.didPromptCfMigration = true; + this.showRequiredDialogs().catch((e) => console.error(e)); + }); + } + + public openConsentDialog(): void { + const dialogRef = this._dialog.open(ConsentDialogComponent, { + disableClose: true, + }); + + dialogRef + .afterClosed() + .pipe( + switchMap((result: ConsentDialogResult) => + from(this._addonProviderService.setProviderEnabled("Wago", result.wagoProvider)).pipe(map(() => result)) + ), + switchMap((result) => from(this._addonProviderService.updateWagoConsent()).pipe(map(() => result))), + switchMap((result: ConsentDialogResult) => { + this._analyticsService.setTelemetryEnabled(result.telemetry).catch(console.error); + if (result.telemetry) { + return from(this._analyticsService.trackStartup()); + } + + return of(undefined); + }), + ) + .subscribe(() => { + this.showRequiredDialogs().catch((e) => console.error(e)); + }); + } + + private openInstallFromUrlDialog(path?: string) { + if (!path) { + return; + } + + const dialogRef = this._dialog.open(InstallFromUrlDialogComponent); + dialogRef.componentInstance.query = path; + } + + private async initializeAutoUpdate() { + if (this._autoUpdateInterval !== undefined) { + console.warn(`Auto addon update interval already exists`); + return; + } + + this._autoUpdateInterval = window.setInterval(() => { + this.onAutoUpdateInterval().catch((e) => console.error(e)); + }, AppConfig.autoUpdateIntervalMs); + + await this.onAutoUpdateInterval(); + } + + private onAutoUpdateInterval = async () => { + try { + console.log("onAutoUpdateInterval"); + await this._addonService.syncAllClients(); + const updatedAddons = await this._addonService.processAutoUpdates(); + + await this._wowupAddonService.updateForAllClientTypes(); + + await this.updateBadgeCount(); + + if (!updatedAddons || updatedAddons.length === 0) { + await this.checkQuitEnabled(); + return; + } + + const enableSystemNotifications = await this.wowUpService.getEnableSystemNotifications(); + if (enableSystemNotifications) { + const addonsWithNotificationsEnabled = updatedAddons.filter( + (addon) => addon.autoUpdateNotificationsEnabled === true, + ); + + // Windows notification only shows so many chars + if (this.getAddonNamesLength(addonsWithNotificationsEnabled) > 60) { + await this.showManyAddonsAutoUpdated(addonsWithNotificationsEnabled); + } else { + await this.showFewAddonsAutoUpdated(addonsWithNotificationsEnabled); + } + } else { + await this.checkQuitEnabled(); + } + } catch (e) { + console.error("Error during auto update", e); + } finally { + this.sessionService.autoUpdateComplete(); + } + }; + + private async showManyAddonsAutoUpdated(updatedAddons: Addon[]) { + const iconPath = await this._fileService.getAssetFilePath(WOWUP_LOGO_FILENAME); + const translated: { [key: string]: string } = await this._translateService + .get(["APP.AUTO_UPDATE_NOTIFICATION_TITLE", "APP.AUTO_UPDATE_NOTIFICATION_BODY"], { + count: updatedAddons.length, + }) + .toPromise(); + + const notification = this.electronService.showNotification(translated["APP.AUTO_UPDATE_NOTIFICATION_TITLE"], { + body: translated["APP.AUTO_UPDATE_NOTIFICATION_BODY"], + icon: iconPath, + }); + + notification.addEventListener("click", this.onClickNotification, { once: true }); + notification.addEventListener("close", this.onAutoUpdateNotificationClosed, { once: true }); + } + + private async showFewAddonsAutoUpdated(updatedAddons: Addon[]) { + const addonNames = _.map(updatedAddons, (addon) => addon.name); + const addonText = _.join(addonNames, "\r\n"); + const iconPath = await this._fileService.getAssetFilePath(WOWUP_LOGO_FILENAME); + const translated: { [key: string]: string } = await this._translateService + .get(["APP.AUTO_UPDATE_NOTIFICATION_TITLE", "APP.AUTO_UPDATE_FEW_NOTIFICATION_BODY"], { + addonNames: addonText, + }) + .toPromise(); + + const notification = this.electronService.showNotification(translated["APP.AUTO_UPDATE_NOTIFICATION_TITLE"], { + body: translated["APP.AUTO_UPDATE_FEW_NOTIFICATION_BODY"], + icon: iconPath, + }); + + notification.addEventListener("click", this.onClickNotification, { once: true }); + notification.addEventListener("close", this.onAutoUpdateNotificationClosed, { once: true }); + } + + private onClickNotification = () => { + this.electronService.focusWindow().catch((e) => console.error(`Failed to focus window on notification click`, e)); + }; + + private getAddonNames(addons: Addon[]) { + return _.map(addons, (addon) => addon.name); + } + + private getAddonNamesLength(addons: Addon[]) { + return _.join(this.getAddonNames(addons), " ").length; + } + + private onAutoUpdateNotificationClosed = () => { + this.checkQuitEnabled().catch((e) => console.error(e)); + }; + + private async checkQuitEnabled() { + const appOptions = await this.electronService.getAppOptions(); + if (!appOptions.quit) { + return; + } + + console.debug("checkQuitEnabled"); + this.electronService.quitApplication().catch((e) => console.error(e)); + } + + private onAddonSyncError = (error: AddonSyncError) => { + let errorMessage: string = this._translateService.instant("COMMON.ERRORS.ADDON_SYNC_ERROR", { + providerName: error.providerName, + }); + + if (error.addonName) { + errorMessage = this._translateService.instant("COMMON.ERRORS.ADDON_SYNC_FULL_ERROR", { + installationName: error.installationName, + providerName: error.providerName, + addonName: error.addonName, + }); + } + + if (error.innerError instanceof GitHubLimitError) { + const err = error.innerError; + const max = err.rateLimitMax; + const reset = new Date(err.rateLimitReset * 1000).toLocaleString(); + errorMessage = this._translateService.instant("COMMON.ERRORS.GITHUB_LIMIT_ERROR", { + max, + reset, + }); + } else if ( + error.innerError instanceof GitHubFetchReleasesError || + error.innerError instanceof GitHubFetchRepositoryError + ) { + errorMessage = this._translateService.instant("COMMON.ERRORS.GITHUB_REPOSITORY_FETCH_ERROR", { + addonName: error.addonName, + }); + } else if (error instanceof AddonSyncError) { + return; + } + + this._snackbarService.showErrorSnackbar(errorMessage); + }; + + private async createAppMenu() { + console.log("Creating app menu"); + + // APP MENU + const quitKey = "APP.APP_MENU.QUIT"; + + // EDIT MENU + const editKey = "APP.APP_MENU.EDIT.LABEL"; + const copyKey = "APP.APP_MENU.EDIT.COPY"; + const cutKey = "APP.APP_MENU.EDIT.CUT"; + const pasteKey = "APP.APP_MENU.EDIT.PASTE"; + const redoKey = "APP.APP_MENU.EDIT.REDO"; + const selectAllKey = "APP.APP_MENU.EDIT.SELECT_ALL"; + const undoKey = "APP.APP_MENU.EDIT.UNDO"; + + // VIEW MENU + const viewKey = "APP.APP_MENU.VIEW.LABEL"; + const forceReloadKey = "APP.APP_MENU.VIEW.FORCE_RELOAD"; + const reloadKey = "APP.APP_MENU.VIEW.RELOAD"; + const toggleDevToolsKey = "APP.APP_MENU.VIEW.TOGGLE_DEV_TOOLS"; + const toggleFullScreenKey = "APP.APP_MENU.VIEW.TOGGLE_FULL_SCREEN"; + const zoomInKey = "APP.APP_MENU.VIEW.ZOOM_IN"; + const zoomOutKey = "APP.APP_MENU.VIEW.ZOOM_OUT"; + const zoomResetKey = "APP.APP_MENU.VIEW.ZOOM_RESET"; + + // WINDOW MENU + const windowKey = "APP.APP_MENU.WINDOW.LABEL"; + const windowCloseKey = "APP.APP_MENU.WINDOW.CLOSE"; + + const result = await this._translateService + .get([ + editKey, + viewKey, + zoomInKey, + zoomOutKey, + zoomResetKey, + copyKey, + cutKey, + forceReloadKey, + quitKey, + redoKey, + reloadKey, + selectAllKey, + toggleDevToolsKey, + toggleFullScreenKey, + undoKey, + pasteKey, + windowKey, + windowCloseKey, + ]) + .toPromise(); + + const config: MenuConfig = { + editLabel: result[editKey], + viewLabel: result[viewKey], + zoomInLabel: result[zoomInKey], + zoomOutLabel: result[zoomOutKey], + zoomResetLabel: result[zoomResetKey], + copyLabel: result[copyKey], + cutLabel: result[cutKey], + forceReloadLabel: result[forceReloadKey], + pasteLabel: result[pasteKey], + quitLabel: result[quitKey], + redoLabel: result[redoKey], + reloadLabel: result[reloadKey], + selectAllLabel: result[selectAllKey], + toggleDevToolsLabel: result[toggleDevToolsKey], + toggleFullScreenLabel: result[toggleFullScreenKey], + undoLabel: result[undoKey], + windowLabel: result[windowKey], + windowCloseLabel: result[windowCloseKey], + }; + + try { + const trayCreated = await this.electronService.invoke(IPC_CREATE_APP_MENU_CHANNEL, config); + console.log("App menu created", trayCreated); + } catch (e) { + console.error("Failed to create tray", e); + } + } + + private async createSystemTray() { + console.log("Creating tray"); + const result = await this._translateService + .get(["APP.SYSTEM_TRAY.QUIT_ACTION", "APP.SYSTEM_TRAY.SHOW_ACTION"]) + .toPromise(); + + const config: SystemTrayConfig = { + quitLabel: result["APP.SYSTEM_TRAY.QUIT_ACTION"], + checkUpdateLabel: result["APP.SYSTEM_TRAY.CHECK_UPDATE"], + showLabel: result["APP.SYSTEM_TRAY.SHOW_ACTION"], + }; + + try { + const trayCreated = await this.electronService.invoke(IPC_CREATE_TRAY_MENU_CHANNEL, config); + console.log("Tray created", trayCreated); + } catch (e) { + console.error("Failed to create tray", e); + } + } + + private async updateBadgeCount(): Promise { + const addons = await this._addonService.getAllAddonsAvailableForUpdate(); + const ct = addons.length; + try { + await this.wowUpService.updateAppBadgeCount(ct); + } catch (e) { + console.error("Failed to update badge count", e); + } + } +} diff --git a/WowUp/wowup-electron/src/app/app.module.ts b/WowUp/wowup-electron/src/app/app.module.ts new file mode 100644 index 0000000..93f9d54 --- /dev/null +++ b/WowUp/wowup-electron/src/app/app.module.ts @@ -0,0 +1,106 @@ +import "reflect-metadata"; +import "../polyfills"; + +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from "@angular/common/http"; +import { APP_INITIALIZER, ErrorHandler, NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { BrowserModule } from "@angular/platform-browser"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; +import { GalleryModule } from "ng-gallery"; + +import { AppRoutingModule } from "./app-routing.module"; +import { AppComponent } from "./app.component"; +import { TitlebarComponent } from "./components/common/titlebar/titlebar.component"; +import { DirectiveModule } from "./modules/directive.module"; +import { DefaultHeadersInterceptor } from "./interceptors/default-headers.interceptor"; +import { ErrorHandlerInterceptor } from "./interceptors/error-handler-interceptor"; +import { MatModule } from "./modules/mat-module"; +import { HomeModule } from "./pages/home/home.module"; +import { AnalyticsService } from "./services/analytics/analytics.service"; +import { WowUpApiService } from "./services/wowup-api/wowup-api.service"; +import { WowUpService } from "./services/wowup/wowup.service"; +import { WarcraftInstallationService } from "./services/warcraft/warcraft-installation.service"; +import { AddonService } from "./services/addons/addon.service"; +import { IconService } from "./services/icons/icon.service"; +import { HorizontalTabsComponent } from "./components/common/horizontal-tabs/horizontal-tabs.component"; +import { CommonUiModule } from "./modules/common-ui.module"; +import { FooterComponent } from "./components/common/footer/footer.component"; +import { VerticalTabsComponent } from "./components/common/vertical-tabs/vertical-tabs.component"; +import { AddonProviderFactory } from "./services/addons/addon.provider.factory"; + +// AoT requires an exported function for factories +export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader { + return new TranslateHttpLoader(http, "./assets/i18n/", ".json"); +} + +export function initializeApp( + wowupService: WowUpService, + wowUpApiService: WowUpApiService, + addonService: AddonService, + warcraftInstallationService: WarcraftInstallationService, + iconService: IconService, + addonProviderFactory: AddonProviderFactory +) { + return async (): Promise => { + await wowupService.initializeLanguage(); + await addonProviderFactory.loadProviders(); + }; +} + +@NgModule({ + declarations: [AppComponent, TitlebarComponent, FooterComponent, HorizontalTabsComponent, VerticalTabsComponent], + imports: [ + BrowserModule, + FormsModule, + HttpClientModule, + HomeModule, + AppRoutingModule, + DirectiveModule, + MatModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + BrowserAnimationsModule, + GalleryModule, + CommonUiModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + useFactory: initializeApp, + deps: [ + WowUpService, + WowUpApiService, + AddonService, + WarcraftInstallationService, + IconService, + AddonProviderFactory, + ], + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: DefaultHeadersInterceptor, + multi: true, + }, + { + provide: ErrorHandler, + useClass: ErrorHandlerInterceptor, + deps: [AnalyticsService], + }, + ], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/WowUp/wowup-electron/src/app/business-objects/addon-view-model.ts b/WowUp/wowup-electron/src/app/business-objects/addon-view-model.ts new file mode 100644 index 0000000..e002f66 --- /dev/null +++ b/WowUp/wowup-electron/src/app/business-objects/addon-view-model.ts @@ -0,0 +1,155 @@ +import * as _ from "lodash"; +import { AddonInstallState } from "../models/wowup/addon-install-state"; +import { AddonStatusSortOrder } from "../models/wowup/addon-status-sort-order"; +import * as AddonUtils from "../utils/addon.utils"; +import { ADDON_PROVIDER_UNKNOWN } from "../../common/constants"; +import * as objectHash from "object-hash"; +import { Addon, AddonChannelType, AddonDependency, AddonDependencyType } from "wowup-lib-core"; + +export class AddonViewModel { + public addon: Addon | undefined; + + public installState: AddonInstallState = AddonInstallState.Unknown; + public isInstalling = false; + public installProgress = 0; + public stateTextTranslationKey = ""; + public selected = false; + public releasedAt = 0; + public installedAt = 0; + public isLoadOnDemand = false; + public hasThumbnail = false; + public thumbnailLetter = ""; + public canonicalName = ""; + + public get isIgnored(): boolean { + return this.addon?.isIgnored ?? false; + } + + public get name(): string { + return this.addon?.name ?? ""; + } + + public get latestVersion(): string { + return this.addon?.latestVersion ?? ""; + } + + public get gameVersion(): string[] { + return this.addon?.gameVersion ?? []; + } + + public get externalChannel(): string { + return this.addon?.externalChannel ?? ""; + } + + public get providerName(): string { + return this.addon?.providerName ?? ""; + } + + public get author(): string { + return this.addon?.author ?? ""; + } + + public get hash(): string { + return objectHash(this.addon); + } + + public constructor(addon: Addon | undefined) { + this.addon = addon; + this.installedAt = addon?.installedAt ? new Date(addon?.installedAt).getTime() : 0; + this.releasedAt = addon?.releasedAt ? new Date(addon?.releasedAt).getTime() : 0; + this.stateTextTranslationKey = this.getStateTextTranslationKey(); + this.isLoadOnDemand = addon?.isLoadOnDemand ?? false; + this.hasThumbnail = !!addon?.thumbnailUrl; + this.thumbnailLetter = addon?.name?.charAt(0).toUpperCase() ?? ""; + this.canonicalName = addon?.name?.toLowerCase() ?? ""; + } + + public isUpToDate(): boolean { + return !this.isInstalling && !AddonUtils.needsUpdate(this.addon); + } + + public isStableChannel(): boolean { + return this.addon?.channelType === AddonChannelType.Stable; + } + + public isBetaChannel(): boolean { + return this.addon?.channelType === AddonChannelType.Beta; + } + + public isAlphaChannel(): boolean { + return this.addon?.channelType === AddonChannelType.Alpha; + } + + public isUnMatched(): boolean { + return this.addon?.providerName === ADDON_PROVIDER_UNKNOWN; + } + + public clone(): AddonViewModel { + return new AddonViewModel(this.addon); + } + + public onClicked(): void { + this.selected = !this.selected; + } + + public needsInstall(): boolean { + return !this.isInstalling && this.addon !== undefined && AddonUtils.needsInstall(this.addon); + } + + public needsUpdate(): boolean { + return !this.isInstalling && this.addon !== undefined && AddonUtils.needsUpdate(this.addon); + } + + public get sortOrder(): AddonStatusSortOrder { + if (this.addon?.isIgnored) { + return AddonStatusSortOrder.Ignored; + } + + if (this.addon?.warningType) { + return AddonStatusSortOrder.Warning; + } + + if (this.needsInstall()) { + return AddonStatusSortOrder.Install; + } + + if (this.needsUpdate() || this.isInstalling) { + return AddonStatusSortOrder.Update; + } + + if (this.isUpToDate()) { + return AddonStatusSortOrder.UpToDate; + } + + return AddonStatusSortOrder.Unknown; + } + + public getStateTextTranslationKey(): string { + if (this.isUpToDate()) { + return "COMMON.ADDON_STATE.UPTODATE"; + } + + if (this.addon?.isIgnored) { + return "COMMON.ADDON_STATE.IGNORED"; + } + + if (this.needsUpdate()) { + return "COMMON.ADDON_STATE.UPDATE"; + } + + if (this.needsInstall()) { + return "COMMON.ADDON_STATE.INSTALL"; + } + + console.warn("Unhandled display state", this.isUpToDate(), JSON.stringify(this, null, 2)); + return "COMMON.ADDON_STATE.UNKNOWN"; + } + + public getDependencies(dependencyType: AddonDependencyType | undefined = undefined): AddonDependency[] { + return ( + (dependencyType == undefined + ? this.addon?.dependencies ?? [] + : _.filter(this.addon?.dependencies ?? [], (dep) => dep.type === dependencyType)) ?? [] + ); + } +} diff --git a/WowUp/wowup-electron/src/app/business-objects/generic-network-interface.ts b/WowUp/wowup-electron/src/app/business-objects/generic-network-interface.ts new file mode 100644 index 0000000..a38f1da --- /dev/null +++ b/WowUp/wowup-electron/src/app/business-objects/generic-network-interface.ts @@ -0,0 +1,31 @@ +import { GetConfig, NetworkInterface, PostConfig } from "wowup-lib-core"; +import { CircuitBreakerWrapper } from "../services/network/network.service"; +import * as memcache from "../business-objects/mem-cache"; + +export class GenericNetworkInterface implements NetworkInterface { + public constructor(private _circuitBreaker: CircuitBreakerWrapper) {} + + public async getJson(url: string | URL, config?: GetConfig | undefined): Promise { + return await memcache.transaction( + url.toString(), + () => this._circuitBreaker.getJson(url, config?.headers, config?.timeoutMs), + 30 + ); + } + + public async getText(url: string | URL, config?: GetConfig | undefined): Promise { + return await memcache.transaction(url.toString(), () => this._circuitBreaker.getText(url, config?.timeoutMs), 30); + } + + public async postJson(url: string | URL, config: PostConfig): Promise { + if (config.cache === true) { + const key = `${url.toString()}-${JSON.stringify(config.body).length.toString()}`; + return await memcache.transaction( + key, + () => this._circuitBreaker.postJson(url, config.body, config.headers, config.timeoutMs), + 30 + ); + } + return await this._circuitBreaker.postJson(url, config.body, config.headers, config.timeoutMs); + } +} diff --git a/WowUp/wowup-electron/src/app/business-objects/get-addon-list-item.ts b/WowUp/wowup-electron/src/app/business-objects/get-addon-list-item.ts new file mode 100644 index 0000000..c48359b --- /dev/null +++ b/WowUp/wowup-electron/src/app/business-objects/get-addon-list-item.ts @@ -0,0 +1,39 @@ +import { AddonChannelType, AddonSearchResult } from "wowup-lib-core"; +import { AddonInstallState } from "../models/wowup/addon-install-state"; +import * as SearchResults from "../utils/search-result.utils"; + +export class GetAddonListItem { + public readonly searchResult: AddonSearchResult; + + public releasedAt: number; + public downloadCount: number; + public name: string; + public thumbnailUrl: string; + public author: string; + public providerName: string; + public latestAddonChannel: AddonChannelType; + public canonicalName: string; + + public installState: AddonInstallState = AddonInstallState.Unknown; + + public get externalId(): string { + return this.searchResult.externalId; + } + + public constructor(searchResult: AddonSearchResult, defaultAddonChannel?: AddonChannelType) { + this.searchResult = searchResult; + this.author = this.searchResult.author; + this.name = this.searchResult.name; + this.providerName = this.searchResult.providerName; + this.thumbnailUrl = this.searchResult.thumbnailUrl; + this.downloadCount = this.searchResult.downloadCount || 0; + this.canonicalName = this.name.toLowerCase(); + + if (defaultAddonChannel !== undefined) { + const latestFile = SearchResults.getLatestFile(searchResult, defaultAddonChannel); + this.latestAddonChannel = latestFile?.channelType ?? AddonChannelType.Stable; + + this.releasedAt = new Date(latestFile?.releaseDate ?? new Date()).getTime(); + } + } +} diff --git a/WowUp/wowup-electron/src/app/business-objects/mem-cache.ts b/WowUp/wowup-electron/src/app/business-objects/mem-cache.ts new file mode 100644 index 0000000..887d181 --- /dev/null +++ b/WowUp/wowup-electron/src/app/business-objects/mem-cache.ts @@ -0,0 +1,26 @@ +import * as NodeCache from "node-cache"; + +const _cache = new NodeCache(); + +export function get(key: string): T | undefined { + return _cache.get(key); +} + +export function set(key: string, value: T, ttlSec = 600): boolean { + return _cache.set(key, value, ttlSec); +} + +export async function transaction(key: string, missingAction: () => Promise, ttlSec = 600): Promise { + const cached = get(key); + if (cached !== undefined && cached !== null) { + return cached; + } + + const result = await missingAction?.call(null); + + if (result !== undefined && result !== null) { + set(key, result, ttlSec); + } + + return result; +} diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.html b/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.html new file mode 100644 index 0000000..cdd9400 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.html @@ -0,0 +1,121 @@ +
+
+
+
+ {{ thumbnailLetter }} +
+ + Addon Picture +
+ +
+

{{ title }}

+

{{ "DIALOGS.ADDON_DETAILS.BY_AUTHOR" | translate : { authorName: subtitle } }}

+

{{ version }}

+
+ + +
+
+ + +
+ + + +
+

{{ "DIALOGS.ADDON_DETAILS.MISSING_DEPENDENCIES" | translate }}

+
    +
  • {{ dependency }}
  • +
+
+ +
+ + + DIALOGS.ADDON_DETAILS.DEPENDENCY_TEXT + +
+ + + + + +
+
+ + + +
+ {{ "DIALOGS.ADDON_DETAILS.NO_CHANGELOG_TEXT" | translate }} +
+
+
+ + + + +
+ +
+
+
+
+
+
+ +
+ +
+ {{ "DIALOGS.ADDON_DETAILS.VIEW_ON_PROVIDER_PREFIX" | translate }} + {{ provider }} + + + + +
+
diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.scss b/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.scss new file mode 100644 index 0000000..65ec357 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.scss @@ -0,0 +1,90 @@ +.image-grid { + width: 70vw; + .image-thumb-container { + margin: 1em; + .image-thumb { + max-height: 100%; + max-width: 100%; + border-radius: 4px; + overflow: hidden; + box-sizing: border-box; + + &:hover { + border: 3px solid var(--background-primary); + cursor: pointer; + } + } + } +} + +.title { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: initial; +} +.icon { + width: 60px; + height: 60px; + overflow: hidden; + + div { + font-size: 2em; + font-weight: 400; + text-align: center; + line-height: 60px; + } + img { + height: 100%; + } +} +.image-row { + margin-bottom: 16px; + border-radius: 4px; + overflow: hidden; + + .image { + max-width: 100%; + max-height: 250px; + } +} +.addon-detail-view { + min-width: 600px; + + .icon-link { + width: 24px; + height: 24px; + } + + .addon-dependencies { + padding: 0.5em; + margin-bottom: 0.5em; + } +} + +.id-link { + &:hover { + text-decoration: underline; + cursor: pointer; + } +} + +.funding-link-container { + .addon-funding { + background-color: var(--background-secondary-4); + border-radius: 4px; + padding: 0.5em; + a { + margin-right: 1em; + } + .funding-icon { + width: 28px; + height: 28px; + + &:hover { + cursor: pointer; + } + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.spec.ts new file mode 100644 index 0000000..1f98952 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.spec.ts @@ -0,0 +1,111 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { Subject } from "rxjs"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { AddonViewModel } from "../../../business-objects/addon-view-model"; +import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event"; +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { AddonDetailComponent, AddonDetailModel } from "./addon-detail.component"; +import { mockPreload } from "../../../tests/test-helpers"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { LinkService } from "../../../services/links/link.service"; +import { GalleryModule } from "ng-gallery"; +import { LightboxModule } from "ng-gallery/lightbox"; +import { MatModule } from "../../../modules/mat-module"; +import { AddonUiService } from "../../../services/addons/addon-ui.service"; +import { AddonProviderFactory } from "../../../services/addons/addon.provider.factory"; +import { Addon } from "wowup-lib-core"; + +describe("AddonDetailComponent", () => { + let dialogModel: AddonDetailModel; + let addonServiceSpy: any; + let sessionServiceSpy: SessionService; + let wowUpService: WowUpService; + let linkService: any; + let addonUiService: any; + let addonProviderService: any; + + beforeEach(async () => { + mockPreload(); + + console.log("AddonDetailComponent"); + addonServiceSpy = jasmine.createSpyObj( + "AddonService", + ["logDebugData", "getChangelogForAddon", "canShowChangelog"], + { + addonInstalled$: new Subject().asObservable(), + getChangelog: () => "", + }, + ); + + addonUiService = jasmine.createSpyObj("AddonUiService", [""], {}); + wowUpService = jasmine.createSpyObj("WowUpService", [""], {}); + linkService = jasmine.createSpyObj("LinkService", [""], {}); + addonProviderService = jasmine.createSpyObj("AddonProviderFactory", [""], {}); + + sessionServiceSpy = jasmine.createSpyObj("SessionService", ["getSelectedClientType", "getSelectedDetailsTab"], { + getSelectedWowInstallation: () => "description", + }); + + const viewModel = new AddonViewModel({ + installedVersion: "1.0.0", + externalId: "52001", + } as Addon); + + dialogModel = { listItem: viewModel } as AddonDetailModel; + + const testBed = TestBed.configureTestingModule({ + declarations: [AddonDetailComponent], + imports: [ + MatModule, + HttpClientModule, + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + GalleryModule, + LightboxModule, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [{ provide: MAT_DIALOG_DATA, useValue: dialogModel }], + }).overrideComponent(AddonDetailComponent, { + set: { + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: AddonService, useValue: addonServiceSpy }, + { provide: SessionService, useValue: sessionServiceSpy }, + { provide: LinkService, useValue: linkService }, + { + provide: WowUpService, + useValue: wowUpService, + }, + { provide: AddonUiService, useValue: addonUiService }, + { provide: AddonProviderFactory, useValue: addonProviderService }, + ], + }, + }); + + await testBed.compileComponents(); + }); + + it("should create", () => { + const fixture = TestBed.createComponent(AddonDetailComponent); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.ts b/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.ts new file mode 100644 index 0000000..f4d36d4 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-detail/addon-detail.component.ts @@ -0,0 +1,459 @@ +import { last } from "lodash"; +import { Gallery, GalleryItem, ImageItem } from "ng-gallery"; +import { BehaviorSubject, from, Observable, of, Subject } from "rxjs"; +import { catchError, filter, first, map, takeUntil, tap } from "rxjs/operators"; + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + OnDestroy, + OnInit, + ViewChild, + ViewChildren, +} from "@angular/core"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { MatTabChangeEvent, MatTabGroup } from "@angular/material/tabs"; +import { TranslateService } from "@ngx-translate/core"; + +import { ADDON_PROVIDER_GITHUB, ADDON_PROVIDER_UNKNOWN, TAB_INDEX_MY_ADDONS } from "../../../../common/constants"; +import { AddonViewModel } from "../../../business-objects/addon-view-model"; +import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event"; +import { AddonService } from "../../../services/addons/addon.service"; + +import { SessionService } from "../../../services/session/session.service"; +import { SnackbarService } from "../../../services/snackbar/snackbar.service"; +import * as SearchResult from "../../../utils/search-result.utils"; +import { AddonUiService } from "../../../services/addons/addon-ui.service"; +import { AddonProviderFactory } from "../../../services/addons/addon.provider.factory"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { + Addon, + AddonChannelType, + AddonDependency, + AddonDependencyType, + AddonFundingLink, + AddonSearchResult, + AddonSearchResultDependency, +} from "wowup-lib-core"; +import { QueryList } from "@angular/core"; + +export interface AddonDetailModel { + listItem?: AddonViewModel; + searchResult?: AddonSearchResult; + channelType?: AddonChannelType; +} + +@Component({ + selector: "app-addon-detail", + templateUrl: "./addon-detail.component.html", + styleUrls: ["./addon-detail.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChildren("descriptionContainer", { read: ElementRef }) public descriptionContainer!: QueryList; + @ViewChildren("changelogContainer", { read: ElementRef }) public changelogContainer!: QueryList; + @ViewChild("providerLink", { read: ElementRef }) public providerLink!: ElementRef; + @ViewChild("tabs", { static: false }) public tabGroup!: MatTabGroup; + + private readonly _dependencies: AddonSearchResultDependency[]; + private readonly _changelogSrc = new BehaviorSubject(""); + private readonly _descriptionSrc = new BehaviorSubject(""); + private readonly _destroy$ = new Subject(); + + public readonly changelog$ = this._changelogSrc.asObservable(); + public readonly description$ = this._descriptionSrc.asObservable(); + public fetchingChangelog = true; + public fetchingFullDescription = true; + public selectedTabIndex; + public requiredDependencyCount = 0; + public canShowChangelog = true; + public hasIconUrl = false; + public thumbnailLetter = ""; + public hasChangeLog = false; + public showInstallButton = false; + public showUpdateButton = false; + public showRemoveButton = false; + public hasRequiredDependencies = false; + public title = ""; + public subtitle = ""; + public provider = ""; + public summary = ""; + public externalUrl = ""; + public defaultImageUrl = ""; + public version = ""; + public fundingLinks: AddonFundingLink[] = []; + public hasFundingLinks = false; + public fullExternalId = ""; + public externalId = ""; + public displayExternalId = ""; + public isUnknownProvider = false; + public isMissingUnknownDependencies = false; + public missingDependencies: string[] = []; + public previewItems: GalleryItem[] = []; + + public constructor( + @Inject(MAT_DIALOG_DATA) public model: AddonDetailModel, + private _dialogRef: MatDialogRef, + private _addonService: AddonService, + private _addonProviderService: AddonProviderFactory, + private _cdRef: ChangeDetectorRef, + private _snackbarService: SnackbarService, + private _translateService: TranslateService, + private _sessionService: SessionService, + + private _addonUiService: AddonUiService, + private _wowupService: WowUpService, + public gallery: Gallery, + ) { + this._dependencies = this.getDependencies(); + + this._addonService.addonInstalled$ + .pipe(takeUntil(this._destroy$), filter(this.isSameAddon)) + .subscribe(this.onAddonInstalledUpdate); + } + + public ngOnInit(): void { + console.log("model", this.model); + + this.canShowChangelog = this._addonProviderService.canShowChangelog(this.getProviderName()); + + this.thumbnailLetter = this.getThumbnailLetter(); + + this.showInstallButton = !!this.model.searchResult; + + this.showUpdateButton = !!this.model.listItem; + + this.isAddonInstalled() + .then((isInstalled) => (this.showRemoveButton = isInstalled)) + .catch((e) => console.error(e)); + + this.title = this.model.listItem?.addon?.name || this.model.searchResult?.name || "UNKNOWN"; + + this.subtitle = this.model.listItem?.addon?.author || this.model.searchResult?.author || "UNKNOWN"; + + this.provider = this.model.listItem?.addon?.providerName || this.model.searchResult?.providerName || "UNKNOWN"; + + this.summary = this.model.listItem?.addon?.summary || this.model.searchResult?.summary || ""; + + this.externalUrl = this.model.listItem?.addon?.externalUrl || this.model.searchResult?.externalUrl || "UNKNOWN"; + + this.defaultImageUrl = this.model.listItem?.addon?.thumbnailUrl || this.model.searchResult?.thumbnailUrl || ""; + + this.hasIconUrl = !!this.defaultImageUrl; + + this.hasRequiredDependencies = this._dependencies.length > 0; + + this.requiredDependencyCount = this._dependencies.length; + + this.version = + (this.model.searchResult + ? this.getLatestSearchResultFile()?.version + : this.model.listItem?.addon?.installedVersion) ?? ""; + + this.fundingLinks = this.model.listItem?.addon?.fundingLinks ?? []; + + this.hasFundingLinks = !!this.model.listItem?.addon?.fundingLinks?.length; + + this.fullExternalId = + (this.model.searchResult ? this.model.searchResult?.externalId : this.model.listItem?.addon?.externalId) ?? ""; + + this.displayExternalId = this.getDisplayExternalId(this.fullExternalId); + + this.isUnknownProvider = this.model.listItem?.addon?.providerName === ADDON_PROVIDER_UNKNOWN; + + this.missingDependencies = this.model.listItem?.addon?.missingDependencies ?? []; + + this.isMissingUnknownDependencies = !!this.missingDependencies.length; + + const imageUrlList = this.model.listItem?.addon?.screenshotUrls ?? this.model.searchResult?.screenshotUrls ?? []; + + this.previewItems = imageUrlList.map((url) => { + return new ImageItem({ src: url, thumb: url }); + }); + + this.gallery.ref().load(this.previewItems); + + this.selectInitialTab().subscribe(); + } + + public ngAfterViewInit(): void { + from(this.getChangelog()) + .pipe( + first(), + takeUntil(this._destroy$), + tap(() => { + this.fetchingChangelog = false; + }), + ) + .subscribe((changelog) => { + this.hasChangeLog = changelog.length > 0; + this._changelogSrc.next(changelog); + }); + + from(this.getFullDescription()) + .pipe( + takeUntil(this._destroy$), + tap(() => { + this.fetchingFullDescription = false; + }), + ) + .subscribe((description) => { + this._descriptionSrc.next(description); + }); + } + + public ngOnDestroy(): void { + this._destroy$.next(true); + this._destroy$.complete(); + window.getSelection()?.empty(); + } + + public onInstallUpdated(): void { + this._cdRef.detectChanges(); + } + + public async onSelectedTabChange(evt: MatTabChangeEvent): Promise { + await this._sessionService.setSelectedDetailsTab(this.getSelectedTabTypeFromIndex(evt.index)); + } + + public onClickExternalId(): void { + this._snackbarService.showSuccessSnackbar("DIALOGS.ADDON_DETAILS.COPY_ADDON_ID_SNACKBAR", { + timeout: 2000, + }); + } + + public async onClickRemoveAddon(): Promise { + let addon: Addon | null = null; + + // Addon is expected to be available through the model when browsing My Addons tab + if (this._sessionService.getSelectedHomeTab() === TAB_INDEX_MY_ADDONS) { + if (typeof this.model.listItem?.addon?.name !== "string" || this.model.listItem.addon.name.length === 0) { + console.warn("Invalid model list item addon"); + return; + } + + addon = this.model.listItem?.addon; + } else { + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + const externalId = this.model.searchResult?.externalId ?? ""; + const providerName = this.model.searchResult?.providerName ?? ""; + + if (!externalId || !providerName || !selectedInstallation) { + console.warn("Invalid search result when identifying which addon to remove", { + selectedInstallation, + externalId, + providerName, + }); + return; + } + + addon = await this._addonService.getByExternalId(externalId, providerName, selectedInstallation.id); + } + + if (!addon) { + console.warn("Invalid addon when attempting removal"); + return; + } + + this._addonUiService + .handleRemoveAddon(addon) + .pipe( + takeUntil(this._destroy$), + first(), + map((result) => { + if (result.removed) { + this._dialogRef.close(); + } + }), + ) + .subscribe(); + } + + private getSelectedTabTypeFromIndex(index: number): DetailsTabType { + switch (index) { + case 0: + return "description"; + case 1: + return "changelog"; + case 2: + return "previews"; + default: + return "description"; + } + } + + private getSelectedTabTypeIndex(tabType: DetailsTabType): number { + switch (tabType) { + case "description": + return 0; + case "changelog": + return 1; + case "previews": + return this.previewItems.length === 0 ? 0 : 2; + default: + return 0; + } + } + + private getThumbnailLetter(): string { + return this.model?.listItem?.thumbnailLetter ?? this.model.searchResult?.name?.charAt(0).toUpperCase() ?? ""; + } + + private getProviderName(): string { + return this.model.listItem?.addon?.providerName ?? this.model.searchResult?.providerName ?? ""; + } + + private onAddonInstalledUpdate = (evt: AddonUpdateEvent): void => { + if (this.model.listItem) { + this.model.listItem.addon = evt.addon; + this.model.listItem.installState = evt.installState; + } + + this._cdRef.detectChanges(); + }; + + private isSameAddon = (evt: AddonUpdateEvent): boolean => { + return ( + evt.addon.id === this.model.listItem?.addon?.id || evt.addon.externalId === this.model.searchResult?.externalId + ); + }; + + private getChangelog = (): Promise => { + if (this.model.listItem) { + return this.getMyAddonChangelog(); + } else if (this.model.searchResult) { + return this.getSearchResultChangelog(); + } + + return Promise.resolve(""); + }; + + private getFullDescription = async (): Promise => { + const externalId = this.model.searchResult?.externalId ?? this.model.listItem?.addon?.externalId ?? ""; + const providerName = this.model.searchResult?.providerName ?? this.model.listItem?.addon?.providerName ?? ""; + + try { + if (providerName === ADDON_PROVIDER_GITHUB) { + if (this.model.listItem?.addon?.summary) { + return this.model.listItem?.addon?.summary; + } + + throw new Error("Invalid model list item addon"); + } + + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + if (!selectedInstallation) { + throw new Error("No selected installation"); + } + + const description = await this._addonService.getFullDescription( + selectedInstallation, + providerName, + externalId, + this.model?.listItem?.addon, + ); + + return description || this._translateService.instant("DIALOGS.ADDON_DETAILS.DESCRIPTION_NOT_FOUND"); + } catch (e) { + return ""; + } + }; + + private getDependencies(): AddonDependency[] { + if (this.model.searchResult) { + return SearchResult.getDependencyType( + this.model.searchResult, + this.model.channelType ?? AddonChannelType.Stable, + AddonDependencyType.Required, + ); + } else if (this.model.listItem) { + return this.model.listItem.getDependencies(AddonDependencyType.Required); + } + + return []; + } + + private async getSearchResultChangelog() { + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + if (!selectedInstallation) { + console.warn("No selected installation"); + return ""; + } + if (!this.model.searchResult) { + console.warn("Invalid model searchResult"); + return ""; + } + + return await this._addonService.getChangelogForSearchResult( + selectedInstallation, + this.model.channelType ?? AddonChannelType.Stable, + this.model.searchResult, + ); + } + + private async getMyAddonChangelog() { + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + if (!selectedInstallation) { + console.warn("No selected installation"); + return ""; + } + + if (!this.model.listItem?.addon) { + console.warn("Invalid list item addon"); + return ""; + } + + return await this._addonService.getChangelogForAddon(selectedInstallation, this.model.listItem.addon); + } + + private getDisplayExternalId(externalId: string): string { + if (externalId.indexOf("/") !== -1) { + return `...${last(externalId.split("/")) ?? ""}`; + } + + return externalId; + } + + private getLatestSearchResultFile() { + return SearchResult.getLatestFile(this.model.searchResult, this.model.channelType ?? AddonChannelType.Stable); + } + + private async isAddonInstalled(): Promise { + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + if (!selectedInstallation) { + console.warn("No selected installation"); + return false; + } + + const externalId = this.model.searchResult?.externalId ?? this.model.listItem?.addon?.externalId ?? ""; + const providerName = this.model.searchResult?.providerName ?? this.model.listItem?.addon?.providerName ?? ""; + + if (externalId && providerName) { + return await this._addonService.isInstalled(externalId, providerName, selectedInstallation); + } + + console.warn("Invalid list item addon when verifying if installed"); + return false; + } + + private selectInitialTab(): Observable { + return from(this._wowupService.getKeepLastAddonDetailTab()).pipe( + takeUntil(this._destroy$), + first(), + map((shouldUseLastTab) => { + this.selectedTabIndex = shouldUseLastTab + ? this.getSelectedTabTypeIndex(this._sessionService.getSelectedDetailsTab()) + : 0; + this._cdRef.detectChanges(); + }), + catchError((e) => { + console.error(e); + return of(undefined); + }), + ); + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.html b/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.html new file mode 100644 index 0000000..6189aca --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.html @@ -0,0 +1,8 @@ + + {{ buttonText$ | async }} + diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.scss b/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.spec.ts new file mode 100644 index 0000000..0077489 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.spec.ts @@ -0,0 +1,66 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { Subject } from "rxjs"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TestBed } from "@angular/core/testing"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event"; +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { ProgressButtonComponent } from "../../common/progress-button/progress-button.component"; +import { AddonInstallButtonComponent } from "./addon-install-button.component"; +import { WowClientType } from "wowup-lib-core"; + +describe("AddonInstallButtonComponent", () => { + let addonServiceSpy: AddonService; + let sessionServiceSpy: SessionService; + + beforeEach(async () => { + addonServiceSpy = jasmine.createSpyObj( + "AddonService", + { + isInstalled: () => false, + }, + { + addonInstalled$: new Subject().asObservable(), + } + ); + sessionServiceSpy = jasmine.createSpyObj("SessionService", ["getSelectedClientType"], { + selectedClientType: WowClientType.Retail, + }); + + await TestBed.configureTestingModule({ + declarations: [AddonInstallButtonComponent, ProgressButtonComponent], + imports: [ + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(AddonInstallButtonComponent, { + set: { + providers: [ + { provide: AddonService, useValue: addonServiceSpy }, + { provide: SessionService, useValue: sessionServiceSpy }, + ], + }, + }) + .compileComponents(); + }); + + it("should create", () => { + const fixture = TestBed.createComponent(AddonInstallButtonComponent); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.ts b/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.ts new file mode 100644 index 0000000..423b197 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-install-button/addon-install-button.component.ts @@ -0,0 +1,136 @@ +import { BehaviorSubject, Subject } from "rxjs"; +import { filter, takeUntil } from "rxjs/operators"; + +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +import { AddonInstallState } from "../../../models/wowup/addon-install-state"; +import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event"; +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { AddonSearchResult } from "wowup-lib-core"; + +@Component({ + selector: "app-addon-install-button", + templateUrl: "./addon-install-button.component.html", + styleUrls: ["./addon-install-button.component.scss"], +}) +export class AddonInstallButtonComponent implements OnInit, OnDestroy { + @Input() public addonSearchResult!: AddonSearchResult; + + @Output() public onViewUpdated: EventEmitter = new EventEmitter(); + + private readonly _destroy$ = new Subject(); + + public disableButton$ = new BehaviorSubject(false); + public showProgress$ = new BehaviorSubject(false); + public unavailable$ = new BehaviorSubject(false); + public progressValue$ = new BehaviorSubject(0); + public buttonText$ = new BehaviorSubject(""); + + public constructor( + private _addonService: AddonService, + private _sessionService: SessionService, + private _translate: TranslateService, + private _cdRef: ChangeDetectorRef + ) { + this._addonService.addonInstalled$ + .pipe(takeUntil(this._destroy$), filter(this.isSameAddon)) + .subscribe(this.onAddonInstalledUpdate); + } + + public ngOnInit(): void { + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + if (!selectedInstallation) { + console.warn("No selected installation"); + return; + } + + this.unavailable$.next(this.addonSearchResult.externallyBlocked); + + this._addonService + .isInstalled(this.addonSearchResult.externalId, this.addonSearchResult.providerName, selectedInstallation) + .then((isInstalled) => { + this.disableButton$.next(this.addonSearchResult.externallyBlocked || isInstalled); + + if (this.addonSearchResult.externallyBlocked) { + this.buttonText$.next(this._translate.instant("COMMON.ADDON_STATE.UNAVAILABLE") as string); + } else { + this.buttonText$.next( + this.getButtonText(isInstalled ? AddonInstallState.Complete : AddonInstallState.Unknown) + ); + } + }) + .catch((e) => console.error(e)); + } + + public ngOnDestroy(): void { + this._destroy$.next(true); + } + + public getIsButtonActive(installState: AddonInstallState): boolean { + return installState !== AddonInstallState.Unknown && installState !== AddonInstallState.Complete; + } + + public getIsButtonDisabled(installState: AddonInstallState): boolean { + return installState !== AddonInstallState.Unknown; + } + + public getInstallStateText(installState: AddonInstallState): string { + switch (installState) { + case AddonInstallState.BackingUp: + return this._translate.instant("COMMON.ADDON_STATUS.BACKINGUP"); + case AddonInstallState.Complete: + return this._translate.instant("COMMON.ADDON_STATUS.COMPLETE"); + case AddonInstallState.Downloading: + return this._translate.instant("COMMON.ADDON_STATUS.DOWNLOADING"); + case AddonInstallState.Installing: + return this._translate.instant("COMMON.ADDON_STATUS.INSTALLING"); + case AddonInstallState.Pending: + return this._translate.instant("COMMON.ADDON_STATUS.PENDING"); + default: + return ""; + } + } + + public getButtonText(installState: AddonInstallState): string { + if (installState !== AddonInstallState.Unknown) { + return this.getInstallStateText(installState); + } + + return this._translate.instant("COMMON.ADDON_STATE.INSTALL"); + } + + public async onInstallUpdateClick(): Promise { + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + if (!selectedInstallation) { + console.warn("No selected installation"); + return; + } + + this.disableButton$.next(true); + try { + await this._addonService.installPotentialAddon(this.addonSearchResult, selectedInstallation); + } catch (e) { + console.error("onInstallUpdateClick failed", e); + console.error(this.addonSearchResult); + this.disableButton$.next(false); + } + } + + private onAddonInstalledUpdate = (evt: AddonUpdateEvent): void => { + this.showProgress$.next(this.getIsButtonActive(evt.installState)); + this.disableButton$.next(this.getIsButtonDisabled(evt.installState)); + this.progressValue$.next(evt.progress); + this.buttonText$.next(this.getButtonText(evt.installState)); + this.onViewUpdated.emit(true); + this._cdRef.detectChanges(); + }; + + private isSameAddon = (evt: AddonUpdateEvent): boolean => { + return ( + evt.addon.externalId === this.addonSearchResult.externalId && + evt.addon.providerName === this.addonSearchResult.providerName + ); + }; +} diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.html b/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.html new file mode 100644 index 0000000..c72251d --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.html @@ -0,0 +1,158 @@ +
+
+

+ {{ "ADDON_IMPORT.DIALOG_TITLE" | translate: { clientType: selectedInstallation.displayName } }} +

+ +
+
+ +
{{ "ADDON_IMPORT.GENERIC_IMPORT_ERROR" | translate }}
+ + + +
+

+ {{ "ADDON_IMPORT.ACTIVE_ADDON_COUNT" | translate: { count: exportSummary.activeCount } }} + + {{ "ADDON_IMPORT.IGNORED_ADDON_COUNT" | translate: { count: exportSummary.ignoreCount } }} + +

+
+
+ + {{ "ADDON_IMPORT.EXPORT_TEXT_LABEL" | translate }} + + +
+
+ + + +
+
+

+ {{ "ADDON_IMPORT.IMPORT_TEXT_INSTRUCTIONS" | translate }} +

+
+ + {{ "ADDON_IMPORT.IMPORT_TEXT_LABEL" | translate }} + + +
+ +
+

+ {{ "ADDON_IMPORT.IMPORT_TOTAL_COUNT" | translate: { count: importSummaryComparisonCt$ | async } }} +

+

+ {{ "ADDON_IMPORT.IMPORT_CONFLICT_COUNT" | translate: { count: importSummaryConflictCt$ | async } }} + + {{ "ADDON_IMPORT.IMPORT_ADDED_COUNT" | translate: { count: importSummaryAddedCt$ | async } }} + + + {{ "ADDON_IMPORT.IMPORT_NO_CHANGE_COUNT" | translate: { count: importSummaryNoChangeCt$ | async } }} + +

+
+
+
+
+
+ {{ "ADDON_IMPORT.IMPORT_BADGE_NO_CHANGE" | translate }} +
+
+ {{ "ADDON_IMPORT.IMPORT_BADGE_ADDED" | translate }} +
+
+ {{ "ADDON_IMPORT.IMPORT_BADGE_CONFLICT" | translate }} +
+
+
+
+ +
+
+ +
+
+
+
{{ comp.imported.name }}
+ {{ + "ADDON_IMPORT." + comp.conflictReason | translate + }} +
+
+
+
+
+
+
+
+ + +
+ +
+ + + +
+ + + +
diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.scss b/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.scss new file mode 100644 index 0000000..81be980 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.scss @@ -0,0 +1,61 @@ +.addon-manage-dialog { + min-width: 600px; +} + +.export-content { + resize: none; + width: 100%; + min-height: 200px; +} + +.import-content { + resize: none; + width: 100%; + min-height: 200px; +} + +.comparison-row { + margin: 0.5em 0; + display: flex; + flex-direction: row; + border-bottom: 1px solid var(--background-secondary-1); + padding-bottom: 0.25em; + // align-items: center; +} + +.comp-badge { + display: flex; + padding: 0.25em 0.5em; + margin-right: 0.5em; + font-size: 0.8em; + background-color: var(--background-secondary-1); + + img { + width: 20px; + } + + mat-icon { + height: 18px; + } +} + +.comp-text { + padding: 0.25em 0; +} + +.added-badge { + background-color: var(--success-color); +} + +.conflict-badge { + background-color: var(--warning-color); + color: black; +} + +.no-change-badge { + background-color: var(--background-secondary-1); +} +.success-icon { + text-align: center; + filter: invert(23%) sepia(99%) saturate(1821%) hue-rotate(104deg) brightness(96%) contrast(106%); +} diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.ts b/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.ts new file mode 100644 index 0000000..1ddec23 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-manage-dialog/addon-manage-dialog.component.ts @@ -0,0 +1,208 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { BehaviorSubject, Subscription } from "rxjs"; +import { map } from "rxjs/operators"; +import { AddonInstallState } from "../../../models/wowup/addon-install-state"; +import { + AddonBrokerService, + ExportPayload, + ExportSummary, + ImportComparison, + ImportSummary, +} from "../../../services/addons/addon-broker.service"; +import { SessionService } from "../../../services/session/session.service"; +import { SnackbarService } from "../../../services/snackbar/snackbar.service"; +import { ElectronService } from "../../../services"; +import { WowInstallation } from "wowup-lib-core"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; + +interface ImportComparisonViewModel extends ImportComparison { + isInstalling?: boolean; + isCompleted?: boolean; + didError?: boolean; +} + +interface ImportSummaryViewModel extends ImportSummary { + comparisons: ImportComparisonViewModel[]; +} + +@Component({ + selector: "app-addon-manage-dialog", + templateUrl: "./addon-manage-dialog.component.html", + styleUrls: ["./addon-manage-dialog.component.scss"], +}) +export class AddonManageDialogComponent implements OnInit, OnDestroy { + private readonly _subscriptions: Subscription[] = []; + + public readonly selectedTab$ = new BehaviorSubject(0); + public readonly error$ = new BehaviorSubject(""); + + public readonly TAB_IDX_EXPORT = 0; + public readonly TAB_IDX_IMPORT = 1; + + public selectedInstallation: WowInstallation; + public exportSummary: ExportSummary | undefined; + public exportPayload!: string; + public importData = ""; + public installing$ = new BehaviorSubject(false); + public importSummary$ = new BehaviorSubject(undefined); + public hasImportSummary$ = this.importSummary$.pipe(map((summary) => summary !== undefined)); + public importSummaryAddedCt$ = this.importSummary$.pipe(map((summary) => summary?.addedCt ?? 0)); + public importSummaryConflictCt$ = this.importSummary$.pipe(map((summary) => summary?.conflictCt ?? 0)); + public importSummaryNoChangeCt$ = this.importSummary$.pipe(map((summary) => summary?.noChangeCt ?? 0)); + public importSummaryComparisons$ = this.importSummary$.pipe(map((summary) => summary?.comparisons ?? [])); + public importSummaryComparisonCt$ = this.importSummary$.pipe(map((summary) => summary?.comparisons?.length ?? 0)); + public canInstall$ = this.importSummary$.pipe( + map((summary) => { + if (!summary) { + return false; + } + + // if there are any new addons, we can install + return summary.comparisons.some((comp) => comp.state === "added"); + }) + ); + + public constructor( + private _electronService: ElectronService, + private _addonBrokerService: AddonBrokerService, + private _sessionService: SessionService, + private _snackbarService: SnackbarService, + private _warcraftInstallationService: WarcraftInstallationService + ) {} + + public ngOnInit(): void { + this.initAsync().catch((e) => console.error(e)); + } + + public ngOnDestroy(): void { + this._subscriptions.forEach((sub) => sub.unsubscribe()); + } + + public onClickCopy(): void { + this._snackbarService.showSuccessSnackbar("ADDON_IMPORT.EXPORT_STRING_COPIED", { + timeout: 2000, + }); + } + + public async onClickPaste(): Promise { + try { + const txt = await this._electronService.readClipboardText(); + this.importData = txt; + + this._snackbarService.showSuccessSnackbar("ADDON_IMPORT.EXPORT_STRING_PASTED", { + timeout: 2000, + }); + } catch (e) { + console.error(e); + } + } + + public async onClickInstall(): Promise { + try { + this.installing$.next(true); + await this._addonBrokerService.installImportSummary(this.importSummary$.value, this.selectedInstallation); + } catch (e) { + console.error(e); + } finally { + this.installing$.next(false); + } + } + + public async onClickImport(): Promise { + let importJson: ExportPayload; + try { + importJson = await this._addonBrokerService.parseImportString(this.importData); + console.debug(importJson); + } catch (e) { + console.error(e); + this._snackbarService.showErrorSnackbar("ADDON_IMPORT.IMPORT_STRING_INVALID", { + timeout: 2000, + }); + return; + } + + try { + const importSummary = await this._addonBrokerService.getImportSummary(importJson, this.selectedInstallation); + console.debug(importSummary); + + if (importSummary.errorCode !== undefined) { + this._snackbarService.showErrorSnackbar(`ADDON_IMPORT.${importSummary.errorCode}`, { + timeout: 2000, + }); + return; + } + + const viewModel = this.getImportSummaryViewModel(importSummary); + this.importSummary$.next(viewModel); + } catch (e) { + console.error(e); + this._snackbarService.showErrorSnackbar("ADDON_IMPORT.GENERIC_IMPORT_ERROR", { + timeout: 2000, + }); + } + } + + private async initAsync() { + try { + const installation = this._sessionService.getSelectedWowInstallation(); + if (installation !== undefined) { + this.selectedInstallation = { ...installation }; + this.selectedInstallation.label = await this._warcraftInstallationService.getInstallationDisplayName( + this.selectedInstallation + ); + } + + this.exportSummary = await this._addonBrokerService.getExportSummary(this.selectedInstallation); + + const payload = await this._addonBrokerService.getExportPayload(this.selectedInstallation); + + this._electronService + .invoke("base64-encode", JSON.stringify(payload)) + .then((b64) => { + console.debug("B64", b64); + this.exportPayload = b64; + }) + .catch((e) => { + console.error(e); + this.error$.next(`ERROR`); + }); + + const installSub = this._addonBrokerService.addonInstall$.subscribe((evt) => { + console.log("Install", evt); + + const viewModel = { ...this.importSummary$.value }; + if (viewModel?.comparisons === undefined) { + return; + } + + const compVm = viewModel.comparisons.find((comp) => comp.id === evt.comparisonId); + if (compVm === undefined) { + return; + } + + compVm.isInstalling = true; + compVm.isCompleted = evt.installState === AddonInstallState.Complete; + compVm.didError = evt.installState === AddonInstallState.Error; + + this.importSummary$.next(viewModel); + }); + + this._subscriptions.push(installSub); + } catch (e) { + console.error(e); + this.error$.next(`ERROR`); + } + } + + private getImportSummaryViewModel(importSummary: ImportSummary): ImportSummaryViewModel { + const viewModel: ImportSummaryViewModel = { ...importSummary }; + + viewModel.comparisons.forEach((comp) => { + comp.isInstalling = false; + comp.didError = false; + comp.isCompleted = false; + }); + + return viewModel; + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.html b/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.html new file mode 100644 index 0000000..e2f72ce --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.html @@ -0,0 +1,9 @@ +
+ +
+
+
+ {{ getLetter() }} +
+
\ No newline at end of file diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.scss b/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.scss new file mode 100644 index 0000000..dadb647 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.scss @@ -0,0 +1,26 @@ +.addon-logo-container { + width: 40px; + height: 40px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex-shrink: 0; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + overflow: hidden; + + .addon-logo { + height: 100%; + } + + .addon-logo-letter { + font-size: 2em; + font-weight: 400; + } + + img { + height: 100%; + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.spec.ts new file mode 100644 index 0000000..25dc411 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { AddonThumbnailComponent } from "./addon-thumbnail.component"; + +describe("AddonThumbnailComponent", () => { + let component: AddonThumbnailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AddonThumbnailComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AddonThumbnailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.ts b/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.ts new file mode 100644 index 0000000..e5d8275 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-thumbnail/addon-thumbnail.component.ts @@ -0,0 +1,24 @@ +import { Component, Input, OnInit } from "@angular/core"; + +@Component({ + selector: "app-addon-thumbnail", + templateUrl: "./addon-thumbnail.component.html", + styleUrls: ["./addon-thumbnail.component.scss"], +}) +export class AddonThumbnailComponent implements OnInit { + @Input() public url = ""; + @Input() public name = ""; + @Input() public size = 40; + + public constructor() {} + + public ngOnInit(): void {} + + public hasUrl(): boolean { + return !!this.url; + } + + public getLetter(): string { + return this.name?.charAt(0).toUpperCase() ?? ""; + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.html b/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.html new file mode 100644 index 0000000..f8f36f5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.html @@ -0,0 +1,8 @@ + + {{ getButtonText() }} + diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.scss b/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.spec.ts new file mode 100644 index 0000000..650b167 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.spec.ts @@ -0,0 +1,64 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { Subject } from "rxjs"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TestBed } from "@angular/core/testing"; +import { MatDialog } from "@angular/material/dialog"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event"; +import { ElectronService } from "../../../services"; +import { AddonService } from "../../../services/addons/addon.service"; +import { AnalyticsService } from "../../../services/analytics/analytics.service"; +import { ProgressButtonComponent } from "../../common/progress-button/progress-button.component"; +import { AddonUpdateButtonComponent } from "./addon-update-button.component"; + +describe("AddonUpdateButtonComponent", () => { + let addonServiceSpy: AddonService; + let analyticsServiceSpy: AnalyticsService; + + beforeEach(async () => { + addonServiceSpy = jasmine.createSpyObj("AddonService", [""], { + addonInstalled$: new Subject().asObservable(), + }); + + await TestBed.configureTestingModule({ + declarations: [AddonUpdateButtonComponent, ProgressButtonComponent], + providers: [MatDialog, ElectronService], + imports: [ + MatModule, + HttpClientModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(AddonUpdateButtonComponent, { + set: { + providers: [ + MatDialog, + { provide: AddonService, useValue: addonServiceSpy }, + { provide: AnalyticsService, useValue: analyticsServiceSpy }, + ], + }, + }) + .compileComponents(); + }); + + it("should create", () => { + const fixture = TestBed.createComponent(AddonUpdateButtonComponent); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.ts b/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.ts new file mode 100644 index 0000000..4cd55fc --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/addon-update-button/addon-update-button.component.ts @@ -0,0 +1,175 @@ +import { Subscription } from "rxjs"; +import { filter } from "rxjs/operators"; + +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +import { AddonViewModel } from "../../../business-objects/addon-view-model"; +import { AddonInstallState } from "../../../models/wowup/addon-install-state"; +import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event"; +import { AddonService } from "../../../services/addons/addon.service"; +import { getEnumName } from "wowup-lib-core"; +import { ADDON_PROVIDER_UNKNOWN } from "../../../../common/constants"; +import { WowClientType } from "wowup-lib-core"; + +@Component({ + selector: "app-addon-update-button", + templateUrl: "./addon-update-button.component.html", + styleUrls: ["./addon-update-button.component.scss"], +}) +export class AddonUpdateButtonComponent implements OnInit, OnDestroy { + @Input() public listItem!: AddonViewModel; + @Input() public extInstallState?: AddonInstallState; + @Input() public value?: number; + + @Output() public onViewUpdated: EventEmitter = new EventEmitter(); + + private _subscriptions: Subscription[] = []; + private _resetTimeout = -1; + + public installState = AddonInstallState.Unknown; + public installProgress = 0; + public providerName = ""; + public externalId = ""; + + public constructor( + private _addonService: AddonService, + private _translateService: TranslateService, + private _cdRef: ChangeDetectorRef + ) { + const addonInstalledSub = this._addonService.addonInstalled$ + .pipe(filter(this.isSameAddon)) + .subscribe(this.onAddonInstalledUpdate); + + this._subscriptions.push(addonInstalledSub); + } + + public ngOnInit(): void { + if (this.listItem.addon?.providerName === ADDON_PROVIDER_UNKNOWN) { + return; + } + + if ( + this.listItem.addon === undefined || + !this.listItem.addon.id || + !this.listItem.addon.externalId || + !this.listItem.addon.providerName + ) { + console.warn("Invalid list item addon", this.listItem); + return; + } + + this.providerName = this.listItem.addon.providerName; + this.externalId = this.listItem.addon.externalId; + this.installProgress = this.value ?? 0; + + const installStatus = this._addonService.getInstallStatus(this.listItem.addon.id); + if (installStatus) { + this.installProgress = installStatus.progress; + this.installState = installStatus.installState; + } + } + + public ngOnDestroy(): void { + this._subscriptions.forEach((sub) => sub.unsubscribe()); + } + + public getActionLabel(): string { + return `${getEnumName(WowClientType, this.listItem?.addon?.clientType)}|${ + this.listItem?.addon?.providerName ?? "" + }|${this.listItem?.addon?.externalId ?? ""}|${this.listItem?.addon?.name ?? ""}`; + } + + public getIsButtonActive(): boolean { + return ( + this.installState !== AddonInstallState.Unknown && + this.installState !== AddonInstallState.Complete && + this.installState !== AddonInstallState.Error + ); + } + + public getIsButtonDisabled(): boolean { + return this.listItem?.isUpToDate() || this.installState < AddonInstallState.Unknown; + } + + public getButtonText(): string { + if (this.installState !== AddonInstallState.Unknown) { + return this.getInstallStateText(this.installState); + } + + return this.getStatusText(); + } + + public async onInstallUpdateClick(): Promise { + try { + if (this.listItem?.addon?.id === undefined) { + throw new Error("Invalid list item addon"); + } + + if (this.listItem.needsUpdate()) { + await this._addonService.updateAddon(this.listItem.addon); + } else { + await this._addonService.installAddon(this.listItem.addon); + } + } catch (e) { + console.error(e); + } + } + + public getStatusText(): string { + if (this.listItem?.needsInstall()) { + return this._translateService.instant("PAGES.MY_ADDONS.TABLE.ADDON_INSTALL_BUTTON"); + } + + if (this.listItem?.needsUpdate()) { + return this._translateService.instant("PAGES.MY_ADDONS.TABLE.ADDON_UPDATE_BUTTON"); + } + + if (!this.listItem) { + return ""; + } + + return this._translateService.instant(this.listItem.stateTextTranslationKey); + } + + private onAddonInstalledUpdate = (evt: AddonUpdateEvent) => { + this.installState = evt.installState; + this.installProgress = evt.progress; + if (this.installState === AddonInstallState.Error) { + window.clearTimeout(this._resetTimeout); + this._resetTimeout = window.setTimeout(() => { + if (this.installState === AddonInstallState.Error) { + this.installState = AddonInstallState.Unknown; + this.installProgress = 0; + } + this._cdRef.detectChanges(); + }, 2000); + } + this._cdRef.detectChanges(); + }; + + private isSameAddon = (evt: AddonUpdateEvent) => { + return evt.addon.externalId === this.externalId && evt.addon.providerName === this.providerName; + }; + + private getInstallStateText(installState: AddonInstallState) { + switch (installState) { + case AddonInstallState.BackingUp: + return this._translateService.instant("COMMON.ADDON_STATUS.BACKINGUP"); + case AddonInstallState.Complete: + return this._translateService.instant("COMMON.ADDON_STATE.UPTODATE"); + case AddonInstallState.Downloading: + return this._translateService.instant("COMMON.ADDON_STATUS.DOWNLOADING"); + case AddonInstallState.Installing: + return this._translateService.instant("COMMON.ADDON_STATUS.INSTALLING"); + case AddonInstallState.Pending: + return this._translateService.instant("COMMON.ADDON_STATUS.PENDING"); + case AddonInstallState.Error: + return this._translateService.instant("COMMON.ADDON_STATUS.ERROR"); + case AddonInstallState.Retry: + return this._translateService.instant("COMMON.ADDON_STATUS.RETRY"); + default: + return ""; + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.html b/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.html new file mode 100644 index 0000000..c0777f3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.html @@ -0,0 +1 @@ +{{ relativeTime$ | async }} diff --git a/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.scss b/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.spec.ts new file mode 100644 index 0000000..08a3128 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.spec.ts @@ -0,0 +1,52 @@ +import { DatePipe } from "@angular/common"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Subject } from "rxjs"; +import { NgxDatePipe } from "../../../pipes/ngx-date.pipe"; +import { RelativeDurationPipe } from "../../../pipes/relative-duration-pipe"; +import { ElectronService } from "../../../services"; +import { getStandardTestImports } from "../../../utils/test.utils"; + +import { DateTooltipCellComponent } from "./date-tooltip-cell.component"; + +describe("DateTooltipCellComponent", () => { + let component: DateTooltipCellComponent; + let fixture: ComponentFixture; + let electronService: any; + + beforeEach(async () => { + electronService = jasmine.createSpyObj("ElectronService", [""], { + windowFocused$: new Subject(), + }); + + await TestBed.configureTestingModule({ + declarations: [DateTooltipCellComponent, RelativeDurationPipe, NgxDatePipe], + imports: [...getStandardTestImports()], + providers: [RelativeDurationPipe, NgxDatePipe, DatePipe], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(DateTooltipCellComponent, { + set: { + providers: [{ provide: ElectronService, useValue: electronService }], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DateTooltipCellComponent); + component = fixture.componentInstance; + + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + component.agInit({ + value: new Date().getTime(), + } as any); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.ts b/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.ts new file mode 100644 index 0000000..1dac6ef --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/date-tooltip-cell/date-tooltip-cell.component.ts @@ -0,0 +1,53 @@ +import { Component, OnDestroy } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; +import { AgRendererComponent } from "ag-grid-angular"; +import { ICellRendererParams } from "ag-grid-community"; +import { BehaviorSubject, combineLatest, Subject, takeUntil, timer } from "rxjs"; +import { ElectronService } from "../../../services"; +import { getRelativeDateFormat } from "../../../utils/string.utils"; + +@Component({ + selector: "app-date-tooltip-cell", + templateUrl: "./date-tooltip-cell.component.html", + styleUrls: ["./date-tooltip-cell.component.scss"], +}) +export class DateTooltipCellComponent implements AgRendererComponent, OnDestroy { + private readonly _destroy$: Subject = new Subject(); + + public params!: ICellRendererParams; + public time$ = new BehaviorSubject(new Date().toISOString()); + public relativeTime$ = new BehaviorSubject(""); + + public constructor(private _translateService: TranslateService, private _electronService: ElectronService) {} + + public agInit(params: ICellRendererParams): void { + this.params = params; + + this.time$.next(this.params.value as string); + + combineLatest([this._electronService.windowFocused$, timer(0, 30000)]) + .pipe(takeUntil(this._destroy$)) + .subscribe(([focused]) => { + if (!focused && this.relativeTime$.value.length > 0) { + return; + } + + const [fmt, val] = getRelativeDateFormat(this.params.value as string); + if (!fmt) { + return this.relativeTime$.next("ERR"); + } + this.relativeTime$.next(this._translateService.instant(fmt, val) as string); + }); + } + + public ngOnDestroy(): void { + this._destroy$.next(true); + this._destroy$.complete(); + } + + public refresh(): boolean { + return false; + } + + public afterGuiAttached?(): void {} +} diff --git a/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.html b/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.html new file mode 100644 index 0000000..acc189c --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.html @@ -0,0 +1,12 @@ + + + + {{ fundingName }} + + + + + + diff --git a/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.scss b/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.scss new file mode 100644 index 0000000..d7877fe --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.scss @@ -0,0 +1,47 @@ +:host { + margin-right: 0.5em; + + &:last-child { + margin-right: 0; + } +} +.funding-button { + &.large { + margin-right: 0.5em; + .funding-icon { + height: 20px; + width: 20px; + margin-right: 0.5em; + } + + .mat-icon { + height: 20px; + width: 20px; + margin-right: 0.5em; + } + } + + &.small { + padding: 0.25em; + border-radius: 4px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + .funding-icon { + height: 15px; + width: 15px; + } + + .mat-icon { + height: 15px; + width: 15px; + } + } +} +.patreon-icon { + color: var(--patreon-color); +} +.custom-icon { + color: var(--wow-gold-color); +} diff --git a/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.spec.ts new file mode 100644 index 0000000..435f81c --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.spec.ts @@ -0,0 +1,55 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatIconTestingModule } from "@angular/material/icon/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { FundingButtonComponent } from "./funding-button.component"; + +describe("FundingButtonComponent", () => { + let component: FundingButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FundingButtonComponent], + imports: [ + MatModule, + HttpClientModule, + NoopAnimationsModule, + MatIconTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FundingButtonComponent); + component = fixture.componentInstance; + component.funding = { + platform: "TEST", + url: "TEST", + }; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.ts b/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.ts new file mode 100644 index 0000000..a612409 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/funding-button/funding-button.component.ts @@ -0,0 +1,101 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { AddonFundingLink } from "wowup-lib-core"; + +@Component({ + selector: "app-funding-button", + templateUrl: "./funding-button.component.html", + styleUrls: ["./funding-button.component.scss"], +}) +export class FundingButtonComponent implements OnInit { + @Input("funding") public funding!: AddonFundingLink; + @Input("size") public size: "large" | "small" = "large"; + + public isFontIcon = false; + public iconSrc = ""; + public tooltipKey = ""; + public fundingName = ""; + + public ngOnInit(): void { + this.isFontIcon = this.getIsFontIcon(); + this.iconSrc = this.isFontIcon ? this.getFontIcon() : this.getFundingIcon(); + this.fundingName = this.getFundingName(); + this.tooltipKey = this.getFundingLocaleKey(this.fundingName); + } + + public getTooltipKey(): string { + return `PAGES.MY_ADDONS.FUNDING_TOOLTIP.${this.funding.platform.toUpperCase()}`; + } + + public getClassName(): string { + switch (this.funding.platform) { + case "PATREON": + return "patreon-icon"; + case "GITHUB": + return "github-icon"; + default: + return "custom-icon"; + } + } + private getIsFontIcon(): boolean { + switch (this.funding.platform) { + case "PATREON": + case "GITHUB": + return true; + case "LIBERAPAY": + case "CUSTOM": + default: + return true; + } + } + + private getFontIcon(): string { + switch (this.funding.platform) { + case "PATREON": + return "fab:patreon"; + case "GITHUB": + return "fab:github"; + case "LIBERAPAY": + case "CUSTOM": + default: + return "fas:coins"; + } + } + + private getFundingIcon(): string { + switch (this.funding.platform) { + case "LIBERAPAY": + return "assets/images/librepay_logo_small.png"; + case "PATREON": + return "assets/images/patreon_logo_white.png"; + case "GITHUB": + return "assets/images/github_logo_small.png"; + case "CUSTOM": + default: + return "assets/images/custom_funding_logo_small.png"; + } + } + + private getFundingLocaleKey(fundingName: string): string { + return fundingName && fundingName.toUpperCase() !== "CUSTOM" + ? "PAGES.MY_ADDONS.FUNDING_TOOLTIP.GENERIC" + : "PAGES.MY_ADDONS.FUNDING_TOOLTIP.CUSTOM"; + } + + private getFundingName(): string { + switch (this.funding.platform) { + case "LIBERAPAY": + return "Liberapay"; + case "PATREON": + return "Patreon"; + case "GITHUB": + return "GitHub"; + case "PAYPAL": + return "PayPal"; + case "KO_FI": + return "Ko-fi"; + case "CUSTOM": + default: + return "Custom"; + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.html b/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.html new file mode 100644 index 0000000..14498ab --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.html @@ -0,0 +1,13 @@ +
+
+
+ {{ "COMMON.ADDON_STATE.UNAVAILABLE" | translate }} +
+
+ + +
diff --git a/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.scss b/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.scss new file mode 100644 index 0000000..4323f00 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.scss @@ -0,0 +1,13 @@ +.addon-status-column { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 62px; +} +.unavailable { + background-color: var(--background-secondary-3); + display: inline; + padding: 1em; + border-radius: 4px; +} diff --git a/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.spec.ts new file mode 100644 index 0000000..dd0a9be --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { GetAddonStatusColumnComponent } from "./get-addon-status-cell.component"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; + +describe("GetAddonStatusColumnComponent", () => { + let component: GetAddonStatusColumnComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GetAddonStatusColumnComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(GetAddonStatusColumnComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.ts b/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.ts new file mode 100644 index 0000000..498a779 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/get-addon-status-cell/get-addon-status-cell.component.ts @@ -0,0 +1,30 @@ +import { AgRendererComponent } from "ag-grid-angular"; +import { ICellRendererParams } from "ag-grid-community"; + +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { AddonSearchResult } from "wowup-lib-core"; + +@Component({ + selector: "app-get-addon-status-cell", + templateUrl: "./get-addon-status-cell.component.html", + styleUrls: ["./get-addon-status-cell.component.scss"], +}) +export class GetAddonStatusColumnComponent implements AgRendererComponent { + @Input() public addonSearchResult!: AddonSearchResult; + + @Output() public onInstallViewUpdated: EventEmitter = new EventEmitter(); + + public refresh(): boolean { + return false; + } + + public agInit(params: ICellRendererParams): void { + this.addonSearchResult = params.data.searchResult; + } + + public afterGuiAttached?(): void {} + + public onInstallButtonUpdated(): void { + this.onInstallViewUpdated.emit(true); + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.html b/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.html new file mode 100644 index 0000000..1c3dc88 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.html @@ -0,0 +1,69 @@ +

+ {{ "DIALOGS.INSTALL_FROM_PROTOCOL.TITLE" | translate: { providerName: getProviderName() } }} +

+
+
+ +
+
+
+ +
+

{{ getName() }}

+

{{ getAuthor() }}

+

{{ getVersion() }}

+
+
+ + + WoW Installation + + {{ installation.displayName }} + + + + + +
+
+

Error

+

{{ error | translate: { protocol: data.protocol } }}

+
+
+

{{ "DIALOGS.INSTALL_FROM_PROTOCOL.ADDON_INSTALLING" | translate }}

+ +
+
+
+ +
+

{{ "DIALOGS.INSTALL_FROM_PROTOCOL.ADDON_INSTALLED" | translate }}

+
+
+
+ + +
diff --git a/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.scss b/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.scss new file mode 100644 index 0000000..5a274c7 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.scss @@ -0,0 +1,22 @@ +.content { + min-width: 300px; + + .control { + width: 100%; + } + + .success-icon { + text-align: center; + filter: invert(23%) sepia(99%) saturate(1821%) hue-rotate(104deg) brightness(96%) contrast(106%); + } +} +.error { + color: #f04747; +} + +.select { + .mat-icon { + height: 17px; + width: 17px; + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.spec.ts new file mode 100644 index 0000000..672342f --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.spec.ts @@ -0,0 +1,87 @@ +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { AddonService } from "../../../services/addons/addon.service"; +import { IconService } from "../../../services/icons/icon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; + +import { + InstallFromProtocolDialogComponent, + InstallFromProtocolDialogComponentData, +} from "./install-from-protocol-dialog.component"; + +describe("InstallFromProtocolDialogComponent", () => { + let component: InstallFromProtocolDialogComponent; + let fixture: ComponentFixture; + let addonService: AddonService; + let sessionService: SessionService; + let warcraftInstallationService: WarcraftInstallationService; + let dialogModel: InstallFromProtocolDialogComponentData; + + beforeEach(async () => { + addonService = jasmine.createSpyObj( + "AddonService", + { + getAddonForProtocol: () => Promise.resolve(undefined), + }, + {}, + ); + + sessionService = jasmine.createSpyObj("SessionService", [""], {}); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], {}); + + dialogModel = { protocol: "" }; + + const testBed = TestBed.configureTestingModule({ + declarations: [InstallFromProtocolDialogComponent], + imports: [ + MatModule, + HttpClientModule, + NoopAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [{ provide: MAT_DIALOG_DATA, useValue: dialogModel }], + }).overrideComponent(InstallFromProtocolDialogComponent, { + set: { + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: AddonService, useValue: addonService }, + { provide: SessionService, useValue: sessionService }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + { provide: IconService }, + ], + }, + }); + + await testBed.compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InstallFromProtocolDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.ts b/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.ts new file mode 100644 index 0000000..6c4ec8f --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component.ts @@ -0,0 +1,187 @@ +import * as _ from "lodash"; +import { from, of } from "rxjs"; +import { catchError, delay, first, switchMap } from "rxjs/operators"; + +import { AfterViewInit, Component, Inject, OnInit } from "@angular/core"; +import { UntypedFormControl } from "@angular/forms"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; + +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { ProtocolSearchResult } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +export interface InstallFromProtocolDialogComponentData { + protocol: string; +} + +export interface WowInstallationWrapper extends WowInstallation { + isInstalled?: boolean; +} + +const ERROR_ADDON_NOT_FOUND = "DIALOGS.INSTALL_FROM_PROTOCOL.ERRORS.ADDON_NOT_FOUND"; +const ERROR_GENERIC = "DIALOGS.INSTALL_FROM_PROTOCOL.ERRORS.GENERIC"; +const ERROR_NO_VALID_WOW_INSTALLATIONS = "DIALOGS.INSTALL_FROM_PROTOCOL.ERRORS.NO_VALID_WOW_INSTALLATIONS"; + +@Component({ + selector: "app-install-from-protocol-dialog", + templateUrl: "./install-from-protocol-dialog.component.html", + styleUrls: ["./install-from-protocol-dialog.component.scss"], +}) +export class InstallFromProtocolDialogComponent implements OnInit, AfterViewInit { + public error = ""; + public ready = false; + public addon!: ProtocolSearchResult; + public installations = new UntypedFormControl(); + public validWowInstallations: WowInstallationWrapper[] = []; + public installProgress = 0; + public isInstalling = false; + public isComplete = false; + + public constructor( + private _addonService: AddonService, + private _sessionService: SessionService, + private _warcraftInstallationService: WarcraftInstallationService, + @Inject(MAT_DIALOG_DATA) public data: InstallFromProtocolDialogComponentData, + public dialogRef: MatDialogRef, + ) {} + + public ngOnInit(): void {} + + public ngAfterViewInit(): void { + of(true) + .pipe( + first(), + delay(1000), + switchMap(() => from(this.loadAddon())), + catchError((e) => { + console.error(e); + return of(undefined); + }), + ) + .subscribe(); + } + + public getVersion(): string { + return _.first(this.addon?.files)?.version ?? ""; + } + + public getName(): string { + return this.addon?.name ?? ""; + } + + public getThumbnailUrl(): string { + return this.addon?.thumbnailUrl ?? ""; + } + + public getAuthor(): string { + return this.addon?.author ?? "'"; + } + + public getProviderName(): string { + return this.addon?.providerName ?? ""; + } + + public onClose(): void { + this.dialogRef.close(); + } + + public onInstall = async (): Promise => { + console.debug("selectedInstallationId", this.installations.value); + const selectedInstallationIds: string[] = this.installations.value; + const selectedInstallations = this.validWowInstallations.filter((installation) => + selectedInstallationIds.includes(installation.id), + ); + const targetFile = _.first(this.addon.files); + + try { + this.isInstalling = true; + + const totalInstalls = selectedInstallations.length; + let installIdx = 0; + for (const installation of selectedInstallations) { + await this._addonService.installPotentialAddon( + this.addon, + installation, + (state, progress) => { + console.debug("Install Progress", progress); + this.installProgress = (installIdx * 100 + progress) / totalInstalls; + }, + targetFile, + ); + installIdx += 1; + this._sessionService.notifyTargetFileInstallComplete(); + } + + this.isComplete = true; + } catch (e) { + console.error(`Failed to install addon for protocol: ${this.data.protocol}`, e); + this.error = ERROR_GENERIC; + } finally { + this.isInstalling = false; + } + }; + + private async loadAddon(): Promise { + try { + console.log("this.data.protocol", this.data.protocol); + const searchResult = await this._addonService.getAddonForProtocol(this.data.protocol); + if (!searchResult) { + this.error = ERROR_ADDON_NOT_FOUND; + return; + } + + this.addon = searchResult; + + if (Array.isArray(searchResult.validClientGroups)) { + this.validWowInstallations = await this._warcraftInstallationService.getWowInstallationsByClientGroups( + searchResult.validClientGroups, + ); + } else if (Array.isArray(searchResult.validClientTypes)) { + this.validWowInstallations = await this._warcraftInstallationService.getWowInstallationsByClientTypes( + searchResult.validClientTypes, + ); + } else { + throw new Error("No valid clients found"); + } + + if (this.validWowInstallations.length === 0) { + this.error = ERROR_NO_VALID_WOW_INSTALLATIONS; + return; + } + + for (const installation of this.validWowInstallations) { + installation.isInstalled = await this._addonService.isInstalled( + this.addon.externalId, + this.addon.providerName, + installation, + ); + + installation.label = await this._warcraftInstallationService.getInstallationDisplayName(installation); + } + + if (this.validWowInstallations.length === 0) { + return; + } + + const allInstalled = _.every(this.validWowInstallations, (installation) => installation.isInstalled); + if (allInstalled) { + this.isComplete = true; + this.installations.setValue(this.validWowInstallations.map((installation) => installation.id)); + return; + } + + const installationId = _.find(this.validWowInstallations, (installation) => !installation.isInstalled)?.id; + if (!installationId) { + return; + } + this.installations.setValue([installationId]); + } catch (e) { + console.error(`Failed to load protocol addon`, e); + this.error = ERROR_GENERIC; + } finally { + this.ready = true; + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.html b/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.html new file mode 100644 index 0000000..f934b9a --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.html @@ -0,0 +1,72 @@ +

{{ "DIALOGS.INSTALL_FROM_URL.TITLE" | translate }}

+
+

+ {{ "DIALOGS.INSTALL_FROM_URL.DESCRIPTION" | translate }} +

+

{{ "DIALOGS.INSTALL_FROM_URL.SUPPORTED_SOURCES" | translate }}

+ + {{ "DIALOGS.INSTALL_FROM_URL.ADDON_URL_INPUT_LABEL" | translate }} + + + +
+ +
+
+
+
+
+ {{ thumbnailLetter }} +
+
+
+

{{ addon.name }}

+

{{ addon.author }}

+

{{ addon.files?.length ? addon?.files[0]?.version ?? "" : "" }}

+

+ {{ "DIALOGS.INSTALL_FROM_URL.DOWNLOAD_COUNT" | translate: getDownloadCountParams() }} +

+
+
+ + +
+
+ +
+
+ {{ "DIALOGS.INSTALL_FROM_URL.INSTALL_SUCCESS_LABEL" | translate }} +
+
+
+
+
+
+ + +
diff --git a/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.scss b/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.scss new file mode 100644 index 0000000..46d1c6e --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.scss @@ -0,0 +1,60 @@ +.url-input-container { + width: 100%; +} +.busy-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.addon-logo-letter { + font-size: 2em; + font-weight: 400; +} +.addon-container { + display: flex; + flex-direction: row; + + .addon-thumb { + flex-shrink: 0; + width: 50px; + height: 50px; + margin-right: 1em; + background-color: var(--background-secondary-2); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + display: flex; + justify-content: center; + align-items: center; + } + .addon-info { + flex-grow: 1; + h4, + p { + margin: 0; + } + + .addon-download-count { + color: var(--text-3); + } + } + + .install-container { + margin-left: 1em; + flex-shrink: 0; + display: flex; + flex-direction: column; + justify-content: center; + + .success-icon { + text-align: center; + filter: invert(23%) sepia(99%) saturate(1821%) hue-rotate(104deg) brightness(96%) contrast(106%); + } + + .icon-larger { + width: 38px; + height: 38px; + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.spec.ts new file mode 100644 index 0000000..c0f9751 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.spec.ts @@ -0,0 +1,66 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialogRef } from "@angular/material/dialog"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { DownloadCountPipe } from "../../../pipes/download-count.pipe"; +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { InstallFromUrlDialogComponent } from "./install-from-url-dialog.component"; +import { IconService } from "../../../services/icons/icon.service"; + +describe("InstallFromUrlDialogComponent", () => { + console.log("InstallFromUrlDialogComponent"); + let component: InstallFromUrlDialogComponent; + let fixture: ComponentFixture; + let sessionServiceSpy: any; + let addonServiceSpy: any; + + beforeEach(async () => { + const testBed = TestBed.configureTestingModule({ + declarations: [InstallFromUrlDialogComponent], + imports: [ + MatModule, + HttpClientModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }).overrideComponent(InstallFromUrlDialogComponent, { + set: { + providers: [ + DownloadCountPipe, + { provide: MatDialogRef, useValue: {} }, + { provide: AddonService, useValue: addonServiceSpy }, + { provide: SessionService, useValue: sessionServiceSpy }, + { provide: IconService }, + ], + }, + }); + + await testBed.compileComponents(); + + fixture = TestBed.createComponent(InstallFromUrlDialogComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.ts b/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.ts new file mode 100644 index 0000000..93e780c --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/install-from-url-dialog/install-from-url-dialog.component.ts @@ -0,0 +1,243 @@ +import { HttpErrorResponse } from "@angular/common/http"; +import { Component, OnDestroy } from "@angular/core"; +import { MatDialog, MatDialogRef } from "@angular/material/dialog"; +import { from, Subscription } from "rxjs"; +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { AlertDialogComponent } from "../../common/alert-dialog/alert-dialog.component"; +import { TranslateService } from "@ngx-translate/core"; +import { roundDownloadCount, shortenDownloadCount } from "../../../utils/number.utils"; +import { DownloadCountPipe } from "../../../pipes/download-count.pipe"; +import { NO_SEARCH_RESULTS_ERROR } from "../../../../common/constants"; +import { AssetMissingError, GitHubLimitError, NoReleaseFoundError } from "../../../errors"; +import { AddonSearchResult, SearchByUrlResult, WowClientGroup } from "wowup-lib-core"; + +interface DownloadCounts { + count: number; + shortCount: number; + simpleCount: string; + myriadCount: string; + textCount: string; + provider: string; +} + +@Component({ + selector: "app-install-from-url-dialog", + templateUrl: "./install-from-url-dialog.component.html", + styleUrls: ["./install-from-url-dialog.component.scss"], +}) +export class InstallFromUrlDialogComponent implements OnDestroy { + public isBusy = false; + public showInstallSpinner = false; + public showInstallButton = false; + public showInstallSuccess = false; + public query = ""; + public addon?: AddonSearchResult; + public hasThumbnail = false; + public thumbnailLetter = ""; + + private _installSubscription?: Subscription; + + public constructor( + private _addonService: AddonService, + private _dialog: MatDialog, + private _sessionService: SessionService, + private _translateService: TranslateService, + private _downloadCountPipe: DownloadCountPipe, + public dialogRef: MatDialogRef, + ) {} + + public ngOnDestroy(): void { + this._installSubscription?.unsubscribe(); + } + + public onClose(): void { + this.dialogRef.close(); + } + + public onClearSearch(): void { + this.query = ""; + this.onImportUrl().catch((error) => console.error(error)); + } + + public onInstall(): void { + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + if (!selectedInstallation || !this.addon) { + return; + } + + const addonFile = Array.isArray(this.addon?.files) ? this.addon.files[0] : undefined; + if (addonFile === undefined) { + console.error("addon file was undefined, nothing to install"); + this.showErrorMessage(this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.INSTALL_FAILED") as string); + return; + } + + this.showInstallButton = false; + this.showInstallSpinner = true; + + this._installSubscription = from( + this._addonService.installPotentialAddon(this.addon, selectedInstallation, undefined, addonFile), + ).subscribe({ + next: () => { + this.showInstallSpinner = false; + this.showInstallSuccess = true; + }, + error: (err) => { + console.error(err); + this.showInstallSpinner = false; + this.showInstallButton = true; + this.showErrorMessage( + this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.INSTALL_FAILED") as string, + ); + }, + }); + } + + public getDownloadCountParams(): DownloadCounts { + const count = this.addon?.downloadCount ?? 0; + return { + count, + shortCount: roundDownloadCount(count), + simpleCount: shortenDownloadCount(count, 1), + myriadCount: shortenDownloadCount(count, 4), + textCount: this._downloadCountPipe.transform(count), + provider: this.addon?.providerName ?? "", + }; + } + + public async onImportUrl(): Promise { + this.addon = undefined; + this.showInstallSuccess = false; + this.showInstallSpinner = false; + this.hasThumbnail = false; + this.thumbnailLetter = ""; + + if (!this.query) { + return; + } + + const url: URL | undefined = this.getUrlFromQuery(); + if (!url) { + return; + } + + try { + const selectedInstallation = this._sessionService.getSelectedWowInstallation(); + if (!selectedInstallation) { + throw new Error(`Selected installation not found`); + } + + const searchByUrlResult = await this._addonService.getAddonByUrl(url, selectedInstallation); + if (!searchByUrlResult) { + throw new Error("Addon not found"); + } + + this.addon = searchByUrlResult.searchResult; + this.hasThumbnail = !!this.addon.thumbnailUrl; + this.thumbnailLetter = this.addon.name.charAt(0).toUpperCase(); + + const addonInstalled = await this._addonService.isInstalled( + this.addon.externalId, + this.addon.providerName, + selectedInstallation, + ); + + this.handleImportErrors(searchByUrlResult); + + if (addonInstalled) { + this.showInstallSuccess = true; + this.showInstallButton = false; + return; + } + + this.showInstallButton = true; + } catch (err) { + console.error(err); + + let message: string = err.message; + if (err instanceof HttpErrorResponse) { + message = this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.NO_ADDON_FOUND"); + } else if (err.code && err.code === "EOPENBREAKER") { + // Provider circuit breaker is open + message = this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.FAILED_TO_CONNECT"); + } else if (message === NO_SEARCH_RESULTS_ERROR) { + message = this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.NO_SEARCH_RESULTS"); + } else if (err instanceof AssetMissingError) { + let key = "ERROR.ASSET_NOT_FOUND"; + switch (err.clientGroup) { + case WowClientGroup.BurningCrusade: + key = "DIALOGS.INSTALL_FROM_URL.ERROR.BURNING_CRUSADE_ASSET_NOT_FOUND"; + break; + case WowClientGroup.Classic: + key = "DIALOGS.INSTALL_FROM_URL.ERROR.CLASSIC_ASSET_NOT_FOUND"; + break; + case WowClientGroup.WOTLK: + key = "DIALOGS.INSTALL_FROM_URL.ERROR.WRATH_ASSET_NOT_FOUND"; + break; + case WowClientGroup.Cata: + key = "DIALOGS.INSTALL_FROM_URL.ERROR.CATACLYSM_ASSET_NOT_FOUND"; + break; + case WowClientGroup.Retail: + default: + } + message = this._translateService.instant(key, { + message: err.message, + }); + } else if (err instanceof NoReleaseFoundError) { + message = this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.NO_RELEASE_FOUND", { + message: err.message, + }); + } else if (err instanceof GitHubLimitError) { + const max = err.rateLimitMax; + const reset = new Date(err.rateLimitReset * 1000).toLocaleString(); + message = this._translateService.instant("COMMON.ERRORS.GITHUB_LIMIT_ERROR", { + max, + reset, + }); + } + + this.showErrorMessage(message); + } + } + + private handleImportErrors(result: SearchByUrlResult) { + if (!Array.isArray(result.errors)) { + return; + } + + for (const error of result.errors) { + if (error instanceof AssetMissingError) { + const message: string = this._translateService.instant("DIALOGS.INSTALL_FROM_URL.IMPORT_ASSET_WARNING", { + zipName: result.searchResult.files[0].version, + }); + const title: string = this._translateService.instant("DIALOGS.INSTALL_FROM_URL.IMPORT_WARNING_TITLE"); + + this.showErrorMessage(message, title); + } + } + } + + private getUrlFromQuery(): URL | undefined { + try { + return new URL(this.query); + } catch (err) { + console.error(`Invalid url: ${this.query}`); + this.showErrorMessage(this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.INVALID_URL") as string); + return undefined; + } + } + + private showErrorMessage(errorMessage: string, title?: string) { + const dialogRef = this._dialog.open(AlertDialogComponent, { + minWidth: 250, + data: { + title: title || this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.TITLE"), + message: errorMessage, + positiveButtonStyle: "raised", + positiveButtonColor: "primary", + }, + }); + dialogRef.afterClosed().subscribe(); + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.html b/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.html new file mode 100644 index 0000000..510d7f4 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.html @@ -0,0 +1,21 @@ +
+ + +
+ {{ statusText | translate }} +
+
+ +
+
diff --git a/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.scss b/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.spec.ts new file mode 100644 index 0000000..dfe0285 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.spec.ts @@ -0,0 +1,69 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { Subject } from "rxjs"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialog } from "@angular/material/dialog"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event"; +import { AddonService } from "../../../services/addons/addon.service"; +import { MyAddonStatusCellComponent } from "./my-addon-status-cell.component"; + +describe("MyAddonStatusCellComponent", () => { + let component: MyAddonStatusCellComponent; + let fixture: ComponentFixture; + let addonServiceSpy: AddonService; + + beforeEach(async () => { + addonServiceSpy = jasmine.createSpyObj( + "AddonService", + { + getAddons: Promise.resolve([]), + backfillAddons: Promise.resolve(undefined), + }, + { + addonInstalled$: new Subject().asObservable(), + addonRemoved$: new Subject().asObservable(), + }, + ); + + await TestBed.configureTestingModule({ + declarations: [MyAddonStatusCellComponent], + providers: [MatDialog], + imports: [ + MatModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(MyAddonStatusCellComponent, { + set: { + providers: [{ provide: AddonService, useValue: addonServiceSpy }], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MyAddonStatusCellComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.ts b/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.ts new file mode 100644 index 0000000..58a5ecb --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/my-addon-status-cell/my-addon-status-cell.component.ts @@ -0,0 +1,135 @@ +import { AgRendererComponent } from "ag-grid-angular"; +import { ICellRendererParams } from "ag-grid-community"; +import { Subject } from "rxjs"; +import { filter, takeUntil } from "rxjs/operators"; + +import { Component, NgZone, OnDestroy } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { TranslateService } from "@ngx-translate/core"; + +import { AddonViewModel } from "../../../business-objects/addon-view-model"; +import { AddonInstallState } from "../../../models/wowup/addon-install-state"; +import { AddonService } from "../../../services/addons/addon.service"; +import * as AddonUtils from "../../../utils/addon.utils"; +import { AlertDialogComponent } from "../../common/alert-dialog/alert-dialog.component"; +import { Addon, AddonWarningType } from "wowup-lib-core"; + +@Component({ + selector: "app-my-addon-status-cell", + templateUrl: "./my-addon-status-cell.component.html", + styleUrls: ["./my-addon-status-cell.component.scss"], +}) +export class MyAddonStatusCellComponent implements AgRendererComponent, OnDestroy { + private readonly _destroy$: Subject = new Subject(); + + public listItem!: AddonViewModel; + public warningType?: AddonWarningType; + public hasWarning = false; + public showStatusText = false; + public statusText = ""; + public isIgnored = false; + public installState?: AddonInstallState; + public installProgress?: number; + + public constructor( + private _dialog: MatDialog, + private _addonService: AddonService, + private _translateService: TranslateService, + private _ngZone: NgZone, + ) { + this._addonService.addonInstalled$ + .pipe( + takeUntil(this._destroy$), + filter( + (evt) => + evt.addon.externalId === this.listItem.addon?.externalId && + evt.addon.providerName === this.listItem.addon?.providerName, + ), + ) + .subscribe((evt) => { + this._ngZone.run(() => { + this.installState = evt.installState; + this.installProgress = evt.progress; + + if (evt.installState !== AddonInstallState.Complete) { + this.showStatusText = false; + } else { + this.showStatusText = !AddonUtils.needsUpdate(evt.addon) || (this.listItem?.addon?.isIgnored ?? true); + } + this.statusText = this.getStatusText(evt.addon); + }); + }); + } + + public agInit(params: ICellRendererParams): void { + this.listItem = params.data; + + this.warningType = this.listItem?.addon?.warningType; + this.hasWarning = this.warningType !== undefined && this.warningType !== AddonWarningType.GameVersionTocMissing; + this.showStatusText = this.listItem?.isUpToDate() || (this.listItem?.addon?.isIgnored ?? true); + this.statusText = this.getStatusText(this.listItem?.addon); + this.isIgnored = this.listItem.addon?.isIgnored ?? true; + } + + public ngOnDestroy(): void { + this._destroy$.next(true); + this._destroy$.complete(); + } + + public refresh(): boolean { + return false; + } + + public afterGuiAttached?(): void {} + + public getStatusText(addon: Addon | undefined, installState = AddonInstallState.Unknown): string { + if (!addon) { + return ""; + } + + if (addon.isIgnored) { + return "COMMON.ADDON_STATE.IGNORED"; + } + + if (installState === AddonInstallState.Pending) { + return "COMMON.ADDON_STATE.PENDING"; + } + + if (!AddonUtils.needsUpdate(addon)) { + return "COMMON.ADDON_STATE.UPTODATE"; + } + + return this.listItem.stateTextTranslationKey; + } + + public getWarningDescriptionKey(): string { + if (!this.warningType) { + return ""; + } + + switch (this.warningType) { + case AddonWarningType.MissingOnProvider: + return "COMMON.ADDON_WARNING.MISSING_ON_PROVIDER_DESCRIPTION"; + case AddonWarningType.NoProviderFiles: + return "COMMON.ADDON_WARNING.NO_PROVIDER_FILES_DESCRIPTION"; + case AddonWarningType.TocNameMismatch: + return "COMMON.ADDON_WARNING.TOC_NAME_MISMATCH_DESCRIPTION"; + case AddonWarningType.GameVersionTocMissing: + return "COMMON.ADDON_WARNING.GAME_VERSION_TOC_MISSING_DESCRIPTION"; + default: + return "COMMON.ADDON_WARNING.GENERIC_DESCRIPTION"; + } + } + + public onWarningButton(): void { + const descriptionKey = this.getWarningDescriptionKey(); + this._dialog.open(AlertDialogComponent, { + data: { + title: this._translateService.instant("COMMON.ADDON_STATE.WARNING"), + message: this._translateService.instant(descriptionKey, { + providerName: this.listItem.providerName, + }), + }, + }); + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.html b/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.html new file mode 100644 index 0000000..1f2e246 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.html @@ -0,0 +1,91 @@ +
+
+
+ +
+
+ + +
+
+ + +
+
+ {{ channelTranslationKey$ | async | translate }} +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ {{ installedVersion$ | async }} +
+ +
{{ latestVersion$ | async }}
+
+
+
+
+
diff --git a/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.scss b/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.scss new file mode 100644 index 0000000..09610bf --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.scss @@ -0,0 +1,102 @@ +.addon-column { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + padding-top: 0.5em; + padding-bottom: 0.5em; + + .thumbnail-container { + margin-right: 11px; + + .addon-logo-container { + width: 40px; + height: 40px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex-shrink: 0; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + overflow: hidden; + + .addon-logo { + height: 100%; + } + + .addon-logo-letter { + font-size: 2em; + font-weight: 400; + } + + img { + height: 100%; + } + } + } + + .channel { + text-align: center; + padding: 0 4px; + border-radius: 4px; + + &.beta { + color: var(--rare-color); + } + + &.alpha { + color: var(--epic-color); + } + } + + .version-container { + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 40px; + } + + .title-container { + // padding-bottom: 0.5em; + } + + .addon-title { + display: -webkit-box; + font-size: 16px; + line-height: 16px; + overflow: hidden; + text-decoration: none; + white-space: normal; + word-break: break-word; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + &:hover { + cursor: pointer; + text-decoration: underline; + color: var(--text-2); + } + } + + .addon-version { + color: var(--text-2); + font-size: 14px; + line-height: 14px; + } + .upgrade-icon { + height: 10px; + } + + .auto-update-icon { + height: 11px; + width: 11px; + } + + .addon-funding { + line-height: 22px; + display: flex; + flex-direction: row; + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.spec.ts new file mode 100644 index 0000000..c944008 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.spec.ts @@ -0,0 +1,72 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { AddonViewModel } from "../../../business-objects/addon-view-model"; +import { MatModule } from "../../../modules/mat-module"; +import { SessionService } from "../../../services/session/session.service"; +import { MyAddonsAddonCellComponent } from "./my-addons-addon-cell.component"; +import { BehaviorSubject } from "rxjs"; +import { Addon, AddonChannelType, WowClientType } from "wowup-lib-core"; + +describe("MyAddonsAddonCellComponent", () => { + let component: MyAddonsAddonCellComponent; + let fixture: ComponentFixture; + let sessionService: SessionService; + + beforeEach(async () => { + sessionService = jasmine.createSpyObj("SessionService", [""], { + myAddonsCompactVersion$: new BehaviorSubject(false), + }); + + await TestBed.configureTestingModule({ + declarations: [MyAddonsAddonCellComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(MyAddonsAddonCellComponent, { + set: { + providers: [{ provide: SessionService, useValue: sessionService }], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(MyAddonsAddonCellComponent); + component = fixture.componentInstance; + + component.listItem = new AddonViewModel({ + name: "Test Tool", + dependencies: [], + isIgnored: false, + isLoadOnDemand: false, + autoUpdateEnabled: false, + autoUpdateNotificationsEnabled: false, + clientType: WowClientType.Retail, + channelType: AddonChannelType.Stable, + } as Addon); + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.ts b/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.ts new file mode 100644 index 0000000..09c6fea --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/my-addons-addon-cell/my-addons-addon-cell.component.ts @@ -0,0 +1,199 @@ +import { AgRendererComponent } from "ag-grid-angular"; +import { ICellRendererParams } from "ag-grid-community"; +import { BehaviorSubject, combineLatest } from "rxjs"; +import { filter, map } from "rxjs/operators"; + +import { Component } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +import { ADDON_PROVIDER_UNKNOWN } from "../../../../common/constants"; +import { AddonViewModel } from "../../../business-objects/addon-view-model"; +import { DialogFactory } from "../../../services/dialog/dialog.factory"; +import { SessionService } from "../../../services/session/session.service"; +import * as AddonUtils from "../../../utils/addon.utils"; +import { AddonChannelType, AddonDependencyType, AddonWarningType } from "wowup-lib-core"; + +@Component({ + selector: "app-my-addons-addon-cell", + templateUrl: "./my-addons-addon-cell.component.html", + styleUrls: ["./my-addons-addon-cell.component.scss"], +}) +export class MyAddonsAddonCellComponent implements AgRendererComponent { + private readonly _listItemSrc = new BehaviorSubject(undefined); + + public readonly listItem$ = this._listItemSrc.asObservable().pipe(filter((item) => item !== undefined)); + + public readonly name$ = this.listItem$.pipe(map((item) => item.name)); + + public readonly isIgnored$ = this.listItem$.pipe(map((item) => item.isIgnored)); + + public readonly hasWarning$ = this.listItem$.pipe(map((item) => this.hasWarning(item))); + + public readonly hasFundingLinks$ = this.listItem$.pipe( + map((item) => Array.isArray(item.addon?.fundingLinks) && item.addon.fundingLinks.length > 0), + ); + + public readonly fundingLinks$ = this.listItem$.pipe(map((item) => item.addon?.fundingLinks ?? [])); + + public readonly showChannel$ = this.listItem$.pipe(map((item) => item.isBetaChannel() || item.isAlphaChannel())); + + public readonly channelClass$ = this.listItem$.pipe( + map((item) => { + if (item.isBetaChannel()) { + return "beta"; + } + if (item.isAlphaChannel()) { + return "alpha"; + } + return ""; + }), + ); + + public readonly channelTranslationKey$ = this.listItem$.pipe( + map((item) => { + const channelType = item.addon?.channelType ?? AddonChannelType.Stable; + return channelType === AddonChannelType.Alpha + ? "COMMON.ENUM.ADDON_CHANNEL_TYPE.ALPHA" + : "COMMON.ENUM.ADDON_CHANNEL_TYPE.BETA"; + }), + ); + + public readonly hasMultipleProviders$ = this.listItem$.pipe( + map((item) => (item.addon === undefined ? false : AddonUtils.hasMultipleProviders(item.addon))), + ); + + public readonly autoUpdateEnabled$ = this.listItem$.pipe(map((item) => item.addon?.autoUpdateEnabled ?? false)); + + public readonly hasIgnoreReason$ = this.listItem$.pipe(map((item) => this.hasIgnoreReason(item))); + + public readonly hasRequiredDependencies$ = this.listItem$.pipe( + map((item) => this.getRequireDependencyCount(item) > 0), + ); + + public readonly dependencyTooltip$ = this.listItem$.pipe( + map((item) => { + return { + dependencyCount: this.getRequireDependencyCount(item), + }; + }), + ); + + public readonly isLoadOnDemand$ = this.listItem$.pipe(map((item) => item.isLoadOnDemand)); + + public readonly ignoreTooltipKey$ = this.listItem$.pipe(map((item) => this.getIgnoreTooltipKey(item))); + + public readonly ignoreIcon$ = this.listItem$.pipe(map((item) => this.getIgnoreIcon(item))); + + public readonly warningText$ = this.listItem$.pipe( + filter((item) => this.hasWarning(item)), + map((item) => this.getWarningText(item)), + ); + + public readonly isUnknownAddon$ = this.listItem$.pipe( + map((item) => { + return ( + !item.isLoadOnDemand && + !this.hasIgnoreReason(item) && + !this.hasWarning(item) && + item.addon.providerName === ADDON_PROVIDER_UNKNOWN + ); + }), + ); + + public readonly installedVersion$ = this.listItem$.pipe(map((item) => item.addon.installedVersion)); + + public readonly latestVersion$ = this.listItem$.pipe(map((item) => item.addon.latestVersion)); + + public readonly thumbnailUrl$ = this.listItem$.pipe(map((item) => item.addon.thumbnailUrl)); + + public readonly showUpdateVersion$ = combineLatest([ + this.listItem$, + this.sessionService.myAddonsCompactVersion$, + ]).pipe( + map(([item, compactVersion]) => { + return compactVersion && item.needsUpdate(); + }), + ); + + public set listItem(item: AddonViewModel) { + this._listItemSrc.next(item); + } + + public constructor( + private _translateService: TranslateService, + private _dialogFactory: DialogFactory, + public sessionService: SessionService, + ) {} + + public agInit(params: ICellRendererParams): void { + this._listItemSrc.next(params.data as AddonViewModel); + } + + public refresh(): boolean { + return false; + } + + public afterGuiAttached?(): void {} + + public viewDetails(): void { + this._dialogFactory.getAddonDetailsDialog(this._listItemSrc.value); + } + + public getRequireDependencyCount(item: AddonViewModel): number { + return item.getDependencies(AddonDependencyType.Required).length; + } + + public hasIgnoreReason(item: AddonViewModel): boolean { + return !!item?.addon?.ignoreReason; + } + + public getIgnoreTooltipKey(item: AddonViewModel): string { + switch (item.addon?.ignoreReason) { + case "git_repo": + return "PAGES.MY_ADDONS.ADDON_IS_CODE_REPOSITORY"; + case "missing_dependency": + case "unknown": + default: + return ""; + } + } + + public getIgnoreIcon(item: AddonViewModel): string { + switch (item.addon?.ignoreReason) { + case "git_repo": + return "fas:code"; + case "missing_dependency": + case "unknown": + default: + return ""; + } + } + + public hasWarning(item: AddonViewModel): boolean { + return item?.addon?.warningType !== undefined; + } + + public getWarningText(item: AddonViewModel): string { + console.log(item); + if (!this.hasWarning(this._listItemSrc.value)) { + return "data"; + } + + const toolTipParams = { + providerName: this._listItemSrc.value.providerName, + }; + + switch (item.addon.warningType) { + case AddonWarningType.MissingOnProvider: + return this._translateService.instant("COMMON.ADDON_WARNING.MISSING_ON_PROVIDER_TOOLTIP", toolTipParams); + case AddonWarningType.NoProviderFiles: + return this._translateService.instant("COMMON.ADDON_WARNING.NO_PROVIDER_FILES_TOOLTIP", toolTipParams); + case AddonWarningType.TocNameMismatch: + return this._translateService.instant("COMMON.ADDON_WARNING.TOC_NAME_MISMATCH_TOOLTIP", toolTipParams); + case AddonWarningType.GameVersionTocMissing: + return this._translateService.instant("COMMON.ADDON_WARNING.GAME_VERSION_TOC_MISSING_TOOLTIP", toolTipParams); + default: + return this._translateService.instant("COMMON.ADDON_WARNING.GENERIC_TOOLTIP", toolTipParams); + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.html b/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.html new file mode 100644 index 0000000..6ad25bc --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.html @@ -0,0 +1,23 @@ + +
+
+ +
+
+ {{ addon.name }} +
+
+ {{ channelTranslationKey | translate }} +
+ + + + + {{ addon | getAddonListItemFileProp: "version":channel }} + +
+
+
+
\ No newline at end of file diff --git a/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.scss b/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.scss new file mode 100644 index 0000000..bc6922e --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.scss @@ -0,0 +1,98 @@ +.addon-column { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + height: 100%; + + .thumbnail-container { + margin-right: 11px; + + .addon-logo-container { + width: 40px; + height: 40px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex-shrink: 0; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + overflow: hidden; + + .addon-logo { + width: 100%; + } + + img { + height: 100%; + } + } + + .addon-logo-letter { + font-size: 2em; + font-weight: 400; + } + } + + .channel { + background: var(--background-secondary-4); + text-align: center; + font-weight: 400; + padding: 1px 4px; + border-radius: 4px; + + &.beta { + color: var(--rare-color); + } + + &.alpha { + color: var(--epic-color); + } + } + + .addon-text { + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 40px; + overflow: hidden; + } + + .addon-title { + display: -webkit-box; + font-size: 16px; + line-height: 16px; + overflow: hidden; + text-decoration: none; + white-space: normal; + word-break: break-word; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + &:hover { + cursor: pointer; + text-decoration: underline; + color: var(--text-2); + } + } + + .addon-version { + color: var(--text-2); + font-size: 14px; + line-height: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + flex-direction: row; + } + + .dependency-icon { + .mat-icon { + width: 11px; + height: 11px; + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.spec.ts new file mode 100644 index 0000000..d4abaf5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.spec.ts @@ -0,0 +1,46 @@ +import { TestBed } from "@angular/core/testing"; +import { GetAddonListItemFilePropPipe } from "../../../pipes/get-addon-list-item-file-prop.pipe"; +import { DialogFactory } from "../../../services/dialog/dialog.factory"; +import { PotentialAddonTableCellComponent } from "./potential-addon-table-cell.component"; + +describe("PotentialAddonTableColumnComponent", () => { + let dialogFactory: DialogFactory; + + beforeEach(async () => { + dialogFactory = jasmine.createSpyObj("DialogFactory", [""], {}); + + await TestBed.configureTestingModule({ + declarations: [PotentialAddonTableCellComponent, GetAddonListItemFilePropPipe], + imports: [ + // MatModule, + // NoopAnimationsModule, + // HttpClientModule, + // MatDialogModule, + // TranslateModule.forRoot({ + // loader: { + // provide: TranslateLoader, + // useFactory: httpLoaderFactory, + // deps: [HttpClient], + // }, + // compiler: { + // provide: TranslateCompiler, + // useClass: TranslateMessageFormatCompiler, + // }, + // }), + ], + providers: [GetAddonListItemFilePropPipe], + }) + .overrideComponent(PotentialAddonTableCellComponent, { + set: { + providers: [{ provide: DialogFactory, useValue: dialogFactory }], + }, + }) + .compileComponents(); + }); + + it("should create", () => { + const fixture = TestBed.createComponent(PotentialAddonTableCellComponent); + const component = fixture.componentInstance; + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.ts b/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.ts new file mode 100644 index 0000000..7d97a08 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/potential-addon-table-cell/potential-addon-table-cell.component.ts @@ -0,0 +1,119 @@ +import { AgRendererComponent } from "ag-grid-angular"; +import { ICellRendererParams } from "ag-grid-community"; + +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core"; + +import { GetAddonListItem } from "../../../business-objects/get-addon-list-item"; +import { GetAddonListItemFilePropPipe } from "../../../pipes/get-addon-list-item-file-prop.pipe"; +import { DialogFactory } from "../../../services/dialog/dialog.factory"; +import * as SearchResults from "../../../utils/search-result.utils"; +import { + AddonChannelType, + AddonDependencyType, + AddonSearchResult, + AddonSearchResultDependency, + WowClientType, +} from "wowup-lib-core"; + +export interface PotentialAddonViewDetailsEvent { + searchResult: AddonSearchResult; + channelType: AddonChannelType; +} + +@Component({ + selector: "app-potential-addon-table-cell", + templateUrl: "./potential-addon-table-cell.component.html", + styleUrls: ["./potential-addon-table-cell.component.scss"], +}) +export class PotentialAddonTableCellComponent implements AgRendererComponent, OnChanges { + @Input("addon") public addon!: GetAddonListItem; + @Input() public channel!: AddonChannelType; + @Input() public clientType!: WowClientType; + + @Output() public onViewDetails: EventEmitter = new EventEmitter(); + + private _latestChannelType: AddonChannelType = AddonChannelType.Stable; + private _requiredDependencies: AddonSearchResultDependency[] = []; + + public get isBetaChannel(): boolean { + return this._latestChannelType === AddonChannelType.Beta; + } + + public get isAlphaChannel(): boolean { + return this._latestChannelType === AddonChannelType.Alpha; + } + + public get hasThumbnail(): boolean { + return !!this.addon.thumbnailUrl; + } + + public get thumbnailLetter(): string { + return this.addon.name.charAt(0).toUpperCase(); + } + + public get dependencyTooltip(): { dependencyCount: number } { + return { + dependencyCount: this.getRequiredDependencyCount(), + }; + } + + public get channelTranslationKey(): string { + return this._latestChannelType === AddonChannelType.Alpha + ? "COMMON.ENUM.ADDON_CHANNEL_TYPE.ALPHA" + : "COMMON.ENUM.ADDON_CHANNEL_TYPE.BETA"; + } + + public constructor( + private _getAddonListItemFileProp: GetAddonListItemFilePropPipe, + private _dialogFactory: DialogFactory + ) {} + + public agInit(params: ICellRendererParams): void { + this.clientType = (params as any).clientType; + this.channel = (params as any).channel; + this.addon = params.data; + this._latestChannelType = this.addon.latestAddonChannel; + } + + public refresh(): boolean { + return false; + } + + public afterGuiAttached?(): void {} + + public ngOnChanges(changes: SimpleChanges): void { + if (changes.clientType) { + if (this.addon.latestAddonChannel !== this.channel) { + this._latestChannelType = this.addon.latestAddonChannel; + } else { + this._latestChannelType = this._getAddonListItemFileProp.transform( + this.addon, + "channelType", + this.channel + ) as AddonChannelType; + } + + this._requiredDependencies = this.getRequiredDependencies(); + } + } + + public viewDetails(): void { + this._dialogFactory.getPotentialAddonDetailsDialog(this.addon.searchResult, this.channel); + } + + public getRequiredDependencyCount(): number { + return this._requiredDependencies.length; + } + + public hasRequiredDependencies(): boolean { + return this._requiredDependencies.length > 0; + } + + public getRequiredDependencies(): AddonSearchResultDependency[] { + return SearchResults.getDependencyType( + this.addon.searchResult, + this._latestChannelType, + AddonDependencyType.Required + ); + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.html b/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.html new file mode 100644 index 0000000..c05f59c --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.html @@ -0,0 +1,13 @@ +
+
+ + {{params.displayName}} + + + + + + + +
+
\ No newline at end of file diff --git a/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.scss b/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.scss new file mode 100644 index 0000000..550bdd7 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.scss @@ -0,0 +1,12 @@ +.ag-cell-label-container { + font-family: Roboto, "Helvetica Neue", sans-serif; + font-size: 1.1em; + font-weight: 500; +} + +.ag-header-icon { + mat-icon { + width: 14px; + height: 14px; + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.spec.ts b/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.spec.ts new file mode 100644 index 0000000..65c7b93 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.spec.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { SessionService } from "../../../services/session/session.service"; +import { TableContextHeaderCellComponent } from "./table-context-header-cell.component"; + +describe("TableContextHeaderCellComponent", () => { + let component: TableContextHeaderCellComponent; + let fixture: ComponentFixture; + let sessionService: SessionService; + + beforeEach(async () => { + sessionService = jasmine.createSpyObj("SessionService", [""], {}); + + await TestBed.configureTestingModule({ + declarations: [TableContextHeaderCellComponent], + }) + .overrideComponent(TableContextHeaderCellComponent, { + set: { + providers: [{ provide: SessionService, useValue: sessionService }], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TableContextHeaderCellComponent); + component = fixture.componentInstance; + + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + component.agInit({ + column: { + addEventListener: () => {}, + isSortAscending: () => false, + isSortDescending: () => false, + }, + } as any); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.ts b/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.ts new file mode 100644 index 0000000..114fb50 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/table-context-header-cell/table-context-header-cell.component.ts @@ -0,0 +1,69 @@ +import { IHeaderAngularComp } from "ag-grid-angular"; +import { IHeaderParams } from "ag-grid-community"; +import { BehaviorSubject } from "rxjs"; + +import { Component, NgZone } from "@angular/core"; + +import { SessionService } from "../../../services/session/session.service"; + +interface HeaderParams extends IHeaderParams { + menuIcon: string; + onHeaderContext: (event: MouseEvent) => void; +} + +@Component({ + selector: "app-table-context-header-cell", + templateUrl: "./table-context-header-cell.component.html", + styleUrls: ["./table-context-header-cell.component.scss"], + host: { + class: "ag-cell-label-container", + }, +}) +export class TableContextHeaderCellComponent implements IHeaderAngularComp { + public params!: HeaderParams; + public sorted$ = new BehaviorSubject(""); + + public constructor(private _ngZone: NgZone, private _sessionService: SessionService) {} + + public agInit(params: HeaderParams): void { + this.params = params; + this.params.column.addEventListener("sortChanged", this.onSortChanged); + this.onSortChanged(); + } + + public refresh(): boolean { + return false; + } + + public afterGuiAttached?(): void {} + + public onSortRequested(event: KeyboardEvent): void { + if (this.params.enableSorting !== true) { + return; + } + + const nextSort = this.getNextSort(this.sorted$.value); + this.params.setSort(nextSort, event.shiftKey); + } + + private getNextSort(sorted: string): "asc" | "desc" | null { + switch (sorted) { + case "asc": + return "desc"; + case "desc": + return null; + default: + return "asc"; + } + } + + private onSortChanged = () => { + if (this.params.column.isSortAscending()) { + this.sorted$.next("asc"); + } else if (this.params.column.isSortDescending()) { + this.sorted$.next("desc"); + } else { + this.sorted$.next(""); + } + }; +} diff --git a/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.html b/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.html new file mode 100644 index 0000000..7d9cbfc --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.html @@ -0,0 +1,74 @@ +
+
+

+ {{ "WTF_BACKUP.DIALOG_TITLE" | translate: { clientType: selectedInstallation.displayName } }} +

+ +
+
+ +
+
+ +
{{ this.busyText$ | async | translate: (busyTextParams$ | async) }}
+
+
+
+ + +
+

No backups were found at:

+

{{ backupPath }}

+
+
+

+ {{ "WTF_BACKUP.BACKUP_COUNT_TEXT" | translate: { count: backupCt$ | async } }} +

+
    +
  • +
    +
    +
    +
    {{ backup.title }}
    +
    +
    +
    {{ backup.date | relativeDuration }}
    +
    {{ backup.size }}
    +
    +
    + +
    {{ "WTF_BACKUP.ERROR." + backup.error | translate }}
    +
    + + +
    +
    +
  • +
+
+
+ + +
+ +
diff --git a/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.scss b/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.scss new file mode 100644 index 0000000..ea96abe --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.scss @@ -0,0 +1,36 @@ +.wtf-backup-dialog { + width: 600px; + max-width: 100%; +} + +.backup-list { + background-color: var(--background-secondary-2); + margin: 0; + padding: 0; + overflow: auto; + box-sizing: border-box; + + .backup-list-item:not(:last-child) { + border-bottom: 1px solid var(--background-secondary-1); + } + + .backup-list-item { + padding: 1em; + list-style: none; + border-left: 3px solid transparent; + + .status-badge { + width: 20px; + height: 20px; + } + + .title { + font-size: 1.2em; + } + + &:hover { + border-left-color: var(--background-primary); + background-color: var(--background-secondary-1); + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.ts b/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.ts new file mode 100644 index 0000000..60c5d33 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/addons/wtf-backup/wtf-backup.component.ts @@ -0,0 +1,184 @@ +import { Component, OnInit } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; +import { BehaviorSubject, from, of } from "rxjs"; +import { catchError, first, map, switchMap } from "rxjs/operators"; +import { WowInstallation } from "wowup-lib-core"; +import { ElectronService } from "../../../services"; +import { DialogFactory } from "../../../services/dialog/dialog.factory"; +import { SessionService } from "../../../services/session/session.service"; +import { SnackbarService } from "../../../services/snackbar/snackbar.service"; +import { WtfBackup, WtfService } from "../../../services/wtf/wtf.service"; +import { formatSize } from "../../../utils/number.utils"; + +interface WtfBackupViewModel { + title: string; + size: string; + date: number; + error?: string; +} + +@Component({ + selector: "app-wtf-backup", + templateUrl: "./wtf-backup.component.html", + styleUrls: ["./wtf-backup.component.scss"], +}) +export class WtfBackupComponent implements OnInit { + public readonly busy$ = new BehaviorSubject(false); + public readonly backups$ = new BehaviorSubject([]); + public readonly busyText$ = new BehaviorSubject(""); + public readonly busyTextParams$ = new BehaviorSubject({ count: "" }); + + public readonly selectedInstallation: WowInstallation; + public readonly hasBackups$ = this.backups$.pipe(map((backups) => backups.length > 0)); + public readonly backupCt$ = this.backups$.pipe(map((backups) => backups.length)); + public readonly backupPath: string; + + public constructor( + private _electronService: ElectronService, + private _sessionService: SessionService, + private _wtfService: WtfService, + private _snackbarService: SnackbarService, + private _dialogFactory: DialogFactory, + private _translateService: TranslateService + ) { + this.selectedInstallation = this._sessionService.getSelectedWowInstallation(); + this.backupPath = this._wtfService.getBackupPath(this.selectedInstallation); + } + + public ngOnInit(): void { + this.loadBackups().catch((e) => console.error(e)); + } + + public async onShowFolder(): Promise { + const backupPath = this._wtfService.getBackupPath(this.selectedInstallation); + await this._electronService.showItemInFolder(backupPath); + } + + public onClickApplyBackup(backup: WtfBackupViewModel): void { + const title: string = this._translateService.instant("WTF_BACKUP.APPLY_CONFIRMATION.TITLE"); + const message: string = this._translateService.instant("WTF_BACKUP.APPLY_CONFIRMATION.MESSAGE", { + name: backup.title, + }); + const dialogRef = this._dialogFactory.getConfirmDialog(title, message); + + dialogRef + .afterClosed() + .pipe( + first(), + switchMap((result) => { + if (!result) { + return of(undefined); + } + + this.busy$.next(true); + this.busyText$.next("WTF_BACKUP.BUSY_TEXT.APPLYING_BACKUP"); + + return from(this._wtfService.applyBackup(backup.title, this.selectedInstallation)).pipe(map(() => true)); + }), + catchError((e) => { + console.error(e); + this._snackbarService.showErrorSnackbar("WTF_BACKUP.ERROR.BACKUP_APPLY_FAILED", { + timeout: 2000, + localeArgs: { + name: backup.title, + }, + }); + + return of(false); + }) + ) + .subscribe((result) => { + this.busy$.next(false); + + if (result === true) { + this._snackbarService.showSuccessSnackbar("WTF_BACKUP.BACKUP_APPLY_SUCCESS", { + timeout: 2000, + localeArgs: { + name: backup.title, + }, + }); + } + }); + } + + public onClickDeleteBackup(backup: WtfBackupViewModel): void { + const title: string = this._translateService.instant("WTF_BACKUP.DELETE_CONFIRMATION.TITLE"); + const message: string = this._translateService.instant("WTF_BACKUP.DELETE_CONFIRMATION.MESSAGE", { + name: backup.title, + }); + const dialogRef = this._dialogFactory.getConfirmDialog(title, message); + + dialogRef + .afterClosed() + .pipe( + first(), + switchMap((result) => { + if (!result) { + return of(undefined); + } + + this.busyText$.next("WTF_BACKUP.BUSY_TEXT.REMOVING_BACKUP"); + this.busy$.next(true); + + return from(this._wtfService.deleteBackup(backup.title, this.selectedInstallation)).pipe( + switchMap(() => from(this.loadBackups())) + ); + }), + catchError((e) => { + console.error("Failed to delete backup", e); + this._snackbarService.showErrorSnackbar("WTF_BACKUP.ERROR.FAILED_TO_DELETE", { + timeout: 2000, + localeArgs: { + name: backup.title, + }, + }); + + return of(undefined); + }) + ) + .subscribe(() => { + this.busy$.next(false); + }); + } + + public async onCreateBackup(): Promise { + this.busyText$.next("WTF_BACKUP.BUSY_TEXT.CREATING_BACKUP"); + this.busy$.next(true); + + try { + await this._wtfService.createBackup(this.selectedInstallation, (count) => { + this.busyTextParams$.next({ count }); + }); + await this.loadBackups(); + } catch (e) { + console.error(e); + } finally { + this.busy$.next(false); + this.busyTextParams$.next({ count: "" }); + } + } + + private async loadBackups() { + this.busyText$.next("WTF_BACKUP.BUSY_TEXT.LOADING_BACKUPS"); + this.busy$.next(true); + + try { + const backups = await this._wtfService.getBackupList(this.selectedInstallation); + const viewModels = backups.map((b) => this.toViewModel(b)); + this.backups$.next(viewModels); + } catch (e) { + console.error(e); + } finally { + this.busy$.next(false); + } + } + + private toViewModel(backup: WtfBackup): WtfBackupViewModel { + return { + title: backup.fileName, + size: formatSize(backup.size), + date: backup.metadata?.createdAt ?? backup.birthtimeMs, + error: backup.error, + }; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.html b/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.html new file mode 100644 index 0000000..cc7fea8 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.html @@ -0,0 +1,12 @@ +

{{ data.title }}

+
+

+
+
+ + +
diff --git a/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.scss b/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.scss new file mode 100644 index 0000000..e4798b9 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.scss @@ -0,0 +1,5 @@ +.message { + font-family: Roboto, "Helvetica Neue", sans-serif; + word-break: break-word; + white-space: pre-wrap; +} diff --git a/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.spec.ts new file mode 100644 index 0000000..4dd8f00 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.spec.ts @@ -0,0 +1,50 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TestBed } from "@angular/core/testing"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { AlertDialogComponent } from "./alert-dialog.component"; +import { LinkService } from "../../../services/links/link.service"; + +describe("AlertDialogComponent", () => { + let linkService: any; + + beforeEach(async () => { + linkService = jasmine.createSpyObj("LinkService", [""], {}); + + await TestBed.configureTestingModule({ + declarations: [AlertDialogComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: {} }, + { provide: LinkService, useValue: linkService }, + ], + }).compileComponents(); + }); + + it("should create", () => { + const fixture = TestBed.createComponent(AlertDialogComponent); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.ts b/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.ts new file mode 100644 index 0000000..00e6bef --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/alert-dialog/alert-dialog.component.ts @@ -0,0 +1,35 @@ +import { AfterViewChecked, Component, ElementRef, Inject, ViewChild } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; + +export interface AlertDialogData { + title: string; + message: string; + positiveButton?: string; + positiveButtonColor?: "primary" | "accent" | "warn"; + positiveButtonStyle?: "raised" | "flat" | "stroked"; +} + +@Component({ + selector: "app-alert-dialog", + templateUrl: "./alert-dialog.component.html", + styleUrls: ["./alert-dialog.component.scss"], +}) +export class AlertDialogComponent implements AfterViewChecked { + @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef; + + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AlertDialogData, + ) {} + + public ngAfterViewChecked(): void { + // formatDynamicLinks(descriptionContainer, this.onOpenLink); + } + + // private onOpenLink = (element: HTMLAnchorElement): boolean => { + + // this._linkService.confirmLinkNavigation(element.href).subscribe(); + + // return false; + // }; +} diff --git a/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.html b/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.html new file mode 100644 index 0000000..d2b1fd4 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.scss b/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.scss new file mode 100644 index 0000000..ef7edb1 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.scss @@ -0,0 +1,21 @@ +.pre-load-container { + width: 100%; + height: 100vh; + display: flex; + flex-direction: column; + color: white; + justify-content: center; + align-items: center; + + .logo { + width: 150px; + // border-radius: 10%; + animation: pulse 2s infinite; + } +} + +.darwin { + .logo { + border-radius: 23px; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.spec.ts new file mode 100644 index 0000000..e70a325 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ElectronService } from "../../../services"; + +import { AnimatedLogoComponent } from "./animated-logo.component"; + +describe("AnimatedLogoComponent", () => { + let component: AnimatedLogoComponent; + let fixture: ComponentFixture; + let electronServiceSpy: ElectronService; + + beforeEach(async () => { + electronServiceSpy = jasmine.createSpyObj( + "ElectronService", + {}, + { + platform: "test-harness", + } + ); + + await TestBed.configureTestingModule({ + declarations: [AnimatedLogoComponent], + }) + .overrideComponent(AnimatedLogoComponent, { + set: { + providers: [{ provide: ElectronService, useValue: electronServiceSpy }], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AnimatedLogoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.ts b/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.ts new file mode 100644 index 0000000..2689915 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/animated-logo/animated-logo.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; +import { ElectronService } from "../../../services"; + +@Component({ + selector: "app-animated-logo", + templateUrl: "./animated-logo.component.html", + styleUrls: ["./animated-logo.component.scss"], +}) +export class AnimatedLogoComponent { + public constructor(public electronService: ElectronService) {} +} diff --git a/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.html b/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.html new file mode 100644 index 0000000..260aa11 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.html @@ -0,0 +1,5 @@ +
+
+

{{params.value}}

+
+
\ No newline at end of file diff --git a/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.scss b/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.scss new file mode 100644 index 0000000..6725980 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.scss @@ -0,0 +1,34 @@ +.cell-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + + .cell-wrap { + white-space: normal; + text-decoration: none; + font-size: 1em; + line-height: 1em; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-all; + } +} + +.overflow { + p { + white-space: normal; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + display: -webkit-box; + text-overflow: ellipsis; + line-height: 1.2em !important ; + max-height: calc(3 * 1.2em); + margin: 0; + padding: 0; + overflow: hidden; + word-break: break-all; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.spec.ts new file mode 100644 index 0000000..932774e --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { CellWrapTextComponent } from "./cell-wrap-text.component"; + +describe("CellWrapTextComponent", () => { + let component: CellWrapTextComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CellWrapTextComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CellWrapTextComponent); + component = fixture.componentInstance; + + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + component.agInit({ + value: "TEST", + } as any); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.ts b/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.ts new file mode 100644 index 0000000..bd42f75 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/cell-wrap-text/cell-wrap-text.component.ts @@ -0,0 +1,24 @@ +import { Component } from "@angular/core"; +import { AgRendererComponent } from "ag-grid-angular"; +import { ICellRendererParams } from "ag-grid-community"; + +@Component({ + selector: "app-cell-wrap-text", + templateUrl: "./cell-wrap-text.component.html", + styleUrls: ["./cell-wrap-text.component.scss"], +}) +export class CellWrapTextComponent implements AgRendererComponent { + public params!: ICellRendererParams; + + public constructor() {} + + public agInit(params: ICellRendererParams): void { + this.params = params; + } + + public refresh(): boolean { + return false; + } + + public afterGuiAttached?(): void {} +} diff --git a/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.html b/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.html new file mode 100644 index 0000000..2385364 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.html @@ -0,0 +1 @@ +
{{ message }}
diff --git a/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.scss b/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.spec.ts new file mode 100644 index 0000000..d29a096 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MAT_SNACK_BAR_DATA } from "@angular/material/snack-bar"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; + +import { MatModule } from "../../../modules/mat-module"; +import { CenteredSnackbarComponent, CenteredSnackbarComponentData } from "./centered-snackbar.component"; + +describe("CenteredSnackbarComponent", () => { + let component: CenteredSnackbarComponent; + let fixture: ComponentFixture; + let dialogData: CenteredSnackbarComponentData; + + beforeEach(async () => { + dialogData = { + message: "TEST MESSAGE", + }; + + await TestBed.configureTestingModule({ + declarations: [CenteredSnackbarComponent], + imports: [MatModule, NoopAnimationsModule], + providers: [{ provide: MAT_SNACK_BAR_DATA, useValue: dialogData }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CenteredSnackbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.ts b/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.ts new file mode 100644 index 0000000..b87efcb --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/centered-snackbar/centered-snackbar.component.ts @@ -0,0 +1,21 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { MAT_SNACK_BAR_DATA } from "@angular/material/snack-bar"; + +export interface CenteredSnackbarComponentData { + message: string; +} + +@Component({ + selector: "app-centered-snackbar", + templateUrl: "./centered-snackbar.component.html", + styleUrls: ["./centered-snackbar.component.scss"], +}) +export class CenteredSnackbarComponent implements OnInit { + public message?: string; + + public constructor(@Inject(MAT_SNACK_BAR_DATA) public data: CenteredSnackbarComponentData) {} + + public ngOnInit(): void { + this.message = this.data.message; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.html b/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.html new file mode 100644 index 0000000..33b7267 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.html @@ -0,0 +1,39 @@ +
+ + +
+
PAGES.MY_ADDONS.CLIENT_TYPE_SELECT_LABEL
+
+ PAGES.MY_ADDONS.CLIENT_TYPE_SELECT_BADGE +
+
+
+ + + {{ selectedWowInstallationLabel$ | async }} + + +
+
{{ installation.displayName }}
+
+
+ {{ installation.availableUpdateCount }} +
+
+
+
+
+
+
diff --git a/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.scss b/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.spec.ts new file mode 100644 index 0000000..c9f0bf2 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.spec.ts @@ -0,0 +1,59 @@ +import { BehaviorSubject, Observable } from "rxjs"; + +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { PipesModule } from "../../../modules/pipes.module"; +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { getStandardTestImports } from "../../../utils/test.utils"; +import { ClientSelectorComponent } from "./client-selector.component"; + +describe("ClientSelectorComponent", () => { + let component: ClientSelectorComponent; + let fixture: ComponentFixture; + + let addonService; + let sessionService; + let warcraftInstallationService; + + beforeEach(async () => { + addonService = jasmine.createSpyObj("AddonService", [""], { + anyUpdatesAvailable$: new BehaviorSubject(false), + }); + + sessionService = jasmine.createSpyObj("SessionService", [""], { + selectedWowInstallation$: new Observable(), + enableControls$: new BehaviorSubject(true), + }); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], { + wowInstallations$: new BehaviorSubject([]), + }); + + await TestBed.configureTestingModule({ + declarations: [ClientSelectorComponent], + imports: [...getStandardTestImports(), PipesModule], + }) + .overrideComponent(ClientSelectorComponent, { + set: { + providers: [ + { provide: AddonService, useValue: addonService }, + { provide: SessionService, useValue: sessionService }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + ], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ClientSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.ts b/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.ts new file mode 100644 index 0000000..fcc41b9 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/client-selector/client-selector.component.ts @@ -0,0 +1,58 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { BehaviorSubject, combineLatest, Observable } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; +import { WowInstallation } from "wowup-lib-core"; +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; + +@Component({ + selector: "app-client-selector", + templateUrl: "./client-selector.component.html", + styleUrls: ["./client-selector.component.scss"], +}) +export class ClientSelectorComponent implements OnInit { + @Input() public updates = false; + + public readonly totalAvailableUpdateCt$ = new BehaviorSubject(0); + + public readonly enableControls$ = this._sessionService.enableControls$; + + public readonly selectedWowInstallationId$ = this._sessionService.selectedWowInstallation$.pipe( + map((wowInstall) => wowInstall?.id) + ); + + public readonly selectedWowInstallationLabel$ = this._sessionService.selectedWowInstallation$.pipe( + map((wowInstall) => wowInstall?.displayName ?? "") + ); + + public wowInstallations$: Observable = combineLatest([ + this._warcraftInstallationService.wowInstallations$, + this._addonService.anyUpdatesAvailable$, + ]).pipe(switchMap(([installations]) => this.mapInstallations(installations))); + + public constructor( + private _addonService: AddonService, + private _sessionService: SessionService, + private _warcraftInstallationService: WarcraftInstallationService + ) {} + + public ngOnInit(): void {} + + public async onClientChange(evt: any): Promise { + const val: string = evt.value.toString(); + await this._sessionService.setSelectedWowInstallation(val); + } + + private async mapInstallations(installations: WowInstallation[]): Promise { + let total = 0; + for (const inst of installations) { + const addons = await this._addonService.getAllAddonsAvailableForUpdate(inst); + inst.availableUpdateCount = addons.length; + total += inst.availableUpdateCount; + } + + this.totalAvailableUpdateCt$.next(total); + return installations; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.html b/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.html new file mode 100644 index 0000000..c510de3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.html @@ -0,0 +1,14 @@ +

{{ data.title }}

+
+

+
+
+
+ + +
+
diff --git a/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.scss b/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.scss new file mode 100644 index 0000000..5e43c3b --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.scss @@ -0,0 +1,13 @@ +.message { + font-family: Roboto, "Helvetica Neue", sans-serif; + word-break: break-word; + white-space: pre-wrap; + max-width: 40vw; +} + +.dialog-buttons { + display: flex; + .btn-wrapper { + margin-left: auto; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts new file mode 100644 index 0000000..5d8a664 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.spec.ts @@ -0,0 +1,50 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TestBed } from "@angular/core/testing"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { ConfirmDialogComponent } from "./confirm-dialog.component"; +import { LinkService } from "../../../services/links/link.service"; + +describe("ConfirmDialogComponent", () => { + let linkService; + + beforeEach(async () => { + linkService = jasmine.createSpyObj("LinkService", [""], {}); + + await TestBed.configureTestingModule({ + declarations: [ConfirmDialogComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: {} }, + { provide: LinkService, useValue: linkService }, + ], + }).compileComponents(); + }); + + it("should create", () => { + const fixture = TestBed.createComponent(ConfirmDialogComponent); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.ts b/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.ts new file mode 100644 index 0000000..2f67d2a --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/confirm-dialog/confirm-dialog.component.ts @@ -0,0 +1,39 @@ +import { Component, ElementRef, Inject, ViewChild } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; + +export interface DialogData { + title: string; + message: string; + positiveKey?: string; + negativeKey?: string; +} + +@Component({ + selector: "app-confirm-dialog", + templateUrl: "./confirm-dialog.component.html", + styleUrls: ["./confirm-dialog.component.scss"], +}) +export class ConfirmDialogComponent { + @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef; + + public positiveKey: string; + public negativeKey: string; + + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData, + ) { + this.positiveKey = data.positiveKey ?? "DIALOGS.CONFIRM.POSITIVE_BUTTON"; + this.negativeKey = data.negativeKey ?? "DIALOGS.CONFIRM.NEGATIVE_BUTTON"; + } + + public ngAfterViewChecked(): void { + // formatDynamicLinks(descriptionContainer, this.onOpenLink); + } + + // private onOpenLink = (element: HTMLAnchorElement): boolean => { + // this._linkService.confirmLinkNavigation(element.href).subscribe(); + + // return false; + // }; +} diff --git a/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.html b/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.html new file mode 100644 index 0000000..cd761a2 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.html @@ -0,0 +1,43 @@ +

{{ "DIALOGS.PERMISSIONS.TITLE" | translate }}

+ +
+

{{ "DIALOGS.PERMISSIONS.MESSAGE" | translate }}

+
+
+ {{ + "DIALOGS.PERMISSIONS.TELEMETRY.TOGGLE_LABEL" | translate + }} +
+ {{ "DIALOGS.PERMISSIONS.TELEMETRY.DESCRIPTION" | translate }} +
+
+

{{ "DIALOGS.PERMISSIONS.CURSEFORGE.TITLE" | translate }}

+ + {{ "DIALOGS.PERMISSIONS.CURSEFORGE.DESCRIPTION_TOP" | translate }} + {{ + "DIALOGS.PERMISSIONS.CURSEFORGE.DESCRIPTION_AD_LINK" | translate + }} + {{ "DIALOGS.PERMISSIONS.CURSEFORGE.DESCRIPTION_BOTTOM" | translate }} + +
+
+
+ {{ + "DIALOGS.PERMISSIONS.WAGO.TOGGLE_LABEL" | translate + }} +
+ +
+
+
+ + +
+ diff --git a/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.scss b/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.scss new file mode 100644 index 0000000..bb2f528 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.scss @@ -0,0 +1,3 @@ +a { + color: var(--control-color); +} diff --git a/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.ts b/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.ts new file mode 100644 index 0000000..1f0b2cb --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/consent-dialog/consent-dialog.component.ts @@ -0,0 +1,81 @@ +import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from "@angular/core"; +import { UntypedFormControl, UntypedFormGroup } from "@angular/forms"; + +import { MatDialogRef } from "@angular/material/dialog"; +import { IPC_OW_IS_CMP_REQUIRED, IPC_OW_OPEN_CMP } from "../../../../common/constants"; +import { ElectronService } from "../../../services"; +import { AppConfig } from "../../../../environments/environment"; + +export interface ConsentDialogResult { + telemetry: boolean; + wagoProvider: boolean; +} + +@Component({ + selector: "app-consent-dialog", + templateUrl: "./consent-dialog.component.html", + styleUrls: ["./consent-dialog.component.scss"], +}) +export class ConsentDialogComponent implements AfterViewChecked, OnInit { + @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef; + + public consentOptions: UntypedFormGroup; + public requiresCmp = false; + + public readonly wagoTermsUrl = AppConfig.wago.termsUrl; + public readonly wagoDataUrl = AppConfig.wago.dataConsentUrl; + + public constructor( + public dialogRef: MatDialogRef, + private _electronService: ElectronService, + ) { + this.consentOptions = new UntypedFormGroup({ + telemetry: new UntypedFormControl(true), + wagoProvider: new UntypedFormControl(true), + }); + } + + public ngOnInit(): void { + this._electronService + .invoke(IPC_OW_IS_CMP_REQUIRED) + .then((cmpRequired) => { + console.log("cmpRequired", cmpRequired); + this.requiresCmp = cmpRequired; + }) + .catch((e) => console.error("IPC_OW_IS_CMP_REQUIRED failed", e)); + } + + public ngAfterViewChecked(): void { + // formatDynamicLinks(descriptionContainer, this.onOpenLink); + } + + public onClickAdVendors(evt: MouseEvent): void { + evt.preventDefault(); + + this._electronService.invoke(IPC_OW_OPEN_CMP, "vendors").catch((e) => console.error("onClickAdVendors failed", e)); + } + + public onClickManage(evt: MouseEvent): void { + evt.preventDefault(); + + this._electronService.invoke(IPC_OW_OPEN_CMP).catch((e) => console.error("onClickManage failed", e)); + } + + public onNoClick(): void { + this.dialogRef.close(); + } + + public onSubmit(evt: any): void { + evt.preventDefault(); + + console.log(this.consentOptions.value); + + this.dialogRef.close(this.consentOptions.value); + } + + // private onOpenLink = (element: HTMLAnchorElement): boolean => { + // this._linkService.confirmLinkNavigation(element.href).subscribe(); + + // return false; + // }; +} diff --git a/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.html b/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.html new file mode 100644 index 0000000..cbaf060 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.html @@ -0,0 +1,47 @@ +

+ {{ "DIALOGS.CURSE_MIGRATION.TITLE" | translate }} +

+
+

+
+ +

{{ "APP.STATUS_TEXT.ADDON_SCAN_STARTED" | translate }}

+
+
+
+

{{ "DIALOGS.CURSE_MIGRATION.RE_SCAN_SUCCESS" | translate }}

+
+
+

{{ "DIALOGS.CURSE_MIGRATION.RE_SCAN_ERROR" | translate }}

+
+
+ + + +
diff --git a/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.scss b/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.scss new file mode 100644 index 0000000..a55758c --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.scss @@ -0,0 +1,4 @@ +.icon-header { + display: flex; + align-items: center; +} diff --git a/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.ts b/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.ts new file mode 100644 index 0000000..1504ab3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/curse-migration-dialog/curse-migration-dialog.component.ts @@ -0,0 +1,84 @@ +import { AfterViewChecked, Component, ElementRef, ViewChild } from "@angular/core"; +import { MatDialogRef } from "@angular/material/dialog"; +import { BehaviorSubject, map } from "rxjs"; +import { ADDON_PROVIDER_CURSEFORGEV2 } from "../../../../common/constants"; +import { AddonService } from "../../../services/addons/addon.service"; +import { LinkService } from "../../../services/links/link.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { formatDynamicLinks } from "../../../utils/dom.utils"; + +export interface ConsentDialogResult { + telemetry: boolean; + wagoProvider: boolean; +} + +@Component({ + selector: "app-curse-migration-dialog", + templateUrl: "./curse-migration-dialog.component.html", + styleUrls: ["./curse-migration-dialog.component.scss"], +}) +export class CurseMigrationDialogComponent implements AfterViewChecked { + @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef; + + public readonly isBusy$ = new BehaviorSubject(false); + public readonly autoError$ = new BehaviorSubject(undefined); + public readonly autoComplete$ = new BehaviorSubject(false); + public readonly autoIncomplete$ = this.autoComplete$.pipe(map((complete) => !complete)); + + public constructor( + public dialogRef: MatDialogRef, + private _addonService: AddonService, + private _linkService: LinkService, + private _sessionService: SessionService, + private _warcraftInstallationService: WarcraftInstallationService + ) {} + + public ngAfterViewChecked(): void { + const descriptionContainer: HTMLDivElement = this.dialogContent?.nativeElement; + formatDynamicLinks(descriptionContainer, this.onOpenLink); + } + + public onNoClick(): void { + this.dialogRef.close(); + } + + public async onAutomaticClick(): Promise { + this.isBusy$.next(true); + + try { + // Fetch all installations + let scanCompleted = false; + const wowInstallations = await this._warcraftInstallationService.getWowInstallationsAsync(); + for (const wowInstall of wowInstallations) { + // If there are any old Curse addons, re-scan that installation + let addons = await this._addonService.getAddons(wowInstall); + addons = addons.filter( + (addon) => + addon.isIgnored === false && + (addon.providerName === ADDON_PROVIDER_CURSEFORGEV2) + ); + if (addons.length > 0) { + await this._addonService.rescanInstallation(wowInstall); + scanCompleted = true; + } + } + + if (scanCompleted) { + this._sessionService.rescanCompleted(); + } + } catch (e) { + console.error(e); + this.autoError$.next(e as Error); + } finally { + this.isBusy$.next(false); + this.autoComplete$.next(true); + } + } + + private onOpenLink = (element: HTMLAnchorElement): boolean => { + this._linkService.confirmLinkNavigation(element.href).subscribe(); + + return false; + }; +} diff --git a/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.html b/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.html new file mode 100644 index 0000000..a9d2a5f --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.html @@ -0,0 +1,15 @@ +

{{ data.title }}

+ +
{{ data.message }}
+
+ {{ "DIALOGS.TRUST_DOMAIN_CHECKBOX" | translate }} +
+
+ + + + diff --git a/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.scss b/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.scss new file mode 100644 index 0000000..9c0fb2b --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.scss @@ -0,0 +1,6 @@ +.message { + font-family: Roboto, "Helvetica Neue", sans-serif; + word-break: break-word; + white-space: pre-wrap; + max-width: 40vw; +} \ No newline at end of file diff --git a/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000..083f85b --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.spec.ts @@ -0,0 +1,55 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { getStandardImports } from "../../../tests/test-helpers"; + +import { ExternalUrlConfirmationDialogComponent } from "./external-url-confirmation-dialog.component"; + +describe("ExternalUrlConfirmationDialogComponent", () => { + let component: ExternalUrlConfirmationDialogComponent; + let fixture: ComponentFixture; + let wowUpService: any; + + beforeEach(async () => { + wowUpService = jasmine.createSpyObj("WowUpService", [""], { + getTrustedDomains() { + return Promise.resolve([]); + }, + }); + + await TestBed.configureTestingModule({ + declarations: [ExternalUrlConfirmationDialogComponent], + imports: [...getStandardImports()], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { + url: "https://wowup.io", + }, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + ], + }) + .overrideComponent(ExternalUrlConfirmationDialogComponent, { + set: { + providers: [{ provide: WowUpService, useValue: wowUpService }], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalUrlConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.ts b/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.ts new file mode 100644 index 0000000..fcb29b5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component.ts @@ -0,0 +1,65 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { from } from "rxjs"; +import { first, map } from "rxjs/operators"; + +import { WowUpService } from "../../../services/wowup/wowup.service"; + +export interface DialogData { + title: string; + message: string; + url: string; + domains: string[]; +} + +export interface DialogResult { + success: boolean; + trustDomain: string; +} + +@Component({ + selector: "app-external-url-confirmation-dialog", + templateUrl: "./external-url-confirmation-dialog.component.html", + styleUrls: ["./external-url-confirmation-dialog.component.scss"], +}) +export class ExternalUrlConfirmationDialogComponent implements OnInit { + public domain = ""; + public trustDomain = false; + + private _trustedDomains: string[] = []; + + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData, + private _wowupService: WowUpService, + ) { + const url = new URL(data.url); + this.domain = url.hostname; + } + + public ngOnInit(): void { + if (this.data.domains) { + this._trustedDomains = this.data.domains; + this.trustDomain = this._trustedDomains.includes(this.domain); + } else { + from(this._wowupService.getTrustedDomains()) + .pipe( + first(), + map((trustedDomains) => { + this._trustedDomains = trustedDomains; + + this.trustDomain = this._trustedDomains.includes(this.domain); + }), + ) + .subscribe(); + } + } + + public onConfirm(success: boolean): void { + const result: DialogResult = { + success, + trustDomain: this.trustDomain ? this.domain : "", + }; + this.dialogRef.close(result); + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/footer/footer.component.html b/WowUp/wowup-electron/src/app/components/common/footer/footer.component.html new file mode 100644 index 0000000..71a28fc --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/footer/footer.component.html @@ -0,0 +1,38 @@ +
+ +

{{ sessionService.statusText$ | async }}

+
+

{{ sessionService.pageContextText$ | async }}

+

v{{ versionNumber | async }}

+
+
+

{{'APP.WOWUP_UPDATE.CHECKING_FOR_UPDATE' | translate}}

+
+
+

{{'APP.WOWUP_UPDATE.UPDATE_AVAILABLE' | translate}}

+
+
+ + {{'APP.WOWUP_UPDATE.DOWNLOADING_UPDATE' | translate}} +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/WowUp/wowup-electron/src/app/components/common/footer/footer.component.scss b/WowUp/wowup-electron/src/app/components/common/footer/footer.component.scss new file mode 100644 index 0000000..23fddd3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/footer/footer.component.scss @@ -0,0 +1,105 @@ +footer { + height: 22px; + padding: 0.25em 0.5em; + display: flex; + align-items: center; + + .btn { + height: 100%; + overflow: hidden; + background-color: transparent; + border: none; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0.5em; + + &:hover { + cursor: pointer; + background-color: var(--footer-hover); + border-radius: 4px; + } + + img { + height: 100%; + } + + mat-icon { + height: 22px; + } + } + p { + font-size: 0.85em; + margin: 0; + } + + a, + img { + -webkit-app-region: no-drag; + } + + .footer-button { + height: 22px; + padding: 0 0.5em; + margin: 0; + overflow: hidden; + border-radius: 4px; + border: none; + background-color: transparent; + + &.update-button { + background-color: rgb(0, 128, 0); + + .mat-icon { + height: 12px; + width: 12px; + font-size: 12px; + vertical-align: -2px; + } + + &:hover { + background-color: rgba(0, 128, 0, 0.1); + cursor: pointer; + } + + &:disabled { + background-color: var(--background-secondary-2); + } + } + + .mat-icon { + pointer-events: none; + height: 20px; + width: 20px; + } + + &.check-update-button { + &:hover { + cursor: pointer; + } + } + + &.downloading-button { + &.animate { + animation: fadeInDown 1s infinite linear; + } + } + } +} + +@keyframes rotate { + to { + transform: rotate(-360deg); + } +} + +@keyframes fadeInDown { + 0% { + opacity: 1; + transform: translateY(-5px); + } + 100% { + opacity: 0; + transform: translateY(5px); + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/footer/footer.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/footer/footer.component.spec.ts new file mode 100644 index 0000000..0a1b3a1 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/footer/footer.component.spec.ts @@ -0,0 +1,89 @@ +import { UpdateCheckResult } from "electron-updater"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { BehaviorSubject, Subject } from "rxjs"; + +import { OverlayModule } from "@angular/cdk/overlay"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatIconTestingModule } from "@angular/material/icon/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { ElectronService } from "../../../services"; +import { LinkService } from "../../../services/links/link.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { mockPreload } from "../../../tests/test-helpers"; +import { FooterComponent } from "./footer.component"; + +/** Fix icon warning? https://stackoverflow.com/a/62277810 */ +describe("FooterComponent", () => { + let fixture: ComponentFixture; + let wowUpServiceSpy: WowUpService; + let sessionServiceSpy: SessionService; + let electronService: ElectronService; + let linkService: LinkService; + + beforeEach(async () => { + mockPreload(); + + wowUpServiceSpy = jasmine.createSpyObj("WowUpService", [], { + getApplicationVersion: () => Promise.resolve("TESTV"), + wowupUpdateCheck$: new Subject().asObservable(), + wowupUpdateDownloaded$: new Subject().asObservable(), + wowupUpdateDownloadInProgress$: new Subject().asObservable(), + }); + + sessionServiceSpy = jasmine.createSpyObj("SessionService", [""], { + statusText$: new BehaviorSubject(""), + pageContextText$: new BehaviorSubject(""), + wowUpAccount$: new Subject(), + }); + + linkService = jasmine.createSpyObj("LinkService", [""], {}); + + electronService = jasmine.createSpyObj("ElectronService", [""], { + appUpdate$: new Subject(), + }); + + await TestBed.configureTestingModule({ + declarations: [FooterComponent], + imports: [ + MatModule, + NoopAnimationsModule, + MatIconTestingModule, + OverlayModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(FooterComponent, { + set: { + providers: [ + { provide: ElectronService, useValue: electronService }, + { provide: WowUpService, useValue: wowUpServiceSpy }, + { provide: SessionService, useValue: sessionServiceSpy }, + { provide: LinkService, useValue: linkService }, + ], + }, + }) + .compileComponents(); + }); + + it("should create", () => { + fixture = TestBed.createComponent(FooterComponent); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/footer/footer.component.ts b/WowUp/wowup-electron/src/app/components/common/footer/footer.component.ts new file mode 100644 index 0000000..369b200 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/footer/footer.component.ts @@ -0,0 +1,154 @@ +import { combineLatest, from, Observable, of } from "rxjs"; +import { catchError, map, switchMap } from "rxjs/operators"; + +import { Component, NgZone, OnInit } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { TranslateService } from "@ngx-translate/core"; + +import { TAB_INDEX_ABOUT } from "../../../../common/constants"; +import { AppUpdateState } from "../../../../common/wowup/models"; +import { AppConfig } from "../../../../environments/environment"; +import { ElectronService } from "../../../services"; +import { LinkService } from "../../../services/links/link.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { ConfirmDialogComponent } from "../../common/confirm-dialog/confirm-dialog.component"; + +@Component({ + selector: "app-footer", + templateUrl: "./footer.component.html", + styleUrls: ["./footer.component.scss"], +}) +export class FooterComponent implements OnInit { + public isUpdatingWowUp = false; + public isWowUpUpdateAvailable = false; + public isWowUpUpdateDownloaded = false; + public isCheckingForUpdates = false; + public isWowUpdateDownloading = false; + public updateIconTooltip = "APP.WOWUP_UPDATE.TOOLTIP"; + public versionNumber = from(this.wowUpService.getApplicationVersion()); + public appUpdateState = AppUpdateState; + + public appUpdateState$: Observable = this.electronService.appUpdate$.pipe(map((evt) => evt.state)); + public accountDisplayName$: Observable = this.sessionService.wowUpAccount$.pipe( + map((account) => account?.displayName ?? ""), + ); + + public appUpdateProgress$: Observable = combineLatest([ + of(0), + this.electronService.appUpdate$.pipe(map((evt) => evt.progress?.percent ?? 0)), + ]).pipe(map(([def, val]) => Math.max(def, val))); + + public constructor( + private _dialog: MatDialog, + private _translateService: TranslateService, + private _zone: NgZone, + public wowUpService: WowUpService, + public sessionService: SessionService, + private electronService: ElectronService, + private _wowupService: WowUpService, + private _linkService: LinkService, + ) {} + + public ngOnInit(): void { + // Force the angular zone to pump for every progress update since its outside the zone + this.sessionService.statusText$.subscribe(() => { + this._zone.run(() => {}); + }); + + this.sessionService.pageContextText$.subscribe(() => { + this._zone.run(() => {}); + }); + } + + public onClickCheckForUpdates(): void { + this.wowUpService.checkForAppUpdate(); + } + + public onClickAccount(): void { + this.sessionService.selectedHomeTab = TAB_INDEX_ABOUT; + } + + private portableUpdate() { + const dialogRef = this._dialog.open(ConfirmDialogComponent, { + data: { + title: this._translateService.instant("APP.WOWUP_UPDATE.PORTABLE_DOWNLOAD_TITLE"), + message: this._translateService.instant("APP.WOWUP_UPDATE.PORTABLE_DOWNLOAD_MESSAGE"), + }, + }); + + dialogRef + .afterClosed() + .pipe( + switchMap((result) => { + if (!result) { + return of(undefined); + } + + return from( + this._linkService.openExternalLink( + `${AppConfig.wowupRepositoryUrl}/releases/tag/v${this.wowUpService.availableVersion}`, + ), + ); + }), + catchError((e) => { + console.error(e); + return of(undefined); + }), + ) + .subscribe(); + + return; + } + + public onClickInstallUpdate(): void { + this._wowupService.installUpdate(); + } + + // public async onClickUpdateWowup(): Promise { + // if (!this.isWowUpUpdateAvailable) { + // return; + // } + + // if (this.electronService.isPortable) { + // this.portableUpdate(); + // return; + // } + + // if (this.isWowUpUpdateDownloaded) { + // const dialogRef = this._dialog.open(ConfirmDialogComponent, { + // data: { + // title: this._translateService.instant("APP.WOWUP_UPDATE.INSTALL_TITLE"), + // message: this._translateService.instant("APP.WOWUP_UPDATE.INSTALL_MESSAGE"), + // }, + // }); + + // dialogRef + // .afterClosed() + // .pipe( + // switchMap((result) => { + // if (!result) { + // return of(undefined); + // } + // return from(this.wowUpService.installUpdate()); + // }), + // catchError((e) => { + // console.error(e); + // return of(undefined); + // }) + // ) + // .subscribe(); + + // return; + // } + + // this.isUpdatingWowUp = true; + // try { + // await this.wowUpService.downloadUpdate(); + // } catch (e) { + // console.error("onClickUpdateWowup", e); + // } finally { + // this.isUpdatingWowUp = false; + // } + // } +} diff --git a/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.html b/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.html new file mode 100644 index 0000000..18383c7 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.html @@ -0,0 +1,72 @@ +
+
+ + +
{{ tab.titleKey | translate }}
+ +
+
+ + +
{{ "PAGES.HOME.GUIDE_TAB_TITLE" | translate }}
+
+ + +
+ + +
+
{{ "PAGES.HOME.ACCOUNT_TAB_TITLE" | translate }}
+
Logged in
+
+
+ +
+ + + + +
{{ tab.titleKey | translate }}
+
+
diff --git a/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.scss b/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.scss new file mode 100644 index 0000000..2efe1f4 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.scss @@ -0,0 +1,93 @@ +.tab-strip { + box-sizing: border-box; + display: flex; + flex-direction: row; + height: 40px; + padding-left: 87px; + padding-right: 5px; + + &.mac { + padding-right: 87px; + padding-left: 5px; + } + + .patreon-link { + padding: 0 0.25em; + .patron-img { + height: 25px; + } + } + + .tab { + min-width: 40px; + height: 40px; + padding: 0 0.5em; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + text-decoration: none; + + .icon-active { + svg { + fill: var(--text-1) !important; + } + } + + .tab-title { + color: var(--text-4); + margin: 0; + padding: 0; + line-height: 1em; + max-width: 200px; + overflow: hidden; + transition: all 0.3s ease 0.1s; + transition-property: color, max-width; + white-space: nowrap; + } + + .tab-subtitle { + font-size: 0.8em; + margin: 0; + padding: 0; + line-height: 1em; + } + + .tab-icon { + margin-right: 0.5em; + transition: margin-right 0.3s ease 0.1s; + } + + &:hover { + cursor: pointer; + background-color: var(--background-primary-2); + } + + &.selected { + // border-bottom: 2px solid var(--text-1); + background-color: var(--background-primary-3); + + .tab-icon { + margin-right: 0.5em; + } + + .tab-title { + color: var(--text-1); + max-width: 200px; + // margin-left: 0.5em; + } + } + + &.disabled { + pointer-events: none; + } + } + + .spacer { + flex-grow: 1; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.spec.ts new file mode 100644 index 0000000..2865a9d --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.spec.ts @@ -0,0 +1,60 @@ +import { BehaviorSubject, Subject } from "rxjs"; + +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { ElectronService } from "../../../services"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { getStandardImports, mockPreload } from "../../../tests/test-helpers"; +import { HorizontalTabsComponent } from "./horizontal-tabs.component"; + +describe("HorizontalTabsComponent", () => { + let component: HorizontalTabsComponent; + let fixture: ComponentFixture; + let sessionService: SessionService; + let electronService: any; + let warcraftInstallationService: WarcraftInstallationService; + + beforeEach(async () => { + mockPreload(); + + sessionService = jasmine.createSpyObj("SessionService", ["getSelectedClientType", "getSelectedDetailsTab"], { + getSelectedWowInstallation: () => "description", + selectedHomeTab$: new BehaviorSubject(1), + wowUpAccount$: new Subject(), + }); + + electronService = jasmine.createSpyObj("ElectronService", [""], {}); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], { + wowInstallations$: new BehaviorSubject([]), + }); + + let testBed = TestBed.configureTestingModule({ + declarations: [HorizontalTabsComponent], + imports: [...getStandardImports()], + }); + + testBed = testBed.overrideComponent(HorizontalTabsComponent, { + set: { + providers: [ + { provide: ElectronService, useValue: electronService }, + { provide: SessionService, useValue: sessionService }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + ], + }, + }); + + await testBed.compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HorizontalTabsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.ts b/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.ts new file mode 100644 index 0000000..0ce8022 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/horizontal-tabs/horizontal-tabs.component.ts @@ -0,0 +1,116 @@ +import { Observable, of } from "rxjs"; +import { map } from "rxjs/operators"; + +import { Component, OnInit } from "@angular/core"; + +import { + FEATURE_ACCOUNTS_ENABLED, + TAB_INDEX_ABOUT, + TAB_INDEX_GET_ADDONS, + TAB_INDEX_MY_ADDONS, + TAB_INDEX_NEWS, + TAB_INDEX_SETTINGS, +} from "../../../../common/constants"; +import { AppConfig } from "../../../../environments/environment"; +import { ElectronService } from "../../../services"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; + +interface Tab { + titleKey?: string; + tooltipKey: string; + icon: string; + badge?: boolean; + isSelected$: Observable; + isDisabled$: Observable; + onClick: (tab: Tab) => void; +} + +@Component({ + selector: "app-horizontal-tabs", + templateUrl: "./horizontal-tabs.component.html", + styleUrls: ["./horizontal-tabs.component.scss"], +}) +export class HorizontalTabsComponent implements OnInit { + public wowUpWebsiteUrl = AppConfig.wowUpWebsiteUrl; + public TAB_INDEX_ACCOUNT = TAB_INDEX_ABOUT; + public FEATURE_ACCOUNTS_ENABLED = FEATURE_ACCOUNTS_ENABLED; + + public isAccountSelected$ = this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_ABOUT)); + + private myAddonsTab: Tab = { + titleKey: "PAGES.HOME.MY_ADDONS_TAB_TITLE", + tooltipKey: "PAGES.HOME.MY_ADDONS_TAB_TITLE", + icon: "fas:dice-d6", + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_MY_ADDONS)), + isDisabled$: this._warcraftInstallationService.wowInstallations$.pipe( + map((installations) => installations.length === 0) + ), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_MY_ADDONS; + }, + }; + + private getAddonsTab: Tab = { + titleKey: "PAGES.HOME.GET_ADDONS_TAB_TITLE", + tooltipKey: "PAGES.HOME.GET_ADDONS_TAB_TITLE", + icon: "fas:magnifying-glass", + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_GET_ADDONS)), + isDisabled$: this._warcraftInstallationService.wowInstallations$.pipe( + map((installations) => installations.length === 0) + ), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_GET_ADDONS; + }, + }; + + private aboutTab: Tab = { + titleKey: "PAGES.HOME.ACCOUNT_TAB_TITLE", + tooltipKey: "PAGES.HOME.ACCOUNT_TAB_TITLE", + icon: "fas:user-circle", + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_ABOUT)), + isDisabled$: of(false), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_ABOUT; + }, + }; + + private newsTab: Tab = { + titleKey: "PAGES.HOME.NEWS_TAB_TITLE", + tooltipKey: "PAGES.HOME.NEWS_TAB_TITLE", + icon: "fas:newspaper", + badge: true, + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_NEWS)), + isDisabled$: of(false), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_NEWS; + }, + }; + + private settingsTab: Tab = { + titleKey: "PAGES.HOME.OPTIONS_TAB_TITLE", + tooltipKey: "PAGES.HOME.OPTIONS_TAB_TITLE", + icon: "fas:gear", + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_SETTINGS)), + isDisabled$: of(false), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_SETTINGS; + }, + }; + + public tabsTop: Tab[] = [this.myAddonsTab, this.getAddonsTab, this.newsTab]; + + public tabsBottom: Tab[] = [this.settingsTab]; + + public constructor( + public electronService: ElectronService, + public sessionService: SessionService, + private _warcraftInstallationService: WarcraftInstallationService + ) {} + + public ngOnInit(): void {} + + public onClickTab(tabIndex: number): void { + this.sessionService.selectedHomeTab = tabIndex; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.html b/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.html new file mode 100644 index 0000000..4059cec --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.html @@ -0,0 +1,9 @@ +

{{ title | async }}

+ +
+
+ + + diff --git a/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.scss b/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.spec.ts new file mode 100644 index 0000000..4275938 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.spec.ts @@ -0,0 +1,66 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { TrustHtmlPipe } from "../../../pipes/trust-html.pipe"; +import { ElectronService } from "../../../services"; +import { DialogFactory } from "../../../services/dialog/dialog.factory"; +import { LinkService } from "../../../services/links/link.service"; +import { PatchNotesService } from "../../../services/wowup/patch-notes.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { getStandardImports } from "../../../tests/test-helpers"; +import { PatchNotesDialogComponent } from "./patch-notes-dialog.component"; + +describe("PatchNotesDialogComponent", () => { + let component: PatchNotesDialogComponent; + let fixture: ComponentFixture; + + let electronService: ElectronService; + let patchNotesService: PatchNotesService; + let dialogFactory: DialogFactory; + let wowupService: WowUpService; + let linkService: any; + + beforeEach(async () => { + electronService = jasmine.createSpyObj("ElectronService", { + getVersionNumber: Promise.resolve("30.0.0"), + }); + + dialogFactory = jasmine.createSpyObj("DialogFactory", [""], {}); + wowupService = jasmine.createSpyObj("WowUpService", [""], {}); + linkService = jasmine.createSpyObj("LinkService", [""], {}); + + patchNotesService = jasmine.createSpyObj("PatchNotesService", [""], { + changeLogs: [ + { + html: "", + }, + ], + }); + + await TestBed.configureTestingModule({ + declarations: [PatchNotesDialogComponent, TrustHtmlPipe], + imports: [...getStandardImports()], + }) + .overrideComponent(PatchNotesDialogComponent, { + set: { + providers: [ + { provide: ElectronService, useValue: electronService }, + { provide: PatchNotesService, useValue: patchNotesService }, + { provide: DialogFactory, useValue: dialogFactory }, + { provide: WowUpService, useValue: wowupService }, + { provide: LinkService, useValue: linkService }, + ], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PatchNotesDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.ts b/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.ts new file mode 100644 index 0000000..2fd933e --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/patch-notes-dialog/patch-notes-dialog.component.ts @@ -0,0 +1,46 @@ +import * as _ from "lodash"; +import { from } from "rxjs"; +import { first, switchMap } from "rxjs/operators"; + +import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +import { ChangeLog } from "../../../models/wowup/change-log"; +import { ElectronService } from "../../../services/electron/electron.service"; +import { PatchNotesService } from "../../../services/wowup/patch-notes.service"; + +@Component({ + selector: "app-patch-notes-dialog", + templateUrl: "./patch-notes-dialog.component.html", + styleUrls: ["./patch-notes-dialog.component.scss"], +}) +export class PatchNotesDialogComponent implements OnInit, AfterViewChecked { + @ViewChild("descriptionContainer", { read: ElementRef }) public descriptionContainer!: ElementRef; + + public title = from(this._electronService.getVersionNumber()).pipe( + first(), + switchMap((versionNumber) => this._translateService.get("DIALOGS.NEW_VERSION_POPUP.TITLE", { versionNumber })) + ); + + public changeLog: ChangeLog; + + public constructor( + private _translateService: TranslateService, + private _electronService: ElectronService, + private _patchNotesService: PatchNotesService + ) { + this.changeLog = _.first(this._patchNotesService.changeLogs) ?? { Version: "" }; + } + + public ngOnInit(): void {} + + public ngAfterViewChecked(): void { + // formatDynamicLinks(descriptionContainer, this.onOpenLink); + } + + // private onOpenLink = (element: HTMLAnchorElement): boolean => { + // this._linkService.confirmLinkNavigation(element.href).subscribe(); + + // return false; + // }; +} diff --git a/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.html b/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.html new file mode 100644 index 0000000..08948ce --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.scss b/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.scss new file mode 100644 index 0000000..075e8d1 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.scss @@ -0,0 +1,12 @@ +.progress-background { + width: 100%; + height: 0.5rem; + background-color: var(--background-secondary-1); + overflow: hidden; + border-radius: 4px; + + .progress-value { + background-color: var(--background-primary-3); + height: 100%; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.ts b/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.ts new file mode 100644 index 0000000..ae79399 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/progress-bar/progress-bar.component.ts @@ -0,0 +1,10 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "app-progress-bar", + templateUrl: "./progress-bar.component.html", + styleUrls: ["./progress-bar.component.scss"], +}) +export class ProgressBarComponent { + @Input() public value: number; +} diff --git a/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.html b/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.html new file mode 100644 index 0000000..02fa21e --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.html @@ -0,0 +1,12 @@ + diff --git a/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.scss b/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.scss new file mode 100644 index 0000000..3eccbc4 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.scss @@ -0,0 +1,3 @@ +.progress-button { + max-width: 100%; +} diff --git a/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.spec.ts new file mode 100644 index 0000000..297f48a --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { MatModule } from "../../../modules/mat-module"; +import { ProgressButtonComponent } from "./progress-button.component"; + +describe("ProgressButtonComponent", () => { + let component: ProgressButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ProgressButtonComponent], + imports: [MatModule, NoopAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProgressButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.ts b/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.ts new file mode 100644 index 0000000..2fe8316 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/progress-button/progress-button.component.ts @@ -0,0 +1,21 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +@Component({ + selector: "app-progress-button", + templateUrl: "./progress-button.component.html", + styleUrls: ["./progress-button.component.scss"], +}) +export class ProgressButtonComponent { + @Input() public value = 0; + @Input() public showProgress = false; + @Input() public disable = false; + + @Output() public btnClick: EventEmitter = new EventEmitter(); + + public onClickButton(evt: Event): void { + evt.preventDefault(); + evt.stopPropagation(); + + this.btnClick.emit(evt); + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.html b/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.html new file mode 100644 index 0000000..ff4abb0 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.html @@ -0,0 +1,12 @@ +

{{ "DIALOGS.TELEMETRY.TITLE" | translate }}

+
+

{{ "DIALOGS.TELEMETRY.DESCRIPTION" | translate }}

+
+
+ + +
diff --git a/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.scss b/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.spec.ts new file mode 100644 index 0000000..f1a1148 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.spec.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { TelemetryDialogComponent } from "./telemetry-dialog.component"; +import { MatDialogRef } from "@angular/material/dialog"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { httpLoaderFactory } from "../../../app.module"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { MatModule } from "../../../modules/mat-module"; + +describe("TelemetryDialogComponent", () => { + let component: TelemetryDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TelemetryDialogComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + providers: [{ provide: MatDialogRef, useValue: {} }], + }).compileComponents(); + + fixture = TestBed.createComponent(TelemetryDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.ts b/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.ts new file mode 100644 index 0000000..c566eb6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/telemetry-dialog/telemetry-dialog.component.ts @@ -0,0 +1,15 @@ +import { Component } from "@angular/core"; +import { MatDialogRef } from "@angular/material/dialog"; + +@Component({ + selector: "app-telemetry-dialog", + templateUrl: "./telemetry-dialog.component.html", + styleUrls: ["./telemetry-dialog.component.scss"], +}) +export class TelemetryDialogComponent { + public constructor(public dialogRef: MatDialogRef) {} + + public onNoClick(): void { + this.dialogRef.close(); + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.html b/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.html new file mode 100644 index 0000000..cd1f830 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.html @@ -0,0 +1,57 @@ +
+ +
+
+
{{ (isFullscreen ? "APP.WINDOW_TITLE_FULLSCREEN" : "APP.WINDOW_TITLE") | translate }}
+
+
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.scss b/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.scss new file mode 100644 index 0000000..3e1b211 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.scss @@ -0,0 +1,120 @@ +.titlebar { + display: flex; + flex-direction: row; + align-items: center; + position: relative; + margin-top: 4px; + + .debug-button { + height: 15px; + width: 15px; + } + + .theme-logo { + z-index: 1; + position: fixed; + top: 2px; + left: 4px; + height: 72px; + width: 72px; + opacity: 0.3; + overflow: hidden; + + .logo-img { + width: 100%; + height: 100%; + background-image: var(--title-logo); + background-repeat: no-repeat; + background-size: contain; + background-position: center; + } + img { + position: relative; + left: 0; + height: 150%; + width: auto; + } + } +} + +.mac { + height: 22px; + + .theme-logo { + height: 74px; + right: 4px; + left: initial; + } +} +.windows, +.linux { + height: 30px; +} +.titlebar-drag-region { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: block; + -webkit-user-select: none; + -webkit-app-region: drag; +} +.window-logo-container { + margin-left: 4px; + width: 35px; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + + img { + width: 20px; + } +} +.title-container { + flex-grow: 1; + text-align: center; +} +.window-control-container { + display: flex; + flex-grow: 0; + flex-shrink: 0; + text-align: center; + position: relative; + z-index: 3000; + -webkit-app-region: no-drag; + height: 100%; + + .bnet-control { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + z-index: 2500; + margin-top: -8px; + } + + .window-control { + height: 100%; + width: 46px; + display: flex; + justify-content: center; + align-items: center; + z-index: 2500; + margin-top: -8px; + + &:hover { + cursor: pointer; + } + + .icon { + height: 16px; + } + + img { + height: 16px; + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.spec.ts b/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.spec.ts new file mode 100644 index 0000000..06ecbbf --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.spec.ts @@ -0,0 +1,76 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { BehaviorSubject } from "rxjs"; + +import { OverlayContainer } from "@angular/cdk/overlay"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { MatModule } from "../../../modules/mat-module"; +import { ElectronService } from "../../../services"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { TitlebarComponent } from "./titlebar.component"; + +describe("TitlebarComponent", () => { + let component: TitlebarComponent; + let fixture: ComponentFixture; + let electronServiceSpy: any; + let wowUpServiceSpy: any; + + beforeEach(async () => { + electronServiceSpy = jasmine.createSpyObj( + "ElectronService", + { + on: undefined, + invoke: new Promise(() => {}), + }, + { + windowMaximized$: new BehaviorSubject(false).asObservable(), + } + ); + wowUpServiceSpy = jasmine.createSpyObj("WowUpService", [""], { + currentTheme: "horde ofc", + }); + + await TestBed.configureTestingModule({ + declarations: [TitlebarComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(TitlebarComponent, { + set: { + providers: [ + OverlayContainer, + { provide: ElectronService, useValue: electronServiceSpy }, + { provide: WowUpService, useValue: wowUpServiceSpy }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(TitlebarComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.ts b/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.ts new file mode 100644 index 0000000..4dcb3b0 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/titlebar/titlebar.component.ts @@ -0,0 +1,101 @@ +import { first, from, Subscription } from "rxjs"; + +import { Component, NgZone, OnDestroy } from "@angular/core"; +import { MatSnackBar, MatSnackBarRef } from "@angular/material/snack-bar"; +import { TranslateService } from "@ngx-translate/core"; + +import { + IPC_WINDOW_IS_MAXIMIZED, + IPC_MAXIMIZE_WINDOW, + IPC_MINIMIZE_WINDOW, + IPC_WINDOW_ENTER_FULLSCREEN, + IPC_WINDOW_IS_FULLSCREEN, + IPC_WINDOW_LEAVE_FULLSCREEN, +} from "../../../../common/constants"; +import { AppConfig } from "../../../../environments/environment"; +import { ElectronService } from "../../../services/electron/electron.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { CenteredSnackbarComponent } from "../../common/centered-snackbar/centered-snackbar.component"; + +@Component({ + selector: "app-titlebar", + templateUrl: "./titlebar.component.html", + styleUrls: ["./titlebar.component.scss"], +}) +export class TitlebarComponent implements OnDestroy { + private _subscriptions: Subscription[] = []; + private _snackBarRef: MatSnackBarRef | undefined; + + public isProd = AppConfig.production; + public isMaximized = false; + public isFullscreen = false; + + public constructor( + public electronService: ElectronService, + private _wowUpService: WowUpService, + private _ngZone: NgZone, + private _snackBar: MatSnackBar, + private _translateService: TranslateService, + ) { + const windowMaximizedSubscription = this.electronService.windowMaximized$.subscribe((maximized) => { + this._ngZone.run(() => (this.isMaximized = maximized)); + }); + + this._subscriptions = [windowMaximizedSubscription]; + + from(this.electronService.invoke(IPC_WINDOW_IS_FULLSCREEN)) + .pipe(first()) + .subscribe((isFullscreen) => { + this.isFullscreen = isFullscreen; + }); + + from(this.electronService.invoke(IPC_WINDOW_IS_MAXIMIZED)) + .pipe(first()) + .subscribe((isMaximized) => { + this.isMaximized = isMaximized; + }); + + this.electronService.on(IPC_WINDOW_ENTER_FULLSCREEN, () => { + this.isFullscreen = true; + const localeKey = this.electronService.isMac ? "APP.FULLSCREEN_SNACKBAR.MAC" : "APP.FULLSCREEN_SNACKBAR.WINDOWS"; + const message = this._translateService.instant(localeKey); + this._snackBarRef = this._snackBar.openFromComponent(CenteredSnackbarComponent, { + duration: 5000, + panelClass: ["wowup-snackbar", "text-1"], + data: { + message, + }, + verticalPosition: "top", + }); + }); + + this.electronService.on(IPC_WINDOW_LEAVE_FULLSCREEN, () => { + this.isFullscreen = false; + this._snackBarRef?.dismiss(); + this._snackBarRef = undefined; + }); + } + + public ngOnDestroy(): void { + this._subscriptions.forEach((subscription) => subscription.unsubscribe()); + } + + public async onClickClose(): Promise { + await this.electronService.closeWindow(); + } + + public async onDblClick(): Promise { + if (this.electronService.isMac) { + const action = await this.electronService.getUserDefaultSystemPreference( + "AppleActionOnDoubleClick", + "string", + ); + + if (action === "Maximize") { + await this.electronService.invoke(IPC_MAXIMIZE_WINDOW); + } else if (action === "Minimize") { + await this.electronService.invoke(IPC_MINIMIZE_WINDOW); + } + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.html b/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.html new file mode 100644 index 0000000..219a7bc --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.html @@ -0,0 +1,138 @@ +
+
+ + +
+ + +
{{ tab.titleKey | translate }}
+ +
+ + +
+ + +
+
{{ "PAGES.HOME.ACCOUNT_TAB_TITLE" | translate }}
+
Logged in
+
+
+ +
+ + + + +
{{ tab.titleKey | translate }}
+
+ +
+ + + + +
{{ "PAGES.HOME.GUIDE_TAB_TITLE" | translate }}
+
+ + + +
{{ "PAGES.HOME.DISCORD_TAB_TITLE" | translate }}
+
+ + + + + + +
+ + + + + + +
{{ "PAGES.HOME.COLLAPSE_BUTTON_TITLE" | translate }}
+
+ + +
+
+ {{ "ADS.AD_EXPLAINER_BUTTON" | translate }} +
+
+ +
+
+
diff --git a/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.scss b/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.scss new file mode 100644 index 0000000..8abc327 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.scss @@ -0,0 +1,180 @@ +.patreon-link { + .patron-img { + height: 25px; + } +} + +.tab-strip { + box-sizing: border-box; + display: flex; + flex-direction: column; + max-width: 400px; + height: 100%; + padding-top: calc(1em + 50px); + + &.collapsed { + .theme-logo { + width: 50px; + height: 50px; + } + + .tab-title { + display: none; + } + } + + .theme-logo { + z-index: 1; + position: fixed; + top: 1em; + left: 0.5em; + height: 72px; + width: 72px; + overflow: hidden; + + .logo-img { + width: 100%; + height: 100%; + background-image: var(--title-logo); + background-repeat: no-repeat; + background-size: contain; + background-position: center; + } + img { + position: relative; + left: 0; + height: 150%; + width: auto; + } + } + + .tab { + min-width: 40px; + height: 40px; + padding: 0 1em; + display: flex; + align-items: center; + box-sizing: border-box; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + text-decoration: none; + border-left: 0.5em solid transparent; + + a { + display: flex; + align-items: center; + } + + .icon-active { + svg { + fill: var(--text-1) !important; + } + } + + .tab-title { + color: var(--text-2); + margin: 0; + padding: 0; + line-height: 1em; + max-width: 200px; + overflow: hidden; + transition: all 0.3s ease 0.1s; + transition-property: color, max-width; + white-space: nowrap; + } + + .tab-subtitle { + font-size: 0.8em; + margin: 0; + padding: 0; + line-height: 1em; + } + + .tab-icon { + margin-right: 0.5em; + transition: margin-right 0.3s ease 0.1s; + } + + &.selected { + border-left: 0.5em solid var(--background-primary); + // background-color: var(--background-primary-3); + + .tab-icon { + margin-right: 0.5em; + } + + .tab-title { + color: var(--text-1); + max-width: 200px; + // margin-left: 0.5em; + } + } + + &:hover { + cursor: pointer; + background-color: var(--background-secondary-4); + } + + &.disabled { + pointer-events: none; + } + } + + .spacer { + flex-grow: 1; + } +} + +.theme-logo-glow { + z-index: 1; + position: fixed; + border-radius: 50%; + top: -60px; + left: -60px; + height: 150px; + width: 150px; + background: var(--background-primary); + background: radial-gradient(circle, var(--background-primary) 0%, rgba(0, 0, 0, 0) 70%); +} + +.ad-space { + width: 400px; + display: flex; + flex-direction: column; + + .addon-info-btn { + display: block; + width: 100%; + text-align: center; + line-height: 40px; + + &:hover { + border: 2px solid var(--background-primary); + box-sizing: border-box; + } + } + + .ad-details { + flex-grow: 1; + + .center-col { + display: flex; + flex-direction: column; + justify-content: flex-end; + height: 100%; + } + } + + .ad { + flex-shrink: 0; + width: 400px; + height: 300px; + background-color: var(--background-secondary-4); + background-image: var(--ad-placeholder); + background-position: top 32px center; + background-repeat: no-repeat; + background-size: 55%; + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.ts b/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.ts new file mode 100644 index 0000000..9bbdd02 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/vertical-tabs/vertical-tabs.component.ts @@ -0,0 +1,192 @@ +import { BehaviorSubject, combineLatest, Observable, Subject } from "rxjs"; +import { map, takeUntil } from "rxjs/operators"; + +import { Component, OnDestroy, OnInit } from "@angular/core"; + +import { + FEATURE_ACCOUNTS_ENABLED, + PREF_TABS_COLLAPSED, + TAB_INDEX_ABOUT, + TAB_INDEX_GET_ADDONS, + TAB_INDEX_MY_ADDONS, + TAB_INDEX_NEWS, + TAB_INDEX_SETTINGS, + TRUE_STR, +} from "../../../../common/constants"; +import { AppConfig } from "../../../../environments/environment"; +import { ElectronService } from "../../../services"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { AddonProviderFactory } from "../../../services/addons/addon.provider.factory"; +import { MatDialog } from "@angular/material/dialog"; +import { AlertDialogComponent } from "../alert-dialog/alert-dialog.component"; +import { TranslateService } from "@ngx-translate/core"; +import { PreferenceStorageService } from "../../../services/storage/preference-storage.service"; +import { AdPageOptions } from "wowup-lib-core"; + +interface Tab { + titleKey?: string; + tooltipKey: string; + icon: string; + badge?: boolean; + isSelected$: Observable; + isDisabled$: Observable; + onClick: (tab: Tab) => void; +} + +@Component({ + selector: "app-vertical-tabs", + templateUrl: "./vertical-tabs.component.html", + styleUrls: ["./vertical-tabs.component.scss"], +}) +export class VerticalTabsComponent implements OnInit, OnDestroy { + private readonly destroy$: Subject = new Subject(); + + public wowUpWebsiteUrl = AppConfig.wowUpWebsiteUrl; + public TAB_INDEX_ACCOUNT = TAB_INDEX_ABOUT; + public FEATURE_ACCOUNTS_ENABLED = FEATURE_ACCOUNTS_ENABLED; + public adPageParams$ = new BehaviorSubject([]); + public isCollapsedSrc = new BehaviorSubject(false); + + public isCollapsed$ = combineLatest([this.isCollapsedSrc, this.sessionService.adSpace$]).pipe( + map(([isCollapsed, adSpace]) => { + if (adSpace) { + return false; + } + return isCollapsed; + }), + ); + + public isAccountSelected$ = this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_ABOUT)); + + private myAddonsTab: Tab = { + titleKey: "PAGES.HOME.MY_ADDONS_TAB_TITLE", + tooltipKey: "PAGES.HOME.MY_ADDONS_TAB_TITLE", + icon: "fas:dice-d6", + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_MY_ADDONS)), + isDisabled$: combineLatest([ + this._warcraftInstallationService.wowInstallations$, + this.sessionService.enableControls$, + ]).pipe(map(([installations, enableControls]) => !enableControls || installations.length === 0)), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_MY_ADDONS; + }, + }; + + private getAddonsTab: Tab = { + titleKey: "PAGES.HOME.GET_ADDONS_TAB_TITLE", + tooltipKey: "PAGES.HOME.GET_ADDONS_TAB_TITLE", + icon: "fas:magnifying-glass", + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_GET_ADDONS)), + isDisabled$: combineLatest([ + this._warcraftInstallationService.wowInstallations$, + this.sessionService.enableControls$, + ]).pipe(map(([installations, enableControls]) => !enableControls || installations.length === 0)), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_GET_ADDONS; + }, + }; + + private aboutTab: Tab = { + titleKey: "PAGES.HOME.ACCOUNT_TAB_TITLE", + tooltipKey: "PAGES.HOME.ACCOUNT_TAB_TITLE", + icon: "fas:user-circle", + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_ABOUT)), + isDisabled$: this.sessionService.enableControls$.pipe(map((enabled) => !enabled)), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_ABOUT; + }, + }; + + private newsTab: Tab = { + titleKey: "PAGES.HOME.NEWS_TAB_TITLE", + tooltipKey: "PAGES.HOME.NEWS_TAB_TITLE", + icon: "fas:newspaper", + badge: true, + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_NEWS)), + isDisabled$: this.sessionService.enableControls$.pipe(map((enabled) => !enabled)), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_NEWS; + }, + }; + + private settingsTab: Tab = { + titleKey: "PAGES.HOME.OPTIONS_TAB_TITLE", + tooltipKey: "PAGES.HOME.OPTIONS_TAB_TITLE", + icon: "fas:gear", + isSelected$: this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_SETTINGS)), + isDisabled$: this.sessionService.enableControls$.pipe(map((enabled) => !enabled)), + onClick: (): void => { + this.sessionService.selectedHomeTab = TAB_INDEX_SETTINGS; + }, + }; + + public tabsTop: Tab[] = [this.myAddonsTab, this.getAddonsTab]; + + public tabsBottom: Tab[] = [this.settingsTab]; + + public constructor( + public electronService: ElectronService, + public sessionService: SessionService, + private _dialog: MatDialog, + private _translateService: TranslateService, + private _addonProviderService: AddonProviderFactory, + private _warcraftInstallationService: WarcraftInstallationService, + private _preferences: PreferenceStorageService, + ) { + this.sessionService.adSpace$.pipe(takeUntil(this.destroy$)).subscribe((enabled) => { + if (enabled) { + const providers = this._addonProviderService.getAdRequiredProviders(); + const providerParams = providers + .map((provider) => provider.getAdPageParams()) + .filter((param) => param !== undefined); + + console.debug("providerParams", providerParams); + + this.adPageParams$.next(providerParams); + } else { + this.adPageParams$.next([]); + } + }); + } + + public ngOnInit(): void { + this._preferences + .getAsync(PREF_TABS_COLLAPSED) + .then((val) => { + this.isCollapsedSrc.next(val === TRUE_STR); + }) + .catch(console.error); + } + + public ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + public async toggleCollapse(): Promise { + try { + const nextVal = !this.isCollapsedSrc.value; + await this._preferences.setAsync(PREF_TABS_COLLAPSED, nextVal); + this.isCollapsedSrc.next(!this.isCollapsedSrc.value); + } catch (e) { + console.error(e); + } + } + + public onClickTab(tabIndex: number): void { + this.sessionService.selectedHomeTab = tabIndex; + } + + public onClickAdExplainer(): void { + this._dialog.open(AlertDialogComponent, { + minWidth: 250, + maxWidth: 400, + disableClose: true, + data: { + title: this._translateService.instant("ADS.AD_EXPLAINER_DIALOG.TITLE"), + message: this._translateService.instant("ADS.AD_EXPLAINER_DIALOG.MESSAGE"), + }, + }); + } +} diff --git a/WowUp/wowup-electron/src/app/components/common/webview/webview.component.html b/WowUp/wowup-electron/src/app/components/common/webview/webview.component.html new file mode 100644 index 0000000..a5c4a94 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/webview/webview.component.html @@ -0,0 +1 @@ +
diff --git a/WowUp/wowup-electron/src/app/components/common/webview/webview.component.scss b/WowUp/wowup-electron/src/app/components/common/webview/webview.component.scss new file mode 100644 index 0000000..3c4c10f --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/webview/webview.component.scss @@ -0,0 +1,4 @@ +.webview-container { + width: 100%; + height: 100%; +} diff --git a/WowUp/wowup-electron/src/app/components/common/webview/webview.component.ts b/WowUp/wowup-electron/src/app/components/common/webview/webview.component.ts new file mode 100644 index 0000000..7190cb5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/common/webview/webview.component.ts @@ -0,0 +1,145 @@ +import { AfterViewInit, Component, ElementRef, Input, NgZone, OnDestroy, ViewChild } from "@angular/core"; +import { nanoid } from "nanoid"; +import { filter, Subject, takeUntil } from "rxjs"; +import { AdPageOptions } from "wowup-lib-core"; +import { ElectronService } from "../../../services/electron/electron.service"; +import { FileService } from "../../../services/files/file.service"; +import { LinkService } from "../../../services/links/link.service"; +import { SessionService } from "../../../services/session/session.service"; +import { UiMessageService } from "../../../services/ui-message/ui-message.service"; + +@Component({ + selector: "app-webview", + templateUrl: "./webview.component.html", + styleUrls: ["./webview.component.scss"], +}) +export class WebViewComponent implements OnDestroy, AfterViewInit { + @Input("options") public options: AdPageOptions; + + @ViewChild("webviewContainer", { read: ElementRef }) public webviewContainer!: ElementRef; + + private readonly destroy$: Subject = new Subject(); + + private _tag: Electron.WebviewTag; + private _id: string = nanoid(); + private _webviewReady = false; + + public constructor( + private _electronService: ElectronService, + private _fileService: FileService, + private _linkService: LinkService, + private _sessionService: SessionService, + private _uiMessageService: UiMessageService, + private _ngZone: NgZone, + ) {} + + public ngAfterViewInit(): void { + this.initWebview(this.webviewContainer).catch((e) => console.error(e)); + this._electronService.on("webview-new-window", this.onWebviewNewWindow); + this._uiMessageService.message$ + .pipe( + takeUntil(this.destroy$), + filter((msg) => msg.action === "ad-frame-reload"), + ) + .subscribe(() => { + if (this._webviewReady) { + this._tag?.reloadIgnoringCache(); + } + }); + } + + public ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + + this._electronService.off("webview-new-window", this.onWebviewNewWindow); + + // Clean up the webview element + if (this._tag) { + if (this._tag.isDevToolsOpened()) { + this._tag.closeDevTools(); + } + this._tag = undefined; + } + + this.webviewContainer.nativeElement.innerHTML = 0; + } + + private async initWebview(element: ElementRef) { + // const pageReferrer = this.options.referrer ? `httpreferrer="${this.options.referrer}"` : ""; + // const userAgent = this.options.userAgent ?? ""; + const preloadPath = `file://${await this._fileService.getAssetFilePath(this.options.preloadFilePath)}`; + // const preload = this.options.preloadFilePath ? `preload="${preloadPath}"` : ""; + const partition = this.options.partition ?? "memcache"; + + console.debug("initWebview", this.options); + + const placeholder = document.createElement("div"); + placeholder.style.width = "400px"; + placeholder.style.height = "300px"; + + /* eslint-disable no-irregular-whitespace */ + const webview: Electron.WebviewTag = document.createElement("webview"); + webview.id = this._id; + webview.src = this.options.pageUrl; + webview.setAttribute("style", "width: 100%; height: 100%;"); + webview.nodeintegration = false; + webview.nodeintegrationinsubframes = false; + webview.plugins = false; + webview.allowpopups = true; + webview.partition = partition; + webview.preload = preloadPath; + // webview.useragent = userAgent; + + // placeholder.innerHTML = ` + // + // `; + /* eslint-enable no-irregular-whitespace */ + + this._tag = webview; // placeholder.firstElementChild as Electron.WebviewTag; + + this._tag.addEventListener("error", (evt) => { + console.error("ERROR", evt); + }); + + this._tag.addEventListener("did-fail-load", (evt) => { + console.error("did-fail-load", evt); + }); + + this._tag.addEventListener("dom-ready", this.onWebviewReady); + + placeholder.appendChild(webview); + element.nativeElement.appendChild(placeholder); + } + + private onWebviewReady = () => { + console.debug("onWebviewReady", this._tag); + + this._webviewReady = true; + + this._sessionService.debugAdFrame$.pipe(takeUntil(this.destroy$)).subscribe(() => { + if (!this._tag.isDevToolsOpened()) { + this._tag?.openDevTools(); + } + }); + + this._tag.removeEventListener("dom-ready", this.onWebviewReady); + // this._tag.openDevTools(); + }; + + private onWebviewNewWindow = (evt, details: Electron.HandlerDetails) => { + console.debug(`webview-new-window`, details); + this._ngZone.run(() => { + this._linkService.confirmLinkNavigation(details.url).subscribe(); + }); + }; +} diff --git a/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.html b/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.html new file mode 100644 index 0000000..573beb2 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.html @@ -0,0 +1,53 @@ +
+ + +
+ +
+
+
+
+
+

+
+

{{ news.publishedAt | localeDate }} - {{ news.publishedBy }}

+ +
+

+
+
+
+
+ +
+
diff --git a/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.scss b/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.scss new file mode 100644 index 0000000..db307df --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.scss @@ -0,0 +1,97 @@ +.busy-container { + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: relative; +} +.news-container { + display: flex; + justify-content: center; + position: relative; + overflow: hidden; + + .theme-logo { + content: ""; + opacity: 0.02; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 1; + overflow: hidden; + pointer-events: none; + + .logo-img { + height: 100vh; + background-image: var(--theme-logo); + background-repeat: no-repeat; + background-position: 0 0; + background-size: contain; + } + } + + .fab-container { + position: absolute; + left: 1em; + top: 1em; + } + + .news-list { + overflow: auto; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + padding-left: 1em; + padding-right: 1em; + } + + .news-item { + max-width: 1024px; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + height: 200px; + margin: 1.5em 0; + box-sizing: border-box; + border-left: 3px solid var(--background-primary); + padding-right: 1em; + background-color: var(--background-secondary-4); + transition: background-color 0.5s ease; + + &:last-child { + margin-bottom: 3em; + } + + &:hover { + background-color: var(--background-secondary-5); + cursor: pointer; + } + + .title { + font-size: 0.8em; + } + + .thumbnail { + width: 300px; + height: 100%; + flex-shrink: 0; + margin-right: 1em; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + } + + .description { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.spec.ts b/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.spec.ts new file mode 100644 index 0000000..316dab9 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.spec.ts @@ -0,0 +1,72 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; +import { ElectronService } from "../../services"; +import { DialogFactory } from "../../services/dialog/dialog.factory"; +import { LinkService } from "../../services/links/link.service"; +import { NewsService } from "../../services/news/news.service"; +import { SessionService } from "../../services/session/session.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +import { getStandardImports } from "../../tests/test-helpers"; + +import { NewsPanelComponent } from "./news-panel.component"; + +describe("NewsPanelComponent", () => { + let component: NewsPanelComponent; + let fixture: ComponentFixture; + let electronService: ElectronService; + let newsService: NewsService; + let dialogFactory: DialogFactory; + let wowUpService: WowUpService; + let sessionService: SessionService; + let linkService: any; + + beforeEach(async () => { + electronService = jasmine.createSpyObj("ElectronService", [""], { + onRendererEvent: () => undefined, + }); + + newsService = jasmine.createSpyObj("NewsService", [""], { + newsItems$: new BehaviorSubject([]), + }); + dialogFactory = jasmine.createSpyObj("DialogFactory", [""], {}); + linkService = jasmine.createSpyObj("LinkService", [""], {}); + wowUpService = jasmine.createSpyObj("WowUpService", [""], {}); + sessionService = jasmine.createSpyObj("SessionService", [""], { + selectedHomeTab$: new BehaviorSubject(0), + setContextText: () => {}, + }); + + let testBed = TestBed.configureTestingModule({ + declarations: [NewsPanelComponent], + imports: [...getStandardImports()], + }); + + testBed = testBed.overrideComponent(NewsPanelComponent, { + set: { + providers: [ + { provide: NewsService, useValue: newsService }, + { provide: WowUpService, useValue: wowUpService }, + { provide: SessionService, useValue: sessionService }, + { provide: DialogFactory, useValue: dialogFactory }, + { provide: LinkService, useValue: linkService }, + { + provide: ElectronService, + useValue: electronService, + }, + ], + }, + }); + + await testBed.compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NewsPanelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.ts b/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.ts new file mode 100644 index 0000000..0433309 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/news-panel/news-panel.component.ts @@ -0,0 +1,122 @@ +import { from, of, Subscription } from "rxjs"; +import { catchError, delay, first, map, switchMap } from "rxjs/operators"; + +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +import { AppConfig } from "../../../environments/environment"; +import { ElectronService } from "../../services/electron/electron.service"; +import { LinkService } from "../../services/links/link.service"; +import { NewsItem, NewsService } from "../../services/news/news.service"; +import { SessionService } from "../../services/session/session.service"; +import { SnackbarService } from "../../services/snackbar/snackbar.service"; + +@Component({ + selector: "app-news-panel", + templateUrl: "./news-panel.component.html", + styleUrls: ["./news-panel.component.scss"], +}) +export class NewsPanelComponent implements OnInit, OnDestroy { + @Input("tabIndex") public tabIndex!: number; + + public isBusy = false; + public lastUpdated = 0; + + private _isLazyLoaded = false; + private _isSelectedTab = false; + private _subscriptions: Subscription[] = []; + + public constructor( + public newsService: NewsService, + public electronService: ElectronService, + private _sessionService: SessionService, + private _translateService: TranslateService, + private _linkService: LinkService, + private _snackbarService: SnackbarService + ) { + const homeTabSub = _sessionService.selectedHomeTab$.subscribe((tabIndex) => { + this._isSelectedTab = tabIndex === this.tabIndex; + if (!this._isSelectedTab) { + return; + } + + this.lazyLoad(); + }); + + const newsItemCountSub = this.newsService.newsItems$ + .pipe(map((items) => this.setPageContextText(items.length))) + .subscribe(); + + this._subscriptions.push(homeTabSub, newsItemCountSub); + } + + public ngOnInit(): void {} + + public ngOnDestroy(): void { + this._subscriptions.forEach((sub) => sub.unsubscribe()); + this._subscriptions = []; + } + + public onClickRefresh(): void { + this.isBusy = true; + this.setPageContextLoading(); + + of(true) + .pipe( + first(), + delay(500), + switchMap(() => from(this.newsService.loadFeeds())), + map(() => { + this.lastUpdated = Date.now(); + }), + catchError((err) => { + console.error(err); + return of(undefined); + }) + ) + .subscribe(() => { + this.isBusy = false; + }); + } + + public onClickItem(item: NewsItem): void { + this._linkService.confirmLinkNavigation(item.link).subscribe(); + } + + public onClickLink(item: NewsItem, evt: MouseEvent): void { + evt.preventDefault(); + evt.stopPropagation(); + + this._snackbarService.showSuccessSnackbar("PAGES.NEWS.NEWS_LINK_COPY_TOAST", { + timeout: 2000, + }); + } + + private lazyLoad(): void { + if (!this.shouldReloadFeeds() && this._isLazyLoaded) { + this.setPageContextText(this.newsService.newsItems$.value.length); + return; + } + this.onClickRefresh(); + this._isLazyLoaded = true; + } + + private setPageContextLoading() { + const text: string = this._translateService.instant("COMMON.PROGRESS_SPINNER.LOADING"); + this._sessionService.setContextText(this.tabIndex, text); + } + + private setPageContextText(rowCount: number) { + const contextStr: string = + rowCount > 0 ? this._translateService.instant("PAGES.NEWS.PAGE_CONTEXT_FOOTER", { count: rowCount }) : ""; + + this._sessionService.setContextText(this.tabIndex, contextStr); + } + + private shouldReloadFeeds() { + return ( + this.newsService.lastFetchedAt !== 0 && + Date.now() - this.newsService.lastFetchedAt >= AppConfig.newsRefreshIntervalMs + ); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/about/about.component.html b/WowUp/wowup-electron/src/app/components/options/about/about.component.html new file mode 100644 index 0000000..268d23a --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/about/about.component.html @@ -0,0 +1,51 @@ +
+ +
+ +
+

{{ "PAGES.ABOUT.ATTRIBUTIONS_TITLE" | translate }}

+ +
+
+

+ {{ "PAGES.ABOUT.CHANGE_LOG_SECTION_LABEL" | translate }} +

+
    +
  • +
    {{ cl.Version }}
    +
    {{ cl.Description }}
    +
    {{ formatChanges(cl) }}
    +
    +
  • +
+
+
+
diff --git a/WowUp/wowup-electron/src/app/components/options/about/about.component.scss b/WowUp/wowup-electron/src/app/components/options/about/about.component.scss new file mode 100644 index 0000000..04d0e64 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/about/about.component.scss @@ -0,0 +1,92 @@ +.theme-logo { + content: ""; + opacity: 0.02; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 1; + overflow: hidden; + pointer-events: none; + + .logo-img { + height: 100vh; + background-image: var(--theme-logo); + background-repeat: no-repeat; + background-position: 0 0; + background-size: contain; + } +} + +.about-container { + display: flex; + justify-content: center; +} +.about { + max-width: 1024px; + z-index: 2; + + .header { + text-align: center; + + .logo { + width: 150px; + } + + h2 { + margin-bottom: 0; + } + + .link-container { + margin-top: 1em; + display: flex; + flex-direction: row; + justify-content: center; + } + + .patron-img { + width: 120px; + } + } + + .header-2 { + a { + color: inherit; + + &:visited { + color: inherit; + } + } + } + + .changelog-container { + padding: 1em; + + .change-log-list { + padding-left: 0; + } + + .changelog { + list-style-type: none; + margin: 1.5em 0; + border-left-width: 3px; + border-left-style: solid; + padding: 0.5em 1em 1em 1em; + + .description { + margin: 0; + word-break: break-all; + white-space: pre-wrap; + } + } + } +} + +.mac { + .header { + .logo { + border-radius: 23px; + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/about/about.component.spec.ts b/WowUp/wowup-electron/src/app/components/options/about/about.component.spec.ts new file mode 100644 index 0000000..99a8ffd --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/about/about.component.spec.ts @@ -0,0 +1,85 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { BehaviorSubject } from "rxjs"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { ElectronService } from "../../../services"; +import { DialogFactory } from "../../../services/dialog/dialog.factory"; +import { LinkService } from "../../../services/links/link.service"; +import { SessionService } from "../../../services/session/session.service"; +import { PatchNotesService } from "../../../services/wowup/patch-notes.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { AboutComponent } from "./about.component"; +import { MatModule } from "../../../modules/mat-module"; + +describe("AboutComponent", () => { + let component: AboutComponent; + let fixture: ComponentFixture; + let wowupService: WowUpService; + let electronServiceSpy: any; + let sessionService: SessionService; + let patchNotesService: PatchNotesService; + let dialogFactory: DialogFactory; + let linkService: any; + + beforeEach(async () => { + wowupService = jasmine.createSpyObj("WowUpService", [""]); + electronServiceSpy = jasmine.createSpyObj("ElectronService", [""], { + getVersionNumber: () => Promise.resolve("2.0.0"), + }); + + sessionService = jasmine.createSpyObj("SessionService", [""], { + selectedHomeTab$: new BehaviorSubject(1), + }); + + linkService = jasmine.createSpyObj("LinkService", [""], {}); + patchNotesService = jasmine.createSpyObj("PatchNotesService", [""], {}); + dialogFactory = jasmine.createSpyObj("DialogFactory", [""], {}); + + await TestBed.configureTestingModule({ + declarations: [AboutComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(AboutComponent, { + set: { + providers: [ + { provide: LinkService, useValue: linkService }, + { provide: WowUpService, useValue: wowupService }, + { provide: ElectronService, useValue: electronServiceSpy }, + { provide: SessionService, useValue: sessionService }, + { provide: PatchNotesService, useValue: patchNotesService }, + { provide: DialogFactory, useValue: dialogFactory }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(AboutComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/options/about/about.component.ts b/WowUp/wowup-electron/src/app/components/options/about/about.component.ts new file mode 100644 index 0000000..14c4359 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/about/about.component.ts @@ -0,0 +1,73 @@ +import { from, Subscription } from "rxjs"; +import { filter, map } from "rxjs/operators"; + +import { + AfterViewChecked, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnDestroy, + ViewChild, +} from "@angular/core"; +import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; + +import { ChangeLog } from "../../../models/wowup/change-log"; +import { ElectronService } from "../../../services"; +import { SessionService } from "../../../services/session/session.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { PatchNotesService } from "../../../services/wowup/patch-notes.service"; + +@Component({ + selector: "app-about", + templateUrl: "./about.component.html", + styleUrls: ["./about.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AboutComponent implements OnDestroy, AfterViewChecked { + @Input("tabIndex") public tabIndex!: number; + + @ViewChild("changelogContainer", { read: ElementRef }) public changelogContainer!: ElementRef; + + private _subscriptions: Subscription[] = []; + + public changeLogs: ChangeLog[] = []; + public versionNumber = from(this.electronService.getVersionNumber()); + + public constructor( + public wowUpService: WowUpService, + public electronService: ElectronService, + private _sessionService: SessionService, + private _patchNotesService: PatchNotesService, + private _sanitizer: DomSanitizer + ) { + this.changeLogs = this._patchNotesService.changeLogs; + const tabIndexSub = this._sessionService.selectedHomeTab$ + .pipe( + filter((newTabIndex) => newTabIndex === this.tabIndex), + map(() => { + window.getSelection()?.empty(); + }) + ) + .subscribe(); + + this._subscriptions.push(tabIndexSub); + } + + public ngOnDestroy(): void { + this._subscriptions.forEach((subscription) => subscription.unsubscribe()); + this._subscriptions = []; + } + + public ngAfterViewChecked(): void { + // formatDynamicLinks(descriptionContainer, this.onOpenLink); + } + + public formatChanges(changeLog: ChangeLog): string { + return (changeLog.changes ?? []).join("\n"); + } + + public trustHtml(html: string): SafeHtml { + return this._sanitizer.bypassSecurityTrustHtml(html); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.html b/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.html new file mode 100644 index 0000000..4e2562a --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.html @@ -0,0 +1,85 @@ +
+

+ {{ "PAGES.OPTIONS.ADDON.TITLE" | translate }} +

+ +
+
+ {{ "PAGES.OPTIONS.ADDON.ENABLED_PROVIDERS.FIELD_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.ADDON.ENABLED_PROVIDERS.DESCRIPTION" | translate }} +
+ + +
+ {{ state.providerName }} + + - {{ "PAGES.OPTIONS.ADDON.AD_REQUIRED_HINT" | translate }} + - {{ state.providerNote | translate }} +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ {{ "PAGES.OPTIONS.ADDON.CURSE_FORGE_V2.API_KEY_TITLE" | translate }} +
+ {{ "PAGES.OPTIONS.ADDON.CURSE_FORGE_V2.API_KEY_DESCRIPTION" | translate }} +
+ + {{ "PAGES.OPTIONS.ADDON.CURSE_FORGE_V2.API_KEY_TITLE" | translate }} + + +
+
+
+
+
+ {{ "PAGES.OPTIONS.ADDON.GITHUB_PERSONAL_ACCESS_TOKEN.TITLE" | translate }} +
+ +
+ + {{ "PAGES.OPTIONS.ADDON.GITHUB_PERSONAL_ACCESS_TOKEN.PLACEHOLDER" | translate }} + + +
+
+
+
+ {{ "PAGES.OPTIONS.ADDON.WAGO_ACCESS_KEY.TITLE" | translate }} +
+ +
+ + {{ "PAGES.OPTIONS.ADDON.WAGO_ACCESS_KEY.PLACEHOLDER" | translate }} + + +
+
+
diff --git a/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.scss b/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.scss new file mode 100644 index 0000000..8224ad6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.scss @@ -0,0 +1,21 @@ +.container { + padding: 1em; + + .hint { + color: var(--text-3); + } + + .section { + margin-top: 1em; + } +} + +.setting { + margin-top: 1em; + margin-bottom: 20px; +} +.divider { + margin-top: 20px; + height: 1px; + border-top: thin solid var(--divider-color); +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts b/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts new file mode 100644 index 0000000..3b0feb6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-addon-section/options-addon-section.component.ts @@ -0,0 +1,207 @@ +import { + BehaviorSubject, + catchError, + debounceTime, + first, + from, + Observable, + of, + Subject, + switchMap, + takeUntil, + zip, +} from "rxjs"; + +import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { FormGroup, UntypedFormControl } from "@angular/forms"; +import { + MatSelectionListChange, + MatListOption, +} from "@angular/material/list"; +import { TranslateService } from "@ngx-translate/core"; + +import { + ADDON_PROVIDER_CURSEFORGE, + ADDON_PROVIDER_WAGO, + PREF_CF2_API_KEY, + PREF_GITHUB_PERSONAL_ACCESS_TOKEN, + PREF_WAGO_ACCESS_KEY, +} from "../../../../common/constants"; +import { AppConfig } from "../../../../environments/environment"; +import { AddonProviderState } from "../../../models/wowup/addon-provider-state"; +import { AddonProviderFactory } from "../../../services/addons/addon.provider.factory"; +import { DialogFactory } from "../../../services/dialog/dialog.factory"; +import { SensitiveStorageService } from "../../../services/storage/sensitive-storage.service"; + +import { AddonProviderType } from "wowup-lib-core"; +import { CurseMigrationDialogComponent } from "../../../components/common/curse-migration-dialog/curse-migration-dialog.component"; +import { MatDialog } from "@angular/material/dialog"; + +interface AddonProviderStateModel extends AddonProviderState { + adRequired: boolean; + providerNote?: string; +} + +@Component({ + selector: "app-options-addon-section", + templateUrl: "./options-addon-section.component.html", + styleUrls: ["./options-addon-section.component.scss"], +}) +export class OptionsAddonSectionComponent implements OnInit, OnDestroy { + @ViewChild("prefForm", { read: ElementRef }) public prefForm!: ElementRef; + + private destroy$: Subject = new Subject(); + + public addonProviderStates$ = new BehaviorSubject([]); + + public preferenceForm = new FormGroup({ + cfV2ApiKey: new UntypedFormControl(""), + ghPersonalAccessToken: new UntypedFormControl(""), + wagoAccessToken: new UntypedFormControl(""), + }); + + public constructor( + private _dialog: MatDialog, + private _addonProviderService: AddonProviderFactory, + private _sensitiveStorageService: SensitiveStorageService, + private _translateService: TranslateService, + private _dialogFactory: DialogFactory, + ) { + this._addonProviderService.addonProviderChange$.subscribe(() => { + this.loadProviderStates(); + }); + + this.preferenceForm.valueChanges + .pipe( + takeUntil(this.destroy$), + debounceTime(300), + switchMap((ch) => { + const tasks: Observable[] = []; + if (typeof ch?.cfV2ApiKey === "string") { + tasks.push(from(this._sensitiveStorageService.setAsync(PREF_CF2_API_KEY, ch.cfV2ApiKey))); + } + if (typeof ch?.ghPersonalAccessToken === "string") { + tasks.push( + from(this._sensitiveStorageService.setAsync(PREF_GITHUB_PERSONAL_ACCESS_TOKEN, ch.ghPersonalAccessToken)), + ); + } + if (typeof ch?.wagoAccessToken === "string") { + tasks.push(from(this.onWagoAccessTokenChange(ch.wagoAccessToken))); + } + return zip(tasks); + }), + catchError((e) => { + console.error(e); + return of(undefined); + }), + ) + .subscribe(); + } + + public insertCurseApiKey = (): void => { + this.preferenceForm.get("cfV2ApiKey")?.setValue(AppConfig.curseforge.apiKey); + }; + + public ngOnInit(): void { + this.loadProviderStates(); + this.loadSensitiveData().catch(console.error); + } + + public openCurseMigrationDialog = (): void => { + this._dialog.open(CurseMigrationDialogComponent, { + disableClose: true, + }); + }; + + public ngAfterViewChecked(): void { + // formatDynamicLinks(descriptionContainer, this.onOpenLink); + } + + public ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + public async onProviderStateSelectionChange(event: MatSelectionListChange): Promise { + for (const option of event.options) { + const providerName: AddonProviderType = option.value; + if (option.selected && providerName == ADDON_PROVIDER_CURSEFORGE) { + this.insertCurseApiKey(); + } + if (option.selected && providerName === ADDON_PROVIDER_WAGO) { + this.onWagoEnable(option); + } else { + await this._addonProviderService.setProviderEnabled(providerName, option.selected); + } + } + } + + private onWagoEnable(option: MatListOption) { + const providerName: AddonProviderType = option.value; + const title: string = this._translateService.instant("DIALOGS.PERMISSIONS.WAGO.TOGGLE_LABEL"); + const message: string = this._translateService.instant("DIALOGS.PERMISSIONS.WAGO.DESCRIPTION", { + termsUrl: AppConfig.wago.termsUrl, + dataUrl: AppConfig.wago.dataConsentUrl, + }); + + const dialogRef = this._dialogFactory.getConfirmDialog(title, message); + dialogRef + .afterClosed() + .pipe( + first(), + switchMap((result) => { + if (result) { + return from(this._addonProviderService.setProviderEnabled(providerName, option.selected)); + } else { + option.selected = !option.selected; + } + return of(undefined); + }), + catchError((err) => { + console.error(err); + return of(undefined); + }) + ) + .subscribe(); + } + + private async loadSensitiveData() { + try { + const cfV2ApiKey = await this._sensitiveStorageService.getAsync(PREF_CF2_API_KEY); + const ghPersonalAccessToken = await this._sensitiveStorageService.getAsync(PREF_GITHUB_PERSONAL_ACCESS_TOKEN); + const wagoAccessToken = await this._sensitiveStorageService.getAsync(PREF_WAGO_ACCESS_KEY); + + this.preferenceForm.get("cfV2ApiKey")?.setValue(cfV2ApiKey); + this.preferenceForm.get("ghPersonalAccessToken")?.setValue(ghPersonalAccessToken); + this.preferenceForm.get("wagoAccessToken")?.setValue(wagoAccessToken); + } catch (e) { + console.error(e); + } + } + + private loadProviderStates() { + const providerStates = this._addonProviderService.getAddonProviderStates().filter((provider) => provider.canEdit); + const providerStateModels: AddonProviderStateModel[] = providerStates.map((state) => { + const provider = this._addonProviderService.getProvider(state.providerName); + if (provider === undefined) { + throw new Error("loadProviderStates got undefined provider"); + } + + return { ...state, adRequired: provider.adRequired, providerNote: provider.providerNote }; + }); + + this.addonProviderStates$.next(providerStateModels); + } + + private async onWagoAccessTokenChange(accessToken: string | undefined) { + await this._sensitiveStorageService.setAsync(PREF_WAGO_ACCESS_KEY, accessToken); + + const wago = this._addonProviderService.getProvider(ADDON_PROVIDER_WAGO); + if (wago === undefined) { + console.warn("onWagoAccessTokenChange failed to find wago provider"); + return; + } + + await this._addonProviderService.setProviderEnabled(ADDON_PROVIDER_WAGO, wago.enabled); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.html b/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.html new file mode 100644 index 0000000..664e21e --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.html @@ -0,0 +1,249 @@ +
+

+ {{ "PAGES.OPTIONS.APPLICATION.TITLE" | translate }} +

+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.SET_LANGUAGE_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.APPLICATION.SET_LANGUAGE_DESCRIPTION" | translate }} +
+ + {{ "PAGES.OPTIONS.APPLICATION.CURRENT_LANGUAGE_LABEL" | translate }} + + + {{ language.label }} + + + +
+
+
+ + +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.THEME_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.APPLICATION.THEME_DESCRIPTION" | translate }} +
+ + {{ "PAGES.OPTIONS.APPLICATION.THEME_LABEL" | translate }} + + + + {{ theme.display | translate }} + + + + +
+
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.TELEMETRY_LABEL" | translate }} +
+
+ +
+ + {{ "PAGES.OPTIONS.APPLICATION.TELEMETRY_DESCRIPTION" | translate }} + +
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.MINIMIZE_ON_CLOSE_LABEL" | translate }} +
+
+ +
+ {{ minimizeOnCloseDescription }} +
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.ENABLE_SYSTEM_NOTIFICATIONS_LABEL" | translate }} +
+
+ + +
+ + {{ "PAGES.OPTIONS.APPLICATION.ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION" | translate }} + +
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.ENABLE_APP_BADGE_LABEL" | translate }} +
+
+ +
+ + {{ "PAGES.OPTIONS.APPLICATION.ENABLE_APP_BADGE_DESCRIPTION" | translate }} + +
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_LABEL" | translate }} +
+
+ + +
+ +
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.USE_SYMLINK_SUPPORT" | translate }} +
+
+ +
+ {{ "PAGES.OPTIONS.APPLICATION.USE_SYMLINK_SUPPORT_DESCRIPTION" | translate }} +
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.START_WITH_SYSTEM_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.APPLICATION.START_WITH_SYSTEM_DESCRIPTION" | translate }} +
+ + +
+
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.START_MINIMIZED_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.APPLICATION.START_MINIMIZED_DESCRIPTION" | translate }} +
+ + +
+
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.KEEP_LAST_OPENED_TAB_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.APPLICATION.KEEP_LAST_OPENED_TAB_DESCRIPTION" | translate }} +
+ + +
+
+
+ +
+
+
+
{{ "PAGES.OPTIONS.APPLICATION.SCALE_LABEL" | translate }}
+ {{ "PAGES.OPTIONS.APPLICATION.SCALE_DESCRIPTION" | translate }} +
+ + + + {{ value * 100 | number: "1.0-0" }} % + + + +
+
+
+ + + +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.CURSE_PROTOCOL_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.APPLICATION.CURSE_PROTOCOL_DESCRIPTION" | translate }} +
+ + +
+
+
+ +
+
+
+
+ {{ "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_LABEL" | translate }} +
+ {{ "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_DESCRIPTION" | translate }} +
+ + {{ "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_DROPDOWN_LABEL" | translate }} + + + {{ channel.labelKey | translate }} + + + +
+
+
+
diff --git a/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.scss b/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.scss new file mode 100644 index 0000000..1934ef8 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.scss @@ -0,0 +1,19 @@ +.container { + padding: 1em; + max-width: 660px; + + .hint { + color: var(--text-3); + } +} + +.toggle { + margin-top: 1em; + margin-bottom: 20px; + + .divider { + margin-top: 20px; + height: 1px; + border-top: thin solid var(--divider-color); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.spec.ts b/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.spec.ts new file mode 100644 index 0000000..4903178 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.spec.ts @@ -0,0 +1,108 @@ +import { BehaviorSubject } from "rxjs"; + +import { HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormsModule } from "@angular/forms"; +import { MatDialog } from "@angular/material/dialog"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; + +import { ElectronService } from "../../../services"; +import { AddonService } from "../../../services/addons/addon.service"; +import { AnalyticsService } from "../../../services/analytics/analytics.service"; +import { FileService } from "../../../services/files/file.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { ZoomService } from "../../../services/zoom/zoom.service"; +import { createTranslateModule } from "../../../utils/test.utils"; +import { OptionsAppSectionComponent } from "./options-app-section.component"; +import { MatModule } from "../../../modules/mat-module"; + +describe("OptionsAppSectionComponent", () => { + let component: OptionsAppSectionComponent; + let fixture: ComponentFixture; + let electronServiceSpy: any; + let wowUpServiceSpy: any; + let sessionServiceSpy: any; + let fileServiceSpy: any; + let analyticsServiceSpy: any; + let addonService: any; + let zoomService: ZoomService; + + beforeEach(async () => { + addonService = jasmine.createSpyObj("AddonService", [""], {}); + + sessionServiceSpy = jasmine.createSpyObj("SessionService", [""], {}); + + analyticsServiceSpy = jasmine.createSpyObj("AnalyticsService", [""], { + telemetryEnabled$: new BehaviorSubject(false).asObservable(), + getTelemetryEnabled: () => Promise.resolve(false), + }); + + zoomService = jasmine.createSpyObj("ZoomService", [""], { + zoomFactor$: new BehaviorSubject(1.0).asObservable(), + getZoomFactor: Promise.resolve(1.0), + }); + + electronServiceSpy = jasmine.createSpyObj( + "ElectronService", + { + onRendererEvent: () => undefined, + isDefaultProtocolClient: Promise.resolve(false), + }, + { + isWin: false, + isLinux: true, + isMac: false, + }, + ); + + wowUpServiceSpy = jasmine.createSpyObj("WowUpService", ["getStartWithSystem"], { + collapseToTray: false, + useHardwareAcceleration: false, + startWithSystem: false, + startMinimized: false, + currentLanguage: false, + setCurrentTheme: () => Promise.resolve(), + getCollapseToTray: () => Promise.resolve(false), + getEnableSystemNotifications: () => Promise.resolve(false), + getCurrentLanguage: () => Promise.resolve("en"), + getUseSymlinkMode: () => Promise.resolve(false), + getUseHardwareAcceleration: () => Promise.resolve(false), + getEnableAppBadge: () => Promise.resolve(false), + getWowUpReleaseChannel: () => Promise.resolve(false), + getStartWithSystem: () => Promise.resolve(false), + getStartMinimized: () => Promise.resolve(false), + getKeepLastAddonDetailTab: () => Promise.resolve(false), + }); + + await TestBed.configureTestingModule({ + declarations: [OptionsAppSectionComponent], + providers: [MatDialog, ElectronService], + imports: [HttpClientModule, FormsModule, MatModule, BrowserAnimationsModule, createTranslateModule()], + }) + .overrideComponent(OptionsAppSectionComponent, { + set: { + providers: [ + MatDialog, + { provide: ElectronService, useValue: electronServiceSpy }, + { provide: WowUpService, useValue: wowUpServiceSpy }, + { provide: SessionService, useValue: sessionServiceSpy }, + { provide: FileService, useValue: fileServiceSpy }, + { provide: AnalyticsService, useValue: analyticsServiceSpy }, + { provide: ZoomService, useValue: zoomService }, + { provide: AddonService, useValue: addonService }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OptionsAppSectionComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.ts b/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.ts new file mode 100644 index 0000000..5c3e80a --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-app-section/options-app-section.component.ts @@ -0,0 +1,490 @@ +import { BehaviorSubject, firstValueFrom, from, of } from "rxjs"; +import { catchError, map, switchMap } from "rxjs/operators"; + +import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { MatSelectChange } from "@angular/material/select"; +import { MatSlideToggleChange } from "@angular/material/slide-toggle"; +import { TranslateService } from "@ngx-translate/core"; + +import { + ALLIANCE_LIGHT_THEME, + ALLIANCE_THEME, + APP_PROTOCOL_NAME, + CURSE_PROTOCOL_NAME, + DEFAULT_LIGHT_THEME, + DEFAULT_THEME, + HORDE_LIGHT_THEME, + HORDE_THEME, +} from "../../../../common/constants"; +import { ThemeGroup } from "../../../models/wowup/theme"; +import { ElectronService } from "../../../services"; +import { AnalyticsService } from "../../../services/analytics/analytics.service"; +import { DialogFactory } from "../../../services/dialog/dialog.factory"; +import { SessionService } from "../../../services/session/session.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { ZOOM_SCALE } from "../../../utils/zoom.utils"; +import { ConfirmDialogComponent } from "../../common/confirm-dialog/confirm-dialog.component"; +import { ZoomService } from "../../../services/zoom/zoom.service"; +import { AddonService } from "../../../services/addons/addon.service"; +import { WowUpReleaseChannelType } from "../../../../common/wowup/wowup-release-channel-type"; + +interface LocaleListItem { + localeId: string; + label: string; +} + +interface ReleaseChannelViewModel { + value: WowUpReleaseChannelType; + labelKey: string; +} + +@Component({ + selector: "app-options-app-section", + templateUrl: "./options-app-section.component.html", + styleUrls: ["./options-app-section.component.scss"], +}) +export class OptionsAppSectionComponent implements OnInit { + public readonly wowupProtocolName = APP_PROTOCOL_NAME; + public readonly curseProtocolName = CURSE_PROTOCOL_NAME; + + public minimizeOnCloseDescription = ""; + public protocolRegistered = false; + public zoomScale = ZOOM_SCALE; + public currentScale = 1; + public languages: LocaleListItem[] = [ + { localeId: "en", label: "English" }, + { localeId: "cs", label: "Čestina" }, + { localeId: "de", label: "Deutsch" }, + { localeId: "es", label: "Español" }, + { localeId: "fr", label: "Français" }, + { localeId: "it", label: "Italiano" }, + { localeId: "pl", label: "Polski" }, + { localeId: "ko", label: "한국어" }, + { localeId: "nb", label: "Norsk Bokmål" }, + { localeId: "pt", label: "Português" }, + { localeId: "ru", label: "русский" }, + { localeId: "zh", label: "简体中文" }, + { localeId: "zh-TW", label: "繁體中文" }, + ]; + + public themeGroups: ThemeGroup[] = [ + { + name: "APP.THEME.GROUP_DARK", + themes: [ + { display: "APP.THEME.DEFAULT", class: DEFAULT_THEME }, + { display: "APP.THEME.ALLIANCE", class: ALLIANCE_THEME }, + { display: "APP.THEME.HORDE", class: HORDE_THEME }, + ], + }, + { + name: "APP.THEME.GROUP_LIGHT", + themes: [ + { display: "APP.THEME.DEFAULT", class: DEFAULT_LIGHT_THEME }, + { display: "APP.THEME.ALLIANCE", class: ALLIANCE_LIGHT_THEME }, + { display: "APP.THEME.HORDE", class: HORDE_LIGHT_THEME }, + ], + }, + ]; + + public releaseChannels: ReleaseChannelViewModel[] = [ + { value: WowUpReleaseChannelType.Stable, labelKey: "COMMON.ENUM.ADDON_CHANNEL_TYPE.STABLE" }, + { value: WowUpReleaseChannelType.Beta, labelKey: "COMMON.ENUM.ADDON_CHANNEL_TYPE.BETA" }, + ]; + + public curseforgeProtocolHandled$ = from(this.electronService.isDefaultProtocolClient(CURSE_PROTOCOL_NAME)); + public wowupProtocolHandled$ = from(this.electronService.isDefaultProtocolClient(APP_PROTOCOL_NAME)); + + private _currentTheme: string; + public get currentTheme() { + return this._currentTheme; + } + + public set currentTheme(theme: string) { + this.wowupService + .setCurrentTheme(theme) + .then(() => { + this._currentTheme = theme; + }) + .catch(console.error); + } + + public enableSystemNotifications$ = new BehaviorSubject(false); + public currentLanguage$ = new BehaviorSubject(""); + public useSymlinkMode$ = new BehaviorSubject(false); + public useHardwareAcceleration$ = new BehaviorSubject(false); + public telemetryEnabled$ = new BehaviorSubject(false); + public collapseToTray$ = new BehaviorSubject(false); + public enableAppBadge$ = new BehaviorSubject(false); + public startWithSystem$ = new BehaviorSubject(false); + public startMinimized$ = new BehaviorSubject(false); + public currentReleaseChannel$ = new BehaviorSubject(WowUpReleaseChannelType.Stable); + public keepAddonDetailTab$ = new BehaviorSubject(false); + + public constructor( + private _analyticsService: AnalyticsService, + private _dialog: MatDialog, + private _dialogFactory: DialogFactory, + private _translateService: TranslateService, + private _cdRef: ChangeDetectorRef, + private _zoomService: ZoomService, + private _addonService: AddonService, + public electronService: ElectronService, + public sessionService: SessionService, + public wowupService: WowUpService, + ) {} + + public ngOnInit(): void { + this.currentTheme = this.sessionService.currentTheme; + + this.wowupService + .getWowUpReleaseChannel() + .then((channel) => { + this.currentReleaseChannel$.next(channel); + }) + .catch(console.error); + + this._analyticsService.telemetryEnabled$.subscribe((enabled) => { + this.telemetryEnabled$.next(enabled); + }); + + const minimizeOnCloseKey = this.electronService.isWin + ? "PAGES.OPTIONS.APPLICATION.MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS" + : "PAGES.OPTIONS.APPLICATION.MINIMIZE_ON_CLOSE_DESCRIPTION_MAC"; + + this._translateService.get(minimizeOnCloseKey).subscribe((translatedStr) => { + this.minimizeOnCloseDescription = translatedStr; + }); + + this._analyticsService + .getTelemetryEnabled() + .then((enabled) => { + this.telemetryEnabled$.next(enabled); + }) + .catch(console.error); + + this.wowupService + .getCollapseToTray() + .then((collapse) => { + console.log("getCollapseToTray", collapse); + this.collapseToTray$.next(collapse); + }) + .catch(console.error); + + this.wowupService + .getEnableSystemNotifications() + .then((enabled) => { + this.enableSystemNotifications$.next(enabled); + }) + .catch(console.error); + + this.wowupService + .getCurrentLanguage() + .then((curlang) => { + this.currentLanguage$.next(curlang); + }) + .catch(console.error); + + this.wowupService + .getUseSymlinkMode() + .then((useSymlink) => { + this.useSymlinkMode$.next(useSymlink); + }) + .catch(console.error); + + this.wowupService + .getUseHardwareAcceleration() + .then((useHwAccel) => { + this.useHardwareAcceleration$.next(useHwAccel); + }) + .catch(console.error); + + this.wowupService + .getEnableAppBadge() + .then((enabled) => { + this.enableAppBadge$.next(enabled); + }) + .catch(console.error); + + this.wowupService + .getStartWithSystem() + .then((enabled) => { + this.startWithSystem$.next(enabled); + }) + .catch(console.error); + + this.wowupService + .getStartMinimized() + .then((enabled) => { + this.startMinimized$.next(enabled); + }) + .catch(console.error); + + this.wowupService + .getKeepLastAddonDetailTab() + .then((enabled) => { + this.keepAddonDetailTab$.next(enabled); + }) + .catch(console.error); + + this.initScale().catch((e) => console.error(e)); + + this._zoomService.zoomFactor$.subscribe((zoomFactor) => { + this.currentScale = zoomFactor; + this._cdRef.detectChanges(); + }); + + this.electronService + .isDefaultProtocolClient(APP_PROTOCOL_NAME) + .then((isDefault) => { + this.protocolRegistered = isDefault; + }) + .catch((e) => console.error(e)); + } + + private async initScale() { + await this.updateScale(); + this.electronService.onRendererEvent("zoom-changed", () => { + this.updateScale().catch((e) => console.error(e)); + }); + } + + public onEnableSystemNotifications = (evt: MatSlideToggleChange): void => { + this.wowupService + .setEnableSystemNotifications(evt.checked) + .then(() => { + this.enableSystemNotifications$.next(evt.checked); + }) + .catch(console.error); + }; + + public onToggleAppBadge = async (evt: MatSlideToggleChange): Promise => { + await this.wowupService.setEnableAppBadge(evt.checked); + this.enableAppBadge$.next(evt.checked); + + let count = 0; + if (evt.checked) { + const addons = await this._addonService.getAllAddonsAvailableForUpdate(); + count = addons.length; + } + + await this.wowupService.updateAppBadgeCount(count); + }; + + public onTelemetryChange = async (evt: MatSlideToggleChange): Promise => { + await this._analyticsService.setTelemetryEnabled(evt.checked); + }; + + public onCollapseChange = async (evt: MatSlideToggleChange): Promise => { + await this.wowupService.setCollapseToTray(evt.checked); + this.collapseToTray$.next(evt.checked); + }; + + public onStartWithSystemChange = async (evt: MatSlideToggleChange): Promise => { + await this.wowupService.setStartWithSystem(evt.checked); + this.startWithSystem$.next(evt.checked); + }; + + public onStartMinimizedChange = async (evt: MatSlideToggleChange): Promise => { + await this.wowupService.setStartMinimized(evt.checked); + this.startMinimized$.next(evt.checked); + }; + + public onKeepAddonDetailTabChange = async (evt: MatSlideToggleChange): Promise => { + await this.wowupService.setKeepLastAddonDetailTab(evt.checked); + this.keepAddonDetailTab$.next(evt.checked); + }; + + public onProtocolHandlerChange = (evt: MatSlideToggleChange, protocol: string): void => { + // If this is already enabled and the user wants to disable it, don't prompt + if (evt.checked === false) { + from(this.setProtocolHandler(protocol, evt.checked)) + .pipe( + catchError((e) => { + console.error(e); + return of(undefined); + }), + ) + .subscribe(); + return; + } + + // Prompt the user that this may affect their existing CurseForge app + const title: string = this._translateService.instant( + "PAGES.OPTIONS.APPLICATION.USE_CURSE_PROTOCOL_CONFIRMATION_LABEL", + ); + const message: string = this._translateService.instant( + "PAGES.OPTIONS.APPLICATION.USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION", + ); + + const dialogRef = this._dialogFactory.getConfirmDialog(title, message); + + dialogRef + .afterClosed() + .pipe( + switchMap((result) => { + if (!result) { + evt.source.checked = !evt.source.checked; + return of(undefined); + } + + return from(this.setProtocolHandler(protocol, evt.checked)); + }), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ) + .subscribe(); + }; + + public onUseHardwareAccelerationChange = (evt: MatSlideToggleChange): void => { + const title: string = this._translateService.instant( + "PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL", + ); + const message: string = this._translateService.instant( + evt.checked + ? "PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION" + : "PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION", + ); + + const dialogRef = this._dialogFactory.getConfirmDialog(title, message); + + dialogRef + .afterClosed() + .pipe( + switchMap((result) => { + if (!result) { + evt.source.checked = !evt.source.checked; + return of(undefined); + } + + return from(this.wowupService.setUseHardwareAcceleration(evt.checked)).pipe( + switchMap(() => from(this.electronService.restartApplication())), + ); + }), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ) + .subscribe(); + }; + + public onSymlinkModeChange = async (evt: MatSlideToggleChange): Promise => { + if (evt.checked === false) { + await this.wowupService.setUseSymlinkMode(false); + return; + } + + const dialogRef = this._dialog.open(ConfirmDialogComponent, { + data: { + title: this._translateService.instant("PAGES.OPTIONS.APPLICATION.USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL"), + message: this._translateService.instant( + "PAGES.OPTIONS.APPLICATION.USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION", + ), + }, + }); + + dialogRef + .afterClosed() + .pipe( + switchMap((result) => { + if (!result) { + evt.source.checked = !evt.source.checked; + return of(undefined); + } + + return from(this.wowupService.setUseSymlinkMode(evt.checked)).pipe( + map(() => this.useSymlinkMode$.next(evt.checked)), + ); + }), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ) + .subscribe(); + }; + + public async onReleaseChannelChange(evt: MatSelectChange): Promise { + console.debug(evt); + // this._electronService.invoke("set-release-channel", channel); + + const descriptionKey = + evt.source.value === WowUpReleaseChannelType.Beta + ? "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA" + : "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE"; + + const dialogRef = this._dialog.open(ConfirmDialogComponent, { + data: { + title: this._translateService.instant("PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_CONFIRMATION_LABEL"), + message: this._translateService.instant(descriptionKey), + positiveKey: "PAGES.OPTIONS.APPLICATION.APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON", + }, + }); + + try { + const result = await firstValueFrom(dialogRef.afterClosed()); + + if (!result) { + evt.source.value = await this.wowupService.getWowUpReleaseChannel(); + } else { + await this.wowupService.setWowUpReleaseChannel(evt.source.value as WowUpReleaseChannelType); + } + + this.currentReleaseChannel$.next(evt.source.value as WowUpReleaseChannelType); + } catch (e) { + console.error(e); + } + } + + public onCurrentLanguageChange = (evt: MatSelectChange): void => { + const dialogRef = this._dialog.open(ConfirmDialogComponent, { + data: { + title: this._translateService.instant("PAGES.OPTIONS.APPLICATION.SET_LANGUAGE_CONFIRMATION_LABEL"), + message: this._translateService.instant("PAGES.OPTIONS.APPLICATION.SET_LANGUAGE_CONFIRMATION_DESCRIPTION"), + }, + }); + + dialogRef + .afterClosed() + .pipe( + switchMap((result) => { + if (!result) { + evt.source.value = this.currentLanguage$.value; + return of(undefined); + } + + return from(this.wowupService.setCurrentLanguage(evt.value as string)).pipe(map(() => evt.value as string)); + }), + switchMap((result: string) => { + this.currentLanguage$.next(result); + return from(this.electronService.restartApplication()); + }), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ) + .subscribe(); + }; + + public onScaleChange = async (evt: MatSelectChange): Promise => { + const newScale = evt.value as number; + await this._zoomService.setZoomFactor(newScale); + this.currentScale = newScale; + }; + + private async updateScale() { + this.currentScale = await this._zoomService.getZoomFactor(); + } + + private setProtocolHandler(protocol: string, enabled: boolean): Promise { + if (enabled) { + return this.electronService.setAsDefaultProtocolClient(protocol); + } else { + return this.electronService.removeAsDefaultProtocolClient(protocol); + } + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.html b/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.html new file mode 100644 index 0000000..3b041c7 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.html @@ -0,0 +1,21 @@ +
+

+ {{ "PAGES.OPTIONS.CURSEFORGE.TITLE" | translate }} +

+
+
+
+
+ {{ "PAGES.OPTIONS.CURSEFORGE.ADS_OPTION_LABEL" | translate }} +
+
+ +
+ + {{ "PAGES.OPTIONS.CURSEFORGE.ADS_OPTION_DESCRIPTION" | translate }} + +
+
+
diff --git a/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.scss b/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.scss new file mode 100644 index 0000000..08bd3bc --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.scss @@ -0,0 +1,7 @@ +.container { + padding: 1em; + + .section { + margin-top: 1em; + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.ts b/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.ts new file mode 100644 index 0000000..3d9af48 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-curseforge-section/options-curseforge-section.component.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; +import { IPC_OW_OPEN_CMP } from "../../../../common/constants"; +import { ElectronService } from "../../../services"; + +@Component({ + selector: "app-options-curseforge-section", + templateUrl: "./options-curseforge-section.component.html", + styleUrls: ["./options-curseforge-section.component.scss"], +}) +export class OptionsCurseforgeSectionComponent { + public constructor(private _electronService: ElectronService) {} + + public onClickManage(evt: MouseEvent): void { + evt.preventDefault(); + + this._electronService.invoke(IPC_OW_OPEN_CMP).catch((e) => console.error("onClickManage failed", e)); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.html b/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.html new file mode 100644 index 0000000..9eb142e --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.html @@ -0,0 +1,60 @@ +
+

+ {{ "PAGES.OPTIONS.DEBUG.TITLE" | translate }} +

+
+ +
+
{{ "PAGES.OPTIONS.DEBUG.LOG_FILES_LABEL" | translate }}
+ {{ "PAGES.OPTIONS.DEBUG.LOG_FILES_DESCRIPTION" | translate }} +
+
+ +
+ +
+
{{ "PAGES.OPTIONS.DEBUG.CONFIG_FILES_LABEL" | translate }}
+ {{ "PAGES.OPTIONS.DEBUG.CONFIG_FILES_DESCRIPTION" | translate }} +
+
+ +
+ +
+
{{ "PAGES.OPTIONS.DEBUG.DEBUG_DATA_LABEL" | translate }}
+ {{ "PAGES.OPTIONS.DEBUG.DEBUG_DATA_DESCRIPTION" | translate }} +
+
+ +
+ +
+
{{ "Debug ad frame" | translate }}
+ {{ "Show the dev tools for the ad frame" | translate }} +
+
+ +
+
+
diff --git a/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.scss b/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.scss new file mode 100644 index 0000000..211dfd1 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.scss @@ -0,0 +1,14 @@ +.container { + padding: 1em; + + .section { + margin-top: 1em; + } + + .actions { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + row-gap: 1rem; + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.spec.ts b/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.spec.ts new file mode 100644 index 0000000..1324b3e --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.spec.ts @@ -0,0 +1,78 @@ +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { OptionsDebugSectionComponent } from "./options-debug-section.component"; +import { AddonService } from "../../../services/addons/addon.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { httpLoaderFactory } from "../../../app.module"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { MatModule } from "../../../modules/mat-module"; +import { SessionService } from "../../../services/session/session.service"; + +describe("OptionsDebugSectionComponent", () => { + let component: OptionsDebugSectionComponent; + let fixture: ComponentFixture; + let addonServiceSpy: any; + let wowUpServiceSpy: any; + let sessionService: any; + + beforeEach(async () => { + addonServiceSpy = jasmine.createSpyObj(AddonService, ["logDebugData"]); + wowUpServiceSpy = jasmine.createSpyObj(WowUpService, ["showLogsFolder"]); + sessionService = jasmine.createSpyObj("SessionService", [""], {}); + + await TestBed.configureTestingModule({ + declarations: [OptionsDebugSectionComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }) + .overrideComponent(OptionsDebugSectionComponent, { + set: { + providers: [ + { provide: AddonService, useValue: addonServiceSpy }, + { provide: WowUpService, useValue: wowUpServiceSpy }, + { provide: SessionService, useValue: sessionService }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OptionsDebugSectionComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("Should call logDebugData", fakeAsync(() => { + const button = fixture.debugElement.nativeElement.querySelector("#dump-debug-btn"); + button.click(); + tick(); + expect(addonServiceSpy.logDebugData).toHaveBeenCalled(); + })); + + it("Should call showLogFiles", fakeAsync(() => { + const button = fixture.debugElement.nativeElement.querySelector("#show-log-btn"); + button.click(); + tick(); + expect(wowUpServiceSpy.showLogsFolder).toHaveBeenCalled(); + })); +}); diff --git a/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.ts b/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.ts new file mode 100644 index 0000000..7f5d53c --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-debug-section/options-debug-section.component.ts @@ -0,0 +1,44 @@ +import { ChangeDetectorRef, Component } from "@angular/core"; +import { AddonService } from "../../../services/addons/addon.service"; +import { SessionService } from "../../../services/session/session.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; + +@Component({ + selector: "app-options-debug-section", + templateUrl: "./options-debug-section.component.html", + styleUrls: ["./options-debug-section.component.scss"], +}) +export class OptionsDebugSectionComponent { + public dumpingDebugData = false; + + public constructor( + private _cdRef: ChangeDetectorRef, + private _addonService: AddonService, + private _wowupService: WowUpService, + private _sessionService: SessionService + ) {} + + public async onShowLogs(): Promise { + await this._wowupService.showLogsFolder(); + } + + public async onShowConfig(): Promise { + await this._wowupService.showConfigFolder(); + } + + public async onLogDebugData(): Promise { + try { + this.dumpingDebugData = true; + await this._addonService.logDebugData(); + } catch (e) { + console.error(e); + } finally { + this.dumpingDebugData = false; + this._cdRef.detectChanges(); + } + } + + public onDebugAdFrame(): void { + this._sessionService.debugAdFrame$.next(true); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.html b/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.html new file mode 100644 index 0000000..97aedca --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.html @@ -0,0 +1,29 @@ +
+

+ {{ "PAGES.OPTIONS.WOW.TITLE" | translate }} +

+
+
+
+ {{ "PAGES.OPTIONS.WOW.RESCAN_CLIENTS_LABEL" | translate }} +
+ + + +
+

+ {{ "PAGES.OPTIONS.WOW.NO_CLIENTS_FOUND_TEXT" | translate }} +

+ + +
+
diff --git a/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.scss b/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.scss new file mode 100644 index 0000000..a621f53 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.scss @@ -0,0 +1,7 @@ +.container { + padding: 1em; +} + +.client-section { + padding: 0 1em; +} diff --git a/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.spec.ts b/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.spec.ts new file mode 100644 index 0000000..1787260 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.spec.ts @@ -0,0 +1,96 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { BehaviorSubject } from "rxjs"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialog, MatDialogModule } from "@angular/material/dialog"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { WarcraftService } from "../../../services/warcraft/warcraft.service"; +import { WowUpService } from "../../../services/wowup/wowup.service"; +import { WowClientOptionsComponent } from "../wow-client-options/wow-client-options.component"; +import { OptionsWowSectionComponent } from "./options-wow-section.component"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { MatModule } from "../../../modules/mat-module"; +import { WowUpReleaseChannelType } from "../../../../common/wowup/wowup-release-channel-type"; +import { AddonChannelType, WowClientType } from "wowup-lib-core"; +import { InstalledProduct } from "wowup-lib-core"; + +describe("OptionsWowSectionComponent", () => { + let component: OptionsWowSectionComponent; + let fixture: ComponentFixture; + let wowUpServiceSpy: WowUpService; + let warcraftServiceSpy: WarcraftService; + let warcraftInstallationService: WarcraftInstallationService; + + beforeEach(async () => { + warcraftServiceSpy = jasmine.createSpyObj( + "WarcraftService", + { + getClientFolderName: (clientType: WowClientType) => clientType.toString(), + getClientLocation: (clientType: WowClientType) => clientType.toString(), + }, + { + products$: new BehaviorSubject([]).asObservable(), + }, + ); + + wowUpServiceSpy = jasmine.createSpyObj( + "WowUpService", + { + getDefaultAddonChannel: () => AddonChannelType.Stable, + getDefaultAutoUpdate: () => false, + }, + { + wowUpReleaseChannel: WowUpReleaseChannelType.Stable, + }, + ); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], { + wowInstallations$: new BehaviorSubject([]), + }); + + await TestBed.configureTestingModule({ + declarations: [OptionsWowSectionComponent, WowClientOptionsComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + MatDialogModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + providers: [MatDialog], + }) + .overrideComponent(OptionsWowSectionComponent, { + set: { + providers: [ + { provide: WowUpService, useValue: wowUpServiceSpy }, + { provide: WarcraftService, useValue: warcraftServiceSpy }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OptionsWowSectionComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.ts b/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.ts new file mode 100644 index 0000000..21164bf --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/options-wow-section/options-wow-section.component.ts @@ -0,0 +1,98 @@ +import { from, Observable, of } from "rxjs"; +import { catchError } from "rxjs/operators"; + +import { Component, OnInit } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { TranslateService } from "@ngx-translate/core"; + +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { WarcraftService } from "../../../services/warcraft/warcraft.service"; +import { getEnumList } from "wowup-lib-core"; +import { AlertDialogComponent } from "../../common/alert-dialog/alert-dialog.component"; +import { WowClientType } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +@Component({ + selector: "app-options-wow-section", + templateUrl: "./options-wow-section.component.html", + styleUrls: ["./options-wow-section.component.scss"], +}) +export class OptionsWowSectionComponent implements OnInit { + public wowClientTypes: WowClientType[] = getEnumList(WowClientType).filter( + (clientType) => clientType !== WowClientType.None + ) as WowClientType[]; + + public wowInstallations$: Observable; + + public constructor( + private _dialog: MatDialog, + private _warcraftService: WarcraftService, + private _warcraftInstallationService: WarcraftInstallationService, + private _translateService: TranslateService + ) { + this.wowInstallations$ = _warcraftInstallationService.wowInstallations$; + } + + public ngOnInit(): void {} + + public onReScan = (): void => { + this._warcraftInstallationService + .importWowInstallations(this._warcraftInstallationService.blizzardAgentPath) + .catch((e) => console.error(e)); + }; + + public onAddNew(): void { + from(this.addNewClient()) + .pipe( + catchError((error) => { + console.error(error); + return of(undefined); + }) + ) + .subscribe(); + } + + private async addNewClient() { + const selectedPath = await this._warcraftInstallationService.selectWowClientPath(); + if (!selectedPath) { + return; + } + + console.log("dialogResult", selectedPath); + + const isWowApplication = await this._warcraftService.isWowApplication(selectedPath); + + if (!isWowApplication) { + this.showInvalidWowApplication(selectedPath); + return; + } + + console.log("isWowApplication", isWowApplication); + + const wowInstallation = await this._warcraftInstallationService.createWowInstallationForPath(selectedPath); + console.log("wowInstallation", wowInstallation); + + await this._warcraftInstallationService.addInstallation(wowInstallation); + } + + private showInvalidWowApplication(selectedPath: string) { + const dialogMessage: string = this._translateService.instant( + "DIALOGS.SELECT_INSTALLATION.INVALID_INSTALLATION_PATH", + { + selectedPath, + } + ); + + this.showError(dialogMessage); + } + + private showError(message: string) { + const title = this._translateService.instant("DIALOGS.ALERT.ERROR_TITLE"); + this._dialog.open(AlertDialogComponent, { + data: { + title, + message, + }, + }); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.html b/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.html new file mode 100644 index 0000000..a50e4e7 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.html @@ -0,0 +1,117 @@ + +
+ +
+ + +
{{ installationModel.displayName }}
+ + + +
+
+ +
+ + {{ + "PAGES.OPTIONS.WOW.CLIENT_TYPE_PATH_LABEL" | translate: { clientTypeName: (clientTypeName | translate) } + }} + + + {{ + "PAGES.OPTIONS.WOW.CLIENT_TYPE_INPUT_HINT" + | translate + : { + clientTypeName: (clientTypeName | translate)?.toLowerCase(), + clientFolderName: executableName + } + }} + + +
+
+
+

{{ "PAGES.OPTIONS.WOW.DEFAULT_ADDON_CHANNEL_LABEL" | translate }}

+
+ + {{ "PAGES.OPTIONS.WOW.DEFAULT_ADDON_CHANNEL_SELECT_LABEL" | translate }} + + {{ + channel.name | translate + }} + + +
+
+
+

{{ "PAGES.OPTIONS.WOW.AUTO_UPDATE_LABEL" | translate }}

+ {{ "PAGES.OPTIONS.WOW.AUTO_UPDATE_DESCRIPTION" | translate }} +
+ + +
+
+ + + +
+ +
+
+ + +
+ + +
+ +
+ + +
+ + +
+ +
+
+
diff --git a/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.scss b/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.scss new file mode 100644 index 0000000..7b0abf4 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.scss @@ -0,0 +1,60 @@ +.installation-card { + box-sizing: border-box; + border: 3px solid transparent; + + &:hover { + border: 3px solid var(--background-primary); + } +} + +.wow-install-logo-container { + position: absolute; + right: 0.25em; + top: 0.25em; + height: 145px; + + img { + height: 100%; + opacity: 0.08; + } +} +.option-card { + margin-top: 1em; + background-color: var(--background-secondary-2); +} + +.title { + font-size: 1.5em; + flex-grow: 1; + margin-top: 1em; +} + +p { + margin: 0; +} + +.grow { + flex-grow: 1; +} + +.setting-section { + background-color: var(--background-secondary-4); +} + +.folder-input { + margin-right: 1em; +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + // margin: 1em 0; + // padding: 0.5em; + &.np { + padding: 0; + } + &.sub { + padding-left: 2em; + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.spec.ts b/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.spec.ts new file mode 100644 index 0000000..d945649 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.spec.ts @@ -0,0 +1,86 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialog } from "@angular/material/dialog"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../../app.module"; +import { WowClientOptionsComponent } from "./wow-client-options.component"; +import { FormsModule } from "@angular/forms"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { WarcraftService } from "../../../services/warcraft/warcraft.service"; +import { mockPreload } from "../../../tests/test-helpers"; +import { SessionService } from "../../../services/session/session.service"; +import { BehaviorSubject, Observable } from "rxjs"; +import { MatModule } from "../../../modules/mat-module"; +import { WowClientType } from "wowup-lib-core"; + +describe("WowClientOptionsComponent", () => { + let component: WowClientOptionsComponent; + let fixture: ComponentFixture; + let warcraftInstallationService: WarcraftInstallationService; + let warcraftService: WarcraftService; + let sessionService: any; + + beforeEach(async () => { + mockPreload(); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", ["getWowInstallations"], { + wowInstallations$: new BehaviorSubject([]), + getWowInstallation: () => { + return { + clientType: WowClientType.Beta, + }; + }, + getInstallationDisplayName: () => { + return Promise.resolve("test display name"); + }, + }); + + warcraftService = jasmine.createSpyObj("WarcraftService", ["getExecutableName"], {}); + sessionService = jasmine.createSpyObj("SessionService", [""], { + editingWowInstallationId$: new Observable(), + }); + + const testBed = TestBed.configureTestingModule({ + declarations: [WowClientOptionsComponent], + imports: [ + MatModule, + FormsModule, + HttpClientModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + providers: [MatDialog], + }).overrideComponent(WowClientOptionsComponent, { + set: { + providers: [ + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + { provide: WarcraftService, useValue: warcraftService }, + { provide: SessionService, useValue: sessionService }, + ], + }, + }); + await testBed.compileComponents(); + fixture = TestBed.createComponent(WowClientOptionsComponent); + component = fixture.componentInstance; + component.installationId = "1"; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.ts b/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.ts new file mode 100644 index 0000000..3d4cb43 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/wow-client-options/wow-client-options.component.ts @@ -0,0 +1,261 @@ +import { dirname } from "path"; +import { BehaviorSubject, from, of, Subscription } from "rxjs"; +import { filter, map, switchMap } from "rxjs/operators"; + +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { MatSelectChange } from "@angular/material/select"; +import { MatSlideToggleChange } from "@angular/material/slide-toggle"; +import { TranslateService } from "@ngx-translate/core"; + +import { ElectronService } from "../../../services"; +import { SessionService } from "../../../services/session/session.service"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { WarcraftService } from "../../../services/warcraft/warcraft.service"; +import { getEnumList, getEnumName } from "wowup-lib-core"; +import { ConfirmDialogComponent } from "../../common/confirm-dialog/confirm-dialog.component"; +import { AddonChannelType, WowClientType } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +@Component({ + selector: "app-wow-client-options", + templateUrl: "./wow-client-options.component.html", + styleUrls: ["./wow-client-options.component.scss"], +}) +export class WowClientOptionsComponent implements OnInit, OnDestroy { + @Input("installationId") public installationId = ""; + @Input("index") public installationIndex!: number; + + private readonly _editModeSrc = new BehaviorSubject(false); + private readonly _isBusySrc = new BehaviorSubject(false); + private installation: WowInstallation | undefined; + private subscriptions: Subscription[] = []; + + public readonly addonChannelInfos: { + type: AddonChannelType; + name: string; + }[]; + + public clientTypeName = ""; + public clientFolderName = ""; + public clientLocation = ""; + public installationModel!: WowInstallation; + public selectedAddonChannelType: AddonChannelType = AddonChannelType.Stable; + public editMode$ = this._editModeSrc.asObservable(); + public isBusy$ = this._isBusySrc.asObservable(); + public installationCount$ = this._warcraftInstallationService.wowInstallations$.pipe( + map((installations) => installations.length), + ); + + public clientAutoUpdate = false; + + public set isBusy(enabled: boolean) { + this._isBusySrc.next(enabled); + } + + public get installationLabel(): string { + return this.installation?.label ?? ""; + } + + public set installationLabel(input: string) { + if (this.installation) { + this.installation.label = input; + } + } + + public get executableName(): string { + if (!this.installation) { + return ""; + } + + return this._warcraftService.getExecutableName(this.installation.clientType); + } + + public get wowLogoImage(): string { + if (!this.installation) { + return ""; + } + + switch (this.installation.clientType) { + case WowClientType.ClassicEra: + case WowClientType.ClassicEraPtr: + return "assets/images/wow-classic-logo.png"; + case WowClientType.Retail: + case WowClientType.RetailPtr: + case WowClientType.RetailXPtr: + case WowClientType.Beta: + return "assets/images/wow-dragonflight-logo.png"; + case WowClientType.ClassicPtr: + case WowClientType.Classic: + case WowClientType.ClassicBeta: + return "assets/images/wow-classic-cataclysm-logo.png"; + default: + return ""; + } + } + + public constructor( + private _dialog: MatDialog, + private _translateService: TranslateService, + private _warcraftInstallationService: WarcraftInstallationService, + private _warcraftService: WarcraftService, + private _sessionService: SessionService, + private _electronService: ElectronService, + ) { + this.addonChannelInfos = this.getAddonChannelInfos(); + + const editingSub = this._sessionService.editingWowInstallationId$ + .pipe(filter((installationId) => this.installationId !== installationId)) + .subscribe(() => { + this.onClickCancel(); + }); + + this.subscriptions.push(editingSub); + } + + public ngOnInit(): void { + this.installation = this._warcraftInstallationService.getWowInstallation(this.installationId); + if (!this.installation) { + throw new Error(`Failed to find installation: ${this.installationId}`); + } + + this.resetInstallationModel(); + + this.selectedAddonChannelType = this.installation.defaultAddonChannelType; + this.clientAutoUpdate = this.installation.defaultAutoUpdate; + this.clientTypeName = `COMMON.CLIENT_TYPES.${getEnumName( + WowClientType, + this.installation.clientType, + ).toUpperCase()}`; + + // `COMMON.CLIENT_TYPES.${getEnumName( + // WowClientType, + // this.installation.clientType + // ).toUpperCase()}`; + this.clientFolderName = this.installation.label; + this.clientLocation = this.installation.location; + } + + public ngOnDestroy(): void { + this.subscriptions.forEach((sub) => sub.unsubscribe()); + } + + public onDefaultAddonChannelChange(evt: MatSelectChange): void { + if (!this.installationModel) { + return; + } + this.installationModel.defaultAddonChannelType = evt.value; + } + + public onDefaultAutoUpdateChange(evt: MatSlideToggleChange): void { + if (!this.installationModel) { + return; + } + + this.installationModel.defaultAutoUpdate = evt.checked; + } + + public async onClickOpenFolder(): Promise { + try { + await this._electronService.showItemInFolder(dirname(this.installation.location)); + } catch (e) { + console.error(e); + } + } + + public onClickMoveUp(): void { + this._warcraftInstallationService.reOrderInstallation(this.installationId, -1).catch(console.error); + } + + public onClickMoveDown(): void { + this._warcraftInstallationService.reOrderInstallation(this.installationId, 1).catch(console.error); + } + + public onClickEdit(): void { + this._editModeSrc.next(true); + this._sessionService.editingWowInstallationId$.next(this.installationId); + } + + public onClickCancel(): void { + this.resetInstallationModel(); + this._editModeSrc.next(false); + } + + public async onClickSave(): Promise { + if (!this.installationModel) { + return; + } + + this.isBusy = true; + try { + // const saveAutoUpdate = this.installationModel.defaultAutoUpdate !== this.installation.defaultAutoUpdate; + + this.installation = { ...this.installationModel }; + if (this.installation) { + await this._warcraftInstallationService.updateWowInstallation(this.installation); + } + + // if (saveAutoUpdate) { + // await this._addonService.setInstallationAutoUpdate(this.installation); + // this._sessionService.notifyAddonsChanged(); + // } + } catch (e) { + console.error(e); + } finally { + this.isBusy = false; + this._editModeSrc.next(false); + } + } + + public onClickRemove(): void { + const dialogRef = this._dialog.open(ConfirmDialogComponent, { + data: { + title: this._translateService.instant("PAGES.OPTIONS.WOW.CLEAR_INSTALL_LOCATION_DIALOG.TITLE"), + message: this._translateService.instant("PAGES.OPTIONS.WOW.CLEAR_INSTALL_LOCATION_DIALOG.MESSAGE", { + location: this._translateService.instant(this.installation?.location ?? ""), + }), + }, + }); + + dialogRef + .afterClosed() + .pipe( + switchMap((result) => { + if (result === undefined) { + return of(undefined); + } + + if (this.installation) { + return from(this._warcraftInstallationService.removeWowInstallation(this.installation)); + } + }), + ) + .subscribe(); + } + + private resetInstallationModel() { + if (this.installation === undefined) { + return; + } + + this.installationModel = { ...this.installation }; + this._warcraftInstallationService + .getInstallationDisplayName(this.installation) + .then((name) => { + this.installationModel.label = name; + }) + .catch((e) => { + console.error(e); + }); + } + + private getAddonChannelInfos(): { type: AddonChannelType; name: string }[] { + return getEnumList(AddonChannelType).map((type: any) => { + const channelName = getEnumName(AddonChannelType, type as number).toUpperCase(); + return { + type: type, + name: `COMMON.ENUM.ADDON_CHANNEL_TYPE.${channelName}`, + }; + }); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.html b/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.html new file mode 100644 index 0000000..e1c21b7 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.html @@ -0,0 +1,139 @@ +
+

+ {{ "PAGES.OPTIONS.WTF_EXPLORER.TITLE" | translate }} +

+
{{ "PAGES.OPTIONS.WTF_EXPLORER.PAGE_EXPLANATION" | translate }}
+
+
+
+ + {{ "PAGES.GET_ADDONS.CLIENT_TYPE_SELECT_LABEL" | translate }} + + + {{ installation.displayName }} + + + +
+
+ +
+
+ + + + + +
+

+ {{ "PAGES.OPTIONS.WTF_EXPLORER.FOLDER_PATH_LABEL" | translate }} + {{ wtfPath }} +

+
+ + + + + + + {{ node.name }} + + + + + + + {{ node.name }} + + + + + +
+ + +
+
diff --git a/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.scss b/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.scss new file mode 100644 index 0000000..74dbcc3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.scss @@ -0,0 +1,9 @@ +.container { + padding: 1em; + + .divider { + margin-top: 20px; + height: 1px; + border-top: thin solid var(--divider-color); + } +} diff --git a/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.spec.ts b/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.spec.ts new file mode 100644 index 0000000..7fcc018 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ElectronService } from "../../../services"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { WarcraftService } from "../../../services/warcraft/warcraft.service"; +import { WtfService } from "../../../services/wtf/wtf.service"; +import { getStandardImports } from "../../../tests/test-helpers"; + +import { WtfExplorerComponent } from "./wtf-explorer.component"; + +describe("WtfExplorerComponent", () => { + let component: WtfExplorerComponent; + let fixture: ComponentFixture; + let electronService: ElectronService; + let warcraftService: WarcraftService; + let warcraftInstallationService: WarcraftInstallationService; + let wtfService: WtfService; + + beforeEach(async () => { + electronService = jasmine.createSpyObj("ElectronService", [""], {}); + warcraftService = jasmine.createSpyObj("WarcraftService", [""], {}); + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], {}); + wtfService = jasmine.createSpyObj("WtfService", [""], {}); + + await TestBed.configureTestingModule({ + declarations: [WtfExplorerComponent], + imports: [getStandardImports()], + }) + .overrideComponent(WtfExplorerComponent, { + set: { + providers: [ + { provide: ElectronService, useValue: electronService }, + { provide: WarcraftService, useValue: warcraftService }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + { provide: WtfService, useValue: wtfService }, + ], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(WtfExplorerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.ts b/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.ts new file mode 100644 index 0000000..59a33fa --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/options/wtf-explorer/wtf-explorer.component.ts @@ -0,0 +1,376 @@ +import * as _ from "lodash"; +import { BehaviorSubject, from, of } from "rxjs"; +import { catchError, first, tap } from "rxjs/operators"; + +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; + +import { ElectronService } from "../../../services"; +import { WarcraftService } from "../../../services/warcraft/warcraft.service"; +import { WtfNode, WtfService } from "../../../services/wtf/wtf.service"; +import { removeExtension } from "../../../utils/string.utils"; +import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service"; +import { formatSize } from "../../../utils/number.utils"; +import { AddonFolder } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; +import { FlatTreeControl } from "@angular/cdk/tree"; +import { MatTreeFlatDataSource, MatTreeFlattener } from "@angular/material/tree"; + +interface SavedVariable { + name: string; + size: number; + hasAddon: boolean; +} + +interface Server { + name: string; + size: number; + sizeMb: string; + characters: Character[]; +} + +interface Character { + name: string; + size: number; + sizeMb: string; + variables: SavedVariable[]; +} + +interface AccountItem { + name: string; + globalVariables: SavedVariable[]; + servers: Server[]; + size: number; + sizeMb: string; +} + +interface NodeModel { + name: string; + isLua: boolean; + ignore: boolean; + hasAddon: boolean; + children?: NodeModel[]; +} + +interface TreeNode { + name: string; + children?: TreeNode[]; + warn: boolean; + ignore: boolean; +} + +interface FlatTreeNode { + expandable: boolean; + name: string; + level: number; + ignore: boolean; + warn: boolean; +} + +@Component({ + selector: "app-wtf-explorer", + templateUrl: "./wtf-explorer.component.html", + styleUrls: ["./wtf-explorer.component.scss"], +}) +export class WtfExplorerComponent implements OnInit, OnDestroy { + @Input("tabIndex") public tabIndex!: number; + + @Input() + public get active(): boolean { + return this._active; + } + public set active(active: boolean) { + this._active = active; + if (this._active) { + this.lazyLoad(); + } + } + private _active = false; + + public accountMap = new BehaviorSubject([]); + public loading$ = new BehaviorSubject(false); + public error$ = new BehaviorSubject(""); + public nodes$ = new BehaviorSubject([]); + public installations: WowInstallation[] = []; + public selectedInstallationId = ""; + public wtfPath = ""; + + public get selectedInstallationLabel(): string { + return this.installations.find((inst) => inst.id === this.selectedInstallationId)?.displayName ?? ""; + } + + private _treeTransformer = (node: TreeNode, level: number): FlatTreeNode => { + return { + expandable: Array.isArray(node.children) && node.children.length > 0, + level, + name: node.name, + ignore: node.ignore, + warn: node.warn, + }; + }; + + public treeControl = new FlatTreeControl( + (node) => node.level, + (node) => node.expandable + ); + + public treeFlattener = new MatTreeFlattener( + this._treeTransformer, + (node) => node.level, + (node) => node.expandable, + (node) => node.children + ); + + public treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + + public constructor( + public electronService: ElectronService, + private _warcraftService: WarcraftService, + private _warcraftInstallationService: WarcraftInstallationService, + private _wtfService: WtfService + ) {} + + public ngOnInit(): void {} + + public ngOnDestroy(): void {} + + public getClientName(installation: WowInstallation): Promise { + return this._warcraftInstallationService.getInstallationDisplayName(installation); + } + + public onClientChange(): void { + const installation = this.installations.find((inst) => inst.id === this.selectedInstallationId); + + this.wtfPath = this._wtfService.getWtfPath(installation); + + // from(this.loadAccounts(installation)) + // .pipe(first()) + // .subscribe((accounts) => { + // this.accountMap.next(accounts); + // }); + + from(this.loadWtfStructure(installation)).pipe(first()).subscribe(); + } + + public onClickRefresh(): void { + this.onClientChange(); + } + + public hasChild = (_: number, node: FlatTreeNode) => node.expandable; + + private lazyLoad() { + this.loading$.next(true); + this.error$.next(""); + from(this._warcraftInstallationService.getWowInstallationsAsync()) + .pipe( + first(), + tap((installations) => { + this.installations = installations; + + const installation = this.installations[0]; + this.selectedInstallationId = installation?.id ?? ""; + + this.wtfPath = this._wtfService.getWtfPath(installation); + + return from(this.loadWtfStructure(installation)); + }), + catchError((e) => { + console.error(e); + return of(undefined); + }) + ) + .subscribe(); + + // from(this.loadAccounts(installation)) + // .pipe(first()) + // .subscribe((accounts) => { + // this.accountMap.next(accounts); + // }); + } + + private async loadWtfStructure(installation: WowInstallation) { + this.loading$.next(true); + this.error$.next(""); + this.treeDataSource.data = []; + + try { + const addonFolders = await this._warcraftService.listAddons(installation); + const wtfTree = await this._wtfService.getWtfContents(installation); + + this.treeDataSource.data = this.createTreeNodes(wtfTree.children, addonFolders); + console.log("treeNodes", this.treeDataSource.data); + + // this.nodes$.next(wtfTree.children.map((tn) => this.getNode(tn, addonFolders))); + } catch (e) { + console.error(e); + this.error$.next(e.message as string); + } finally { + this.loading$.next(false); + } + } + + private createTreeNodes(wtfNodes: WtfNode[], addonFolders: AddonFolder[]): TreeNode[] { + const treeNodes: TreeNode[] = []; + + wtfNodes.forEach((wtfNode) => { + let name = `${wtfNode.name} (${formatSize(wtfNode.size)})`; + if (wtfNode.isDirectory) { + name = `${wtfNode.name} (${wtfNode.children.length} files ${formatSize(wtfNode.size)})`; + } + + const treeNode: TreeNode = { + name, + children: this.createTreeNodes(wtfNode.children, addonFolders), + warn: false, + ignore: wtfNode.ignore, + }; + + if (!wtfNode.ignore && wtfNode.isLua) { + treeNode.warn = !this.addonFolderExists(treeNode.name, addonFolders); + } + + treeNodes.push(treeNode); + }); + + return treeNodes; + } + + private getNode(treeNode: WtfNode, addonFolders: AddonFolder[]): NodeModel { + let name = `${treeNode.name} (${formatSize(treeNode.size)})`; + if (treeNode.isDirectory) { + name = `${treeNode.name} (${treeNode.children.length} files ${formatSize(treeNode.size)})`; + } + const nodeModel: NodeModel = { + name: name, + children: treeNode.children.map((tn) => this.getNode(tn, addonFolders)), + hasAddon: false, + isLua: treeNode.isLua, + ignore: treeNode.ignore, + }; + + if (treeNode.isLua) { + nodeModel.hasAddon = this.addonFolderExists(treeNode.name, addonFolders); + } + + return nodeModel; + } + + private addonFolderExists(fileName: string, addonFolders: AddonFolder[]): boolean { + return addonFolders.some((af) => af.name === removeExtension(fileName)); + } + + private async loadAccounts(installation: WowInstallation): Promise { + try { + if (!installation) { + return []; + } + + const accounts = await this._wtfService.getAccounts(installation); + const addonFolders = await this._warcraftService.listAddons(installation); + + const accountMap: AccountItem[] = []; + for (const account of accounts) { + const accountGlobalVars = await this.getAccountGlobalVars(installation, account, addonFolders); + const serverList = await this.getServers(installation, account, addonFolders); + console.debug("serverList", serverList); + + const totalSize = _.sumBy(accountGlobalVars, (gvar) => gvar.size); + accountMap.push({ + globalVariables: accountGlobalVars, + name: account, + size: totalSize, + sizeMb: (totalSize / 1024 / 1024).toFixed(2), + servers: serverList, + }); + } + + return accountMap; + } catch (e) { + console.error(e); + this.error$.next(e.message as string); + return []; + } finally { + this.loading$.next(false); + } + } + + private async getAccountGlobalVars( + installation: WowInstallation, + account: string, + addonFolders: AddonFolder[] + ): Promise { + const globalVariables = await this._wtfService.getGlobalVariables(installation, account); + const gVars: SavedVariable[] = globalVariables.map((gv) => { + return { + hasAddon: addonFolders.some((af) => af.name === removeExtension(gv.name)), + name: gv.name, + size: gv.stats.size, + }; + }); + + return gVars; + } + + private async getServers( + installation: WowInstallation, + account: string, + addonFolders: AddonFolder[] + ): Promise { + const serverNames = await this._wtfService.getServers(installation, account); + const servers: Server[] = serverNames.map((server) => { + return { + name: server, + characters: [], + size: 0, + sizeMb: "", + }; + }); + + for (const server of servers) { + const charNames = await this._wtfService.getCharacters(installation, account, server.name); + const chars: Character[] = []; + for (const charName of charNames) { + const variables = await this.getCharacterSavedVariables( + installation, + account, + server.name, + charName, + addonFolders + ); + const totalSize = _.sumBy(variables, (svar) => svar.size); + + chars.push({ + name: charName, + size: totalSize, + sizeMb: (totalSize / 1024 / 1025).toFixed(2), + variables, + }); + } + + server.characters = chars; + server.size = _.sumBy(server.characters, (char) => char.size); + server.sizeMb = (server.size / 1024 / 1024).toFixed(2); + } + + return servers; + } + + private async getCharacterSavedVariables( + installation: WowInstallation, + account: string, + server: string, + character: string, + addonFolders: AddonFolder[] + ): Promise { + const savedVars = await this._wtfService.getCharacterVariables(installation, account, server, character); + + const vars: SavedVariable[] = savedVars.map((gv) => { + return { + hasAddon: addonFolders.some((af) => af.name === removeExtension(gv.name)), + name: gv.name, + size: gv.stats.size, + }; + }); + + return vars; + } +} diff --git a/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.html b/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.html new file mode 100644 index 0000000..411cf9f --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.html @@ -0,0 +1,4 @@ +
+ +
{{ message || defaultMessage }}
+
diff --git a/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.scss b/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.scss new file mode 100644 index 0000000..52feb76 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.scss @@ -0,0 +1,12 @@ +.busy-container { + display: flex; + width: 100%; + flex-direction: column; + align-items: center; + margin-top: 2em; +} + +pre { + font-family: Roboto, "Helvetica Neue", sans-serif; + text-align: center; +} diff --git a/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.spec.ts b/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.spec.ts new file mode 100644 index 0000000..c5cc81b --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.spec.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ProgressSpinnerComponent } from "./progress-spinner.component"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { httpLoaderFactory } from "../../app.module"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { MatModule } from "../../modules/mat-module"; + +describe("ProgressSpinnerComponent", () => { + let component: ProgressSpinnerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ProgressSpinnerComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ProgressSpinnerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.ts b/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.ts new file mode 100644 index 0000000..45ad828 --- /dev/null +++ b/WowUp/wowup-electron/src/app/components/progress-spinner/progress-spinner.component.ts @@ -0,0 +1,21 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +@Component({ + selector: "app-progress-spinner", + templateUrl: "./progress-spinner.component.html", + styleUrls: ["./progress-spinner.component.scss"], +}) +export class ProgressSpinnerComponent implements OnInit { + @Input("message") public message = ""; + + public defaultMessage = ""; + + public constructor(private _translateService: TranslateService) {} + + public ngOnInit(): void { + this._translateService.get("COMMON.PROGRESS_SPINNER.LOADING").subscribe((translatedStr) => { + this.defaultMessage = translatedStr; + }); + } +} diff --git a/WowUp/wowup-electron/src/app/directives/external-link.directive.spec.ts b/WowUp/wowup-electron/src/app/directives/external-link.directive.spec.ts new file mode 100644 index 0000000..ef6f942 --- /dev/null +++ b/WowUp/wowup-electron/src/app/directives/external-link.directive.spec.ts @@ -0,0 +1,57 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ExternalLinkDirective } from "./external-link.directive"; +import { Component } from "@angular/core"; +import { getStandardImports, mockPreload } from "../tests/test-helpers"; +import { WowUpService } from "../services/wowup/wowup.service"; +import { MatDialog } from "@angular/material/dialog"; +import { LinkService } from "../services/links/link.service"; + +@Component({ + template: `test link`, +}) +class TestAppExternalLinkComponent {} + +describe("ExternalLinkDirective", () => { + let component: TestAppExternalLinkComponent; + let fixture: ComponentFixture; + let wowUpService: any; + let linkService: any; + + beforeEach(async () => { + mockPreload(); + + linkService = jasmine.createSpyObj("LinkService", [""], {}); + wowUpService = jasmine.createSpyObj("WowUpService", ["openExternalLink"], {}); + + await TestBed.configureTestingModule({ + declarations: [TestAppExternalLinkComponent, ExternalLinkDirective], + providers: [MatDialog], + imports: [...getStandardImports()], + }) + .overrideComponent(TestAppExternalLinkComponent, { + set: { + providers: [ + { provide: LinkService, useValue: linkService }, + { provide: WowUpService, useValue: wowUpService }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(TestAppExternalLinkComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + // it("should call openExternal on click", () => { + // const a = fixture.debugElement.nativeElement.querySelector("a"); + // a.click(); + // fixture.detectChanges(); + // expect(wowUpService.openExternalLink).toHaveBeenCalledWith("http://localhost:2020/"); + // }); +}); diff --git a/WowUp/wowup-electron/src/app/directives/external-link.directive.ts b/WowUp/wowup-electron/src/app/directives/external-link.directive.ts new file mode 100644 index 0000000..d2c6e16 --- /dev/null +++ b/WowUp/wowup-electron/src/app/directives/external-link.directive.ts @@ -0,0 +1,18 @@ +import { Directive, HostListener } from "@angular/core"; + + +@Directive({ + selector: "[appExternalLink]", +}) +export class ExternalLinkDirective { + @HostListener("click", ["$event"]) public onClick(): void { + // $event.preventDefault(); + // $event.stopPropagation(); + + // const target = ($event as any).path?.find((t) => t.tagName === "A"); + + // this._linkService.confirmLinkNavigation(target.href as string).subscribe(); + } + + public constructor() {} +} diff --git a/WowUp/wowup-electron/src/app/errors/index.ts b/WowUp/wowup-electron/src/app/errors/index.ts new file mode 100644 index 0000000..14038b5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/errors/index.ts @@ -0,0 +1,117 @@ +export * from "./install-error"; +import { CustomError } from "ts-custom-error"; +import { AddonWarningType, WowClientGroup } from "wowup-lib-core"; + +export class TocNotFoundError extends CustomError { + public constructor(message?: string){ + super(message); + } +} + +export class ErrorContainer extends CustomError { + public readonly innerError?: Error; + public readonly warningType?: AddonWarningType; + + public constructor(innerError?: Error, message?: string, warningType?: AddonWarningType) { + super(message); + this.innerError = innerError; + this.warningType = warningType; + } +} + +export class AssetMissingError extends CustomError { + public clientGroup?: WowClientGroup; + + public constructor(message?: string, clientGroup?: WowClientGroup) { + super(message); + + this.clientGroup = clientGroup; + } +} + +export class NoReleaseFoundError extends CustomError {} + +export interface AddonScanErrorConfig { + providerName: string; + innerError?: CustomError; + message?: string; + addonName?: string; +} + +export class AddonScanError extends CustomError { + public readonly innerError: CustomError | undefined; + public readonly providerName: string; + public readonly addonName?: string; + + public constructor(config: AddonScanErrorConfig) { + super(config.message); + + this.providerName = config.providerName; + this.innerError = config.innerError; + this.addonName = config.addonName; + } +} + +export interface AddonSyncErrorConfig { + providerName: string; + innerError?: CustomError; + message?: string; + addonName?: string; + installationName?: string; +} + +export class AddonSyncError extends CustomError { + public readonly innerError: CustomError | undefined; + public readonly providerName: string; + public readonly addonName?: string; + public readonly installationName?: string; + + public constructor(config: AddonSyncErrorConfig) { + super(config.message); + + this.providerName = config.providerName; + this.innerError = config.innerError; + this.addonName = config.addonName; + this.installationName = config.installationName; + } +} + +export class GenericProviderError extends ErrorContainer {} + +export class GitHubError extends ErrorContainer {} + +export class GitHubLimitError extends GitHubError { + public readonly rateLimitMax: number; + public readonly rateLimitUsed: number; + public readonly rateLimitRemaining: number; + public readonly rateLimitReset: number; + + public constructor(max: number, used: number, remaining: number, reset: number, message?: string) { + super(undefined, message); + + this.rateLimitMax = max; + this.rateLimitUsed = used; + this.rateLimitRemaining = remaining; + this.rateLimitReset = reset; + } +} + +export class GitHubFetchReleasesError extends GitHubError { + public readonly addonId: string; + + public constructor(addonId: string, error?: Error) { + super(error); + + this.addonId = addonId; + } +} + +export class GitHubFetchRepositoryError extends GitHubError { + public readonly addonId: string; + + public constructor(addonId: string, error?: Error) { + super(error); + + this.addonId = addonId; + } +} diff --git a/WowUp/wowup-electron/src/app/errors/install-error.ts b/WowUp/wowup-electron/src/app/errors/install-error.ts new file mode 100644 index 0000000..750bcc6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/errors/install-error.ts @@ -0,0 +1,7 @@ +import { CustomError } from "ts-custom-error"; + +export class InstallError extends CustomError { + public constructor(message?: string, public addonName?: string) { + super(message); + } +} diff --git a/WowUp/wowup-electron/src/app/interceptors/default-headers.interceptor.ts b/WowUp/wowup-electron/src/app/interceptors/default-headers.interceptor.ts new file mode 100644 index 0000000..4a9630f --- /dev/null +++ b/WowUp/wowup-electron/src/app/interceptors/default-headers.interceptor.ts @@ -0,0 +1,37 @@ +import { Observable } from "rxjs"; + +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { tap } from "rxjs/operators"; + +@Injectable({ + providedIn: "root", +}) +export class DefaultHeadersInterceptor implements HttpInterceptor { + public intercept(req: HttpRequest, next: HttpHandler): Observable> { + // Get the auth token from the service. + + // Clone the request and replace the original headers with + // cloned headers, updated with the authorization. + // const cloneReq = req.clone({ + // headers: req.headers.set("startTimestamp", Date.now().toString()), + // }); + const start = Date.now(); + const method = req.method; + const url = req.urlWithParams; + + // send cloned request with header to the next handler. + return next.handle(req).pipe( + tap((response: any) => { + try { + if (response instanceof HttpResponse) { + const t = Date.now() - start; + console.log(`[${method}] ${url} ${response.status} ${t}ms`); + } + } catch (e) { + console.error(e); + } + }) + ); + } +} diff --git a/WowUp/wowup-electron/src/app/interceptors/error-handler-interceptor.ts b/WowUp/wowup-electron/src/app/interceptors/error-handler-interceptor.ts new file mode 100644 index 0000000..4f58b9c --- /dev/null +++ b/WowUp/wowup-electron/src/app/interceptors/error-handler-interceptor.ts @@ -0,0 +1,13 @@ +import { ErrorHandler } from "@angular/core"; +import { AnalyticsService } from "../services/analytics/analytics.service"; + +export class ErrorHandlerInterceptor implements ErrorHandler { + public constructor(private _analytics: AnalyticsService) {} + + // ErrorHandler + public handleError(error: Error): void { + console.error("Caught error", error); + + this._analytics.trackError(((error as any).innerError as Error) ?? error); + } +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/addon-install-state.ts b/WowUp/wowup-electron/src/app/models/wowup/addon-install-state.ts new file mode 100644 index 0000000..d72ebdb --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/addon-install-state.ts @@ -0,0 +1,10 @@ +export enum AddonInstallState { + Pending, + Downloading, + BackingUp, + Installing, + Complete, + Retry, + Error, + Unknown, +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/addon-provider-state.ts b/WowUp/wowup-electron/src/app/models/wowup/addon-provider-state.ts new file mode 100644 index 0000000..2c94a26 --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/addon-provider-state.ts @@ -0,0 +1,7 @@ +import { AddonProviderType } from "wowup-lib-core"; + +export interface AddonProviderState { + providerName: AddonProviderType; + enabled: boolean; + canEdit: boolean; +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/addon-status-sort-order.ts b/WowUp/wowup-electron/src/app/models/wowup/addon-status-sort-order.ts new file mode 100644 index 0000000..5992c88 --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/addon-status-sort-order.ts @@ -0,0 +1,8 @@ +export enum AddonStatusSortOrder { + Warning, + Install, + Update, + UpToDate, + Ignored, + Unknown, +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/addon-update-event.ts b/WowUp/wowup-electron/src/app/models/wowup/addon-update-event.ts new file mode 100644 index 0000000..05a8416 --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/addon-update-event.ts @@ -0,0 +1,8 @@ +import { Addon } from "wowup-lib-core"; +import { AddonInstallState } from "./addon-install-state"; + +export interface AddonUpdateEvent { + addon: Addon; + installState: AddonInstallState; + progress: number; +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/change-log.ts b/WowUp/wowup-electron/src/app/models/wowup/change-log.ts new file mode 100644 index 0000000..fbe4f8c --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/change-log.ts @@ -0,0 +1,6 @@ +export interface ChangeLog { + Version: string; + Description?: string; + changes?: string[]; + html?: string; +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/column-state.ts b/WowUp/wowup-electron/src/app/models/wowup/column-state.ts new file mode 100644 index 0000000..d863591 --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/column-state.ts @@ -0,0 +1,6 @@ +export interface ColumnState { + name: string; + display: string; + visible: boolean; + allowToggle?: boolean; +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/preference-change.ts b/WowUp/wowup-electron/src/app/models/wowup/preference-change.ts new file mode 100644 index 0000000..ae5d236 --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/preference-change.ts @@ -0,0 +1,4 @@ +export interface PreferenceChange { + key: string; + value: string; +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/select-item.ts b/WowUp/wowup-electron/src/app/models/wowup/select-item.ts new file mode 100644 index 0000000..9d04c1f --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/select-item.ts @@ -0,0 +1,5 @@ +// Attempt to make a simple container for localizing select items +export interface SelectItem { + display: string; + value: T; +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/sort-order.ts b/WowUp/wowup-electron/src/app/models/wowup/sort-order.ts new file mode 100644 index 0000000..d679d23 --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/sort-order.ts @@ -0,0 +1,4 @@ +export interface SortOrder { + colId: string; + sort: 'asc' | 'desc' | null; +} diff --git a/WowUp/wowup-electron/src/app/models/wowup/theme.ts b/WowUp/wowup-electron/src/app/models/wowup/theme.ts new file mode 100644 index 0000000..88a2d16 --- /dev/null +++ b/WowUp/wowup-electron/src/app/models/wowup/theme.ts @@ -0,0 +1,9 @@ +export interface ThemeGroup { + name: string; + themes: Theme[]; +} + +interface Theme { + display: string; + class: string; +} diff --git a/WowUp/wowup-electron/src/app/modules/addons.module.ts b/WowUp/wowup-electron/src/app/modules/addons.module.ts new file mode 100644 index 0000000..de143ca --- /dev/null +++ b/WowUp/wowup-electron/src/app/modules/addons.module.ts @@ -0,0 +1,81 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { TranslateModule } from "@ngx-translate/core"; +import { LightboxModule, LIGHTBOX_CONFIG } from "ng-gallery/lightbox"; +import { AddonUpdateButtonComponent } from "../components/addons/addon-update-button/addon-update-button.component"; +import { AddonDetailComponent } from "../components/addons/addon-detail/addon-detail.component"; +import { AddonInstallButtonComponent } from "../components/addons/addon-install-button/addon-install-button.component"; +import { AddonThumbnailComponent } from "../components/addons/addon-thumbnail/addon-thumbnail.component"; +import { DateTooltipCellComponent } from "../components/addons/date-tooltip-cell/date-tooltip-cell.component"; +import { MatModule } from "./mat-module"; +import { CommonUiModule } from "./common-ui.module"; +import { PipesModule } from "./pipes.module"; +import { FundingButtonComponent } from "../components/addons/funding-button/funding-button.component"; +import { GetAddonStatusColumnComponent } from "../components/addons/get-addon-status-cell/get-addon-status-cell.component"; +import { PotentialAddonTableCellComponent } from "../components/addons/potential-addon-table-cell/potential-addon-table-cell.component"; +import { TableContextHeaderCellComponent } from "../components/addons/table-context-header-cell/table-context-header-cell.component"; +import { MyAddonStatusCellComponent } from "../components/addons/my-addon-status-cell/my-addon-status-cell.component"; +import { InstallFromProtocolDialogComponent } from "../components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component"; +import { MyAddonsAddonCellComponent } from "../components/addons/my-addons-addon-cell/my-addons-addon-cell.component"; +import { InstallFromUrlDialogComponent } from "../components/addons/install-from-url-dialog/install-from-url-dialog.component"; +import { DirectiveModule } from "./directive.module"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { AddonManageDialogComponent } from "../components/addons/addon-manage-dialog/addon-manage-dialog.component"; +import { WtfBackupComponent } from "../components/addons/wtf-backup/wtf-backup.component"; + +@NgModule({ + declarations: [ + AddonUpdateButtonComponent, + AddonDetailComponent, + DateTooltipCellComponent, + AddonInstallButtonComponent, + AddonThumbnailComponent, + FundingButtonComponent, + GetAddonStatusColumnComponent, + PotentialAddonTableCellComponent, + TableContextHeaderCellComponent, + MyAddonStatusCellComponent, + InstallFromProtocolDialogComponent, + MyAddonsAddonCellComponent, + InstallFromUrlDialogComponent, + AddonManageDialogComponent, + WtfBackupComponent, + ], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TranslateModule, + MatModule, + CommonUiModule, + PipesModule, + DirectiveModule, + LightboxModule, + ], + exports: [ + AddonUpdateButtonComponent, + AddonDetailComponent, + DateTooltipCellComponent, + AddonInstallButtonComponent, + AddonThumbnailComponent, + FundingButtonComponent, + GetAddonStatusColumnComponent, + PotentialAddonTableCellComponent, + TableContextHeaderCellComponent, + MyAddonStatusCellComponent, + InstallFromProtocolDialogComponent, + MyAddonsAddonCellComponent, + InstallFromUrlDialogComponent, + AddonManageDialogComponent, + WtfBackupComponent, + ], + providers: [ + { + provide: LIGHTBOX_CONFIG, + useValue: { + keyboardShortcuts: true, + }, + }, + ], +}) +export class AddonsModule {} diff --git a/WowUp/wowup-electron/src/app/modules/common-ui.module.ts b/WowUp/wowup-electron/src/app/modules/common-ui.module.ts new file mode 100644 index 0000000..42d402f --- /dev/null +++ b/WowUp/wowup-electron/src/app/modules/common-ui.module.ts @@ -0,0 +1,61 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { TranslateModule } from "@ngx-translate/core"; + +import { AlertDialogComponent } from "../components/common/alert-dialog/alert-dialog.component"; +import { AnimatedLogoComponent } from "../components/common/animated-logo/animated-logo.component"; +import { CellWrapTextComponent } from "../components/common/cell-wrap-text/cell-wrap-text.component"; +import { CenteredSnackbarComponent } from "../components/common/centered-snackbar/centered-snackbar.component"; +import { ClientSelectorComponent } from "../components/common/client-selector/client-selector.component"; +import { ConfirmDialogComponent } from "../components/common/confirm-dialog/confirm-dialog.component"; +import { ConsentDialogComponent } from "../components/common/consent-dialog/consent-dialog.component"; +import { CurseMigrationDialogComponent } from "../components/common/curse-migration-dialog/curse-migration-dialog.component"; +import { ExternalUrlConfirmationDialogComponent } from "../components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component"; +import { PatchNotesDialogComponent } from "../components/common/patch-notes-dialog/patch-notes-dialog.component"; +import { ProgressButtonComponent } from "../components/common/progress-button/progress-button.component"; +import { TelemetryDialogComponent } from "../components/common/telemetry-dialog/telemetry-dialog.component"; +import { WebViewComponent } from "../components/common/webview/webview.component"; +import { ProgressSpinnerComponent } from "../components/progress-spinner/progress-spinner.component"; +import { MatModule } from "./mat-module"; +import { PipesModule } from "./pipes.module"; +import { ProgressBarComponent } from "../components/common/progress-bar/progress-bar.component"; + +@NgModule({ + declarations: [ + ProgressSpinnerComponent, + ProgressButtonComponent, + ProgressBarComponent, + ConfirmDialogComponent, + AlertDialogComponent, + AnimatedLogoComponent, + ExternalUrlConfirmationDialogComponent, + PatchNotesDialogComponent, + TelemetryDialogComponent, + ConsentDialogComponent, + CellWrapTextComponent, + CenteredSnackbarComponent, + ClientSelectorComponent, + CurseMigrationDialogComponent, + WebViewComponent, + ], + imports: [CommonModule, FormsModule, TranslateModule, MatModule, PipesModule, ReactiveFormsModule], + exports: [ + ProgressSpinnerComponent, + ProgressButtonComponent, + ProgressBarComponent, + ConfirmDialogComponent, + AlertDialogComponent, + AnimatedLogoComponent, + ExternalUrlConfirmationDialogComponent, + PatchNotesDialogComponent, + TelemetryDialogComponent, + ConsentDialogComponent, + CellWrapTextComponent, + CenteredSnackbarComponent, + ClientSelectorComponent, + CurseMigrationDialogComponent, + WebViewComponent, + ], +}) +export class CommonUiModule {} diff --git a/WowUp/wowup-electron/src/app/modules/directive.module.ts b/WowUp/wowup-electron/src/app/modules/directive.module.ts new file mode 100644 index 0000000..ca7ffe6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/modules/directive.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from "@angular/core"; + +import { ExternalLinkDirective } from "../directives/external-link.directive"; + +@NgModule({ + declarations: [ExternalLinkDirective], + exports: [ExternalLinkDirective], +}) +export class DirectiveModule {} diff --git a/WowUp/wowup-electron/src/app/modules/mat-module.ts b/WowUp/wowup-electron/src/app/modules/mat-module.ts new file mode 100644 index 0000000..80fd6fb --- /dev/null +++ b/WowUp/wowup-electron/src/app/modules/mat-module.ts @@ -0,0 +1,108 @@ +import { NgModule } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; +import { MatCardModule } from "@angular/material/card"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatDividerModule } from "@angular/material/divider"; +import { MatIconModule } from "@angular/material/icon"; +import { MatInputModule } from "@angular/material/input"; +import { MatMenuModule } from "@angular/material/menu"; +import { MatProgressBarModule } from "@angular/material/progress-bar"; +import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; +import { MatRadioModule } from "@angular/material/radio"; +import { MatSelectModule } from "@angular/material/select"; +import { MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS, MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { MatSliderModule } from "@angular/material/slider"; +import { MatSnackBarModule } from "@angular/material/snack-bar"; +import { MatSortModule } from "@angular/material/sort"; +import { MatTableModule } from "@angular/material/table"; +import { MatTabsModule } from "@angular/material/tabs"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { MatSidenavModule } from "@angular/material/sidenav"; +import { MatListModule } from "@angular/material/list"; +import { MatBadgeModule } from "@angular/material/badge"; +import { ClipboardModule } from "@angular/cdk/clipboard"; +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { MatGridListModule } from "@angular/material/grid-list"; +import { MatExpansionModule } from "@angular/material/expansion"; +import { ObserversModule } from "@angular/cdk/observers"; +import { MatTreeModule } from "@angular/material/tree"; +import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from "@angular/material/form-field"; + +@NgModule({ + exports: [ + MatSliderModule, + MatTabsModule, + MatSelectModule, + MatButtonModule, + MatTableModule, + MatSortModule, + MatInputModule, + MatIconModule, + MatSlideToggleModule, + MatProgressBarModule, + MatCheckboxModule, + MatDividerModule, + MatProgressSpinnerModule, + MatMenuModule, + MatRadioModule, + MatTooltipModule, + MatDialogModule, + MatCardModule, + MatSnackBarModule, + MatSidenavModule, + MatListModule, + MatBadgeModule, + ClipboardModule, + ObserversModule, + ScrollingModule, + MatGridListModule, + MatExpansionModule, + MatTreeModule, + ], + imports: [ + MatSliderModule, + MatTabsModule, + MatSelectModule, + MatButtonModule, + MatTableModule, + MatSortModule, + MatInputModule, + MatIconModule, + MatSlideToggleModule, + MatProgressBarModule, + MatCheckboxModule, + MatDividerModule, + MatProgressSpinnerModule, + MatMenuModule, + MatRadioModule, + MatTooltipModule, + MatDialogModule, + MatCardModule, + MatSnackBarModule, + MatSidenavModule, + MatListModule, + MatBadgeModule, + ClipboardModule, + ObserversModule, + ScrollingModule, + MatGridListModule, + MatExpansionModule, + MatTreeModule, + ], + providers: [ + { + provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, + useValue: { + subscriptSizing: "dynamic", + }, + }, + { + provide: MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS, + useValue: { + hideIcon: true, + }, + }, + ], +}) +export class MatModule {} diff --git a/WowUp/wowup-electron/src/app/modules/options.module.ts b/WowUp/wowup-electron/src/app/modules/options.module.ts new file mode 100644 index 0000000..3ead2d7 --- /dev/null +++ b/WowUp/wowup-electron/src/app/modules/options.module.ts @@ -0,0 +1,49 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { TranslateModule } from "@ngx-translate/core"; + +import { AboutComponent } from "../components/options/about/about.component"; +import { OptionsAddonSectionComponent } from "../components/options/options-addon-section/options-addon-section.component"; +import { OptionsAppSectionComponent } from "../components/options/options-app-section/options-app-section.component"; +import { OptionsDebugSectionComponent } from "../components/options/options-debug-section/options-debug-section.component"; +import { OptionsWowSectionComponent } from "../components/options/options-wow-section/options-wow-section.component"; +import { WowClientOptionsComponent } from "../components/options/wow-client-options/wow-client-options.component"; +import { WtfExplorerComponent } from "../components/options/wtf-explorer/wtf-explorer.component"; +import { MatModule } from "./mat-module"; +import { PipesModule } from "./pipes.module"; +import { DirectiveModule } from "./directive.module"; +import { OptionsCurseforgeSectionComponent } from "../components/options/options-curseforge-section/options-curseforge-section.component"; + +@NgModule({ + declarations: [ + WtfExplorerComponent, + AboutComponent, + OptionsAddonSectionComponent, + OptionsAppSectionComponent, + OptionsDebugSectionComponent, + OptionsWowSectionComponent, + OptionsCurseforgeSectionComponent, + WowClientOptionsComponent, + ], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TranslateModule, + MatModule, + PipesModule, + DirectiveModule, + ], + exports: [ + WtfExplorerComponent, + AboutComponent, + OptionsAddonSectionComponent, + OptionsAppSectionComponent, + OptionsDebugSectionComponent, + OptionsCurseforgeSectionComponent, + OptionsWowSectionComponent, + WowClientOptionsComponent, + ], +}) +export class OptionsModule {} diff --git a/WowUp/wowup-electron/src/app/modules/pipes.module.ts b/WowUp/wowup-electron/src/app/modules/pipes.module.ts new file mode 100644 index 0000000..106265b --- /dev/null +++ b/WowUp/wowup-electron/src/app/modules/pipes.module.ts @@ -0,0 +1,35 @@ +import { DatePipe } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { DownloadCountPipe } from "../pipes/download-count.pipe"; +import { GetAddonListItemFilePropPipe } from "../pipes/get-addon-list-item-file-prop.pipe"; +import { InterfaceFormatPipe } from "../pipes/interface-format.pipe"; +import { InvertBoolPipe } from "../pipes/inverse-bool.pipe"; +import { NgxDatePipe } from "../pipes/ngx-date.pipe"; +import { RelativeDurationPipe } from "../pipes/relative-duration-pipe"; +import { SizeDisplayPipe } from "../pipes/size-display.pipe"; +import { TrustHtmlPipe } from "../pipes/trust-html.pipe"; + +@NgModule({ + declarations: [ + TrustHtmlPipe, + SizeDisplayPipe, + NgxDatePipe, + RelativeDurationPipe, + GetAddonListItemFilePropPipe, + DownloadCountPipe, + InterfaceFormatPipe, + InvertBoolPipe, + ], + exports: [ + TrustHtmlPipe, + SizeDisplayPipe, + NgxDatePipe, + RelativeDurationPipe, + GetAddonListItemFilePropPipe, + DownloadCountPipe, + InterfaceFormatPipe, + InvertBoolPipe, + ], + providers: [RelativeDurationPipe, GetAddonListItemFilePropPipe, DownloadCountPipe, DatePipe], +}) +export class PipesModule {} diff --git a/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.html b/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.html new file mode 100644 index 0000000..292dea9 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.html @@ -0,0 +1,95 @@ +
+ +
+ +
+
+
+

+ {{ "PAGES.ACCOUNT.TITLE" | translate }} + {{ "PAGES.ACCOUNT.BETA" | translate }} +

+

You're logged in!

+

You're now able to access our various cloud services to help you maximize your addon experience.

+
+
+
+ {{ "Instant Updates" | translate }} +
+ {{ + "No more waiting for timers, get updates as soon as we see them" | translate + }} +
+ + +
+
+
+
+ {{ "Cloud Addon Sync" | translate }} +
+ {{ + "The simplest way to manage your addon backups for all your machines" | translate + }} +
+ +
+
+
+
+ {{ "Cloud Settings Sync" | translate }} +
+ {{ + "Easy to use, hopefully, way to manage your settings between characters and machines" | translate + }} +
+ +
+
+ +
+ +
+
+
+
diff --git a/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.scss b/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.scss new file mode 100644 index 0000000..cd1f021 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.scss @@ -0,0 +1,47 @@ +.control-container { + display: flex; + flex-direction: column; + overflow: hidden; + max-width: 100%; + justify-content: center; + align-items: center; + height: 100%; +} + +.authorized-container { + display: flex; + flex-direction: column; + overflow-x: hidden; + overflow-y: auto; + max-width: 100%; + box-sizing: border-box; + width: 500px; + + .toggle-row { + margin-top: 1em; + } +} + +.account-container { + width: 400px; +} + +.theme-logo { + content: ""; + opacity: 0.02; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + overflow: hidden; + pointer-events: none; + + .logo-img { + height: 100vh; + background-image: var(--theme-logo); + background-repeat: no-repeat; + background-position: 0 0; + background-size: contain; + } +} diff --git a/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.ts b/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.ts new file mode 100644 index 0000000..64513b6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/account-page/account-page.component.ts @@ -0,0 +1,69 @@ +import { Component } from "@angular/core"; +import { MatSlideToggleChange } from "@angular/material/slide-toggle"; +import { TranslateService } from "@ngx-translate/core"; +import { of } from "rxjs"; +import { catchError, map } from "rxjs/operators"; +import { AppConfig } from "../../../environments/environment"; +import { ElectronService } from "../../services"; +import { DialogFactory } from "../../services/dialog/dialog.factory"; +import { LinkService } from "../../services/links/link.service"; +import { SessionService } from "../../services/session/session.service"; +import { SnackbarService } from "../../services/snackbar/snackbar.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; + +@Component({ + selector: "app-account-page", + templateUrl: "./account-page.component.html", + styleUrls: ["./account-page.component.scss"], +}) +export class AccountPageComponent { + public constructor( + public electronService: ElectronService, + public sessionService: SessionService, + public wowUpService: WowUpService, + private _dialogFactory: DialogFactory, + private _translateService: TranslateService, + private _snackbarService: SnackbarService, + private _linkService: LinkService, + ) {} + + public onClickLogin(): void { + this.sessionService.login(); + } + + public onToggleAccountPush = async (evt: MatSlideToggleChange): Promise => { + try { + await this.sessionService.toggleAccountPush(evt.checked); + } catch (e) { + evt.source.checked = !evt.source.checked; + console.error("Failed to toggle account push", e); + this._snackbarService.showErrorSnackbar("COMMON.ERRORS.ACCOUNT_PUSH_TOGGLE_FAILED_ERROR"); + } + }; + + public onClickLogout(): void { + const title: string = this._translateService.instant("PAGES.ACCOUNT.LOGOUT_CONFIRMATION_TITLE"); + const message: string = this._translateService.instant("PAGES.ACCOUNT.LOGOUT_CONFIRMATION_MESSAGE"); + + const dialogRef = this._dialogFactory.getConfirmDialog(title, message); + + dialogRef + .afterClosed() + .pipe( + map((result) => { + if (result) { + this.sessionService.logout(); + } + }), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ) + .subscribe(); + } + + public onClickManageAccount(): void { + this._linkService.openExternalLink(`${AppConfig.wowUpWebsiteUrl}/account`).catch((e) => console.error(e)); + } +} diff --git a/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.html b/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.html new file mode 100644 index 0000000..3a774b8 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.html @@ -0,0 +1,152 @@ +
+
+
+ +
+
+

+ {{ + "PAGES.GET_ADDONS.ADDON_CATEGORIES_SELECTED_TITLE" + | translate: { category: (selectedAddonCategory?.localeKey ?? "" | translate) } + }} +

+ +
+
+
+ + {{ "PAGES.GET_ADDONS.SEARCH_LABEL" | translate }} + + + +
+
+ + + +
+
+
+ + + +

{{ "PAGES.GET_ADDONS.ADDON_CATEGORIES_MENU_TITLE" | translate }}

+ + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ + +
+
+ {{ "PAGES.MY_ADDONS.COLUMNS_CONTEXT_MENU.TITLE" | translate }} +
+
+ + + {{ column.display | translate }} + +
+
diff --git a/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.scss b/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.scss new file mode 100644 index 0000000..7d7e3bc --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.scss @@ -0,0 +1,90 @@ +.tab-container { + display: flex; + flex-direction: column; +} + +.wu-ag-table { + width: 100%; + height: calc(100% - 86px); +} + +.control-container { + display: flex; + flex-direction: row; + padding: 0 1em 0 1em; + margin: 1em 1em 0 1em; + + .select-container { + flex-grow: 1; + } + + .center-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + h4 { + padding-top: 8px; + } + } + + .right-container { + display: flex; + flex-direction: row; + flex-grow: 1; + justify-content: flex-end; + + .button-container { + display: flex; + flex-direction: row; + align-items: center; + margin-left: 1em; + + button { + &:not(:last-child) { + margin-right: 0.5em; + } + } + } + } +} + +.no-addons-container { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; +} +.spinner-container { + display: flex; + align-items: center; + justify-content: center; +} + +.table-container { + height: calc(100% - 72px); + overflow: auto; + overflow-x: hidden; + + table { + width: 100%; + } + + .cell-padding { + padding-right: 1em; + } +} + +.author-column { + width: 100px; + padding-right: 1em; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-align: left; +} +.provider-column { + min-width: 60px; +} diff --git a/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.spec.ts b/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.spec.ts new file mode 100644 index 0000000..141711d --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.spec.ts @@ -0,0 +1,138 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { BehaviorSubject, Subject } from "rxjs"; + +import { OverlayModule } from "@angular/cdk/overlay"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialog } from "@angular/material/dialog"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../app.module"; +import { ElectronService } from "../../services"; +import { AddonService } from "../../services/addons/addon.service"; +import { SessionService } from "../../services/session/session.service"; +import { WarcraftService } from "../../services/warcraft/warcraft.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +import { GetAddonsComponent } from "./get-addons.component"; +import { SnackbarService } from "../../services/snackbar/snackbar.service"; +import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service"; +import { DownloadCountPipe } from "../../pipes/download-count.pipe"; +import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe"; +import { MatModule } from "../../modules/mat-module"; +import { PipesModule } from "../../modules/pipes.module"; +import { AddonProviderFactory } from "../../services/addons/addon.provider.factory"; +import { AddonChannelType, WowClientType } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +describe("GetAddonsComponent", () => { + let component: GetAddonsComponent; + let fixture: ComponentFixture; + let electronServiceSpy: any; + let wowUpServiceSpy: any; + let sessionServiceSpy: any; + let addonServiceSpy: any; + let warcraftServiceSpy: any; + let snackbarService: SnackbarService; + let warcraftInstallationService: WarcraftInstallationService; + let addonProviderService: any; + + beforeEach(async () => { + wowUpServiceSpy = jasmine.createSpyObj("WowUpService", [""], { + getGetAddonsHiddenColumns: () => Promise.resolve([]), + }); + sessionServiceSpy = jasmine.createSpyObj( + "SessionService", + { + getSelectedWowInstallation: () => { + const inst: WowInstallation = { + defaultAddonChannelType: AddonChannelType.Stable, + id: "test", + clientType: WowClientType.Retail, + location: "C:/fake_wow", + label: "Wow Unit Test Client", + displayName: "Wow Unit Test Client", + defaultAutoUpdate: false, + selected: true, + }; + return inst; + }, + }, + { + selectedHomeTab$: new BehaviorSubject(0).asObservable(), + } + ); + warcraftServiceSpy = jasmine.createSpyObj("WarcraftService", [""], { + installedClientTypesSelectItems$: new BehaviorSubject(undefined).asObservable(), + }); + electronServiceSpy = jasmine.createSpyObj("ElectronService", [""], { + isWin: false, + isLinux: true, + isMac: false, + }); + addonServiceSpy = jasmine.createSpyObj("AddonService", [""], { + searchError$: new Subject(), + }); + + snackbarService = jasmine.createSpyObj("SnackbarService", [""], {}); + addonProviderService = jasmine.createSpyObj("AddonProviderFactory", [""], {}); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], { + wowInstallations$: new BehaviorSubject([]), + }); + + const testBed = TestBed.configureTestingModule({ + declarations: [GetAddonsComponent], + imports: [ + MatModule, + OverlayModule, + BrowserAnimationsModule, + HttpClientModule, + PipesModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + providers: [MatDialog, RelativeDurationPipe, DownloadCountPipe], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).overrideComponent(GetAddonsComponent, { + set: { + providers: [ + { provide: AddonService, useValue: addonServiceSpy }, + { provide: SessionService, useValue: sessionServiceSpy }, + { provide: WowUpService, useValue: wowUpServiceSpy }, + { provide: SnackbarService, useValue: snackbarService }, + { provide: ElectronService, useValue: electronServiceSpy }, + { provide: WarcraftService, useValue: warcraftServiceSpy }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + { provide: AddonProviderFactory, useValue: addonProviderService }, + ], + }, + }); + + await testBed.compileComponents(); + + fixture = TestBed.createComponent(GetAddonsComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.debugElement.nativeElement.remove(); + fixture.destroy(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.ts b/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.ts new file mode 100644 index 0000000..8435b20 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/get-addons/get-addons.component.ts @@ -0,0 +1,643 @@ +import { + ColDef, + ColumnApi, + GridApi, + GridReadyEvent, + IRowNode, + RowClickedEvent, + RowDoubleClickedEvent, + +} from "ag-grid-community"; +import * as _ from "lodash"; +import { BehaviorSubject, combineLatest, from, Observable, of, Subject } from "rxjs"; +import { catchError, filter, first, map, switchMap, takeUntil } from "rxjs/operators"; + +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { MatCheckboxChange } from "@angular/material/checkbox"; +import { MatDialog } from "@angular/material/dialog"; +import { MatMenuTrigger } from "@angular/material/menu"; +import { MatDrawer } from "@angular/material/sidenav"; +import { TranslateService } from "@ngx-translate/core"; + +import { + ADDON_PROVIDER_HUB, + ADDON_PROVIDER_WAGO, + DEFAULT_CHANNEL_PREFERENCE_KEY_SUFFIX, +} from "../../../common/constants"; +import { GetAddonListItem } from "../../business-objects/get-addon-list-item"; +import { CellWrapTextComponent } from "../../components/common/cell-wrap-text/cell-wrap-text.component"; +import { GetAddonStatusColumnComponent } from "../../components/addons/get-addon-status-cell/get-addon-status-cell.component"; +import { InstallFromUrlDialogComponent } from "../../components/addons/install-from-url-dialog/install-from-url-dialog.component"; +import { + PotentialAddonTableCellComponent, + PotentialAddonViewDetailsEvent, +} from "../../components/addons/potential-addon-table-cell/potential-addon-table-cell.component"; +import { TableContextHeaderCellComponent } from "../../components/addons/table-context-header-cell/table-context-header-cell.component"; +import { GenericProviderError } from "../../errors"; +import { ColumnState } from "../../models/wowup/column-state"; +import { DownloadCountPipe } from "../../pipes/download-count.pipe"; +import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe"; +import { ElectronService } from "../../services"; +import { AddonService } from "../../services/addons/addon.service"; +import { DialogFactory } from "../../services/dialog/dialog.factory"; +import { SessionService } from "../../services/session/session.service"; +import { SnackbarService } from "../../services/snackbar/snackbar.service"; +import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service"; +import { WarcraftService } from "../../services/warcraft/warcraft.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +import { getEnumKeys } from "wowup-lib-core"; +import { camelToSnakeCase } from "../../utils/string.utils"; +import { AddonProviderFactory } from "../../services/addons/addon.provider.factory"; +import { AddonCategory, AddonChannelType, AddonSearchResult, WowClientType } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +interface CategoryItem { + category: AddonCategory; + localeKey: string; +} + +@Component({ + selector: "app-get-addons", + templateUrl: "./get-addons.component.html", + styleUrls: ["./get-addons.component.scss"], +}) +export class GetAddonsComponent implements OnInit, OnDestroy { + @Input("tabIndex") public tabIndex!: number; + + @ViewChild("columnContextMenuTrigger") public columnContextMenu!: MatMenuTrigger; + @ViewChild("drawer") public drawer!: MatDrawer; + + private readonly _destroy$ = new Subject(); + + private _isSelectedTab = false; + private _lazyLoaded = false; + private _rowDataSrc = new BehaviorSubject([]); + private _lastSelectionState: IRowNode[] = []; + private _selectedAddonCategory: CategoryItem | undefined; + + public addonCategory = AddonCategory; + public columnDefs$ = new BehaviorSubject([]); + public rowData$ = this._rowDataSrc.asObservable(); + public enableControls$ = this._sessionService.enableControls$; + public columnTypes: { + [key: string]: ColDef; + } = { nonEditableColumn: { editable: false } }; + + public columnStates: ColumnState[] = [ + { name: "name", display: "PAGES.GET_ADDONS.TABLE.ADDON_COLUMN_HEADER", visible: true }, + { + name: "downloadCount", + display: "PAGES.GET_ADDONS.TABLE.DOWNLOAD_COUNT_COLUMN_HEADER", + visible: true, + allowToggle: true, + }, + { + name: "releasedAt", + display: "PAGES.GET_ADDONS.TABLE.RELEASED_AT_COLUMN_HEADER", + visible: true, + allowToggle: true, + }, + { name: "author", display: "PAGES.GET_ADDONS.TABLE.AUTHOR_COLUMN_HEADER", visible: true, allowToggle: true }, + { + name: "providerName", + display: "PAGES.GET_ADDONS.TABLE.PROVIDER_COLUMN_HEADER", + visible: true, + allowToggle: false, + }, + { name: "status", display: "PAGES.GET_ADDONS.TABLE.STATUS_COLUMN_HEADER", visible: true }, + ]; + + public get defaultAddonChannel(): AddonChannelType | undefined { + const installation = this._sessionService.getSelectedWowInstallation(); + return installation?.defaultAddonChannelType ?? undefined; + } + + public query = ""; + public selectedClient = WowClientType.None; + public selectedInstallation: WowInstallation | undefined = undefined; + public selectedInstallationId = ""; + public contextMenuPosition = { x: "0px", y: "0px" }; + public wowInstallations$: Observable; + public overlayNoRowsTemplate = ""; + + public hasData$ = this.rowData$.pipe(map((data) => data.length > 0)); + + private _showTableSrc = new BehaviorSubject(false); + public readonly showTable$ = combineLatest([this._showTableSrc, this.hasData$]).pipe( + map(([enabled]) => { + return enabled === true; + }), + ); + + public gridApi!: GridApi; + public gridColumnApi!: ColumnApi; + + public addonCategories: CategoryItem[] = []; + + public get selectedAddonCategory(): CategoryItem | undefined { + return this._selectedAddonCategory; + } + + public set selectedAddonCategory(categoryItem: CategoryItem | undefined) { + this._selectedAddonCategory = categoryItem; + this.drawer?.close().catch((e) => console.error(e)); + + if (!this.selectedInstallation || !categoryItem) { + return; + } + + if (categoryItem.category === AddonCategory.AllAddons) { + this.loadPopularAddons(this.selectedInstallation); + return; + } + + this._sessionService.setEnableControls(false); + this._showTableSrc.next(false); + + of(true) + .pipe( + first(), + switchMap(() => { + return this.selectedInstallation + ? from(this._addonService.getCategoryPage(categoryItem.category, this.selectedInstallation)) + : of([] as AddonSearchResult[]); + }), + map((searchResults) => { + const searchListItems = this.formatAddons(searchResults); + this._rowDataSrc.next(searchListItems); + this._showTableSrc.next(true); + this._sessionService.setEnableControls(true); + }), + catchError((error) => { + console.error(error); + this.displayError(error as Error); + this._rowDataSrc.next([]); + this._showTableSrc.next(true); + this._sessionService.setEnableControls(true); + return of(undefined); + }), + ) + .subscribe(); + } + + public constructor( + private _addonProviderService: AddonProviderFactory, + private _dialog: MatDialog, + private _dialogFactory: DialogFactory, + private _cdRef: ChangeDetectorRef, + private _addonService: AddonService, + private _sessionService: SessionService, + private _wowUpService: WowUpService, + private _translateService: TranslateService, + private _snackbarService: SnackbarService, + public electronService: ElectronService, + public warcraftService: WarcraftService, + public warcraftInstallationService: WarcraftInstallationService, + public relativeDurationPipe: RelativeDurationPipe, + public downloadCountPipe: DownloadCountPipe, + ) { + this.overlayNoRowsTemplate = `${ + _translateService.instant("COMMON.SEARCH.NO_ADDONS") as string + }`; + + this.wowInstallations$ = warcraftInstallationService.wowInstallations$; + + _sessionService.selectedHomeTab$.pipe(takeUntil(this._destroy$)).subscribe((tabIndex) => { + this._isSelectedTab = tabIndex === this.tabIndex; + if (!this._isSelectedTab) { + return; + } + + this.setPageContextText(this._rowDataSrc.value.length); + this.lazyLoad(); + }); + + this.rowData$ + .pipe( + takeUntil(this._destroy$), + map((rowData) => this.setPageContextText(rowData.length)), + ) + .subscribe(); + + this._addonService.searchError$.pipe(takeUntil(this._destroy$)).subscribe((error) => { + this.displayError(error); + }); + + this.columnDefs$.next(this.createColumns()); + + this.addonCategories = this.buildCategories(); + this.selectedAddonCategory = this.addonCategories[0]; + } + + public resetCategory(silent = false): void { + if (silent) { + this._selectedAddonCategory = this.addonCategories[0]; + } else { + this.selectedAddonCategory = this.addonCategories[0]; + } + } + + private buildCategories() { + const categoryKeys = getEnumKeys(AddonCategory).filter((key) => key.toLowerCase() !== "unknown"); + const categoryItems: CategoryItem[] = categoryKeys.map((key) => { + return { + category: AddonCategory[key], + localeKey: `COMMON.ADDON_CATEGORIES.${camelToSnakeCase(key).toUpperCase()}`, + }; + }); + + // make sure all addons is always first + const allAddonsCategory = _.remove(categoryItems, (item) => item.category === AddonCategory.AllAddons); + categoryItems.unshift(allAddonsCategory[0]); + + return categoryItems; + } + + public onTableBlur(evt: MouseEvent): void { + const ePath = evt.composedPath() as HTMLElement[]; + const tableElem = ePath.find((tag) => tag.tagName === "AG-GRID-ANGULAR"); + if (tableElem) { + return; + } + + evt.stopPropagation(); + evt.preventDefault(); + this._lastSelectionState = []; + this.gridApi?.deselectAll(); + } + + public onRowClicked(event: RowClickedEvent): void { + const selectedNodes = event.api.getSelectedNodes(); + + if ( + selectedNodes.length === 1 && + this._lastSelectionState.length === 1 && + event.node.data.externalId === this._lastSelectionState[0].data.externalId && + event.node.data.providerName === this._lastSelectionState[0].data.providerName + ) { + event.node.setSelected(false); + this._lastSelectionState = []; + } else { + this._lastSelectionState = [...selectedNodes]; + } + } + + public onRowDoubleClicked(evt: RowDoubleClickedEvent): void { + const defaultChannel = this.defaultAddonChannel; + if (defaultChannel === undefined) { + return; + } + + this.openDetailDialog(evt.data.searchResult as AddonSearchResult, this.defaultAddonChannel); + evt.node.setSelected(true); + } + + public onGridReady(params: GridReadyEvent): void { + this.gridApi = params.api; + this.gridColumnApi = params.columnApi; + } + + public ngOnInit(): void { + this._wowUpService + .getGetAddonsHiddenColumns() + .then((columnStates) => { + const colDefs = [...this.columnDefs$.value]; + this.columnStates.forEach((col) => { + if (!col.allowToggle) { + return; + } + + const state = _.find(columnStates, (cs) => cs.name === col.name); + if (state) { + col.visible = state.visible; + } + + const columnDef = _.find(colDefs, (cd) => cd.field === col.name); + if (columnDef) { + columnDef.hide = !col.visible; + } + }); + + this.columnDefs$.next(colDefs); + }) + .catch((e) => console.error(e)); + } + + public ngOnDestroy(): void { + this._destroy$.next(true); + } + + public onStatusColumnUpdated(): void { + this._cdRef.detectChanges(); + } + + public onHeaderContext = (event: MouseEvent): void => { + event.preventDefault(); + this.updateContextMenuPosition(event); + this.columnContextMenu.menuData = { + columns: this.columnStates.filter((col) => col.allowToggle), + }; + this.columnContextMenu.menu.focusFirstItem("mouse"); + this.columnContextMenu.openMenu(); + }; + + private updateContextMenuPosition(event: MouseEvent) { + this.contextMenuPosition.x = `${event.clientX}px`; + this.contextMenuPosition.y = `${event.clientY}px`; + } + + public async onColumnVisibleChange(event: MatCheckboxChange, column: ColumnState): Promise { + const colState = this.columnStates.find((col) => col.name === column.name); + if (!colState) { + return; + } + + colState.visible = event.checked; + await this._wowUpService.setGetAddonsHiddenColumns([...this.columnStates]); + + this.gridColumnApi.setColumnVisible(column.name, event.checked); + } + + // If nodes have the same primary value, use the canonical name as a fallback + private compareElement(nodeA: IRowNode, nodeB: IRowNode, prop: string): number { + if (nodeA.data[prop] === nodeB.data[prop]) { + if (nodeA.data.canonicalName === nodeB.data.canonicalName) { + return 0; + } + return nodeA.data.canonicalName > nodeB.data.canonicalName ? 1 : -1; + } + + return nodeA.data[prop] > nodeB.data[prop] ? 1 : -1; + } + + private createColumns(): ColDef[] { + const baseColumn = { + headerComponent: TableContextHeaderCellComponent, + headerComponentParams: { + onHeaderContext: this.onHeaderContext, + }, + cellStyle: { + lineHeight: "62px", + display: "flex", + flexDirection: "column", + justifyContent: "center", + }, + suppressMovable: true, + }; + + return [ + { + field: "name", + flex: 2, + headerName: this._translateService.instant("PAGES.GET_ADDONS.TABLE.ADDON_COLUMN_HEADER"), + sortable: true, + cellRenderer: PotentialAddonTableCellComponent, + cellRendererParams: { + channel: this.defaultAddonChannel, + clientType: this.selectedClient, + }, + valueGetter: (params) => { + return params.data.canonicalName; + }, + ...baseColumn, + }, + { + field: "downloadCount", + flex: 1, + sortable: true, + headerName: this._translateService.instant("PAGES.GET_ADDONS.TABLE.DOWNLOAD_COUNT_COLUMN_HEADER"), + valueFormatter: (row) => this.downloadCountPipe.transform(row.data.downloadCount as number), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "downloadCount"), + ...baseColumn, + }, + { + field: "releasedAt", + flex: 1, + sortable: true, + headerName: this._translateService.instant("PAGES.GET_ADDONS.TABLE.RELEASED_AT_COLUMN_HEADER"), + valueFormatter: (row) => this.relativeDurationPipe.transform(row.data.releasedAt as string), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "releasedAt"), + ...baseColumn, + }, + { + field: "author", + flex: 1, + sortable: true, + headerName: this._translateService.instant("PAGES.GET_ADDONS.TABLE.AUTHOR_COLUMN_HEADER"), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "author"), + cellRenderer: CellWrapTextComponent, + ...baseColumn, + }, + { + field: "providerName", + flex: 1, + sortable: true, + headerName: this._translateService.instant("PAGES.GET_ADDONS.TABLE.PROVIDER_COLUMN_HEADER"), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "providerName"), + ...baseColumn, + }, + { + field: "status", + flex: 1, + headerName: this._translateService.instant("PAGES.GET_ADDONS.TABLE.STATUS_COLUMN_HEADER"), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "status"), + cellRenderer: GetAddonStatusColumnComponent, + ...baseColumn, + }, + ]; + } + + private lazyLoad() { + if (this._lazyLoaded) { + return; + } + console.debug("GET ADDON LAZY LOAD"); + + this._lazyLoaded = true; + + this._sessionService.selectedWowInstallation$.pipe(takeUntil(this._destroy$)).subscribe((installation) => { + if (!installation) { + return; + } + + this.query = ""; + + this.selectedInstallation = installation; + this.selectedInstallationId = installation.id; + this.loadPopularAddons(this.selectedInstallation); + }); + + this._addonService.addonRemoved$.pipe(takeUntil(this._destroy$)).subscribe(() => { + this.onRefresh(); + }); + + this._wowUpService.preferenceChange$ + .pipe( + takeUntil(this._destroy$), + filter((change) => change.key.indexOf(DEFAULT_CHANNEL_PREFERENCE_KEY_SUFFIX) !== -1), + ) + .subscribe(() => { + this.onSearch(); + }); + } + + public onInstallFromUrl(): void { + const dialogRef = this._dialog.open(InstallFromUrlDialogComponent); + dialogRef.afterClosed().subscribe(() => { + console.log("The dialog was closed"); + }); + } + + public async onClientChange(): Promise { + await this._sessionService.setSelectedWowInstallation(this.selectedInstallationId); + } + + public onRefresh(): void { + if (!this.selectedInstallation) { + return; + } + + if (this.query) { + this.onSearch(); + } else { + this.loadPopularAddons(this.selectedInstallation); + } + } + + public onClearSearch(): void { + this.query = ""; + this.onSearch(); + } + + public onSearch(): void { + if (!this.selectedInstallation) { + return; + } + + this._sessionService.setEnableControls(false); + this._showTableSrc.next(false); + this.resetCategory(true); + + if (!this.query) { + this.loadPopularAddons(this.selectedInstallation); + return; + } + + from(this._addonService.search(this.query, this.selectedInstallation)) + .pipe( + first(), + map((searchResults) => { + const searchListItems = this.formatAddons(searchResults); + this._rowDataSrc.next(searchListItems); + this._showTableSrc.next(true); + this._sessionService.setEnableControls(true); + }), + catchError((error) => { + console.error(error); + this.displayError(error as Error); + this._rowDataSrc.next([]); + this._showTableSrc.next(true); + this._sessionService.setEnableControls(true); + return of(undefined); + }), + ) + .subscribe(); + } + + public onDoubleClickRow(listItem: GetAddonListItem): void { + this.openDetailDialog(listItem.searchResult, this.defaultAddonChannel); + } + + public onAddonColumnDetailDialog(event: PotentialAddonViewDetailsEvent): void { + this.openDetailDialog(event.searchResult, event.channelType); + } + + public openDetailDialog(searchResult: AddonSearchResult, channelType: AddonChannelType): void { + this._dialogFactory.getPotentialAddonDetailsDialog(searchResult, channelType); + } + + private loadPopularAddons(installation: WowInstallation) { + if (!installation) { + return; + } + + if (this._addonProviderService.getEnabledAddonProviders().length === 0) { + this._rowDataSrc.next([]); + this._sessionService.setEnableControls(true); + this._cdRef.detectChanges(); + return; + } + + this._showTableSrc.next(false); + this._sessionService.setEnableControls(false); + + this._addonService + .getFeaturedAddons(installation) + .pipe( + catchError((error) => { + console.error(`getFeaturedAddons failed`, error); + return of([]); + }), + ) + .subscribe((addons) => { + const listItems = this.formatAddons(addons); + this._rowDataSrc.next(listItems); + this._showTableSrc.next(true); + this._sessionService.setEnableControls(true); + }); + } + + private formatAddons(addons: AddonSearchResult[]): GetAddonListItem[] { + const mapped = addons.map((addon) => { + try { + return new GetAddonListItem(addon, this.defaultAddonChannel); + } catch (e) { + console.error("formatAddons", e); + console.error(addon); + } + }); + + const addonList: GetAddonListItem[] = []; + for (const item of mapped) { + if (item) { + addonList.push(item); + } + } + + return this.sortAddons(addonList); + } + + private sortAddons(addons: GetAddonListItem[]) { + // If sorting by download count, push Hub addons to the top for exposure for now. + return _.orderBy( + addons, + [ + (sr) => (sr.providerName === ADDON_PROVIDER_HUB ? 1 : 0), + (sr) => (sr.providerName === ADDON_PROVIDER_WAGO ? 1 : 0), + "downloadCount", + ], + ["desc", "desc", "desc"], + ); + } + + private setPageContextText(rowCount: number) { + const contextStr: string = + rowCount > 0 + ? this._translateService.instant("PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.SEARCH_RESULTS", { count: rowCount }) + : ""; + + this._sessionService.setContextText(this.tabIndex, contextStr); + } + + private displayError(error: Error) { + if (error instanceof GenericProviderError) { + this._snackbarService.showErrorSnackbar("COMMON.PROVIDER_ERROR", { + localeArgs: { + providerName: error.message, + }, + }); + } else { + this._snackbarService.showErrorSnackbar("PAGES.MY_ADDONS.ERROR_SNACKBAR"); + } + } +} diff --git a/WowUp/wowup-electron/src/app/pages/home/home-routing.module.ts b/WowUp/wowup-electron/src/app/pages/home/home-routing.module.ts new file mode 100644 index 0000000..7ef0bf2 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/home/home-routing.module.ts @@ -0,0 +1,18 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { HomeComponent } from "./home.component"; + +const routes: Routes = [ + { + path: "home", + component: HomeComponent, + }, +]; + +@NgModule({ + declarations: [], + imports: [CommonModule, RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class HomeRoutingModule {} diff --git a/WowUp/wowup-electron/src/app/pages/home/home.component.html b/WowUp/wowup-electron/src/app/pages/home/home.component.html new file mode 100644 index 0000000..fd686f6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/home/home.component.html @@ -0,0 +1,37 @@ +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + +
+
diff --git a/WowUp/wowup-electron/src/app/pages/home/home.component.scss b/WowUp/wowup-electron/src/app/pages/home/home.component.scss new file mode 100644 index 0000000..646de93 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/home/home.component.scss @@ -0,0 +1,36 @@ +@import "../../../custom-theme.scss"; + +.app-logo { + position: absolute; + width: 40px; + z-index: 1; + top: 4px; + left: 0.5em; + + img { + width: 100%; + } +} +.page-container { + overflow: hidden; + height: 100%; + // display: flex; + // flex-direction: column; +} +.tabs { + // flex-grow: 1; + height: 100%; + overflow: hidden; +} +.preload-container { + flex-grow: 1; + display: flex; + flex-direction: column; + overflow: hidden; + justify-content: center; + align-items: center; +} +.tab-bar-placeholder { + height: 48px; + width: 100%; +} diff --git a/WowUp/wowup-electron/src/app/pages/home/home.component.spec.ts b/WowUp/wowup-electron/src/app/pages/home/home.component.spec.ts new file mode 100644 index 0000000..003939e --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/home/home.component.spec.ts @@ -0,0 +1,101 @@ +import { TestBed } from "@angular/core/testing"; +import { AddonService, ScanUpdate, ScanUpdateType } from "../../services/addons/addon.service"; +import { SessionService } from "../../services/session/session.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { ElectronService } from "../../services"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { httpLoaderFactory } from "../../app.module"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { BehaviorSubject, Observable, Subject } from "rxjs"; +import { HomeComponent } from "./home.component"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { AddonScanError, AddonSyncError } from "../../errors"; +import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service"; +import { DialogFactory } from "../../services/dialog/dialog.factory"; +import { AddonUpdateEvent } from "../../models/wowup/addon-update-event"; +import { GalleryModule } from "ng-gallery"; +import { LightboxModule } from "ng-gallery/lightbox"; +import { MatModule } from "../../modules/mat-module"; + +describe("HomeComponent", () => { + let electronService: ElectronService; + let wowUpService: WowUpService; + let sessionService: SessionService; + let addonService: AddonService; + let warcraftInstallationService: WarcraftInstallationService; + let dialogFactory: DialogFactory; + + beforeEach(async () => { + dialogFactory = jasmine.createSpyObj("DialogFactory", [""], {}); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], { + wowInstallations$: new BehaviorSubject([]), + }); + + addonService = jasmine.createSpyObj("AddonService", [""], { + scanUpdate$: new BehaviorSubject({ type: ScanUpdateType.Unknown }).asObservable(), + syncError$: new Subject(), + scanError$: new Subject(), + addonInstalled$: new Subject(), + }); + + electronService = jasmine.createSpyObj("ElectronService", [""], { + isWin: false, + isLinux: true, + isMax: false, + powerMonitor$: new Observable(), + customProtocol$: new Observable(), + }); + + wowUpService = jasmine.createSpyObj("WowUpService", { + checkForAppUpdate: () => Promise.resolve(undefined), + }); + + sessionService = jasmine.createSpyObj("SessionService", [""], {}); + + await TestBed.configureTestingModule({ + declarations: [HomeComponent], + imports: [ + MatModule, + HttpClientModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + GalleryModule, + LightboxModule, + ], + providers: [MatSnackBar], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(HomeComponent, { + set: { + providers: [ + { provide: ElectronService, useValue: electronService }, + { provide: SessionService, useValue: sessionService }, + { provide: AddonService, useValue: addonService }, + { provide: WowUpService, useValue: wowUpService }, + { provide: DialogFactory, useValue: dialogFactory }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + ], + }, + }) + .compileComponents(); + }); + + it("should create", () => { + const fixture = TestBed.createComponent(HomeComponent); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/pages/home/home.component.ts b/WowUp/wowup-electron/src/app/pages/home/home.component.ts new file mode 100644 index 0000000..4e5d774 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/home/home.component.ts @@ -0,0 +1,248 @@ +import { from, of, Subject, Subscription } from "rxjs"; +import { catchError, filter, first, map, switchMap, takeUntil, tap } from "rxjs/operators"; + +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { TranslateService } from "@ngx-translate/core"; + +import { + CURSE_PROTOCOL_NAME, + IPC_POWER_MONITOR_RESUME, + IPC_POWER_MONITOR_UNLOCK, + TAB_INDEX_ABOUT, + TAB_INDEX_GET_ADDONS, + TAB_INDEX_MY_ADDONS, + TAB_INDEX_NEWS, + TAB_INDEX_SETTINGS, +} from "../../../common/constants"; +import { AppConfig } from "../../../environments/environment"; +import { InstallFromProtocolDialogComponent } from "../../components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component"; +import { PatchNotesDialogComponent } from "../../components/common/patch-notes-dialog/patch-notes-dialog.component"; +import { AddonScanError } from "../../errors"; +import { AddonInstallState } from "../../models/wowup/addon-install-state"; +import { AddonUpdateEvent } from "../../models/wowup/addon-update-event"; +import { ElectronService } from "../../services"; +import { AddonService, ScanUpdate, ScanUpdateType } from "../../services/addons/addon.service"; +import { DialogFactory } from "../../services/dialog/dialog.factory"; +import { SessionService } from "../../services/session/session.service"; +import { SnackbarService } from "../../services/snackbar/snackbar.service"; +import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +import { WowUpProtocolService } from "../../services/wowup/wowup-protocol.service"; +import { getProtocol } from "../../utils/string.utils"; +import { WowInstallation } from "wowup-lib-core"; + +@Component({ + selector: "app-home", + templateUrl: "./home.component.html", + styleUrls: ["./home.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HomeComponent implements AfterViewInit, OnDestroy { + private _appUpdateInterval?: number; + private _subscriptions: Subscription[] = []; + private _onDestroy$ = new Subject(); + + public readonly TAB_INDEX_MY_ADDONS = TAB_INDEX_MY_ADDONS; + public readonly TAB_INDEX_GET_ADDONS = TAB_INDEX_GET_ADDONS; + public readonly TAB_INDEX_ABOUT = TAB_INDEX_ABOUT; + public readonly TAB_INDEX_NEWS = TAB_INDEX_NEWS; + public readonly TAB_INDEX_SETTINGS = TAB_INDEX_SETTINGS; + + public hasWowClient = false; + public appReady = false; + public preloadSpinnerKey = "COMMON.PROGRESS_SPINNER.LOADING"; + + public constructor( + public electronService: ElectronService, + public sessionService: SessionService, + private _translateService: TranslateService, + private _addonService: AddonService, + private _wowupService: WowUpService, + private _snackBar: MatSnackBar, + private _snackBarService: SnackbarService, + private _cdRef: ChangeDetectorRef, + private _warcraftInstallationService: WarcraftInstallationService, + private _dialogFactory: DialogFactory, + private _wowUpProtocolService: WowUpProtocolService, + ) { + const wowInstalledSub = this._warcraftInstallationService.wowInstallations$.subscribe((installations) => { + this.hasWowClient = installations.length > 0; + }); + + this.electronService.customProtocol$ + .pipe( + takeUntil(this._onDestroy$), + filter((protocol) => getProtocol(protocol) === CURSE_PROTOCOL_NAME), + tap((protocol) => this.handleAddonInstallProtocol(protocol)), + catchError((e) => { + console.error(e); + return of(undefined); + }), + ) + .subscribe(); + + const scanErrorSub = this._addonService.scanError$.subscribe(this.onAddonScanError); + const addonInstallErrorSub = this._addonService.addonInstalled$.subscribe(this.onAddonInstalledEvent); + + const scanUpdateSub = this._addonService.scanUpdate$ + .pipe(filter((update) => update.type !== ScanUpdateType.Unknown)) + .subscribe(this.onScanUpdate); + + this._subscriptions.push(wowInstalledSub, scanErrorSub, scanUpdateSub, addonInstallErrorSub); + } + + private handleAddonInstallProtocol(protocol: string) { + const dialog = this._dialogFactory.getDialog(InstallFromProtocolDialogComponent, { + disableClose: true, + data: { + protocol, + }, + }); + + return dialog.afterClosed().pipe(first()); + } + + public ngAfterViewInit(): void { + const powerMonitorSub = this.electronService.powerMonitor$.pipe(filter((evt) => !!evt)).subscribe((evt) => { + console.log("Stopping app update check..."); + this.destroyAppUpdateCheck(); + + if (evt === IPC_POWER_MONITOR_RESUME || evt === IPC_POWER_MONITOR_UNLOCK) { + this.initAppUpdateCheck(); + } + }); + + this._warcraftInstallationService.wowInstallations$ + .pipe( + first(), + switchMap((installations) => { + return from(this.migrateAddons(installations)).pipe(map(() => installations)); + }), + map(() => this.showNewVersionNotesPopup()), + ) + .subscribe(() => { + this.appReady = true; + this.detectChanges(); + }); + + this.initAppUpdateCheck(); + + this._subscriptions.push(powerMonitorSub); + } + + public ngOnDestroy(): void { + this._onDestroy$.next(true); + this._onDestroy$.complete(); + window.clearInterval(this._appUpdateInterval); + this._subscriptions.forEach((sub) => sub.unsubscribe()); + } + + private initAppUpdateCheck() { + if (this._appUpdateInterval !== undefined) { + console.warn(`App update interval already exists`); + return; + } + + // check for an app update every so often + this._appUpdateInterval = window.setInterval(() => { + this._wowupService.checkForAppUpdate(); + }, AppConfig.appUpdateIntervalMs); + } + + private destroyAppUpdateCheck() { + window.clearInterval(this._appUpdateInterval); + this._appUpdateInterval = undefined; + } + + private async showNewVersionNotesPopup(): Promise { + const shouldShow = await this._wowupService.shouldShowNewVersionNotes(); + if (!shouldShow) { + return; + } + + await this._dialogFactory.getDialog(PatchNotesDialogComponent).afterClosed().toPromise(); + await this._wowupService.setNewVersionNotes(); + } + + private async migrateAddons(installations: WowInstallation[]) { + const shouldDeepMigrate = await this._wowupService.shouldMigrateAddons(); + if (!installations || installations.length === 0) { + return installations; + } + + if (!shouldDeepMigrate) { + return installations; + } + + this.preloadSpinnerKey = "PAGES.HOME.MIGRATING_ADDONS"; + this.detectChanges(); + + console.log("Migrating addons"); + + try { + for (const installation of installations) { + await this._addonService.migrateDeep(installation); + } + + await this._wowupService.setMigrationVersion(); + } catch (e) { + console.error(`Failed to migrate addons`, e); + } + + return installations; + } + + private detectChanges = () => { + try { + this._cdRef.detectChanges(); + } catch (e) { + console.warn(e); + } + }; + + private onAddonScanError = (error: AddonScanError) => { + const durationMs = 4000; + const errorMessage: string = this._translateService.instant("COMMON.ERRORS.ADDON_SCAN_ERROR", { + providerName: error.providerName, + }); + + this._snackBar.open(errorMessage, undefined, { + duration: durationMs, + panelClass: ["wowup-snackbar", "snackbar-error", "text-1"], + }); + }; + + private onScanUpdate = (update: ScanUpdate) => { + switch (update.type) { + case ScanUpdateType.Start: + this.sessionService.statusText = this._translateService.instant("APP.STATUS_TEXT.ADDON_SCAN_STARTED"); + break; + case ScanUpdateType.Complete: + this.sessionService.statusText = this._translateService.instant("APP.STATUS_TEXT.ADDON_SCAN_COMPLETED"); + window.setTimeout(() => { + this.sessionService.statusText = ""; + }, 3000); + break; + case ScanUpdateType.Update: + this.sessionService.statusText = this._translateService.instant("APP.STATUS_TEXT.ADDON_SCAN_UPDATE", { + count: update.totalCount, + }); + break; + default: + break; + } + }; + + private onAddonInstalledEvent = (evt: AddonUpdateEvent) => { + if (evt.installState !== AddonInstallState.Error) { + return; + } + + this._snackBarService.showErrorSnackbar("COMMON.ERRORS.ADDON_INSTALL_ERROR", { + localeArgs: { + addonName: evt.addon.name, + }, + }); + }; +} diff --git a/WowUp/wowup-electron/src/app/pages/home/home.module.ts b/WowUp/wowup-electron/src/app/pages/home/home.module.ts new file mode 100644 index 0000000..ae573e3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/home/home.module.ts @@ -0,0 +1,47 @@ +import { AgGridModule } from "ag-grid-angular"; + +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { TranslateModule } from "@ngx-translate/core"; + +import { NewsPanelComponent } from "../../components/news-panel/news-panel.component"; +import { DirectiveModule } from "../../modules/directive.module"; +import { MatModule } from "../../modules/mat-module"; +import { AddonsModule } from "../../modules/addons.module"; +import { CommonUiModule } from "../../modules/common-ui.module"; +import { OptionsModule } from "../../modules/options.module"; +import { PipesModule } from "../../modules/pipes.module"; +import { AccountPageComponent } from "../account-page/account-page.component"; +import { GetAddonsComponent } from "../get-addons/get-addons.component"; +import { MyAddonsComponent } from "../my-addons/my-addons.component"; +import { OptionsComponent } from "../options/options.component"; +import { HomeRoutingModule } from "./home-routing.module"; +import { HomeComponent } from "./home.component"; + +@NgModule({ + declarations: [ + HomeComponent, + MyAddonsComponent, + GetAddonsComponent, + OptionsComponent, + AccountPageComponent, + NewsPanelComponent, + ], + imports: [ + CommonModule, + CommonUiModule, + PipesModule, + AddonsModule, + HomeRoutingModule, + MatModule, + DirectiveModule, + ReactiveFormsModule, + TranslateModule, + FormsModule, + OptionsModule, + AgGridModule, + ], + providers: [], +}) +export class HomeModule {} diff --git a/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.html b/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.html new file mode 100644 index 0000000..18d9c41 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.html @@ -0,0 +1,331 @@ +
+
+
+ +
+
+
+ + {{ "PAGES.MY_ADDONS.FILTER_LABEL" | translate }} + + + +
+
+ +
+
+ +
+ +
+ +
+
+ + + + + + + + +
+
+
+ +
+ +
+ +
+

{{ "COMMON.SEARCH.NO_ADDONS" | translate }}

+
+ + + +
+ + +
+ + +
+
+ +
+
+
+ {{ listItem.thumbnailLetter }} +
+
+
+
{{ listItem.addon.name }}
+
+
{{ listItem.addon.installedVersion }}
+
+
+
+ + +
+ + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.IGNORE_ADDON_BUTTON" | translate }} + +
+ +
+ + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.AUTO_UPDATE_ADDON_BUTTON" | translate }} + +
+ +
+ + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON" | translate }} + +
+ + + + + + + + + + {{ "DIALOGS.ADDON_DETAILS.VIEW_ON_PROVIDER_PREFIX" | translate }} {{ listItem.addon.providerName }} + + + + + + + + + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.STABLE_ADDON_CHANNEL" | translate }} + + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.BETA_ADDON_CHANNEL" | translate }} + + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.ALPHA_ADDON_CHANNEL" | translate }} + + + + +
+ +
+
+ + + + {{ provider.providerName }} + + + +
+
+ + + + + + + + + +
+ + +
+ PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.ADDONS_SELECTED +
+ + + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.IGNORE_ADDON_BUTTON" | translate }} + + + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.AUTO_UPDATE_ADDON_BUTTON" | translate }} + + + + + + + + + + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.STABLE_ADDON_CHANNEL" | translate }} + + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.BETA_ADDON_CHANNEL" | translate }} + + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.ALPHA_ADDON_CHANNEL" | translate }} + + + +
+
+ + +
+ + +
+
+ {{ "PAGES.MY_ADDONS.COLUMNS_CONTEXT_MENU.TITLE" | translate }} +
+
+ + + {{ column.display | translate }} + +
+
diff --git a/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.scss b/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.scss new file mode 100644 index 0000000..94da70b --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.scss @@ -0,0 +1,119 @@ +.wu-ag-table { + width: 100%; + height: calc(100% - 86px); +} + +.control-container { + display: flex; + flex-direction: row; + padding: 0 1em 0 1em; + margin: 1em 1em 0 1em; + // border-radius: 4px; + + .select-container { + flex-grow: 1; + } + + .right-container { + display: flex; + flex-direction: row; + + .filter-container { + } + + .button-container { + display: flex; + flex-direction: row; + align-items: center; + margin-left: 1em; + + button { + &:not(:last-child) { + margin-right: 0.5em; + } + } + } + } +} + +.switch-button { + padding-left: 0; +} + +.menu-button { + overflow: hidden; + margin-right: 0 !important; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.menu-button-divider { + width: 1px; + height: 36px; +} +.chip { + margin-right: 0.5em; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + width: auto; + min-width: auto; + + .mat-icon { + width: 12px; + height: 1rem; + // padding-bottom: 3px; + } +} + +.no-addons-container { + height: calc(100% - 86px); + display: flex; + color: var(--text-1); + justify-content: center; + flex-direction: column; + align-items: center; +} +.spinner-container { + height: calc(100% - 86px); + display: flex; + align-items: center; + justify-content: center; +} + +.table-container { + height: 100%; + overflow: auto; + overflow-x: hidden; + + table { + width: 100%; + } + + .cell-padding { + padding-right: 1em; + } +} + +.author-column { + width: 100px; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-align: left; +} +.provider-column { + min-width: 70px; +} + +.column-menu { + padding: 0.5em; + color: var(--text-1); + + p { + margin-bottom: 0.5em; + } +} + +.open-in-browser-icon { + transform: scale(0.5); +} diff --git a/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.spec.ts b/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.spec.ts new file mode 100644 index 0000000..41918e4 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.spec.ts @@ -0,0 +1,169 @@ +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { BehaviorSubject, Subject } from "rxjs"; + +import { OverlayModule } from "@angular/cdk/overlay"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { CUSTOM_ELEMENTS_SCHEMA, ElementRef } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatDialog } from "@angular/material/dialog"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; + +import { httpLoaderFactory } from "../../app.module"; +import { AddonUpdateEvent } from "../../models/wowup/addon-update-event"; +import { SortOrder } from "../../models/wowup/sort-order"; +import { ElectronService } from "../../services"; +import { AddonService } from "../../services/addons/addon.service"; +import { SessionService } from "../../services/session/session.service"; +import { WarcraftService } from "../../services/warcraft/warcraft.service"; +import { WowUpAddonService } from "../../services/wowup/wowup-addon.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +import { MyAddonsComponent } from "./my-addons.component"; +import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service"; +import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe"; +import { PushService } from "../../services/push/push.service"; +import { InvertBoolPipe } from "../../pipes/inverse-bool.pipe"; +import { MatModule } from "../../modules/mat-module"; +import { AddonUiService } from "../../services/addons/addon-ui.service"; +import { AddonProviderFactory } from "../../services/addons/addon.provider.factory"; +import { WowClientType } from "wowup-lib-core"; + +export class MockElementRef extends ElementRef { + public constructor() { + super(null); + } +} + +export function mockElementFactory(): ElementRef { + return new ElementRef({ nativeElement: jasmine.createSpyObj("nativeElement", ["value"]) }); +} + +describe("MyAddonsComponent", () => { + let component: MyAddonsComponent; + let fixture: ComponentFixture; + let electronServiceSpy: any; + let wowUpServiceSpy: any; + let wowUpAddonServiceSpy: any; + let sessionServiceSpy: any; + let addonServiceSpy: any; + let warcraftServiceSpy: any; + let warcraftInstallationService: WarcraftInstallationService; + let pushService: PushService; + let addonUiService: AddonUiService; + let addonProviderService: any; + + beforeEach(async () => { + addonUiService = jasmine.createSpyObj("AddonUiService", [""], {}); + addonProviderService = jasmine.createSpyObj("AddonProviderFactory", [""], {}); + + wowUpAddonServiceSpy = jasmine.createSpyObj("WowUpAddonService", ["updateForClientType"], { + persistUpdateInformationToWowUpAddon: () => {}, + }); + addonServiceSpy = jasmine.createSpyObj( + "AddonService", + { + getAddons: Promise.resolve([]), + backfillAddons: Promise.resolve(undefined), + }, + { + addonInstalled$: new Subject().asObservable(), + addonRemoved$: new Subject().asObservable(), + } + ); + + pushService = jasmine.createSpyObj("PushService", [""], { + addonUpdate$: new Subject(), + }); + + const testSortOrder: SortOrder = { + colId: "name", + sort: "asc", + }; + wowUpServiceSpy = jasmine.createSpyObj("WowUpService", [""], { + myAddonsSortOrder: testSortOrder, + getMyAddonsHiddenColumns: () => Promise.resolve([]), + }); + sessionServiceSpy = jasmine.createSpyObj("SessionService", ["getSelectedHomeTab"], { + selectedHomeTab$: new BehaviorSubject(0).asObservable(), + autoUpdateComplete$: new BehaviorSubject(0).asObservable(), + selectedClientType$: new BehaviorSubject(WowClientType.Retail).asObservable(), + targetFileInstallComplete$: new Subject(), + addonsChanged$: new BehaviorSubject([]), + selectedWowInstallation$: new BehaviorSubject({}), + rescanComplete$: new BehaviorSubject(0).asObservable(), + setEnableControls: () => {} + }); + warcraftServiceSpy = jasmine.createSpyObj("WarcraftService", [""], { + installedClientTypesSelectItems$: new BehaviorSubject(undefined).asObservable(), + }); + electronServiceSpy = jasmine.createSpyObj("ElectronService", [""], { + isWin: false, + isLinux: true, + isMac: false, + }); + + warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], { + wowInstallations$: new BehaviorSubject([]), + }); + + const testBed = TestBed.configureTestingModule({ + declarations: [MyAddonsComponent, InvertBoolPipe], + imports: [ + MatModule, + OverlayModule, + HttpClientModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + providers: [MatDialog, RelativeDurationPipe], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).overrideComponent(MyAddonsComponent, { + set: { + providers: [ + { provide: AddonService, useValue: addonServiceSpy }, + { provide: WowUpService, useValue: wowUpServiceSpy }, + { provide: WowUpAddonService, useValue: wowUpAddonServiceSpy }, + { provide: ElectronService, useValue: electronServiceSpy }, + { provide: SessionService, useValue: sessionServiceSpy }, + { provide: WarcraftService, useValue: warcraftServiceSpy }, + { provide: WarcraftInstallationService, useValue: warcraftInstallationService }, + { provide: PushService, useValue: pushService }, + { provide: AddonUiService, useValue: addonUiService }, + { provide: AddonProviderFactory, useValue: addonProviderService }, + ], + }, + }); + + await testBed.compileComponents(); + + fixture = TestBed.createComponent(MyAddonsComponent); + component = fixture.componentInstance; + + component.addonFilter = { + nativeElement: jasmine.createSpyObj("nativeElement", ["value"]), + }; + console.debug("addonFilter", component.addonFilter); + + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.debugElement.nativeElement.remove(); + fixture.destroy(); + }); + + it("should create", () => { + console.debug("addonFilter", component.addonFilter); + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.ts b/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.ts new file mode 100644 index 0000000..bcfce78 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/my-addons/my-addons.component.ts @@ -0,0 +1,1493 @@ +import { + CellContextMenuEvent, + ColDef, + ColumnApi, + GridApi, + GridReadyEvent, + IRowNode, + RowClassParams, + RowClickedEvent, + RowDoubleClickedEvent, + SortChangedEvent, +} from "ag-grid-community"; +import * as _ from "lodash"; +import { join } from "path"; +import { BehaviorSubject, combineLatest, from, fromEvent, Observable, of, Subject, Subscription, zip } from "rxjs"; +import { + catchError, + debounceTime, + distinctUntilChanged, + filter, + first, + map, + switchMap, + takeUntil, + tap, +} from "rxjs/operators"; + +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + NgZone, + OnDestroy, + OnInit, + ViewChild, +} from "@angular/core"; +import { MatCheckboxChange } from "@angular/material/checkbox"; +import { MatDialog } from "@angular/material/dialog"; +import { MatMenuTrigger } from "@angular/material/menu"; +import { MatRadioChange } from "@angular/material/radio"; +import { TranslateService } from "@ngx-translate/core"; + +import { AddonViewModel } from "../../business-objects/addon-view-model"; +import { CellWrapTextComponent } from "../../components/common/cell-wrap-text/cell-wrap-text.component"; +import { ConfirmDialogComponent } from "../../components/common/confirm-dialog/confirm-dialog.component"; +import { DateTooltipCellComponent } from "../../components/addons/date-tooltip-cell/date-tooltip-cell.component"; +import { MyAddonStatusCellComponent } from "../../components/addons/my-addon-status-cell/my-addon-status-cell.component"; +import { MyAddonsAddonCellComponent } from "../../components/addons/my-addons-addon-cell/my-addons-addon-cell.component"; +import { TableContextHeaderCellComponent } from "../../components/addons/table-context-header-cell/table-context-header-cell.component"; +import { AddonInstallState } from "../../models/wowup/addon-install-state"; +import { AddonUpdateEvent } from "../../models/wowup/addon-update-event"; +import { ColumnState } from "../../models/wowup/column-state"; +import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe"; +import { ElectronService } from "../../services"; +import { AddonService } from "../../services/addons/addon.service"; +import { DialogFactory } from "../../services/dialog/dialog.factory"; +import { SessionService } from "../../services/session/session.service"; +import { SnackbarService } from "../../services/snackbar/snackbar.service"; +import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service"; +import { WarcraftService } from "../../services/warcraft/warcraft.service"; +import { WowUpAddonService } from "../../services/wowup/wowup-addon.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +import * as AddonUtils from "../../utils/addon.utils"; +import { stringIncludes } from "../../utils/string.utils"; +import { SortOrder } from "../../models/wowup/sort-order"; +import { PushService } from "../../services/push/push.service"; +import { AddonUiService } from "../../services/addons/addon-ui.service"; +import { AddonManageDialogComponent } from "../../components/addons/addon-manage-dialog/addon-manage-dialog.component"; +import { WtfBackupComponent } from "../../components/addons/wtf-backup/wtf-backup.component"; +import { HasEventTargetAddRemove } from "rxjs/internal/observable/fromEvent"; +import { AddonProviderFactory } from "../../services/addons/addon.provider.factory"; +import { toInterfaceVersion } from "../../utils/addon.utils"; +import { Addon, WowClientType } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +@Component({ + selector: "app-my-addons", + templateUrl: "./my-addons.component.html", + styleUrls: ["./my-addons.component.scss"], +}) +export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit { + @Input("tabIndex") public set tabIndex(value: number) { + this._tabIndexSrc.next(value); + } + + @ViewChild("addonContextMenuTrigger", { static: false }) public contextMenu!: MatMenuTrigger; + @ViewChild("addonMultiContextMenuTrigger", { static: false }) public multiContextMenu!: MatMenuTrigger; + @ViewChild("columnContextMenuTrigger", { static: false }) public columnContextMenu!: MatMenuTrigger; + @ViewChild("addonFilter") public addonFilter: ElementRef; + + // @HostListener("window:keydown", ["$event"]) + private readonly _operationErrorSrc = new Subject(); + private readonly _isBusySrc = new BehaviorSubject(true); + private readonly _tabIndexSrc = new BehaviorSubject(undefined); + private readonly _baseRowDataSrc = new BehaviorSubject([]); + private readonly _filterInputSrc = new BehaviorSubject(""); + private readonly _spinnerMessageSrc = new BehaviorSubject(""); + private readonly _totalAvailableUpdateSrc = new BehaviorSubject(0); + private readonly _destroy$ = new Subject(); + private readonly _visibleSrc = new BehaviorSubject(false); + + public readonly visible$ = this._visibleSrc.pipe(debounceTime(200)); + public readonly totalAvailableUpdateCt$ = this._totalAvailableUpdateSrc.asObservable(); + public readonly enableControls$ = this._sessionService.enableControls$; + public readonly spinnerMessage$ = this._spinnerMessageSrc.asObservable(); + + public readonly selectedWowInstallation$ = this._sessionService.selectedWowInstallation$; + + public readonly selectedWowInstallationLabel$ = this._sessionService.selectedWowInstallation$.pipe( + map((wowInstall) => wowInstall?.displayName ?? ""), + ); + + public readonly selectedWowInstallationId$ = this._sessionService.selectedWowInstallation$.pipe( + map((wowInstall) => wowInstall?.id), + ); + + public readonly hasSelectedWowInstallationId$ = this._sessionService.selectedWowInstallation$.pipe( + map((wowInstall) => wowInstall !== undefined), + ); + + public readonly isBusy$ = combineLatest([this._isBusySrc, this._addonService.syncing$]).pipe( + map((vals) => _.some(vals)), + ); + + public readonly filterInput$ = this._filterInputSrc.asObservable(); + + public readonly rowData$ = combineLatest([this._baseRowDataSrc, this.filterInput$]).pipe( + debounceTime(0), + map(([rowData, filterVal]) => { + return this.filterAddons(rowData, filterVal); + }), + ); + + public readonly hasData$ = this.rowData$.pipe(map((data) => data.length > 0)); + public readonly enableUpdateAll$ = combineLatest([this.enableControls$, this.rowData$]).pipe( + map(([enableControls, rowData]) => { + return enableControls && rowData.some((row) => AddonUtils.needsUpdate(row.addon)); + }), + ); + + public readonly enableUpdateExtra$ = combineLatest([ + this.enableControls$, + this._addonService.anyUpdatesAvailable$, + ]).pipe( + map(([enableControls, updatesAvailable]) => { + return enableControls && updatesAvailable; + }), + ); + + public readonly hideGrid$ = combineLatest([this.isBusy$, this.hasData$]).pipe( + map(([isBusy, hasData]) => { + return isBusy || !hasData; + }), + ); + + public readonly showNoAddons$ = combineLatest([this.isBusy$, this.hasData$]).pipe( + map(([isBusy, hasData]) => { + return !isBusy && !hasData; + }), + ); + + public readonly isSelectedTab$ = combineLatest([this._sessionService.selectedHomeTab$, this._tabIndexSrc]).pipe( + map(([selectedTab, ownTab]) => { + return selectedTab !== undefined && ownTab !== undefined && selectedTab === ownTab; + }), + ); + + public readonly operationError$ = this._operationErrorSrc.asObservable(); + + private _subscriptions: Subscription[] = []; + private _lazyLoaded = false; + private _isRefreshing = false; + private _lastSelectionState: IRowNode[] = []; + + public updateAllContextMenu!: MatMenuTrigger; + public contextMenuPosition = { x: "0px", y: "0px" }; + public overlayNoRowsTemplate = ""; + public addonUtils = AddonUtils; + public overlayRef: OverlayRef | null = null; + + public wowInstallations$: Observable; + + // Grid + public rowDataG: any[] = []; + public columnDefs$ = new BehaviorSubject([]); + public gridApi!: GridApi; + public gridColumnApi!: ColumnApi; + public rowClassRules = { + ignored: (params: RowClassParams): boolean => { + return params.data.addon.isIgnored === true; + }, + }; + + public columns: ColumnState[] = [ + { + name: "name", + display: "PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER", + visible: true, + }, + { + name: "sortOrder", + display: "PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER", + visible: true, + }, + { + name: "installedAt", + display: "PAGES.MY_ADDONS.TABLE.UPDATED_AT_COLUMN_HEADER", + visible: true, + allowToggle: true, + }, + { + name: "latestVersion", + display: "PAGES.MY_ADDONS.TABLE.LATEST_VERSION_COLUMN_HEADER", + visible: true, + allowToggle: true, + }, + { + name: "releasedAt", + display: "PAGES.MY_ADDONS.TABLE.RELEASED_AT_COLUMN_HEADER", + visible: true, + allowToggle: true, + }, + { + name: "gameVersion", + display: "PAGES.MY_ADDONS.TABLE.GAME_VERSION_COLUMN_HEADER", + visible: true, + allowToggle: true, + }, + { + name: "externalChannel", + display: "PAGES.MY_ADDONS.TABLE.PROVIDER_RELEASE_CHANNEL", + visible: false, + allowToggle: true, + }, + { + name: "providerName", + display: "PAGES.MY_ADDONS.TABLE.PROVIDER_COLUMN_HEADER", + visible: true, + allowToggle: true, + }, + { + name: "author", + display: "PAGES.MY_ADDONS.TABLE.AUTHOR_COLUMN_HEADER", + visible: true, + allowToggle: true, + }, + ]; + + private _tabObserver: MutationObserver; + + public constructor( + private _addonService: AddonService, + private _addonProviderService: AddonProviderFactory, + private _addonUiService: AddonUiService, + private _sessionService: SessionService, + private _dialog: MatDialog, + private _dialogFactory: DialogFactory, + private _cdRef: ChangeDetectorRef, + private _wowUpAddonService: WowUpAddonService, + private _translateService: TranslateService, + private _snackbarService: SnackbarService, + private _pushService: PushService, + private _ngZone: NgZone, + public electronService: ElectronService, + public overlay: Overlay, + public warcraftService: WarcraftService, + public wowUpService: WowUpService, + public warcraftInstallationService: WarcraftInstallationService, + public relativeDurationPipe: RelativeDurationPipe, + ) { + this.overlayNoRowsTemplate = `${ + _translateService.instant("COMMON.SEARCH.NO_ADDONS") as string + }`; + + this.wowInstallations$ = combineLatest([ + warcraftInstallationService.wowInstallations$, + _addonService.anyUpdatesAvailable$, + ]).pipe(switchMap(([installations]) => from(this.mapInstallations(installations)))); + + this._sessionService.rescanComplete$ + .pipe( + takeUntil(this._destroy$), + switchMap(() => from(this.onRefresh())), + catchError((err) => { + console.error(err); + return of(undefined); + }), + ) + .subscribe(); + + const addonInstalledSub = this._addonService.addonInstalled$ + .pipe( + map((evt) => this.onAddonInstalledEvent(evt)), + map(() => this.setPageContextText()), + ) + .subscribe(); + + const addonRemovedSub = this._addonService.addonRemoved$ + .pipe( + map((evt) => this.onAddonRemoved(evt)), + map(() => this.setPageContextText()), + ) + .subscribe(); + + // If an update push comes in, check if we have any addons installed with any of the ids, if so refresh + const pushUpdateSub = this._pushService.addonUpdate$ + .pipe( + debounceTime(5000), + switchMap((addons) => { + const addonIds = addons.map((addon) => addon.addonId); + return from(this._addonService.hasAnyWithExternalAddonIds(addonIds)); + }), + filter((hasAny) => hasAny), + switchMap(() => from(this.onRefresh())), + ) + .subscribe(); + + // When the tab becomes visible, auto size the columns. Column sizes could get weird if resized when not visible + this.visible$ + .pipe( + takeUntil(this._destroy$), + filter((visible) => visible === true), + ) + .subscribe(() => this.autoSizeColumns()); + + this._subscriptions.push( + this.isSelectedTab$ + .pipe( + filter((isSelected) => isSelected === true), + switchMap(this.onSelectedTabChange), + ) + .subscribe(), + this._sessionService.addonsChanged$.pipe(switchMap(() => from(this.onRefresh()))).subscribe(), + this._sessionService.targetFileInstallComplete$.pipe(switchMap(() => from(this.onRefresh()))).subscribe(), + pushUpdateSub, + addonInstalledSub, + addonRemovedSub, + ); + + this.columnDefs$.next(this.createColumns()); + } + + public ngOnInit(): void { + this._subscriptions.push( + this.operationError$.subscribe({ + next: () => { + this._snackbarService.showErrorSnackbar("PAGES.MY_ADDONS.ERROR_SNACKBAR"); + }, + }), + ); + + this.wowUpService + .getMyAddonsHiddenColumns() + .then((columnStates) => { + const colDefs = [...this.columnDefs$.value]; + + this.columns.forEach((col) => { + if (!col.allowToggle) { + return; + } + + const state = _.find(columnStates, (cs) => cs.name === col.name); + if (state) { + col.visible = state.visible; + } + + const columnDef = _.find(colDefs, (cd) => cd.field === col.name); + if (columnDef) { + columnDef.hide = !col.visible; + } + }); + + this.columnDefs$.next(colDefs); + this._sessionService.myAddonsCompactVersion = !this.getLatestVersionColumnVisible(); + }) + .catch((e) => console.error(e)); + } + + public ngOnDestroy(): void { + this._tabObserver.disconnect(); + this._destroy$.next(true); + this._destroy$.complete(); + this._subscriptions.forEach((sub) => sub.unsubscribe()); + } + + public onClickClearFilter(): void { + this.addonFilter.nativeElement.value = ""; + this._filterInputSrc.next(""); + } + + public handleKeyboardEvent(event: KeyboardEvent): void { + if (this.selectAllRows(event)) { + return; + } + } + + public async onSortChanged(evt: SortChangedEvent): Promise { + const columnState = evt.columnApi.getColumnState(); + console.debug("columnState", columnState); + const minimalState = columnState.map((column) => { + const sortOrder: SortOrder = { + colId: column.colId ?? "", + sort: column.sort ?? null, + }; + return sortOrder; + }); + + try { + await this.wowUpService.setMyAddonsSortOrder(minimalState); + } catch (e) { + console.error(e); + } + } + + public onFirstDataRendered(): void { + this.autoSizeColumns(); + } + + public onGridReady(params: GridReadyEvent): void { + this.gridApi = params.api; + this.gridColumnApi = params.columnApi; + + // Set initial sort order + this.gridColumnApi.applyColumnState({ + state: [ + { + colId: "sortOrder", + sort: "asc", + }, + ], + defaultState: { sort: null }, + }); + + this.loadSortOrder().catch((e) => console.error(e)); + + this.rowData$ + .pipe( + tap((data) => { + this.gridApi.setRowData(data); + this.setPageContextText(); + }), + debounceTime(200), + tap(() => { + this.autoSizeColumns(); + }), + ) + .subscribe(); + } + + public ngAfterViewInit(): void { + this._tabObserver = new MutationObserver((mutations: MutationRecord[]) => { + const muts = mutations.filter((m) => m.attributeName === "style"); + this._visibleSrc.next(muts.some((m) => (m.target as HTMLElement).style.transform === "none")); + }); + + const elem = document.querySelector(".mat-tab-my-addons .mat-mdc-tab-body-content"); + if (elem === null) { + this._visibleSrc.next(true); + console.warn("visibility observer cannot start"); + } else { + this._tabObserver.observe(elem, { + attributes: true, + characterData: true, + }); + } + + this._sessionService.myAddonsCompactVersion = !this.getLatestVersionColumnVisible(); + + if (this.addonFilter?.nativeElement !== undefined) { + const addonFilterSub = fromEvent(this.addonFilter.nativeElement as HasEventTargetAddRemove, "keyup") + .pipe( + filter(Boolean), + debounceTime(200), + distinctUntilChanged(), + tap(() => { + const val: string = this.addonFilter.nativeElement.value.toString(); + console.debug(val); + this._filterInputSrc.next(val); + }), + ) + .subscribe(); + + this._subscriptions.push(addonFilterSub); + } + + this._sessionService.autoUpdateComplete$ + .pipe( + tap(() => console.log("Checking for addon updates...")), + switchMap(() => from(this.loadAddons())), + ) + .subscribe(() => { + this._cdRef.markForCheck(); + }); + } + + public onSelectedTabChange = (): Observable => { + this.setPageContextText(); + + return from(this.lazyLoad()).pipe( + first(), + catchError((e) => { + console.error(e); + return of(undefined); + }), + ); + }; + + // Get the translated value of the provider name (unknown) + // If the key is returned there's nothing to translate return the normal name + public getProviderName(providerName: string): string { + const key = `APP.PROVIDERS.${providerName.toUpperCase()}`; + const tx = this._translateService.instant(key); + return tx === key ? providerName : tx; + } + + public isLatestUpdateColumnVisible(): boolean { + return this.columns.find((column) => column.name === "addon.latestVersion")?.visible ?? false; + } + + public onRefresh = async (): Promise => { + if (this._isRefreshing) { + return; + } + + this._isRefreshing = true; + this._isBusySrc.next(true); + this._sessionService.setEnableControls(false); + + try { + console.debug("onRefresh"); + await this._addonService.syncAllClients(); + + const selectedWowInstall = this._sessionService.getSelectedWowInstallation(); + if (selectedWowInstall !== undefined) { + await this._wowUpAddonService.updateForInstallation(selectedWowInstall); + } + + await this.loadAddons(); + await this.updateBadgeCount(); + } catch (e) { + console.error(`Failed to refresh addons`, e); + } finally { + this._isBusySrc.next(false); + + this._sessionService.setEnableControls(true); + + this._isRefreshing = false; + } + }; + + // See: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code + public selectAllRows(event: KeyboardEvent): boolean { + if (!(event.ctrlKey || event.metaKey) || event.code !== "KeyA") { + return false; + } + + event.preventDefault(); + + this.gridApi.selectAll(); + + return true; + } + + public canSetAutoUpdate(listItem: AddonViewModel): boolean { + if (!listItem.addon) { + return false; + } + + return listItem.addon.isIgnored === false && listItem.addon.warningType === undefined; + } + + public async canSetAutoUpdateNotifications(listItem: AddonViewModel): Promise { + if (!listItem.addon) { + return false; + } + + const enableSystemNotifications = await this.wowUpService.getEnableSystemNotifications(); + if (enableSystemNotifications === false) { + return false; + } + + return ( + listItem.addon.isIgnored === false && listItem.addon.warningType === undefined && listItem.addon.autoUpdateEnabled + ); + } + + public isForceIgnore(addon: Addon) { + return this._addonProviderService.isForceIgnore(addon.providerName ?? ""); + } + + public canReInstall(listItem: AddonViewModel): boolean { + if (!listItem.addon) { + return false; + } + + return ( + listItem.addon.warningType === undefined && + this._addonProviderService.canReinstall(listItem.addon.providerName ?? "") + ); + } + + public canChangeChannel(addon: Addon) { + return this._addonProviderService.canChangeChannel(addon.providerName ?? ""); + } + + public filterAddons(rowData: AddonViewModel[], filterVal: string): AddonViewModel[] { + if (filterVal.length === 0) { + return rowData; + } + + const filter = filterVal.trim().toLowerCase(); + return rowData.filter((row) => this.filterListItem(row, filter)); + } + + // Handle when the user clicks the update all button + public async onUpdateAll(): Promise { + const selectedWowInstall = this._sessionService.getSelectedWowInstallation(); + if (selectedWowInstall === undefined) { + return; + } + + this._sessionService.setEnableControls(false); + + const addons = await this._addonService.getAddons(selectedWowInstall, false); + try { + const filteredAddons = _.filter(addons, (addon) => AddonUtils.needsUpdate(addon)); + + const promises = _.map(filteredAddons, async (addon) => { + try { + await this._addonService.updateAddon(addon); + } catch (e) { + console.error("Failed to install", e); + } + }); + + await Promise.all(promises); + } catch (err) { + console.error(err); + } + + this._sessionService.setEnableControls(this.calculateControlState()); + } + + // Handle when the user clicks the update all retail/classic button + public async onUpdateAllRetailClassic(): Promise { + let installations = await this.warcraftInstallationService.getWowInstallationsAsync(); + installations = installations.filter( + (installation) => + installation.clientType === WowClientType.Retail || installation.clientType === WowClientType.ClassicEra, + ); + this.updateAllWithSpinner(...installations).catch((e) => console.error(e)); + } + + // Handle when the user clicks update all clients button + public async onUpdateAllClients(): Promise { + const installations = await this.warcraftInstallationService.getWowInstallationsAsync(); + this.updateAllWithSpinner(...installations).catch((e) => console.error(e)); + } + + public onHeaderContext = (event: MouseEvent): void => { + event.preventDefault(); + this.updateContextMenuPosition(event); + this.columnContextMenu.menuData = { + columns: this.columns.filter((col) => col.allowToggle), + }; + this.columnContextMenu.menu.focusFirstItem("mouse"); + this.columnContextMenu.openMenu(); + }; + + public onCellContext(evt: CellContextMenuEvent): void { + evt?.event?.preventDefault(); + this.updateContextMenuPosition(evt.event); + + const selectedRows = this.gridApi.getSelectedRows(); + // const selectedItems = this._dataSubject.value.filter((item) => item.selected); + if (selectedRows.length > 1) { + this.multiContextMenu.menuData = { listItems: selectedRows }; + this.multiContextMenu.menu.focusFirstItem("mouse"); + this.multiContextMenu.openMenu(); + } else { + this.contextMenu.menuData = { listItem: evt.data }; + this.contextMenu.menu.focusFirstItem("mouse"); + this.contextMenu.openMenu(); + } + } + + public closeContextMenu(): void { + this.contextMenu.closeMenu(); + } + + public onUpdateAllContext(event: MouseEvent): void { + event.preventDefault(); + this.updateContextMenuPosition(event); + this.updateAllContextMenu.openMenu(); + } + + public onReInstallAddon(listItems: AddonViewModel): void { + this.onReInstallAddons([listItems]).catch((e) => console.error(e)); + } + + public async onReInstallAddons(listItems: AddonViewModel[]): Promise { + try { + console.debug("onReInstallAddons", listItems); + const addons = _.map(listItems, (li) => li.addon).filter((li): li is Addon => li !== undefined); + const tasks = _.map(addons, (addon) => this._addonService.installAddon(addon)); + await Promise.all(tasks); + } catch (e) { + console.error(`Failed to re-install addons`, e); + } + } + + public async onShowFolder(addon: Addon, folder: string): Promise { + try { + const addonPath = this._addonService.getInstallBasePath(addon); + const folderPath = join(addonPath, folder); + await this.electronService.openPath(folderPath); + } catch (err) { + console.error(err); + } + } + + public async onColumnVisibleChange(event: MatCheckboxChange, column: ColumnState): Promise { + const colState = this.columns.find((col) => col.name === column.name); + if (!colState) { + console.warn(`Column state not found: ${column.name}`); + return; + } + + colState.visible = event.checked; + + await this.wowUpService.setMyAddonsHiddenColumns([...this.columns]); + + this.gridColumnApi.setColumnVisible(column.name, event.checked); + + if (column.name === "latestVersion") { + this._sessionService.myAddonsCompactVersion = !event.checked; + } + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + public getRowNodeId = (data: any) => { + return data.data.addon.id; + }; + + public onClickCreateBackup(): void { + const dialogRef = this._dialogFactory.getDialog(WtfBackupComponent, { + disableClose: true, + data: {}, + }); + + dialogRef.afterClosed().pipe(first()).subscribe(); + } + + public onClickImportExport(): void { + const dialogRef = this._dialogFactory.getDialog(AddonManageDialogComponent, { + disableClose: true, + data: {}, + }); + + dialogRef.afterClosed().pipe(first()).subscribe(); + } + + public onReScan(): void { + const title: string = this._translateService.instant("PAGES.MY_ADDONS.RESCAN_FOLDERS_CONFIRMATION_TITLE"); + const message: string = this._translateService.instant("PAGES.MY_ADDONS.RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION"); + const dialogRef = this._dialogFactory.getConfirmDialog(title, message); + + dialogRef + .afterClosed() + .pipe( + first(), + switchMap((result) => { + if (!result) { + return of(undefined); + } + + console.log("Performing re-scan"); + return from(this.loadAddons(true)).pipe(switchMap(() => from(this.onRefresh()))); + }), + ) + .subscribe(); + } + + public async onClientChange(evt: any): Promise { + const val: string = evt.value.toString(); + await this._sessionService.setSelectedWowInstallation(val); + } + + public onRemoveAddon(addon: Addon): void { + this._addonUiService + .handleRemoveAddon(addon) + .pipe( + first(), + switchMap((result) => { + return result.dependenciesRemoved ? this.loadAddons() : of(undefined); + }), + ) + .subscribe(); + } + + public onRemoveAddons(listItems: AddonViewModel[]): void { + let message = ""; + if (listItems.length > 3) { + message = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_MORE_THAN_THREE", { + count: listItems.length, + }); + } else { + message = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_LESS_THAN_THREE", { + count: listItems.length, + }); + listItems.forEach((listItem) => (message = `${message}\n\t• ${listItem.addon?.name ?? ""}`)); + } + message += + "\n\n" + + (this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_ACTION_EXPLANATION") as string); + + const dialogRef = this._dialog.open(ConfirmDialogComponent, { + data: { + title: this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.TITLE", { count: listItems.length }), + message: message, + }, + }); + + dialogRef + .afterClosed() + .pipe( + first(), + switchMap((result) => { + if (!result) { + return of(undefined); + } + + return zip(listItems.map((listItem) => from(this._addonService.removeAddon(listItem.addon)))); + }), + ) + .subscribe(); + } + + public async onClickIgnoreAddon(listItem: AddonViewModel): Promise { + await this.onClickIgnoreAddons([listItem]); + } + + public async onClickIgnoreAddons(listItems: AddonViewModel[]): Promise { + const isIgnored = _.every(listItems, (listItem) => listItem.addon?.isIgnored === false); + const rows = _.cloneDeep(this._baseRowDataSrc.value); + try { + for (const listItem of listItems) { + const row = _.find(rows, (r) => r.addon?.id === listItem.addon?.id); + if (!row || !row.addon) { + console.warn(`Invalid row data`); + continue; + } + + row.addon.isIgnored = isIgnored; + if (isIgnored) { + row.addon.autoUpdateEnabled = false; + } + + await this._addonService.saveAddon(row.addon); + } + + this._baseRowDataSrc.next(rows); + } catch (e) { + console.error(`Failed to ignore addon(s)`, e); + } finally { + this.updateBadgeCount().catch((e) => console.error(e)); + } + } + + public async onClickAutoUpdateAddon(listItem: AddonViewModel): Promise { + await this.onClickAutoUpdateAddons([listItem]); + } + + public async onClickAutoUpdateAddonNotifications(listItem: AddonViewModel): Promise { + await this.onClickAutoUpdateAddonsNotifications([listItem]); + } + + public onRowClicked(event: RowClickedEvent): void { + const selectedNodes = event.api.getSelectedNodes(); + + if ( + selectedNodes.length === 1 && + this._lastSelectionState.length === 1 && + event.node.data.addon.id === this._lastSelectionState[0].data.addon.id + ) { + event.node.setSelected(false); + this._lastSelectionState = []; + } else { + this._lastSelectionState = [...selectedNodes]; + } + } + + public onRowDoubleClicked(evt: RowDoubleClickedEvent): void { + this._dialogFactory.getAddonDetailsDialog(evt.data as AddonViewModel); + evt.node.setSelected(true); + } + + private getModelById(rows: AddonViewModel[], model: AddonViewModel): AddonViewModel | undefined { + return rows.find((row) => row.addon?.id == model.addon?.id); + } + + public async onClickAutoUpdateAddons(listItems: AddonViewModel[]): Promise { + const isAutoUpdate = _.every(listItems, (listItem) => listItem.addon?.autoUpdateEnabled === false); + const rows = _.cloneDeep(this._baseRowDataSrc.value); + try { + for (const listItem of listItems) { + const row = this.getModelById(rows, listItem); + if (!row || !row.addon) { + console.warn("Invalid row data"); + continue; + } + + row.addon.autoUpdateEnabled = isAutoUpdate; + if (isAutoUpdate) { + row.addon.isIgnored = false; + } + + row.addon.autoUpdateNotificationsEnabled = isAutoUpdate; + + await this._addonService.saveAddon(row.addon); + } + + this._baseRowDataSrc.next(rows); + } catch (e) { + console.error(e); + this._operationErrorSrc.next(e as Error); + } + } + + public async onClickAutoUpdateAddonsNotifications(listItems: AddonViewModel[]): Promise { + const isAutoUpdateNofications = _.every( + listItems, + (listItem) => listItem.addon?.autoUpdateNotificationsEnabled === false, + ); + const rows = _.cloneDeep(this._baseRowDataSrc.value); + try { + for (const listItem of listItems) { + const row = _.find(rows, (r) => r.addon?.id === listItem.addon?.id); + if (!row || !row.addon) { + console.warn("Invalid row data for addon", listItem.addon?.id); + continue; + } + + row.addon.autoUpdateNotificationsEnabled = isAutoUpdateNofications; + + await this._addonService.saveAddon(row.addon); + } + + this._baseRowDataSrc.next(rows); + } catch (e) { + console.error(e); + this._operationErrorSrc.next(e as Error); + } + } + + public onSelectedProviderChange(evt: MatRadioChange, listItem: AddonViewModel): void { + const messageData = { + addonName: listItem.addon?.name ?? "", + providerName: evt.value, + }; + + const dialogRef = this._dialog.open(ConfirmDialogComponent, { + data: { + title: this._translateService.instant("PAGES.MY_ADDONS.CHANGE_ADDON_PROVIDER_CONFIRMATION.TITLE"), + message: this._translateService.instant( + "PAGES.MY_ADDONS.CHANGE_ADDON_PROVIDER_CONFIRMATION.MESSAGE", + messageData, + ), + }, + }); + + dialogRef + .afterClosed() + .pipe( + first(), + switchMap((result) => { + if (!result) { + return of(undefined); + } + + const selectedWowInstall = this._sessionService.getSelectedWowInstallation(); + const externalId = _.find(listItem.addon?.externalIds, (extId) => extId.providerName === evt.value); + if (!externalId || !selectedWowInstall) { + throw new Error("External id not found"); + } + + return from( + this._addonService.setProvider(listItem.addon, externalId.id, externalId.providerName, selectedWowInstall), + ); + }), + catchError((e) => { + console.error(e); + const errorTitle: string = this._translateService.instant("DIALOGS.ALERT.ERROR_TITLE"); + const errorMessage: string = this._translateService.instant( + "COMMON.ERRORS.CHANGE_PROVIDER_ERROR", + messageData, + ); + this.showErrorMessage(errorTitle, errorMessage); + return of(undefined); + }), + ) + .subscribe(); + } + + /** + * Update a single addon with a new channel + */ + public onSelectedAddonChannelChange = (evt: MatRadioChange, listItem: AddonViewModel): Promise => { + return this.onSelectedAddonsChannelChange(evt, [listItem]); + }; + + /** + * Update a batch of addons with a new channel + * We need to call load addons so we pull in any new updates for that channel + */ + public onSelectedAddonsChannelChange = async (evt: MatRadioChange, listItems: AddonViewModel[]): Promise => { + try { + for (const listItem of listItems) { + if (!listItem.addon) { + console.warn("Invalid addon"); + continue; + } + + listItem.addon.channelType = evt.value; + await this._addonService.saveAddon(listItem.addon); + } + + await this.onRefresh(); + } catch (e) { + console.error(`Failed to change addon channel`, e); + } + }; + + public isIndeterminate(listItems: AddonViewModel[], prop: string): boolean { + return _.some(listItems, prop) && !this.isAllItemsSelected(listItems, prop); + } + + public isAllItemsSelected(listItems: AddonViewModel[], prop: string): boolean { + return _.filter(listItems, prop).length === listItems.length; + } + + public getChannelTypeLocaleKey(channelType: string): string { + return channelType ? `COMMON.ENUM.ADDON_CHANNEL_TYPE.${channelType.toUpperCase()}` : "COMMON.ADDON_STATUS.ERROR"; + } + + public onTableBlur(evt: MouseEvent): void { + const ePath = evt.composedPath() as HTMLElement[]; + const tableElem = ePath.find((tag) => tag.tagName === "AG-GRID-ANGULAR"); + if (tableElem) { + return; + } + + evt.stopPropagation(); + evt.preventDefault(); + this._lastSelectionState = []; + this.gridApi?.deselectAll(); + } + + private async lazyLoad(): Promise { + if (this._lazyLoaded) { + return; + } + + this._lazyLoaded = true; + this._isBusySrc.next(true); + + this._sessionService.setEnableControls(false); + + // TODO this shouldn't be here + await this._addonService.backfillAddons(); + + const selectedInstallationSub = this._sessionService.selectedWowInstallation$ + .pipe( + debounceTime(300), + switchMap((installation) => { + // Installs will not be pre-selected on Linux, so wait for one to get added + if (!installation) { + return of(undefined); + } + + return from(this.loadAddons()); + }), + catchError((e) => { + console.error(`selectedInstallationSub failed`, e); + return of(undefined); + }), + ) + .subscribe(); + + this._subscriptions.push(selectedInstallationSub); + } + + private async updateAllWithSpinner(...installations: WowInstallation[]): Promise { + this._isBusySrc.next(true); + + this._spinnerMessageSrc.next(this._translateService.instant("PAGES.MY_ADDONS.SPINNER.GATHERING_ADDONS") as string); + this._sessionService.setEnableControls(false); + + let addons: Addon[] = []; + let updatedCt = 0; + + try { + for (const installation of installations) { + addons = addons.concat(await this._addonService.getAddons(installation)); + } + + // Only care about the ones that need to be updated/installed + addons = addons.filter( + (addon) => !addon.isIgnored && (AddonUtils.needsUpdate(addon) || AddonUtils.needsInstall(addon)), + ); + + if (addons.length === 0) { + await this.loadAddons(); + return; + } + + this._spinnerMessageSrc.next( + this._translateService.instant("PAGES.MY_ADDONS.SPINNER.UPDATING", { + updateCount: updatedCt, + addonCount: addons.length, + }) as string, + ); + + for (const addon of addons) { + if (!addon.id) { + continue; + } + + updatedCt += 1; + + // Find the installation for this addon so we can show the correct name + const installation = installations.find((inst) => inst.id === addon.installationId); + if (!installation) { + console.warn("Installation not found"); + continue; + } + + this._spinnerMessageSrc.next( + this._translateService.instant("PAGES.MY_ADDONS.SPINNER.UPDATING_WITH_ADDON_NAME", { + updateCount: updatedCt, + addonCount: addons.length, + clientType: installation.displayName, + addonName: addon.name, + }) as string, + ); + + await this._addonService.updateAddon(addon); + } + + await this.loadAddons(); + } catch (err) { + console.error("Failed to update classic/retail", err); + this._isBusySrc.next(false); + + this._cdRef.detectChanges(); + } finally { + this._spinnerMessageSrc.next(""); + this._sessionService.setEnableControls(this.calculateControlState()); + } + } + + private updateContextMenuPosition(event: any): void { + this.contextMenuPosition.x = `${event.clientX as number}px`; + this.contextMenuPosition.y = `${event.clientY as number}px`; + } + + private loadAddons = async (reScan = false): Promise => { + const installation = this._sessionService.getSelectedWowInstallation(); + if (!installation) { + return; + } + + this._isBusySrc.next(true); + + this._sessionService.setEnableControls(false); + + if (!installation) { + console.warn("Skipping addon load installation unknown"); + return; + } + + this._cdRef.detectChanges(); + + try { + let addons: Addon[] = []; + addons = await this._addonService.getAddons(installation, reScan); + + const rowData = this.formatAddons(addons); + + this._baseRowDataSrc.next(rowData); + + this.setPageContextText(); + + this._cdRef.detectChanges(); + } catch (e) { + console.error(e); + } finally { + this._isBusySrc.next(false); + this._cdRef.detectChanges(); + this._sessionService.setEnableControls(this.calculateControlState()); + } + }; + + private getLatestVersionColumnVisible(): boolean { + return this.columns.find((col) => col.name === "latestVersion")?.visible ?? true; + } + + private formatAddons(addons: Addon[]): AddonViewModel[] { + const viewModels = addons.map((addon) => { + const listItem = new AddonViewModel(addon); + + if (listItem.addon && !listItem.addon.installedVersion) { + listItem.addon.installedVersion = ""; + } + + return listItem; + }); + + return _.orderBy(viewModels, (vm) => vm.name); + } + + private filterListItem = (item: AddonViewModel, filter: string) => { + if ( + stringIncludes(item.addon?.name, filter) || + stringIncludes(item.addon?.latestVersion, filter) || + stringIncludes(item.addon?.author, filter) + ) { + return true; + } + return false; + }; + + private setPageContextText() { + this.rowData$ + .pipe( + first(), + map((data) => { + if (data.length === 0) { + return; + } + + this._sessionService.setContextText( + this._tabIndexSrc.value, + this._translateService.instant("PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.ADDONS_INSTALLED", { + count: data.length, + }) as string, + ); + }), + ) + .subscribe(); + } + + private onAddonInstalledEvent = (evt: AddonUpdateEvent) => { + if (evt.addon.installationId !== this._sessionService.getSelectedWowInstallation()?.id) { + return; + } + + if ([AddonInstallState.Complete, AddonInstallState.Error].includes(evt.installState) === false) { + this._sessionService.setEnableControls(false); + return; + } + + let rowData = _.cloneDeep(this._baseRowDataSrc.value); + const idx = rowData.findIndex((r) => r.addon?.id === evt.addon.id); + const change = idx !== -1 || evt.installState === AddonInstallState.Complete; + + // If we have a new addon, just put it at the end + if (idx === -1) { + // If this is an update of an addon that is not fully installed yet, ignore it + if (evt.installState === AddonInstallState.Complete) { + console.debug("Adding new addon to list"); + rowData.push(new AddonViewModel(evt.addon)); + rowData = _.orderBy(rowData, (row) => row.canonicalName); + } + } else { + rowData.splice(idx, 1, new AddonViewModel(evt.addon)); + } + + if (change) { + this._baseRowDataSrc.next(rowData); + } + this._sessionService.setEnableControls(this.calculateControlState()); + }; + + private onAddonRemoved = (addonId: string) => { + const rowData = _.cloneDeep(this._baseRowDataSrc.value); + + const listItemIdx = rowData.findIndex((li) => li.addon?.id === addonId); + rowData.splice(listItemIdx, 1); + + this._baseRowDataSrc.next(rowData); + }; + + private showErrorMessage(title: string, message: string) { + this._dialogFactory.getErrorDialog(title, message); + } + + private calculateControlState(): boolean { + return !this._addonService.isInstalling(); + } + + private async mapInstallations(installations: WowInstallation[]): Promise { + let total = 0; + for (const inst of installations) { + const addons = await this._addonService.getAllAddonsAvailableForUpdate(inst); + inst.availableUpdateCount = addons.length; + total += inst.availableUpdateCount; + } + + this._totalAvailableUpdateSrc.next(total); + return installations; + } + + private async updateBadgeCount(): Promise { + const addons = await this._addonService.getAllAddonsAvailableForUpdate(); + const ct = addons.length; + console.debug("updateBadgeCount", ct); + try { + await this.wowUpService.updateAppBadgeCount(ct); + } catch (e) { + console.error("Failed to update badge count", e); + } + } + + private async loadSortOrder(): Promise { + let savedSortOrder = await this.wowUpService.getMyAddonsSortOrder(); + if (!Array.isArray(savedSortOrder) || savedSortOrder.length < 2) { + console.info(`Legacy or missing sort order fixed`); + await this.wowUpService.setMyAddonsSortOrder([]); + savedSortOrder = []; + } + + if (savedSortOrder.length > 0) { + this.gridColumnApi.applyColumnState({ + state: savedSortOrder, + }); + } + } + + /** If we're auto sizing and not visible, skip it as to not attempt to resize with no bounds */ + private autoSizeColumns() { + if (!this._visibleSrc.value) { + return; + } + + this.gridColumnApi?.autoSizeColumns([ + "installedAt", + "latestVersion", + "releasedAt", + "gameVersion", + "externalChannel", + "providerName", + ]); + } + + // Sort the toc version column by converting from 'x.x.x' to interface version 'xxxxxx' + private compareTocVersion(nodeA: IRowNode, nodeB: IRowNode): number { + let v1GameVers = nodeA.data["gameVersion"] as string[]; + if (!Array.isArray(v1GameVers)) { + v1GameVers = []; + } + + let v2GameVers = nodeB.data["gameVersion"] as string[]; + if (!Array.isArray(v2GameVers)) { + v2GameVers = []; + } + + try { + const v1IntVers = v1GameVers.map((x) => +toInterfaceVersion(x || "0.0.0")); + const v2IntVers = v2GameVers.map((x) => +toInterfaceVersion(x || "0.0.0")); + + const v1Max = _.max(v1IntVers) ?? 0; + const v2Max = _.max(v2IntVers) ?? 0; + + return v1Max > v2Max ? 1 : -1; + } catch (e) { + console.error(e); + return -1; + } + } + + // If nodes have the same primary value, use the canonical name as a fallback + private compareElement(nodeA: IRowNode, nodeB: IRowNode, prop: string): number { + if (nodeA.data[prop] === nodeB.data[prop]) { + if (nodeA.data.canonicalName === nodeB.data.canonicalName) { + return 0; + } + return nodeA.data.canonicalName > nodeB.data.canonicalName ? 1 : -1; + } + + return nodeA.data[prop] > nodeB.data[prop] ? 1 : -1; + } + + private createColumns(): ColDef[] { + const baseColumn = { + headerComponent: TableContextHeaderCellComponent, + headerComponentParams: { + onHeaderContext: this.onHeaderContext, + }, + cellStyle: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + }, + suppressMovable: true, + }; + + return [ + { + cellRenderer: MyAddonsAddonCellComponent, + field: "hash", + flex: 2, + minWidth: 300, + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER"), + sortable: true, + // autoHeight: true, + colId: "name", + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "canonicalName"), + ...baseColumn, + }, + { + cellRenderer: MyAddonStatusCellComponent, + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "sortOrder"), + field: "sortOrder", + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER"), + sortable: true, + width: 150, + ...baseColumn, + }, + { + field: "installedAt", + sortable: true, + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.UPDATED_AT_COLUMN_HEADER"), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "installedAt"), + ...baseColumn, + cellRenderer: DateTooltipCellComponent, + }, + { + field: "latestVersion", + sortable: true, + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.LATEST_VERSION_COLUMN_HEADER"), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "latestVersion"), + ...baseColumn, + }, + { + field: "releasedAt", + sortable: true, + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.RELEASED_AT_COLUMN_HEADER"), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "releasedAt"), + ...baseColumn, + cellRenderer: DateTooltipCellComponent, + }, + { + field: "gameVersion", + sortable: true, + minWidth: 125, + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.GAME_VERSION_COLUMN_HEADER"), + comparator: (va, vb, na, nb) => this.compareTocVersion(na, nb), + ...baseColumn, + }, + { + field: "externalChannel", + sortable: true, + flex: 1, + minWidth: 125, + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.PROVIDER_RELEASE_CHANNEL"), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "externalChannel"), + ...baseColumn, + }, + { + field: "providerName", + sortable: true, + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.PROVIDER_COLUMN_HEADER"), + valueFormatter: (row) => this.getProviderName(row.data.providerName as string), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "providerName"), + ...baseColumn, + }, + { + field: "author", + sortable: true, + minWidth: 120, + flex: 1, + headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.AUTHOR_COLUMN_HEADER"), + comparator: (va, vb, na, nb) => this.compareElement(na, nb, "author"), + cellRenderer: CellWrapTextComponent, + ...baseColumn, + }, + ]; + } +} diff --git a/WowUp/wowup-electron/src/app/pages/options/options.component.html b/WowUp/wowup-electron/src/app/pages/options/options.component.html new file mode 100644 index 0000000..e8641cf --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/options/options.component.html @@ -0,0 +1,72 @@ +
+ +
diff --git a/WowUp/wowup-electron/src/app/pages/options/options.component.scss b/WowUp/wowup-electron/src/app/pages/options/options.component.scss new file mode 100644 index 0000000..b82c65f --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/options/options.component.scss @@ -0,0 +1,112 @@ +.tab-container { + display: flex; + align-items: center; + flex-direction: column; +} + +.theme-logo { + content: ""; + opacity: 0.02; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 1; + overflow: hidden; + pointer-events: none; + + .logo-img { + height: 100vh; + // background-image: var(--theme-logo); + background-repeat: no-repeat; + background-position: 0 0; + background-size: contain; + } +} + +.nav-container { + height: 100%; + display: flex; + width: 100%; + + .nav-item-container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-box-pack: end; + justify-content: flex-end; + flex: 1 0 200px; + z-index: 1; + -webkit-box-sizing: border-box; + box-sizing: border-box; + // overflow: hidden; + + .nav-item-list-wrap { + flex: 1 0 auto; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + flex-direction: row; + -webkit-box-align: start; + align-items: flex-start; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-box-pack: end; + justify-content: flex-end; + box-sizing: border-box; + + .nav-item-list { + width: 200px; + } + } + } + .nav-content-container { + position: relative; + display: flex; + -webkit-box-flex: 1; + flex: 1 1 800px; + -webkit-box-align: start; + align-items: flex-start; + box-sizing: border-box; + + .nav-content-wrap { + -webkit-box-flex: 1; + flex: 1; + box-sizing: border-box; + height: 100%; + + .nav-content { + overflow: hidden scroll; + padding-right: 0px; + height: 100%; + position: static; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + flex-direction: row; + -webkit-box-pack: start; + justify-content: flex-start; + -webkit-box-align: start; + align-items: flex-start; + overflow-x: hidden; + box-sizing: border-box; + flex: 1 1 auto; + min-height: 0; + -webkit-box-flex: 1; + z-index: 2; + + .content-tab { + position: relative; + -webkit-box-flex: 1; + flex: 1 1 auto; + max-width: 740px; + min-width: 460px; + min-height: 100%; + } + } + } + } +} diff --git a/WowUp/wowup-electron/src/app/pages/options/options.component.spec.ts b/WowUp/wowup-electron/src/app/pages/options/options.component.spec.ts new file mode 100644 index 0000000..dd46953 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/options/options.component.spec.ts @@ -0,0 +1,86 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ElectronService } from "../../services/electron/electron.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +import { OptionsComponent } from "./options.component"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { httpLoaderFactory } from "../../app.module"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { MatModule } from "../../modules/mat-module"; +import { SessionService } from "../../services/session/session.service"; +import { BehaviorSubject } from "rxjs"; + +describe("OptionsComponent", () => { + let component: OptionsComponent; + let fixture: ComponentFixture; + let electronServiceSpy: any; + let wowUpServiceSpy: any; + let sessionService: any; + + beforeEach(async () => { + sessionService = jasmine.createSpyObj("SessionService", [""], { + currentTheme$: new BehaviorSubject("default-theme"), + }); + + wowUpServiceSpy = jasmine.createSpyObj( + "WowUpService", + {}, + { + currentTheme: "horde ofc", + } + ); + electronServiceSpy = jasmine.createSpyObj( + "ElectronService", + { + invoke: new Promise(() => {}), + }, + { + isWin: false, + isLinux: true, + isMac: false, + } + ); + + await TestBed.configureTestingModule({ + declarations: [OptionsComponent], + imports: [ + MatModule, + NoopAnimationsModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(OptionsComponent, { + set: { + providers: [ + { provide: ElectronService, useValue: electronServiceSpy }, + { provide: SessionService, useValue: sessionService }, + { provide: WowUpService, useValue: wowUpServiceSpy }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OptionsComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/pages/options/options.component.ts b/WowUp/wowup-electron/src/app/pages/options/options.component.ts new file mode 100644 index 0000000..7fdfd0f --- /dev/null +++ b/WowUp/wowup-electron/src/app/pages/options/options.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from "@angular/core"; +import { IPC_OW_IS_CMP_REQUIRED } from "../../../common/constants"; +import { ElectronService } from "../../services"; +import { SessionService } from "../../services/session/session.service"; +import { WowUpService } from "../../services/wowup/wowup.service"; +// import { IPC_OW_IS_CMP_REQUIRED, IPC_OW_OPEN_CMP } from "../../../../common/constants"; + +@Component({ + selector: "app-options", + templateUrl: "./options.component.html", + styleUrls: ["./options.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OptionsComponent implements OnInit { + @Input("tabIndex") public tabIndex!: number; + + public optionTabIndex = 0; + public isCMPRequired = false; + + public constructor( + public wowUpService: WowUpService, + public sessionService: SessionService, + public electronService: ElectronService + ) {} + + public ngOnInit(): void { + this.electronService + .invoke(IPC_OW_IS_CMP_REQUIRED) + .then((cmpRequired) => { + this.isCMPRequired = cmpRequired; + }) + .catch((e) => console.error("IPC_OW_IS_CMP_REQUIRED failed", e)); + } +} diff --git a/WowUp/wowup-electron/src/app/pipes/download-count.pipe.spec.ts b/WowUp/wowup-electron/src/app/pipes/download-count.pipe.spec.ts new file mode 100644 index 0000000..0f85ce0 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/download-count.pipe.spec.ts @@ -0,0 +1,69 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { DownloadCountPipe } from "./download-count.pipe"; +import { Component } from "@angular/core"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { httpLoaderFactory } from "../app.module"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +@Component({ + template: `

{{ number | downloadCount }}

`, +}) +class TestDownloadCountComponent { + public number = 0; +} + +describe("DownloadCountPipe", () => { + let component: TestDownloadCountComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TestDownloadCountComponent, DownloadCountPipe], + imports: [ + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }).compileComponents(); + fixture = TestBed.createComponent(TestDownloadCountComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + const inputs = { + "e+0": 1, + "e+1": 10, + "e+2": 100, + "e+3": 1000, + "e+4": 10000, + "e+5": 100000, + "e+6": 1000000, + "e+7": 10000000, + "e+8": 100000000, + "e+9": 1000000000, + }; + + for (const index in inputs) { + const number: number = inputs[index]; + it(`should transform the number ${number} to ${index}`, () => { + component.number = number; + fixture.detectChanges(); + const p = fixture.debugElement.nativeElement.querySelector("p"); + expect(p.innerHTML).toBe(`COMMON.DOWNLOAD_COUNT.${index}`); + }); + } +}); diff --git a/WowUp/wowup-electron/src/app/pipes/download-count.pipe.ts b/WowUp/wowup-electron/src/app/pipes/download-count.pipe.ts new file mode 100644 index 0000000..cdd88a6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/download-count.pipe.ts @@ -0,0 +1,28 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; +import { shortenDownloadCount } from "../utils/number.utils"; + +@Pipe({ + name: "downloadCount", +}) +export class DownloadCountPipe implements PipeTransform { + public constructor(private translateService: TranslateService) {} + + public transform(value: number): string { + const numMatches = /(e\+\d+)/.exec(value.toExponential()); + if (!numMatches) { + throw new Error("Failed to get matches"); + } + + const suffix = numMatches[1]; + + const params = { + rawCount: value, + count: shortenDownloadCount(value, 3), + simpleCount: shortenDownloadCount(value, 1), + myriadCount: shortenDownloadCount(value, 4), + }; + + return suffix ? this.translateService.instant("COMMON.DOWNLOAD_COUNT." + suffix, params) : value.toString(); + } +} diff --git a/WowUp/wowup-electron/src/app/pipes/get-addon-list-item-file-prop.pipe.spec.ts b/WowUp/wowup-electron/src/app/pipes/get-addon-list-item-file-prop.pipe.spec.ts new file mode 100644 index 0000000..eb6687d --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/get-addon-list-item-file-prop.pipe.spec.ts @@ -0,0 +1,71 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { GetAddonListItemFilePropPipe } from "./get-addon-list-item-file-prop.pipe"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { httpLoaderFactory } from "../app.module"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { Component } from "@angular/core"; +import { GetAddonListItem } from "../business-objects/get-addon-list-item"; +import { AddonChannelType } from "wowup-lib-core"; +import { AddonInstallState } from "../models/wowup/addon-install-state"; + +@Component({ + template: `

{{ item | getAddonListItemFileProp: "version":channel }}

`, +}) +class TestAddonListItemFilePropComponent { + public item: GetAddonListItem = { + searchResult: { + author: "", + externalId: "", + externalUrl: "", + name: "", + providerName: "", + thumbnailUrl: "", + files: [], + }, + releasedAt: 1, + downloadCount: 0, + name: "", + author: "", + thumbnailUrl: "", + providerName: "", + latestAddonChannel: AddonChannelType.Stable, + canonicalName: "", + installState: AddonInstallState.Complete, + externalId: "", + }; + public channel: AddonChannelType = AddonChannelType.Stable; +} + +describe("GetAddonListItemFilePropPipe", () => { + let component: TestAddonListItemFilePropComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TestAddonListItemFilePropComponent, GetAddonListItemFilePropPipe], + imports: [ + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestAddonListItemFilePropComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/pipes/get-addon-list-item-file-prop.pipe.ts b/WowUp/wowup-electron/src/app/pipes/get-addon-list-item-file-prop.pipe.ts new file mode 100644 index 0000000..6e794a5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/get-addon-list-item-file-prop.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { AddonChannelType } from "wowup-lib-core"; +import { GetAddonListItem } from "../business-objects/get-addon-list-item"; +import * as SearchResults from "../utils/search-result.utils"; + +@Pipe({ + name: "getAddonListItemFileProp", +}) +export class GetAddonListItemFilePropPipe implements PipeTransform { + public transform(item: GetAddonListItem, prop: string, channel: AddonChannelType): any { + const file = SearchResults.getLatestFile(item.searchResult, channel); + return file && Object.prototype.hasOwnProperty.call(file, prop) ? file[prop] : ""; + } +} diff --git a/WowUp/wowup-electron/src/app/pipes/interface-format.pipe.spec.ts b/WowUp/wowup-electron/src/app/pipes/interface-format.pipe.spec.ts new file mode 100644 index 0000000..f41c9b8 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/interface-format.pipe.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { InterfaceFormatPipe } from "./interface-format.pipe"; +import { Component } from "@angular/core"; + +@Component({ + template: `

{{ version | interfaceFormat }}

`, +}) +class TestInterfaceFormatComponent { + public version = ""; +} + +describe("InterfaceFormatPipe", () => { + let component: TestInterfaceFormatComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TestInterfaceFormatComponent, InterfaceFormatPipe], + }).compileComponents(); + + fixture = TestBed.createComponent(TestInterfaceFormatComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should transform .toc format to semver", () => { + component.version = "90002"; + fixture.detectChanges(); + const p = fixture.debugElement.nativeElement.querySelector("p"); + expect(p.innerHTML).toBe("9.0.2"); + }); + + it("should leave any dot version alone", () => { + let p: HTMLElement; + + component.version = "0.1"; + fixture.detectChanges(); + p = fixture.debugElement.nativeElement.querySelector("p"); + expect(p.innerHTML).toBe("0.1"); + + component.version = "8.3.1"; + fixture.detectChanges(); + p = fixture.debugElement.nativeElement.querySelector("p"); + expect(p.innerHTML).toBe("8.3.1"); + }); +}); diff --git a/WowUp/wowup-electron/src/app/pipes/interface-format.pipe.ts b/WowUp/wowup-electron/src/app/pipes/interface-format.pipe.ts new file mode 100644 index 0000000..e56a121 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/interface-format.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { getGameVersion } from "wowup-lib-core"; + +@Pipe({ + name: "interfaceFormat", +}) +export class InterfaceFormatPipe implements PipeTransform { + public transform(value: string): string { + return getGameVersion(value); + } +} diff --git a/WowUp/wowup-electron/src/app/pipes/inverse-bool.pipe.ts b/WowUp/wowup-electron/src/app/pipes/inverse-bool.pipe.ts new file mode 100644 index 0000000..b32532b --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/inverse-bool.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: "invertBool", +}) +export class InvertBoolPipe implements PipeTransform { + public transform(value: boolean): boolean { + return !value; + } +} diff --git a/WowUp/wowup-electron/src/app/pipes/ngx-date.pipe.ts b/WowUp/wowup-electron/src/app/pipes/ngx-date.pipe.ts new file mode 100644 index 0000000..864958c --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/ngx-date.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +@Pipe({ + name: "localeDate", +}) +export class NgxDatePipe implements PipeTransform { + public constructor(private translateService: TranslateService) {} + + public transform(value: string | number): any { + return new Date(value).toLocaleString(this.translateService.currentLang); + } +} diff --git a/WowUp/wowup-electron/src/app/pipes/relative-duration-pipe.spec.ts b/WowUp/wowup-electron/src/app/pipes/relative-duration-pipe.spec.ts new file mode 100644 index 0000000..3e37972 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/relative-duration-pipe.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Component } from "@angular/core"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { httpLoaderFactory } from "../app.module"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { RelativeDurationPipe } from "./relative-duration-pipe"; + +@Component({ + template: `

{{ date | relativeDuration }}

`, +}) +class TestRelativeDurationComponent { + public date = new Date().toString(); +} + +describe("RelativeDurationPipe", () => { + let component: TestRelativeDurationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TestRelativeDurationComponent, RelativeDurationPipe], + imports: [ + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestRelativeDurationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/pipes/relative-duration-pipe.ts b/WowUp/wowup-electron/src/app/pipes/relative-duration-pipe.ts new file mode 100644 index 0000000..b3bd52c --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/relative-duration-pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; +import { getRelativeDateFormat } from "../utils/string.utils"; + +@Pipe({ + name: "relativeDuration", +}) +export class RelativeDurationPipe implements PipeTransform { + public constructor(private _translate: TranslateService) {} + + public transform(value: string): string { + if (!value) { + return "EMPTY"; + } + const [fmt, val] = getRelativeDateFormat(value); + return this._translate.instant(fmt, val); + } +} diff --git a/WowUp/wowup-electron/src/app/pipes/size-display.pipe.ts b/WowUp/wowup-electron/src/app/pipes/size-display.pipe.ts new file mode 100644 index 0000000..ba3c040 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/size-display.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { formatSize } from "../utils/number.utils"; + +@Pipe({ name: "sizeDisplay" }) +export class SizeDisplayPipe implements PipeTransform { + public constructor() {} + + public transform(size: number): string { + return formatSize(size); + } +} diff --git a/WowUp/wowup-electron/src/app/pipes/trust-html.pipe.ts b/WowUp/wowup-electron/src/app/pipes/trust-html.pipe.ts new file mode 100644 index 0000000..dd21573 --- /dev/null +++ b/WowUp/wowup-electron/src/app/pipes/trust-html.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from "@angular/core"; +import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; + +@Pipe({ name: "trustHtml" }) +export class TrustHtmlPipe implements PipeTransform { + public constructor(private _sanitizer: DomSanitizer) {} + + public transform(value: string): SafeHtml { + return this._sanitizer.bypassSecurityTrustHtml(value); + } +} diff --git a/WowUp/wowup-electron/src/app/services/addons/addon-broker.service.ts b/WowUp/wowup-electron/src/app/services/addons/addon-broker.service.ts new file mode 100644 index 0000000..a38f6e5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/addons/addon-broker.service.ts @@ -0,0 +1,278 @@ +import _ from "lodash"; + +import { Injectable } from "@angular/core"; +import { nanoid } from "nanoid"; + +import { getEnumName, getWowClientGroupForType } from "wowup-lib-core"; +import { AddonStorageService } from "../storage/addon-storage.service"; +import { WarcraftService } from "../warcraft/warcraft.service"; +import { AddonService } from "./addon.service"; +import { Subject } from "rxjs"; +import { AddonInstallState } from "../../models/wowup/addon-install-state"; +import { ElectronService } from ".."; +import { Addon, WowClientType } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +export type ExportReleaseType = "stable" | "beta" | "alpha"; +export type ImportState = "no-change" | "added" | "conflict"; +export type ConflictReasonCode = "VERSION_MISMATCH" | "PROVIDER_MISMATCH"; +export type ImportErrorCode = "GENERIC_IMPORT_ERROR" | "INVALID_CLIENT_TYPE"; + +export interface ExportSummary { + activeCount: number; + ignoreCount: number; +} + +export interface ExportAddon { + name: string; + provider_name: string; + id: string; + version_id?: string; + release_type?: ExportReleaseType; +} + +export interface ExportPayload { + collection_name?: string; + client_type: string; + addons: ExportAddon[]; +} + +export interface ImportSummary { + addedCt: number; + noChangeCt: number; + conflictCt: number; + comparisons: ImportComparison[]; + errorCode?: ImportErrorCode; +} + +export interface ImportComparison { + id: string; + original?: ExportAddon; + imported: ExportAddon; + state: ImportState; + conflictReason?: ConflictReasonCode; +} + +export interface InstallEvent { + comparisonId: string; + installState: AddonInstallState; + progress: number; +} + +const ImportStateWeights: { [key: string]: number } = { + "no-change": 0, + added: 1, + conflict: 2, +}; + +@Injectable({ + providedIn: "root", +}) +export class AddonBrokerService { + private readonly _addonInstallSrc = new Subject(); + + public readonly addonInstall$ = this._addonInstallSrc.asObservable(); + + public constructor( + private _addonStorage: AddonStorageService, + private _addonService: AddonService, + private _warcraftService: WarcraftService, + private _electronService: ElectronService, + ) {} + + public async getExportSummary(installation: WowInstallation): Promise { + const addons = await this._addonStorage.getAllForInstallationIdAsync(installation.id); + + // If an addon is ignored, ignore it. + const addonCt = addons.filter((addon) => !this.addonIsIgnored(addon)).length; + + // Count the ignored ones + const ignoredCt = addons.filter((addon) => this.addonIsIgnored(addon)).length; + + return { + activeCount: addonCt, + ignoreCount: ignoredCt, + }; + } + + public async getExportPayload(installation: WowInstallation): Promise { + const payload: ExportPayload = { + collection_name: `WowUp_export_${Date.now()}`, + client_type: this.getClientType(installation.clientType), + addons: [], + }; + + let addons = await this._addonStorage.getAllForInstallationIdAsync(installation.id); + addons = addons.filter((addon) => !this.addonIsIgnored(addon)); + + for (const addon of addons) { + try { + payload.addons.push(this.getExportAddon(addon)); + } catch (e) { + console.error(e); + } + } + + return payload; + } + + public async parseImportString(importStr: string): Promise { + let jsonStr: string = importStr; + // try to detect JSON vs b64 json + if (importStr.trim().charAt(0) !== "{") { + // try to decode it from b64 + jsonStr = await this._electronService.invoke("base64-decode", jsonStr); + } + + const importJson: ExportPayload = JSON.parse(jsonStr); + return importJson; + } + + public async installImportSummary(importSummary: ImportSummary, installation: WowInstallation): Promise { + const comps = importSummary.comparisons.filter((comp) => comp.state === "added"); + + const tasks = comps.map((comp) => { + return (async (c) => { + try { + await this._addonService.installBaseAddon( + c.imported.id, + c.imported.provider_name, + installation, + (installState, progress) => { + this._addonInstallSrc.next({ + comparisonId: c.id, + installState, + progress, + }); + }, + ); + } catch (e) { + console.error(`Failed to install imported addon`, e); + } + })(comp); + }); + + await Promise.all(tasks); + } + + public async getImportSummary(exportPayload: ExportPayload, installation: WowInstallation): Promise { + const summary: ImportSummary = { + addedCt: 0, + conflictCt: 0, + noChangeCt: 0, + comparisons: [], + }; + + if (!Array.isArray(exportPayload.addons) || exportPayload.addons.length === 0) { + summary.errorCode = "GENERIC_IMPORT_ERROR"; + return summary; + } + + if (!this.isSameClient(installation.clientType, exportPayload.client_type)) { + summary.errorCode = "INVALID_CLIENT_TYPE"; + return summary; + } + + const currentAddons = await this._addonService.getAllAddons(installation); + for (const impAddon of exportPayload.addons) { + const comparison: ImportComparison = { + id: nanoid(), + imported: impAddon, + state: "added", + }; + + const externalIdMatch = currentAddons.find( + (addon) => addon.externalId === impAddon.id && addon.providerName == impAddon.provider_name, + ); + if (externalIdMatch) { + comparison.original = { + id: externalIdMatch.externalId, + name: externalIdMatch.name, + provider_name: externalIdMatch.providerName, + version_id: externalIdMatch.installedExternalReleaseId, + }; + } else { + const nameMatch = currentAddons.find( + (addon) => impAddon.name.length > 0 && addon.name.toLowerCase() === impAddon.name.toLowerCase(), + ); + if (nameMatch) { + comparison.original = { + id: nameMatch.externalId, + name: nameMatch.name, + provider_name: nameMatch.providerName, + version_id: nameMatch.installedExternalReleaseId, + }; + } + } + + const [state, reason] = this.getImportState(comparison); + comparison.state = state; + comparison.conflictReason = reason; + + summary.comparisons.push(comparison); + } + + summary.addedCt = summary.comparisons.reduce(function (n, val) { + return n + (val.state === "added" ? 1 : 0); + }, 0); + + summary.conflictCt = summary.comparisons.reduce(function (n, val) { + return n + (val.state === "conflict" ? 1 : 0); + }, 0); + + summary.noChangeCt = summary.comparisons.reduce(function (n, val) { + return n + (val.state === "no-change" ? 1 : 0); + }, 0); + + // sort the results for better display + summary.comparisons = _.sortBy(summary.comparisons, (comp) => ImportStateWeights[comp.state]).reverse(); + + return summary; + } + + private isSameClient(srcClient: WowClientType, targetClient: string) { + const tct = WowClientType[targetClient] as WowClientType; + const srcGroup = getWowClientGroupForType(srcClient); + const targetGroup = getWowClientGroupForType(tct); + + return srcGroup === targetGroup; + } + + private getImportState(comparison: ImportComparison): [ImportState, ConflictReasonCode | undefined] { + if (comparison.imported && comparison.original) { + if (comparison.imported.provider_name !== comparison.original.provider_name) { + return ["conflict", "PROVIDER_MISMATCH"]; + } + + return comparison.imported.version_id !== comparison.original.version_id + ? ["conflict", "VERSION_MISMATCH"] + : ["no-change", undefined]; + } + + return ["added", undefined]; + } + + private getExportAddon(addon: Addon): ExportAddon { + if (!addon.externalId) { + throw new Error(`Addon externalId missing, cannot export: ${addon.name}`); + } + + const exportAddon: ExportAddon = { + id: addon.externalId, + name: addon.name, + provider_name: addon.providerName, + // release_type: addon.channelType + version_id: addon.installedExternalReleaseId ?? undefined, + }; + + return exportAddon; + } + + private getClientType(clientType: WowClientType): string { + return getEnumName(WowClientType, clientType); + } + + private addonIsIgnored(addon: Addon): boolean { + return addon.isIgnored !== false; + } +} diff --git a/WowUp/wowup-electron/src/app/services/addons/addon-fingerprint.service.ts b/WowUp/wowup-electron/src/app/services/addons/addon-fingerprint.service.ts new file mode 100644 index 0000000..7c63318 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/addons/addon-fingerprint.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from "@angular/core"; +import { AddonFolder, AddonScanResult } from "wowup-lib-core"; +import { IPC_CURSE_GET_SCAN_RESULTS, IPC_WOWUP_GET_SCAN_RESULTS } from "../../../common/constants"; +import { ElectronService } from "../electron/electron.service"; + +@Injectable({ + providedIn: "root", +}) +export class AddonFingerprintService { + public constructor(private _electronService: ElectronService) {} + + public async getFingerprints(addonFolders: AddonFolder[]) { + const filePaths = addonFolders.map((addonFolder) => addonFolder.path); + + console.time("WowUpScan"); + const wowUpScanResults: AddonScanResult[] = await this._electronService.invoke( + IPC_WOWUP_GET_SCAN_RESULTS, + filePaths + ); + console.timeEnd("WowUpScan"); + + console.time("CFScan"); + const cfScanResults: AddonScanResult[] = await this._electronService.invoke( + IPC_CURSE_GET_SCAN_RESULTS, + filePaths + ); + console.timeEnd("CFScan"); + + addonFolders.forEach((af) => { + af.wowUpScanResults = wowUpScanResults.find((wur) => wur.path === af.path); + af.cfScanResults = cfScanResults.find((cfr) => cfr.path === af.path); + }); + } +} diff --git a/WowUp/wowup-electron/src/app/services/addons/addon-install.service.ts b/WowUp/wowup-electron/src/app/services/addons/addon-install.service.ts new file mode 100644 index 0000000..35ddea5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/addons/addon-install.service.ts @@ -0,0 +1,694 @@ +import * as _ from "lodash"; +import { Injectable } from "@angular/core"; +import { from, mergeMap, Subject } from "rxjs"; +import slug from "slug"; +import { join as pathJoin } from "path"; +import { Addon, AddonExternalId, getGameVersion, getGameVersionList, Toc, WowClientType } from "wowup-lib-core"; +import { AddonInstallState } from "../../models/wowup/addon-install-state"; +import { AddonUpdateEvent } from "../../models/wowup/addon-update-event"; +import { capitalizeString } from "../../utils/string.utils"; +import { DownloadOptions, DownloadService } from "../download/download.service"; +import { WarcraftInstallationService } from "../warcraft/warcraft-installation.service"; +import { WarcraftService } from "../warcraft/warcraft.service"; +import { WowUpService } from "../wowup/wowup.service"; +import { AddonProviderFactory } from "./addon.provider.factory"; +import { nanoid } from "nanoid"; +import { FileService } from "../files/file.service"; +import { WowInstallation } from "wowup-lib-core"; +import { TocService } from "../toc/toc.service"; + +import { + ADDON_PROVIDER_RAIDERIO, + ADDON_PROVIDER_TUKUI, + ADDON_PROVIDER_UNKNOWN, + ADDON_PROVIDER_WAGO, + ADDON_PROVIDER_WOWINTERFACE, + ADDON_PROVIDER_WOWUP_COMPANION, + ADDON_PROVIDER_ZIP, + USER_ACTION_ADDON_INSTALL, +} from "../../../common/constants"; +import { AddonStorageService } from "../storage/addon-storage.service"; +import { AnalyticsService } from "../analytics/analytics.service"; +import { getEnumName } from "wowup-lib-core"; + +export type InstallType = "install" | "update" | "remove"; + +export interface InstallQueueItem { + addon: Addon; + onUpdate: (installState: AddonInstallState, progress: number) => void | undefined | Promise; + completion: any; + originalAddon?: Addon; + installType: InstallType; +} + +const IGNORED_FOLDER_NAMES = ["__MACOSX"]; + +const ADDON_PROVIDER_TOC_EXTERNAL_ID_MAP = { + [ADDON_PROVIDER_WOWINTERFACE]: "wowInterfaceId", + [ADDON_PROVIDER_TUKUI]: "tukUiProjectId", + [ADDON_PROVIDER_WAGO]: "wagoAddonId", +}; + +@Injectable({ + providedIn: "root", +}) +export class AddonInstallService { + private readonly _installQueue = new Subject(); + private readonly _installErrorSrc = new Subject(); + private readonly _addonInstalledSrc = new Subject(); + private readonly _addonRemovedSrc = new Subject(); + + public readonly addonInstalled$ = this._addonInstalledSrc.asObservable(); + public readonly installError$ = this._installErrorSrc.asObservable(); + + public constructor( + private _warcraftInstallationService: WarcraftInstallationService, + private _addonProviderService: AddonProviderFactory, + private _wowUpService: WowUpService, + private _warcraftService: WarcraftService, + private _downloadService: DownloadService, + private _fileService: FileService, + private _tocService: TocService, + private _addonStorage: AddonStorageService, + private _analyticsService: AnalyticsService, + ) { + // Setup our install queue pump here + this._installQueue.pipe(mergeMap((item) => from(this.processInstallQueue(item)), 3)).subscribe({ + next: (addonName) => { + console.log("Install complete", addonName); + }, + error: (error: Error) => { + console.error(error); + this._installErrorSrc.next(error); + }, + }); + } + + public enqueue(queueItem: InstallQueueItem): void { + this._installQueue.next(queueItem); + } + + private processInstallQueue = async (queueItem: InstallQueueItem): Promise => { + const onUpdate = queueItem.onUpdate; + + const addon = queueItem.addon; + + this.logAddonAction( + `Addon${capitalizeString(queueItem.installType)}`, + addon, + `'${addon.installedVersion ?? ""}' -> '${addon.latestVersion ?? ""}'`, + ); + + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + throw new Error(`Installation not found: ${addon.installationId ?? ""}`); + } + + const addonProvider = this._addonProviderService.getProvider(addon.providerName ?? ""); + if (!addonProvider) { + throw new Error(`Addon provider not found: ${addon.providerName ?? ""}`); + } + + const downloadFileName = `${slug(addon.name)}.zip`; + + onUpdate?.call(this, AddonInstallState.Downloading, 25); + this._addonInstalledSrc.next({ + addon, + installState: AddonInstallState.Downloading, + progress: 25, + }); + + let downloadedFilePath = ""; + let unzippedDirectory = ""; + + try { + const downloadAuth = await addonProvider.getDownloadAuth(); + + let retryCt = 0; + while (downloadedFilePath.length === 0) { + const downloadOptions: DownloadOptions = { + fileName: downloadFileName, + outputFolder: this._wowUpService.applicationDownloadsFolderPath, + url: addon.downloadUrl ?? "", + auth: downloadAuth, + }; + + try { + downloadedFilePath = await this._downloadService.downloadZipFile(downloadOptions); + } catch (e) { + if (retryCt === 3) { + throw e; + } + retryCt += 1; + console.log(`install download failed, retry ${retryCt}`); + + this._addonInstalledSrc.next({ + addon, + installState: AddonInstallState.Retry, + progress: 0, + }); + await onUpdate?.call(this, AddonInstallState.Retry, 0); + } + } + + onUpdate?.call(this, AddonInstallState.BackingUp, 50); + this._addonInstalledSrc.next({ + addon, + installState: AddonInstallState.BackingUp, + progress: 50, + }); + + const directoriesToBeRemoved = await this.backupOriginalDirectories(addon); + + onUpdate?.call(this, AddonInstallState.Installing, 75); + this._addonInstalledSrc.next({ + addon, + installState: AddonInstallState.Installing, + progress: 75, + }); + + const unzipPath = pathJoin(this._wowUpService.applicationDownloadsFolderPath, nanoid()); + + try { + unzippedDirectory = await this._fileService.unzipFile(downloadedFilePath, unzipPath); + await this.installUnzippedDirectory(unzippedDirectory, installation); + } catch (err) { + console.error(err); + + this.logAddonAction("RestoreBackup", addon, ...directoriesToBeRemoved); + await this.restoreAddonDirectories(directoriesToBeRemoved); + + throw err; + } finally { + await this._fileService.removeAllSafe(...directoriesToBeRemoved); + } + + const unzippedDirectoryNames = await this._fileService.listDirectories(unzippedDirectory); + _.remove(unzippedDirectoryNames, (dirName) => IGNORED_FOLDER_NAMES.includes(dirName)); + + const existingDirectoryNames = addon.installedFolderList ?? []; + const addedDirectoryNames = _.difference(unzippedDirectoryNames, existingDirectoryNames); + const removedDirectoryNames = _.difference(existingDirectoryNames, unzippedDirectoryNames); + + if (existingDirectoryNames.length > 0) { + this.logAddonAction("AddedDirs", addon, ...addedDirectoryNames); + } + + if (removedDirectoryNames.length > 0) { + this.logAddonAction("DiffDirs", addon, ...removedDirectoryNames); + } + + addon.installedExternalReleaseId = addon.externalLatestReleaseId; + addon.installedVersion = addon.latestVersion; + addon.installedAt = new Date(); + addon.installedFolderList = unzippedDirectoryNames; + addon.installedFolders = unzippedDirectoryNames.join(","); + addon.isIgnored = addonProvider.forceIgnore; + + const allTocFiles = await this._tocService.getAllTocs( + unzippedDirectory, + unzippedDirectoryNames, + addon.clientType, + ); + const gameVersion = this.getLatestGameVersion(allTocFiles); + if (gameVersion) { + addon.gameVersion = getGameVersionList([gameVersion]); + } + + if (!addon.author) { + addon.author = this.getBestGuessAuthor(allTocFiles); + } + + // If this is a zip file addon, try to pull the name out of the toc + if (addonProvider.name === ADDON_PROVIDER_ZIP) { + addon.name = this.getBestGuessTitle(allTocFiles); + } + + await this._addonStorage.setAsync(addon.id, addon); + + this.trackInstallAction(queueItem.installType, addon); + + // await this.installDependencies(addon, onUpdate); + + await this.backfillAddon(addon); + + if (queueItem.originalAddon) { + await this.reconcileExternalIds(addon, queueItem.originalAddon); + } + + await this.reconcileAddonFolders(addon); + + queueItem.completion.resolve(); + + onUpdate?.call(this, AddonInstallState.Complete, 100); + this._addonInstalledSrc.next({ + addon, + installState: AddonInstallState.Complete, + progress: 100, + }); + + this.logAddonAction( + `Addon${capitalizeString(queueItem.installType)}Complete`, + addon, + addon.installedVersion ?? "", + ); + } catch (err) { + console.error(err); + queueItem.completion.reject(err); + + onUpdate?.call(this, AddonInstallState.Error, 100); + this._addonInstalledSrc.next({ + addon, + installState: AddonInstallState.Error, + progress: 100, + }); + } finally { + const unzippedDirectoryExists = await this._fileService.pathExists(unzippedDirectory); + + const downloadedFilePathExists = await this._fileService.pathExists(downloadedFilePath); + + if (unzippedDirectoryExists) { + await this._fileService.remove(unzippedDirectory); + } + + if (downloadedFilePathExists) { + await this._fileService.remove(downloadedFilePath); + } + } + return addon.name; + }; + + private logAddonAction(action: string, addon: Addon, ...extras: string[]) { + console.log( + `[${action}] ${addon.providerName ?? ""} ${addon.externalId ?? "NO_EXT_ID"} ${addon.name} ${extras.join(" ")}`, + ); + } + + private async backupOriginalDirectories(addon: Addon): Promise { + const installedFolders = addon.installedFolderList ?? []; + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + return []; + } + + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + + const backupFolders: string[] = []; + for (const addonFolder of installedFolders) { + const currentAddonLocation = pathJoin(addonFolderPath, addonFolder); + const addonFolderBackupLocation = pathJoin(addonFolderPath, `${addonFolder}-bak`); + + await this._fileService.deleteIfExists(addonFolderBackupLocation); + + if (await this._fileService.pathExists(currentAddonLocation)) { + // Create the backup dir first + await this._fileService.createDirectory(addonFolderBackupLocation); + + // Copy current contents into the new backup dir, doing a rename has other implications so we copy + await this._fileService.copy(currentAddonLocation, addonFolderBackupLocation); + + // Delete the current version + await this._fileService.remove(currentAddonLocation); + + backupFolders.push(addonFolderBackupLocation); + } + } + + return backupFolders; + } + + private async installUnzippedDirectory(unzippedDirectory: string, installation: WowInstallation) { + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + const unzippedFolders = await this._fileService.listDirectories(unzippedDirectory); + for (const unzippedFolder of unzippedFolders) { + if (IGNORED_FOLDER_NAMES.includes(unzippedFolder)) { + continue; + } + const unzippedFilePath = pathJoin(unzippedDirectory, unzippedFolder); + const unzipLocation = pathJoin(addonFolderPath, unzippedFolder); + + try { + // Copy contents from unzipped new directory to existing addon folder location + await this._fileService.copy(unzippedFilePath, unzipLocation); + } catch (err) { + console.error(`Failed to copy addon directory ${unzipLocation}`); + throw err; + } + } + } + + private async restoreAddonDirectories(directories: string[]) { + try { + for (const directory of directories) { + const originalLocation = directory.substring(0, directory.length - 4); + + // If a backup directory exists, attempt to roll back + const dirExists = await this._fileService.pathExists(directory); + if (dirExists) { + // If the new addon folder was already created delete it + const originExists = await this._fileService.pathExists(originalLocation); + if (originExists) { + await this._fileService.remove(originalLocation); + } + + // Move the backup folder into the original location + await this._fileService.copy(directory, originalLocation); + } + } + } catch (e) { + console.error(`Failed to roll back directories`, directories, e); + } + } + + private getLatestGameVersion(tocs: Toc[]) { + const versions = tocs.map((toc) => +toc.interface); + const ordered = _.orderBy(versions, [], "desc"); + return getGameVersion(ordered[0]?.toString() || ""); + } + + private getBestGuessTitle(tocs: Toc[]) { + const titles = tocs.map((toc) => toc.title).filter((title) => !!title); + return _.maxBy(titles, (title) => title?.length ?? 0) ?? ""; + } + + private getBestGuessAuthor(tocs: Toc[]) { + const authors = tocs.map((toc) => toc.author).filter((author) => !!author); + return _.maxBy(authors, (author) => author?.length ?? 0); + } + + private trackInstallAction(installType: InstallType, addon: Addon) { + this._analyticsService.trackAction(USER_ACTION_ADDON_INSTALL, { + clientType: getEnumName(WowClientType, addon.clientType), + provider: addon.providerName, + addon: addon.name, + addonId: addon.externalId, + installType, + }); + } + + public async backfillAddon(addon: Addon): Promise { + if (addon.externalIds && this.containsOwnExternalId(addon)) { + return; + } + + try { + const tocPaths = await this.getTocPaths(addon); + const tocFiles = await Promise.all(tocPaths.map((tocPath) => this._tocService.parse(tocPath))); + const orderedTocFiles = _.orderBy(tocFiles, ["wowInterfaceId", "loadOnDemand"], ["desc", "asc"]); + const primaryToc = _.first(orderedTocFiles); + if (!primaryToc) { + throw new Error("Could not find primary toc"); + } + + this.setExternalIds(addon, primaryToc); + await this.saveAddon(addon); + } catch (e) { + console.error(e); + } + } + + public containsOwnExternalId(addon: Addon, array?: AddonExternalId[]): boolean { + const arr = array || addon.externalIds; + const result = + Array.isArray(arr) && !!arr.find((ext) => ext.id === addon.externalId && ext.providerName === addon.providerName); + return result; + } + + public async getTocPaths(addon: Addon): Promise { + if (!addon.installationId) { + return []; + } + + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + return []; + } + + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + + const addonTocs = await this._tocService.getAllTocs( + addonFolderPath, + addon.installedFolderList ?? [], + installation.clientType, + ); + + const tocPaths = addonTocs.map((toc) => toc.filePath); + return tocPaths; + } + + private setExternalIds(addon: Addon, toc: Toc) { + if (!toc) { + return; + } + + const externalIds: AddonExternalId[] = []; + for (const [key, value] of Object.entries(ADDON_PROVIDER_TOC_EXTERNAL_ID_MAP)) { + this.insertExternalId(externalIds, key, toc[value] as string); + } + + //If the addon does not include the current external id add it + if (!this.containsOwnExternalId(addon, externalIds)) { + if (!addon.providerName || !addon.externalId) { + return; + } + + this.insertExternalId(externalIds, addon.providerName, addon.externalId); + } + + addon.externalIds = externalIds; + } + + private reconcileExternalIds = async (newAddon: Addon, oldAddon: Addon): Promise => { + if (!newAddon || !oldAddon) { + return; + } + + // Ensure all previously existing external ids are brought along during the swap + // some addons are not always the same between providers ;) + oldAddon.externalIds?.forEach((oldExtId) => { + const match = newAddon.externalIds?.find( + (newExtId) => newExtId.id === oldExtId.id && newExtId.providerName === oldExtId.providerName, + ); + if (match) { + return; + } + console.log(`Reconciling external id: ${oldExtId.providerName}|${oldExtId.id}`); + newAddon.externalIds?.push({ ...oldExtId }); + }); + + // Remove external ids that are not valid that we may have saved previously + _.remove( + newAddon.externalIds ?? [], + (extId) => !this._addonProviderService.getProvider(extId.providerName)?.isValidAddonId(extId.id) ?? false, + ); + + await this.saveAddon(newAddon); + }; + + public async saveAddon(addon: Addon | undefined): Promise { + if (!addon) { + throw new Error("Invalid addon"); + } + + await this._addonStorage.setAsync(addon.id, addon); + } + + private async reconcileAddonFolders(addon: Addon) { + if (!addon.installationId) { + console.warn("addon installation id missing", addon); + return; + } + + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + console.warn("addon installation not found", addon.installationId); + return; + } + + let existingAddons = await this.getAddons(installation); + existingAddons = existingAddons.filter( + (ea) => ea.id !== addon.id && _.intersection(addon.installedFolderList, ea.installedFolderList).length > 0, + ); + + for (const existingAddon of existingAddons) { + if (existingAddon.providerName === ADDON_PROVIDER_UNKNOWN) { + await this.removeAddon(existingAddon, false, false); + } + } + } + + public async getAddons(installation: WowInstallation): Promise { + const addons = await this._addonStorage.getAllForInstallationIdAsync(installation.id); + return addons; + } + + public async removeAddon( + addon: Addon | undefined, + removeDependencies = false, + removeDirectories = true, + ): Promise { + if (addon === undefined) { + throw new Error("Invalid addon"); + } + + console.log(`[RemoveAddon] ${addon.providerName ?? ""} ${addon.externalId ?? "NO_EXT_ID"} ${addon.name}`); + + const installedDirectories = addon.installedFolderList ?? []; + if (removeDirectories && installedDirectories.length > 0) { + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + console.warn("No installation found for remove", addon.installationId); + return; + } + + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + + let failureCt = 0; + for (const directory of installedDirectories) { + const addonDirectory = pathJoin(addonFolderPath, directory); + console.log( + `[RemoveAddonDirectory] ${addon.providerName ?? ""} ${addon.externalId ?? "NO_EXT_ID"} ${addonDirectory}`, + ); + try { + await this._fileService.deleteIfExists(addonDirectory); + } catch (e) { + console.error(e); + failureCt += 1; + } + } + + if (failureCt === installedDirectories.length) { + throw new Error("Failed to remove all directories"); + } + } + + await this._addonStorage.removeAsync(addon); + + if (typeof addon.id === "string") { + this._addonRemovedSrc.next(addon.id); + } + + if (removeDependencies) { + await this.removeDependencies(addon); + } + + this.trackInstallAction("remove", addon); + } + + public insertExternalId(externalIds: AddonExternalId[], providerName: string, addonId?: string): void { + if (!addonId || [ADDON_PROVIDER_RAIDERIO, ADDON_PROVIDER_WOWUP_COMPANION].includes(providerName)) { + return; + } + + const exists = externalIds.findIndex((extId) => extId.id === addonId && extId.providerName === providerName) !== -1; + + if (exists) { + console.debug(`External id exists ${providerName}|${addonId}`); + return; + } + + if (this._addonProviderService.getProvider(providerName)?.isValidAddonId(addonId) ?? false) { + externalIds.push({ + id: addonId, + providerName: providerName, + }); + } else { + console.warn(`Invalid provider id ${providerName}|${addonId}`); + console.warn(externalIds); + } + } + + private async removeDependencies(addon: Addon) { + const dependencies = addon.dependencies ?? []; + for (const dependency of dependencies) { + if (!dependency.externalAddonId) { + console.warn("No external addon id for dependency", dependency); + continue; + } + + if (!addon.providerName || !addon.installationId) { + console.warn("Invalid addon for dependency", addon); + continue; + } + + const dependencyAddon = await this.getByExternalId( + dependency.externalAddonId, + addon.providerName, + addon.installationId, + ); + if (!dependencyAddon) { + console.log(`${addon.name}: Dependency not found ${dependency.externalAddonId}`); + continue; + } + + await this.removeAddon(dependencyAddon); + } + } + + public async getByExternalId( + externalId: string, + providerName: string, + installationId: string, + ): Promise { + return await this._addonStorage.getByExternalIdAsync(externalId, providerName, installationId); + } + + // public async installDependencies( + // addon: Addon, + // onUpdate: (installState: AddonInstallState, progress: number) => void = () => {} + // ): Promise { + // if (!addon.dependencies || !addon.providerName || !addon.installationId) { + // console.warn(`Invalid addon: ${addon.id ?? ""}`); + // return; + // } + + // const requiredDependencies = this.getRequiredDependencies(addon); + // if (!requiredDependencies.length) { + // console.log(`${addon.name}: No required dependencies found`); + // return; + // } + + // const maxCt = requiredDependencies.length; + // let currentCt = 0; + // for (const dependency of requiredDependencies) { + // currentCt += 1; + // const percent = (currentCt / maxCt) * 100; + + // onUpdate?.call(this, AddonInstallState.Installing, percent); + + // // If the dependency is already installed, skip it + // const existingAddon = await this.getByExternalId( + // dependency.externalAddonId, + // addon.providerName, + // addon.installationId + // ); + // if (existingAddon) { + // continue; + // } + + // const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + // if (!installation) { + // throw new Error("Installation not found"); + // } + + // const dependencyAddon = await this.getAddon( + // dependency.externalAddonId, + // addon.providerName, + // installation + // ).toPromise(); + + // if (!dependencyAddon || !dependencyAddon.id) { + // console.warn( + // `No addon was found EID: ${dependency.externalAddonId} CP: ${addon.providerName ?? ""} CT: ${ + // addon.clientType + // }` + // ); + // continue; + // } + + // await this._addonStorage.setAsync(dependencyAddon.id, dependencyAddon); + + // await this.installAddon(dependencyAddon); + // } + // } +} diff --git a/WowUp/wowup-electron/src/app/services/addons/addon-ui.service.ts b/WowUp/wowup-electron/src/app/services/addons/addon-ui.service.ts new file mode 100644 index 0000000..7c883fb --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/addons/addon-ui.service.ts @@ -0,0 +1,106 @@ +import { Injectable } from "@angular/core"; +import { MatDialog, MatDialogRef } from "@angular/material/dialog"; +import { TranslateService } from "@ngx-translate/core"; +import { from, Observable, of } from "rxjs"; +import { first, map, switchMap } from "rxjs/operators"; +import { Addon } from "wowup-lib-core"; +import { ConfirmDialogComponent } from "../../components/common/confirm-dialog/confirm-dialog.component"; +import { SnackbarService } from "../snackbar/snackbar.service"; +import { AddonService } from "./addon.service"; + +export interface RemoveAddonResult { + dependenciesRemoved: boolean; + removed: boolean; +} + +@Injectable({ + providedIn: "root", +}) +export class AddonUiService { + public constructor( + private _addonService: AddonService, + private _snackbarService: SnackbarService, + private _translateService: TranslateService, + private _dialog: MatDialog + ) {} + + public handleRemoveAddon(addon: Addon): Observable { + return this.getRemoveAddonPrompt(addon.name) + .afterClosed() + .pipe( + first(), + switchMap((result) => { + if (!result) { + return of({ dependenciesRemoved: false, removed: false }); + } + + if (this._addonService.getRequiredDependencies(addon).length === 0) { + return from(this._addonService.removeAddon(addon)).pipe( + map(() => { + this._snackbarService.showSuccessSnackbar("PAGES.MY_ADDONS.ADDON_REMOVED_SNACKBAR", { + localeArgs: { + addonName: addon.name, + }, + }); + return { dependenciesRemoved: false, removed: true }; + }) + ); + } else { + return this.getRemoveDependenciesPrompt(addon.name, (addon.dependencies ?? []).length) + .afterClosed() + .pipe( + switchMap((result: boolean) => from(this._addonService.removeAddon(addon, result))), + map(() => { + this._snackbarService.showSuccessSnackbar("PAGES.MY_ADDONS.ADDON_REMOVED_SNACKBAR", { + localeArgs: { + addonName: addon.name, + }, + }); + return { dependenciesRemoved: true, removed: true }; + }) + ); + } + }) + ); + } + + private getRemoveAddonPrompt(addonName: string): MatDialogRef { + const title: string = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.TITLE", { count: 1 }); + const message1: string = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_ONE", { + addonName, + }); + const message2: string = this._translateService.instant( + "PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_ACTION_EXPLANATION" + ); + const message = `${message1}\n\n${message2}`; + + return this._dialog.open(ConfirmDialogComponent, { + data: { + title, + message, + }, + }); + } + + private getRemoveDependenciesPrompt( + addonName: string, + dependencyCount: number + ): MatDialogRef { + const title = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.DEPENDENCY_TITLE"); + const message1: string = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.DEPENDENCY_MESSAGE", { + addonName, + dependencyCount, + }); + const message2: string = this._translateService.instant( + "PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_ACTION_EXPLANATION" + ); + const message = `${message1}\n\n${message2}`; + + return this._dialog.open(ConfirmDialogComponent, { + data: { + title, + message, + }, + }); + } +} diff --git a/WowUp/wowup-electron/src/app/services/addons/addon.provider.factory.ts b/WowUp/wowup-electron/src/app/services/addons/addon.provider.factory.ts new file mode 100644 index 0000000..794cebc --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/addons/addon.provider.factory.ts @@ -0,0 +1,299 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { AddonProvider, AddonProviderType } from "wowup-lib-core"; +import { GitHubAddonProvider } from "../../addon-providers/github-addon-provider"; +import { RaiderIoAddonProvider } from "../../addon-providers/raiderio-provider"; +import { ZipAddonProvider } from "../../addon-providers/zip-provider"; +import { WowUpCompanionAddonProvider } from "../../addon-providers/wowup-companion-addon-provider"; +import { CachingService } from "../caching/caching-service"; +import { ElectronService } from "../electron/electron.service"; +import { WowUpService } from "../wowup/wowup.service"; +import { NetworkService } from "../network/network.service"; +import { FileService } from "../files/file.service"; +import { TocService } from "../toc/toc.service"; +import { WarcraftService } from "../warcraft/warcraft.service"; +import { WagoAddonProvider } from "../../addon-providers/wago-addon-provider"; +import { AddonProviderState } from "../../models/wowup/addon-provider-state"; +import { ADDON_PROVIDER_UNKNOWN, WAGO_PROMPT_KEY } from "../../../common/constants"; +import { Subject } from "rxjs"; +import { PreferenceStorageService } from "../storage/preference-storage.service"; +import { SensitiveStorageService } from "../storage/sensitive-storage.service"; +import { UiMessageService } from "../ui-message/ui-message.service"; +import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider"; +import { WowUpAddonProvider, WowInterfaceAddonProvider, TukUiAddonProvider } from "wowup-lib-core"; +import { AppConfig } from "../../../environments/environment"; +import { GenericNetworkInterface } from "../../business-objects/generic-network-interface"; + +@Injectable({ + providedIn: "root", +}) +export class AddonProviderFactory { + private readonly _addonProviderChangeSrc = new Subject(); + + private _providerMap: Map = new Map(); + + public readonly addonProviderChange$ = this._addonProviderChangeSrc.asObservable(); + + private _wowupNetworkInterface: GenericNetworkInterface; + private _wowInterfaceNetworkInterface: GenericNetworkInterface; + private _tukuiNetworkInterface: GenericNetworkInterface; + + public constructor( + private _cachingService: CachingService, + private _electronService: ElectronService, + private _httpClient: HttpClient, + private _wowupService: WowUpService, + private _networkService: NetworkService, + private _fileService: FileService, + private _tocService: TocService, + private _warcraftService: WarcraftService, + private _preferenceStorageService: PreferenceStorageService, + private _sensitiveStorageService: SensitiveStorageService, + private _uiMessageService: UiMessageService + ) { + this._wowupNetworkInterface = new GenericNetworkInterface( + this._networkService.getCircuitBreaker( + "wowup_addon_provider", + AppConfig.defaultHttpResetTimeoutMs, + AppConfig.wowUpHubHttpTimeoutMs + ) + ); + + this._wowInterfaceNetworkInterface = new GenericNetworkInterface( + this._networkService.getCircuitBreaker( + "wow_interface_provider", + AppConfig.defaultHttpResetTimeoutMs, + AppConfig.wowUpHubHttpTimeoutMs + ) + ); + + this._tukuiNetworkInterface = new GenericNetworkInterface( + this._networkService.getCircuitBreaker( + "tukui_provider", + AppConfig.defaultHttpResetTimeoutMs, + AppConfig.wowUpHubHttpTimeoutMs + ) + ); + } + + /** This is part of the APP_INITIALIZER and called before the app is bootstrapped */ + public async loadProviders(): Promise { + if (this._providerMap.size !== 0) { + return; + } + const providers: AddonProvider[] = [ + this.createZipAddonProvider(), + this.createRaiderIoAddonProvider(), + this.createWowUpCompanionAddonProvider(), + this.createWowUpAddonProvider(), + this.createWagoAddonProvider(), + this.createCurseProvider(), + this.createTukUiAddonProvider(), + this.createWowInterfaceAddonProvider(), + this.createGitHubAddonProvider(), + ]; + + for (const provider of providers) { + await this.setProviderState(provider); + this._providerMap.set(provider.name, provider); + } + } + + public async shouldShowConsentDialog(): Promise { + return (await this._preferenceStorageService.getAsync(WAGO_PROMPT_KEY)) === undefined; + } + + public async updateWagoConsent(): Promise { + return await this._preferenceStorageService.setAsync(WAGO_PROMPT_KEY, true); + } + + public async setProviderEnabled(type: AddonProviderType, enabled: boolean): Promise { + if (!this._providerMap.has(type)) { + throw new Error(`cannot set provider state, not found: ${type}`); + } + + const provider = this._providerMap.get(type); + if (!provider || !provider.allowEdit) { + throw new Error(`this provider is not editable: ${type}`); + } + + await this._wowupService.setAddonProviderState({ + providerName: type, + enabled: enabled, + canEdit: true, + }); + + provider.enabled = enabled; + this._addonProviderChangeSrc.next(provider); + } + + public createWagoAddonProvider(): WagoAddonProvider { + return new WagoAddonProvider( + this._electronService, + this._cachingService, + this._warcraftService, + this._tocService, + this._uiMessageService, + this._sensitiveStorageService, + this._networkService + ); + } + + public createWowUpCompanionAddonProvider(): WowUpCompanionAddonProvider { + return new WowUpCompanionAddonProvider(this._fileService, this._tocService); + } + + public createRaiderIoAddonProvider(): RaiderIoAddonProvider { + return new RaiderIoAddonProvider(this._tocService); + } + + public createCurseProvider(): CurseAddonProvider { + return new CurseAddonProvider(this._cachingService, this._networkService, this._tocService, this._sensitiveStorageService); + } + + public createTukUiAddonProvider(): TukUiAddonProvider { + return new TukUiAddonProvider(this._tukuiNetworkInterface); + } + + public createWowInterfaceAddonProvider(): WowInterfaceAddonProvider { + return new WowInterfaceAddonProvider(this._wowInterfaceNetworkInterface); + } + + public createGitHubAddonProvider(): GitHubAddonProvider { + return new GitHubAddonProvider(this._httpClient, this._sensitiveStorageService); + } + + public createWowUpAddonProvider(): WowUpAddonProvider { + return new WowUpAddonProvider(AppConfig.wowUpHubUrl, AppConfig.wowUpWebsiteUrl, this._wowupNetworkInterface); + } + + public createZipAddonProvider(): ZipAddonProvider { + return new ZipAddonProvider(this._httpClient, this._fileService, this._tocService, this._warcraftService); + } + + public getProvider(providerName: string): T | undefined { + if (!providerName || !this.hasProvider(providerName)) { + return undefined; + } + + return this._providerMap.get(providerName) as any; + } + + public hasProvider(providerName: string): boolean { + return this._providerMap.has(providerName); + } + + public getAddonProviderForUri(addonUri: URL): AddonProvider | undefined { + for (const ap of this._providerMap.values()) { + if (ap.isValidAddonUri(addonUri)) { + return ap; + } + } + + return undefined; + } + + public getEnabledAddonProviders(): AddonProvider[] { + const providers: AddonProvider[] = []; + + this._providerMap.forEach((ap) => { + if (ap.enabled) { + providers.push(ap); + } + }); + + return providers; + } + + public getBatchAddonProviders(): AddonProvider[] { + const providers: AddonProvider[] = []; + + this._providerMap.forEach((ap) => { + if (ap.enabled && ap.canBatchFetch) { + providers.push(ap); + } + }); + + return providers; + } + + public getStandardAddonProviders(): AddonProvider[] { + const providers: AddonProvider[] = []; + + this._providerMap.forEach((ap) => { + if (ap.enabled && !ap.canBatchFetch) { + providers.push(ap); + } + }); + + return providers; + } + + public getAdRequiredProviders(): AddonProvider[] { + const providers: AddonProvider[] = []; + + this._providerMap.forEach((ap) => { + if (ap.enabled && ap.adRequired) { + providers.push(ap); + } + }); + + return providers; + } + + public getAddonProviderStates(): AddonProviderState[] { + const states: AddonProviderState[] = []; + + this._providerMap.forEach((ap) => { + states.push({ + providerName: ap.name, + enabled: ap.enabled, + canEdit: ap.allowEdit, + }); + }); + + return states; + } + + public canShowChangelog(providerName: string | undefined): boolean { + if (providerName === undefined) { + return false; + } + + return this.getProvider(providerName)?.canShowChangelog ?? false; + } + + public isForceIgnore(providerName: string): boolean { + const provider = this.getProvider(providerName); + if (!provider) { + return false; + } + + return providerName === ADDON_PROVIDER_UNKNOWN || (provider?.forceIgnore ?? false); + } + + public canReinstall(providerName: string): boolean { + const provider = this.getProvider(providerName); + if (!provider) { + return false; + } + + return providerName !== ADDON_PROVIDER_UNKNOWN && (provider?.allowReinstall ?? false); + } + + public canChangeChannel(providerName: string): boolean { + const provider = this.getProvider(providerName); + if (!provider) { + return false; + } + + return providerName !== ADDON_PROVIDER_UNKNOWN && (provider?.allowChannelChange ?? false); + } + + private setProviderState = async (provider: AddonProvider): Promise => { + const state = await this._wowupService.getAddonProviderState(provider.name); + if (state) { + provider.enabled = state.enabled; + } + }; +} diff --git a/WowUp/wowup-electron/src/app/services/addons/addon.service.ts b/WowUp/wowup-electron/src/app/services/addons/addon.service.ts new file mode 100644 index 0000000..27378e3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/addons/addon.service.ts @@ -0,0 +1,1814 @@ +import * as _ from "lodash"; +import * as path from "path"; +import { BehaviorSubject, firstValueFrom, forkJoin, from, Observable, of, Subject } from "rxjs"; +import { catchError, filter, first, map, switchMap, tap } from "rxjs/operators"; +import { v4 as uuidv4 } from "uuid"; + +import { Injectable } from "@angular/core"; + +import { + ADDON_PROVIDER_CURSEFORGE, + ADDON_PROVIDER_CURSEFORGEV2, + ADDON_PROVIDER_HUB, + ADDON_PROVIDER_HUB_LEGACY, + ADDON_PROVIDER_RAIDERIO, + ADDON_PROVIDER_TUKUI, + ADDON_PROVIDER_UNKNOWN, + ADDON_PROVIDER_WAGO, + ADDON_PROVIDER_WOWINTERFACE, + ADDON_PROVIDER_WOWUP_COMPANION, + ERROR_ADDON_ALREADY_INSTALLED, + USER_ACTION_ADDON_INSTALL, + USER_ACTION_ADDON_PROTOCOL_SEARCH, + USER_ACTION_ADDON_SEARCH, + USER_ACTION_BROWSE_CATEGORY, +} from "../../../common/constants"; + +import { AddonScanError, AddonSyncError, GenericProviderError } from "../../errors"; +import { AddonInstallState } from "../../models/wowup/addon-install-state"; +import { AddonUpdateEvent } from "../../models/wowup/addon-update-event"; +import * as AddonUtils from "../../utils/addon.utils"; +import { getEnumName, getGameVersionList, WowInstallation, WowUpAddonProvider } from "wowup-lib-core"; +import * as SearchResults from "../../utils/search-result.utils"; +import { AnalyticsService } from "../analytics/analytics.service"; +import { FileService } from "../files/file.service"; +import { AddonStorageService } from "../storage/addon-storage.service"; +import { TocService } from "../toc/toc.service"; +import { WarcraftInstallationService } from "../warcraft/warcraft-installation.service"; +import { WarcraftService } from "../warcraft/warcraft.service"; +import { WowUpService } from "../wowup/wowup.service"; +import { AddonProviderFactory } from "./addon.provider.factory"; +import { AddonFingerprintService } from "./addon-fingerprint.service"; +import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider"; +import { + Addon, + AddonCategory, + AddonChannelType, + AddonDependency, + AddonDependencyType, + AddonExternalId, + AddonFolder, + AddonProvider, + AddonSearchResult, + AddonSearchResultDependency, + AddonSearchResultFile, + AddonWarningType, + ProtocolSearchResult, + SearchByUrlResult, + Toc, + WowClientType, +} from "wowup-lib-core"; +import { AddonInstallService, InstallQueueItem, InstallType } from "./addon-install.service"; +import { strIsNotNullOrEmpty } from "../../utils/string.utils"; +import { delayMs } from "../../utils/time.utils"; + +export enum ScanUpdateType { + Start, + Update, + Complete, + Unknown, +} + +export interface ScanUpdate { + type: ScanUpdateType; + totalCount?: number; + currentCount?: number; +} + +export type AddonActionType = "scan" | "sync"; +export interface AddonActionEvent { + type: AddonActionType; + addon?: Addon; +} + +const IGNORED_FOLDER_NAMES = ["__MACOSX"]; + +const ADDON_PROVIDER_TOC_EXTERNAL_ID_MAP = { + [ADDON_PROVIDER_WOWINTERFACE]: "wowInterfaceId", + [ADDON_PROVIDER_TUKUI]: "tukUiProjectId", + [ADDON_PROVIDER_CURSEFORGE]: "curseProjectId", + [ADDON_PROVIDER_CURSEFORGEV2]: "curseProjectId", + [ADDON_PROVIDER_WAGO]: "wagoAddonId", +}; + +@Injectable({ + providedIn: "root", +}) +export class AddonService { + private readonly _addonActionSrc = new Subject(); + private readonly _addonRemovedSrc = new Subject(); + private readonly _scanUpdateSrc = new BehaviorSubject({ type: ScanUpdateType.Unknown }); + private readonly _syncErrorSrc = new Subject(); + private readonly _scanErrorSrc = new Subject(); + private readonly _searchErrorSrc = new Subject(); + private readonly _anyUpdatesAvailableSrc = new BehaviorSubject(false); + private readonly _addonProviderChangeSrc = new Subject(); + private readonly _syncingSrc = new BehaviorSubject(false); + + private _activeInstalls: AddonUpdateEvent[] = []; + + public readonly addonAction$ = this._addonActionSrc.asObservable(); + public readonly addonInstalled$ = this._addonInstallService.addonInstalled$; + public readonly addonRemoved$ = this._addonRemovedSrc.asObservable(); + public readonly scanUpdate$ = this._scanUpdateSrc.asObservable(); + public readonly installError$ = this._addonInstallService.installError$; + public readonly syncError$ = this._syncErrorSrc.asObservable(); + public readonly scanError$ = this._scanErrorSrc.asObservable(); + public readonly searchError$ = this._searchErrorSrc.asObservable(); + public readonly anyUpdatesAvailable$ = this._anyUpdatesAvailableSrc.asObservable(); + public readonly addonProviderChange$ = this._addonProviderChangeSrc.asObservable(); + public readonly syncing$ = this._syncingSrc.asObservable(); + + public constructor( + private _addonStorage: AddonStorageService, + private _addonInstallService: AddonInstallService, + private _analyticsService: AnalyticsService, + private _warcraftService: WarcraftService, + private _wowUpService: WowUpService, + private _fileService: FileService, + private _tocService: TocService, + private _warcraftInstallationService: WarcraftInstallationService, + private _addonProviderService: AddonProviderFactory, + private _addonFingerprintService: AddonFingerprintService, + ) { + // This should keep the current update queue state snapshot up to date + this.addonInstalled$ + .pipe( + tap(() => { + from(this.areAnyAddonsAvailableForUpdate()) + .pipe(first()) + .subscribe((updatesAvailable) => { + this._anyUpdatesAvailableSrc.next(updatesAvailable); + }); + }), + ) + .subscribe(this.updateActiveInstall); + + this.addonRemoved$ + .pipe(switchMap(() => from(this.areAnyAddonsAvailableForUpdate()))) + .subscribe((updatesAvailable) => { + this._anyUpdatesAvailableSrc.next(updatesAvailable); + }); + + this.scanUpdate$ + .pipe(switchMap(() => from(this.areAnyAddonsAvailableForUpdate()))) + .subscribe((updatesAvailable) => { + this._anyUpdatesAvailableSrc.next(updatesAvailable); + }); + + // Attempt to remove addons for clients that were lost + this._warcraftInstallationService.wowInstallations$ + .pipe( + filter((installations) => installations.length > 0), + switchMap((clientTypes) => from(this.reconcileOrphanAddons(clientTypes))), + catchError((e) => { + console.error(`reconcileOrphanAddons failed`, e); + return of(undefined); + }), + ) + .subscribe(); + + // When legacy installation setup is complete, migrate the addons + this._warcraftInstallationService.legacyInstallationSrc$ + .pipe( + first(), + switchMap((installations) => from(this.handleLegacyInstallations(installations))), + ) + .subscribe(() => console.log(`Legacy installation addons finished`)); + + from(this.areAnyAddonsAvailableForUpdate()) + .pipe(first()) + .subscribe((updatesAvailable) => { + this._anyUpdatesAvailableSrc.next(updatesAvailable); + }); + } + + public isInstalling(addonId?: string): boolean { + if (!addonId) { + return this._activeInstalls.length > 0; + } + return this._activeInstalls.find((install) => install.addon.id === addonId) !== undefined; + } + + public getInstallStatus(addonId: string): AddonUpdateEvent | undefined { + return this._activeInstalls.find((install) => install.addon.id === addonId); + } + + public async hasUpdatesAvailable(installation: WowInstallation): Promise { + const addons = await this.getAddons(installation); + return addons.some((addon) => AddonUtils.needsUpdate(addon)); + } + + private async handleLegacyInstallations(installations: WowInstallation[]): Promise { + if (installations.length === 0) { + console.debug(`No legacy installations to migrate`); + return; + } + + const allAddons = await this._addonStorage.getAll(); + + for (const addon of allAddons) { + // Legacy addons will not have an installationId + if (addon.installationId) { + continue; + } + + const installation = installations.find((inst) => inst.clientType === addon.clientType); + if (!installation) { + continue; + } + + addon.installationId = installation.id; + await this.saveAddon(addon); + } + } + + public addonMatchesSearchResult(addon1: Addon, addon2: AddonSearchResult): boolean { + return ( + addon1?.externalId?.toString() === addon2?.externalId?.toString() && addon1.providerName === addon2.providerName + ); + } + + public async getCategoryPage(category: AddonCategory, installation: WowInstallation): Promise { + const providers = this._addonProviderService.getEnabledAddonProviders(); + + this._analyticsService.trackAction(USER_ACTION_BROWSE_CATEGORY, { + clientType: getEnumName(WowClientType, installation.clientType), + category: getEnumName(AddonCategory, category), + }); + + const resultSet: AddonSearchResult[][] = []; + for (const provider of providers) { + try { + const results = await provider.getCategory(category, installation); + resultSet.push(results); + } catch (e) { + console.error(e); + } + } + + return _.flatten(resultSet); + } + + public async getFullDescription( + installation: WowInstallation, + providerName: string, + externalId: string, + addon?: Addon, + ): Promise { + const provider = this._addonProviderService.getProvider(providerName); + if (!provider) { + return ""; + } + + return await provider.getDescription(installation, externalId, addon); + } + + public async getChangelogForSearchResult( + installation: WowInstallation, + channelType: AddonChannelType, + searchResult: AddonSearchResult, + ): Promise { + try { + const provider = this._addonProviderService.getProvider(searchResult.providerName); + if (!provider) { + return ""; + } + + const latestFile = SearchResults.getLatestFile(searchResult, channelType); + if (!latestFile) { + throw new Error("Latest file not found"); + } + + return await provider.getChangelog(installation, searchResult.externalId, latestFile.externalId ?? ""); + } catch (e) { + console.error("Failed to get searchResult changelog", e); + return ""; + } + } + + public async getChangelogForAddon(installation: WowInstallation, addon: Addon): Promise { + if (!addon) { + return ""; + } + + if (addon.latestChangelog && addon.latestChangelogVersion === addon.latestVersion) { + return addon.latestChangelog; + } + + try { + const provider = this._addonProviderService.getProvider(addon.providerName ?? ""); + if (!provider) { + return ""; + } + + const changelog = await provider.getChangelog( + installation, + addon.externalId ?? "", + addon.externalLatestReleaseId ?? "", + ); + + return changelog; + } catch (e) { + console.error("Failed to get addon changelog", e); + return ""; + } + } + + public async saveAddon(addon: Addon | undefined): Promise { + if (!addon) { + throw new Error("Invalid addon"); + } + + await this._addonStorage.setAsync(addon.id, addon); + } + + public async search(query: string, installation: WowInstallation): Promise { + const addonProviders = this._addonProviderService.getEnabledAddonProviders(); + const searchTasks: Promise[] = addonProviders.map(async (p) => { + try { + return await p.searchByQuery(query, installation); + } catch (e) { + console.error(`Failed during search: ${p.name}`, e); + this._searchErrorSrc.next(new GenericProviderError(e as Error, p.name)); + return []; + } + }); + + const searchResults = await Promise.all(searchTasks); + + this._analyticsService.trackAction(USER_ACTION_ADDON_SEARCH, { + clientType: getEnumName(WowClientType, installation.clientType), + query, + }); + + const flatResults = searchResults.flat(1); + + return _.orderBy(flatResults, "downloadCount").reverse(); + } + + public async installBaseAddon( + externalId: string, + providerName: string, + installation: WowInstallation, + onUpdate: (installState: AddonInstallState, progress: number) => void = () => {}, + targetFile?: AddonSearchResultFile, + ): Promise { + const existingAddon = await this.getByExternalId(externalId, providerName, installation.id); + if (existingAddon) { + throw new Error("Addon already installed"); + } + + const addon = await this.getAddon(externalId, providerName, installation, targetFile).toPromise(); + + if (addon?.id !== undefined) { + await this._addonStorage.setAsync(addon.id, addon); + await this.installAddon(addon, onUpdate); + return addon; + } + + return undefined; + } + + public async installPotentialAddon( + potentialAddon: AddonSearchResult, + installation: WowInstallation, + onUpdate: (installState: AddonInstallState, progress: number) => void = () => {}, + targetFile?: AddonSearchResultFile, + ): Promise { + const existingAddon = await this.getByExternalId( + potentialAddon.externalId, + potentialAddon.providerName, + installation.id, + ); + if (existingAddon) { + throw new Error("Addon already installed"); + } + + const latestFile = SearchResults.getLatestFile(potentialAddon, installation.defaultAddonChannelType); + if (!latestFile) { + console.warn(`Latest file not found`); + return undefined; + } + + const addon = this.createAddon(potentialAddon, targetFile ?? latestFile, installation); + + if (addon?.id !== undefined) { + await this.installAddon(addon, onUpdate); + } + } + + public getRequiredDependencies(addon: Addon): AddonDependency[] { + return Array.isArray(addon.dependencies) + ? addon.dependencies.filter((dep) => dep.type === AddonDependencyType.Required) + : []; + } + + public async getAllAddonsAvailableForUpdate(wowInstallation?: WowInstallation): Promise { + return await this._addonStorage.queryAllAsync((addon) => { + if (typeof wowInstallation === "object" && wowInstallation.id !== addon.installationId) { + return false; + } + + return addon.isIgnored !== true && AddonUtils.needsUpdate(addon); + }); + } + + public async installDependencies( + addon: Addon, + onUpdate: (installState: AddonInstallState, progress: number) => void = () => {}, + ): Promise { + if (!addon.dependencies || !addon.providerName || !addon.installationId) { + console.warn(`Invalid addon: ${addon.id ?? ""}`); + return; + } + + const requiredDependencies = this.getRequiredDependencies(addon); + if (!requiredDependencies.length) { + console.log(`${addon.name}: No required dependencies found`); + return; + } + + const maxCt = requiredDependencies.length; + let currentCt = 0; + for (const dependency of requiredDependencies) { + currentCt += 1; + const percent = (currentCt / maxCt) * 100; + + onUpdate?.call(this, AddonInstallState.Installing, percent); + + // If the dependency is already installed, skip it + const existingAddon = await this.getByExternalId( + dependency.externalAddonId, + addon.providerName, + addon.installationId, + ); + if (existingAddon) { + continue; + } + + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + throw new Error("Installation not found"); + } + + const dependencyAddon = await this.getAddon( + dependency.externalAddonId, + addon.providerName, + installation, + ).toPromise(); + + if (!dependencyAddon || !dependencyAddon.id) { + console.warn( + `No addon was found EID: ${dependency.externalAddonId} CP: ${addon.providerName ?? ""} CT: ${ + addon.clientType + }`, + ); + continue; + } + + await this._addonStorage.setAsync(dependencyAddon.id, dependencyAddon); + + await this.installAddon(dependencyAddon); + } + } + + public async processAutoUpdates(): Promise { + const autoUpdateAddons = await this.getAutoUpdateEnabledAddons(); + const addonsWithUpdates = autoUpdateAddons.filter((addon) => AddonUtils.needsUpdate(addon)); + + const tasks = addonsWithUpdates.map((addon) => { + if (typeof addon.id !== "string") { + return Promise.resolve(undefined); + } + return this.updateAddon(addon) + .then(() => addon) + .catch((e) => console.error(e)); + }); + + const results = await Promise.all(tasks); + return results.filter((res) => res !== undefined).map((res) => res as Addon); + } + + public async getAutoUpdateEnabledAddons(): Promise { + return await this._addonStorage.queryAllAsync((addon) => { + return addon.isIgnored !== true && addon.autoUpdateEnabled && !!addon.installationId; + }); + } + + public async getAllByExternalAddonId(externalAddonIds: string[]): Promise { + return await this._addonStorage.queryAllAsync((addon) => { + return externalAddonIds.includes(addon.externalId); + }); + } + + public async hasAnyWithExternalAddonIds(externalAddonIds: string[]): Promise { + const addons = await this.getAllByExternalAddonId(externalAddonIds); + return addons.length > 0; + } + + public updateAddon( + addon: Addon, + onUpdate: (installState: AddonInstallState, progress: number) => void = () => {}, + originalAddon: Addon | undefined = undefined, + ): Promise { + if (typeof addon !== "object") { + return Promise.resolve(undefined); + } + + return this.installOrUpdateAddon(addon, "update", onUpdate, originalAddon); + } + + public async installAddon( + addon: Addon, + onUpdate: (installState: AddonInstallState, progress: number) => void = () => {}, + originalAddon: Addon | undefined = undefined, + ): Promise { + if (typeof addon !== "object") { + console.warn("installAddon invalid addon id"); + return undefined; + } + + await this.installOrUpdateAddon(addon, "install", onUpdate, originalAddon); + await this._addonStorage.setAsync(addon.id, addon); + } + + public async installOrUpdateAddon( + addon: Addon, + installType: InstallType, + onUpdate: (installState: AddonInstallState, progress: number) => void = () => {}, + originalAddon: Addon | undefined = undefined, + ): Promise { + if (typeof addon !== "object" || !addon.downloadUrl) { + console.error("installOrUpdateAddon invalid addon", addon); + throw new Error(`Addon not found or invalid: ${addon?.id ?? "unknown"}`); + } + + const wowInstallation = this.getWowInstallation(addon); + const addonProvider = this.getAddonProvider(addon); + + const _onUpdate = async (installState: AddonInstallState, progress: number) => { + if (installState === AddonInstallState.Retry) { + await this.syncProviderAddons(wowInstallation, [addon], addonProvider); + await delayMs(1000); + return; + } + + onUpdate?.call(this, installState, progress); + }; + + onUpdate?.call(this, AddonInstallState.Pending, 0); + // const updateEvent: AddonUpdateEvent = { + // addon, + // installState: AddonInstallState.Pending, + // progress: 0, + // }; + // this._addonInstalledSrc.next(updateEvent); + + // create a ref for resolving or rejecting once the queue grabs this. + let completion = { resolve: () => {}, reject: () => {} }; + const promise = new Promise((resolve, reject) => { + completion = { resolve, reject }; + }); + + const installQueueItem: InstallQueueItem = { + addon, + onUpdate: _onUpdate, + completion, + installType, + originalAddon: originalAddon ? { ...originalAddon } : undefined, + }; + this._addonInstallService.enqueue(installQueueItem); + + return promise; + } + + /** + * Keep the snapshot of current progress items up to date + * Remove them when complete or error + */ + private updateActiveInstall = (updateEvent: AddonUpdateEvent): void => { + const itemIdx = this._activeInstalls.findIndex((install) => install.addon.id === updateEvent.addon.id); + if (itemIdx === -1) { + this._activeInstalls.push(updateEvent); + } + + if ([AddonInstallState.Complete, AddonInstallState.Error].includes(updateEvent.installState)) { + _.remove(this._activeInstalls, (install) => install.addon.id === updateEvent.addon.id); + } else { + this._activeInstalls.splice(itemIdx, 1, updateEvent); + } + }; + + public async logDebugData(): Promise { + const curseProvider = this._addonProviderService.getProvider(ADDON_PROVIDER_CURSEFORGE); + const hubProvider = this._addonProviderService.getProvider(ADDON_PROVIDER_HUB); + if (curseProvider === undefined) { + throw new Error("curse provider not found"); + } + if (hubProvider === undefined) { + throw new Error("hub provider not found"); + } + + const clientMap = {}; + const installations = await this._warcraftInstallationService.getWowInstallationsAsync(); + for (const installation of installations) { + const clientTypeName = getEnumName(WowClientType, installation.clientType); + + const useSymlinkMode = await this._wowUpService.getUseSymlinkMode(); + const addonFolders = await this._warcraftService.listAddons(installation, useSymlinkMode); + await this._addonFingerprintService.getFingerprints(addonFolders); + + const curseMap = {}; + const hubMap = {}; + addonFolders.forEach((af) => { + if (af.cfScanResults !== undefined) { + curseMap[af.cfScanResults.folderName] = af.cfScanResults.fingerprint; + } + + if (af.wowUpScanResults !== undefined) { + hubMap[af.wowUpScanResults.folderName] = af.wowUpScanResults.fingerprint; + } + }); + + clientMap[clientTypeName] = { + curse: curseMap, + hub: hubMap, + }; + + console.log(`clientType ${clientTypeName} addon fingerprints`); + } + + console.log(JSON.stringify(clientMap)); + } + + private async installUnzippedDirectory(unzippedDirectory: string, installation: WowInstallation) { + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + const unzippedFolders = await this._fileService.listDirectories(unzippedDirectory); + for (const unzippedFolder of unzippedFolders) { + if (IGNORED_FOLDER_NAMES.includes(unzippedFolder)) { + continue; + } + const unzippedFilePath = path.join(unzippedDirectory, unzippedFolder); + const unzipLocation = path.join(addonFolderPath, unzippedFolder); + + try { + // Copy contents from unzipped new directory to existing addon folder location + await this._fileService.copy(unzippedFilePath, unzipLocation); + } catch (err) { + console.error(`Failed to copy addon directory ${unzipLocation}`); + throw err; + } + } + } + + public getAddonById(addonId: string): Promise { + return this._addonStorage.get(addonId); + } + + public async getAddonByUrl(url: URL, installation: WowInstallation): Promise { + const provider = this._addonProviderService.getAddonProviderForUri(url); + if (!provider) { + console.warn(`No provider found for url: ${url.toString()}`); + return undefined; + } + + return await provider.searchByUrl(url, installation); + } + + public getAddon( + externalId: string, + providerName: string, + installation: WowInstallation, + targetFile?: AddonSearchResultFile, + ): Observable { + const targetAddonChannel = installation.defaultAddonChannelType; + const provider = this._addonProviderService.getProvider(providerName); + if (!provider) { + throw new Error(`Provider not found: ${providerName}`); + } + + return from(provider.getById(externalId, installation)).pipe( + map((searchResult) => { + if (!searchResult) { + console.warn("provider get by id returned nothing"); + return undefined; + } + + const latestFile = SearchResults.getLatestFile(searchResult, targetAddonChannel); + if (!latestFile) { + console.warn(`Latest file not found`); + return undefined; + } + + return this.createAddon(searchResult, targetFile ?? latestFile, installation); + }), + ); + } + + public getInstallBasePath(addon: Addon): string { + const installation = this.getWowInstallation(addon); + return this._warcraftService.getAddonFolderPath(installation); + } + + public getFullInstallPath(addon: Addon): string { + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + throw new Error(`installation not found: ${addon.installationId ?? ""}`); + } + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + return path.join(addonFolderPath, _.first(addon.installedFolderList) ?? ""); + } + + public async removeAddon( + addon: Addon | undefined, + removeDependencies = false, + removeDirectories = true, + ): Promise { + if (addon === undefined) { + throw new Error("Invalid addon"); + } + + console.log(`[RemoveAddon] ${addon.providerName ?? ""} ${addon.externalId ?? "NO_EXT_ID"} ${addon.name}`); + + const installedDirectories = addon.installedFolderList ?? []; + if (removeDirectories && installedDirectories.length > 0) { + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + console.warn("No installation found for remove", addon.installationId); + return; + } + + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + + let failureCt = 0; + for (const directory of installedDirectories) { + const addonDirectory = path.join(addonFolderPath, directory); + console.log( + `[RemoveAddonDirectory] ${addon.providerName ?? ""} ${addon.externalId ?? "NO_EXT_ID"} ${addonDirectory}`, + ); + try { + await this._fileService.deleteIfExists(addonDirectory); + } catch (e) { + console.error(e); + failureCt += 1; + } + } + + if (failureCt === installedDirectories.length) { + throw new Error("Failed to remove all directories"); + } + } + + await this._addonStorage.removeAsync(addon); + + this._addonRemovedSrc.next(addon.id); + + if (removeDependencies) { + await this.removeDependencies(addon); + } + + this.trackInstallAction("remove", addon); + } + + private async removeDependencies(addon: Addon) { + const dependencies = addon.dependencies ?? []; + for (const dependency of dependencies) { + if (!dependency.externalAddonId) { + console.warn("No external addon id for dependency", dependency); + continue; + } + + if (!addon.providerName || !addon.installationId) { + console.warn("Invalid addon for dependency", addon); + continue; + } + + const dependencyAddon = await this.getByExternalId( + dependency.externalAddonId, + addon.providerName, + addon.installationId, + ); + if (!dependencyAddon) { + console.log(`${addon.name}: Dependency not found ${dependency.externalAddonId}`); + continue; + } + + await this.removeAddon(dependencyAddon); + } + } + + public async getAllAddons(installation: WowInstallation): Promise { + return await this._addonStorage.getAllForInstallationIdAsync(installation.id); + } + + public async rescanInstallation(installation: WowInstallation): Promise { + if (!installation) { + return []; + } + + console.debug(`[addon-service] rescanInstallation: ${installation.displayName}`); + // Fetch existing installation addons + let addons = await this._addonStorage.getAllForInstallationIdAsync(installation.id); + + // Collect info on filesystem addons + const newAddons = await this.scanAddons(installation, addons); + + await this._addonStorage.removeAllForInstallationAsync(installation.id); + + // Map the old installation addon settings to the new ones + addons = this.updateAddons(addons, newAddons); + + console.debug("addons", addons); + await this._addonStorage.saveAll(addons); + + this._addonActionSrc.next({ type: "scan" }); + + return addons; + } + + public async getProviderAddons(providerName: string): Promise { + if (!providerName) { + return []; + } + + return await this._addonStorage.getAllForProviderAsync(providerName); + } + + public async getAddons(installation: WowInstallation, rescan = false): Promise { + if (!installation) { + return []; + } + + let addons = await this._addonStorage.getAllForInstallationIdAsync(installation.id); + + if (rescan || addons.length === 0) { + addons = await this.rescanInstallation(installation); + } + + return addons; + } + + public async getAddonForProtocol(protocol: string): Promise { + const addonProvider = this.getAddonProviderForProtocol(protocol); + if (!addonProvider) { + throw new Error(`No addon provider found for protocol ${protocol}`); + } + + this._analyticsService.trackAction(USER_ACTION_ADDON_PROTOCOL_SEARCH, { + protocol, + }); + + return await addonProvider.searchProtocol(protocol); + } + + private getAddonProviderForProtocol(protocol: string): AddonProvider | undefined { + return this._addonProviderService.getEnabledAddonProviders().find((provider) => provider.isValidProtocol(protocol)); + } + + /** Iterate over all the installed WoW clients and attempt to check for addon updates */ + public async syncAllClients(): Promise { + console.debug("syncAllClients"); + this._syncingSrc.next(true); + + const installations = await this._warcraftInstallationService.getWowInstallationsAsync(); + + try { + await this.syncBatchProviders(installations); + await this.syncStandardProviders(installations); + } catch (e) { + console.error(e); + } finally { + this._syncingSrc.next(false); + this._addonActionSrc.next({ type: "sync" }); + } + } + + /** Check for updates for all addons installed for the give WoW client */ + public async syncClient(installation: WowInstallation): Promise { + console.debug("syncClient", installation.displayName); + await this.syncBatchProviders([installation]); + + try { + await this.syncStandardProviders([installation]); + } catch (e) { + console.error(e); + } + } + + /** Transform a list of addons into a list of their external IDs while removing empty or undefined values */ + private getExternalIds(addons: Addon[]): string[] { + return addons.map((addon) => addon.externalId).filter((externalId) => !!externalId); + } + + /** Iterate over all batch enabled addon providers and combine all + * external addon IDs into a single request to each batch enabled provider + * */ + private async syncBatchProviders(installations: WowInstallation[]) { + console.debug(`syncBatchProviders`); + const batchedAddonProviders = this._addonProviderService.getBatchAddonProviders(); + + for (const provider of batchedAddonProviders) { + try { + // Get a list of all installed addons for this provider across all WoW installs + const allAddons = await this._addonStorage.getAllForProviderAsync(provider.name); + if (allAddons.length === 0) { + continue; + } + + const batchedAddons = allAddons.filter((addon) => addon.isIgnored === false); + + const addonIds = this.getExternalIds(batchedAddons); + const searchResults = await provider.getAllBatch(installations, addonIds); + + // Process the errors for each installation + for (const key of Object.keys(searchResults.errors)) { + const errors = searchResults.errors[key]; + if (errors.length === 0) { + continue; + } + + const installation = installations.find((i) => i.id === key); + const installationAddons = batchedAddons.filter((addon) => addon.installationId === key); + await this.handleSyncErrors(installation, errors, provider, installationAddons); + } + + // Process the update results for each installation + for (const key of Object.keys(searchResults.installationResults)) { + const addonSearchResults = searchResults.installationResults[key]; + if (addonSearchResults.length === 0) { + continue; + } + + const installation = installations.find((i) => i.id === key); + const installationAddons = batchedAddons.filter((addon) => addon.installationId === key); + await this.handleSyncResults(addonSearchResults, installationAddons, installation); + } + } catch (e) { + console.error(e); + } + } + } + + private async syncStandardProviders(installations: WowInstallation[]): Promise { + console.info(`syncStandardProviders`); + let didSync = true; + + const addonProviders = this._addonProviderService.getStandardAddonProviders(); + for (const provider of addonProviders) { + for (const installation of installations) { + // fetch all the addons for this WoW client + const addons = await this._addonStorage.getAllForInstallationIdAsync(installation.id); + const validAddons = addons.filter((addon) => addon.isIgnored === false); + + try { + await this.syncProviderAddons(installation, validAddons, provider); + } catch (e) { + console.error(`Failed to sync from provider: ${provider.name}`, e); + this._syncErrorSrc.next( + new AddonSyncError({ + providerName: provider.name, + installationName: installation.displayName, + innerError: e, + }), + ); + didSync = false; + } + } + } + + const updatesAvailable = await this.areAnyAddonsAvailableForUpdate(); + this._anyUpdatesAvailableSrc.next(updatesAvailable); + + return didSync; + } + + private updateAddons(existingAddons: Addon[], newAddons: Addon[]) { + for (const newAddon of newAddons) { + const existingAddon = existingAddons.find( + (ea) => + ea.externalId?.toString() === newAddon.externalId?.toString() && ea.providerName == newAddon.providerName, + ); + + if (!existingAddon) { + continue; + } + + newAddon.autoUpdateEnabled = existingAddon.autoUpdateEnabled; + newAddon.isIgnored = existingAddon.isIgnored; + newAddon.installedAt = existingAddon.installedAt; + newAddon.channelType = Math.max(existingAddon.channelType, newAddon.channelType); + } + + return newAddons; + } + + private async syncProviderAddons(installation: WowInstallation, addons: Addon[], addonProvider: AddonProvider) { + const providerAddonIds = this.getExternalIdsForProvider(addonProvider, addons); + if (!providerAddonIds.length) { + return; + } + + const getAllResult = await addonProvider.getAll(installation, providerAddonIds); + await this.handleSyncErrors(installation, getAllResult.errors, addonProvider, addons); + await this.handleSyncResults(getAllResult.searchResults, addons, installation); + } + + private async handleSyncResults( + addonSearchResults: AddonSearchResult[], + addons: Addon[], + installation: WowInstallation, + ): Promise { + for (const result of addonSearchResults) { + const addon = addons.find((addon) => this.addonMatchesSearchResult(addon, result)); + if (!addon) { + continue; + } + + try { + const latestFile = SearchResults.getLatestFile(result, addon?.channelType); + if (!latestFile) { + console.warn(`No latest file found: ${addon.name}, clientType: ${addon.clientType}`); + + addon.warningType = AddonWarningType.NoProviderFiles; + await this._addonStorage.setAsync(addon.id, addon); + + this._syncErrorSrc.next( + new AddonSyncError({ + providerName: addon.providerName ?? "", + installationName: installation.displayName, + addonName: addon?.name, + }), + ); + continue; + } + + await this.setExternalIdString(addon); + + addon.summary = result.summary; + addon.thumbnailUrl = result.thumbnailUrl; + addon.latestChangelog = latestFile?.changelog || addon.latestChangelog; + + if ( + addon.warningType && + [AddonWarningType.MissingOnProvider, AddonWarningType.NoProviderFiles].includes(addon.warningType) + ) { + addon.warningType = undefined; + } + + addon.screenshotUrls = result.screenshotUrls; + + // Check for a new download URL + if (strIsNotNullOrEmpty(latestFile?.downloadUrl) && latestFile.downloadUrl !== addon.downloadUrl) { + addon.downloadUrl = latestFile.downloadUrl || addon.downloadUrl; + } + + if (Array.isArray(result.fundingLinks)) { + addon.fundingLinks = result.fundingLinks; + } + + // If the release ID hasn't changed we don't really need to update the whole record + if (!!latestFile?.externalId && latestFile.externalId === addon.externalLatestReleaseId) { + continue; + } else if ( + !result || + !latestFile || + (latestFile.version === addon.latestVersion && latestFile.releaseDate === addon.releasedAt) + ) { + // There was nothing new to update to, just update what we need to + continue; + } + + addon.latestVersion = latestFile.version; + addon.releasedAt = latestFile.releaseDate; + addon.externalLatestReleaseId = latestFile.externalId; + addon.name = result.name; + addon.author = result.author; + addon.externalChannel = getEnumName(AddonChannelType, latestFile.channelType); + + if (latestFile.gameVersion) { + addon.gameVersion = getGameVersionList([latestFile.gameVersion]); + } else if (addon.gameVersion) { + addon.gameVersion = getGameVersionList(addon.gameVersion ?? []); + } else { + console.warn("No game version found", addon); + } + + addon.externalUrl = result.externalUrl; + } finally { + await this._addonStorage.setAsync(addon.id, addon); + } + } + } + + private async handleSyncErrors( + installation: WowInstallation, + errors: Error[], + addonProvider: AddonProvider, + addons: Addon[], + ): Promise { + for (const error of errors) { + const addonId = (error as any).addonId; + let addon: Addon | undefined = undefined; + if (addonId) { + addon = addons.find((a) => a.externalId === addonId && a.providerName === addonProvider.name); + } + + if (error instanceof GenericProviderError && addon !== undefined) { + addon.warningType = error.warningType; + if (addon.id) { + await this._addonStorage.setAsync(addon.id, addon); + } + } + + this._syncErrorSrc.next( + new AddonSyncError({ + providerName: addonProvider.name, + installationName: installation.displayName, + innerError: error, + addonName: addon?.name, + }), + ); + } + } + + // Legacy TukUI/ElvUI ids were ints, correct them + private async setExternalIdString(addon: Addon) { + if (!addon.id) { + return; + } + if (typeof addon.externalId === "string") { + return; + } + + const nonStrId: any = addon.externalId; + addon.externalId = nonStrId.toString(); + await this._addonStorage.setAsync(addon.id, addon); + } + + private getExternalIdsForProvider(addonProvider: AddonProvider, addons: Addon[]): string[] { + return _.filter(addons, (addon) => addon.providerName === addonProvider.name) + .map((f) => f.externalId) + .filter((id): id is string => typeof id === "string"); + } + + private async removeGitFolders(addonFolders: AddonFolder[]) { + for (const addonFolder of addonFolders) { + const directories = await this._fileService.listDirectories(addonFolder.path); + const hasGitFolder = !!directories.find((dir) => dir.toLowerCase() === ".git"); + if (hasGitFolder) { + addonFolder.ignoreReason = "git_repo"; + } + } + } + + /** + * Determine any addons who have providers with re-scanning disabled then remove any addon folders that match those addons + * Ex: GitHub addons should remain as they cannot be re-scanned at this time via toc + */ + private removeNonRescanFolders(addonFolders: AddonFolder[], currentAddons: Addon[]): Addon[] { + const remainingAddons: Addon[] = []; + const removedAddonFolders: AddonFolder[] = []; + + for (const currentAddon of currentAddons) { + const provider = this._addonProviderService.getProvider(currentAddon.providerName); + if (provider === undefined || provider.allowReScan === true) { + continue; + } + + const removed = _.remove(addonFolders, (af) => currentAddon.installedFolderList.includes(af.name)); + removedAddonFolders.push(...removed); + + remainingAddons.push(currentAddon); + } + + console.log( + `Removed ${removedAddonFolders.length} NonRescan folders: ${removedAddonFolders.map((af) => af.name).join(", ")}`, + ); + console.log(`Kept ${remainingAddons.length} NonRescan addons: ${remainingAddons.map((ad) => ad.name).join(", ")}`); + + return remainingAddons; + } + + private async migrateLocalAddons(installation: WowInstallation): Promise { + const existingAddons = await this.getAllAddons(installation); + if (!existingAddons.length) { + console.log(`[MigrateInstall] ${installation.displayName} no addons found`); + return; + } + + const needsMigration = existingAddons.some((addon) => this.needsMigration(addon)); + if (!needsMigration) { + console.log(`[MigrateInstall] ${installation.displayName} No addons needed to be migrated`); + return; + } + + let migratedCt = 0; + for (const addon of existingAddons) { + const didMigrate = await this.migrateLocalAddon(addon); + if (didMigrate) { + migratedCt += 1; + } + } + + console.log(`[MigrateInstall] Local addons complete: [${migratedCt}] ${installation.displayName}`); + } + + public async migrateDeep(installation: WowInstallation): Promise { + await this.migrateLocalAddons(installation); + + console.log(`[MigrateInstall] ${installation.displayName}`); + const existingAddons = await this.getAllAddons(installation); + if (!existingAddons.length) { + console.log(`[MigrateInstall] ${installation.displayName} no addons found`); + return; + } + + const needsMigration = existingAddons.some((addon) => this.needsMigration(addon)); + if (!needsMigration) { + console.log(`[MigrateInstall] ${installation.displayName} No addons needed to be migrated`); + return; + } + + const scannedAddons = await this.scanAddons(installation); + for (const addon of existingAddons) { + await this.migrateSyncAddon(addon, scannedAddons); + } + } + + private needsMigration(addon: Addon) { + const provider = this._addonProviderService.getProvider(addon.providerName ?? ""); + + const migrationNeeded = + typeof addon.gameVersion === "string" || + addon.providerName === ADDON_PROVIDER_HUB_LEGACY || + typeof addon.autoUpdateNotificationsEnabled === "undefined" || + !addon.installedFolderList || + !addon.externalChannel || + (provider?.shouldMigrate(addon) ?? false); + + return migrationNeeded; + } + + private async migrateLocalAddon(addon: Addon): Promise { + let changed = false; + if (typeof addon.gameVersion === "string") { + console.log(`[MigrateAddon] '${addon.name}' Updating gameVersion array`); + addon.gameVersion = [addon.gameVersion]; + changed = true; + } + + if (typeof addon.autoUpdateNotificationsEnabled === "undefined") { + console.log(`[MigrateAddon] '${addon.name}' Updating autoUpdateNotificationsEnabled`); + addon.autoUpdateNotificationsEnabled = addon.autoUpdateEnabled; + changed = true; + } + + if (addon.providerName === ADDON_PROVIDER_HUB_LEGACY) { + console.log(`[MigrateAddon] '${addon.name}' Updating legacy hub name`); + addon.providerName = ADDON_PROVIDER_HUB; + changed = true; + } + + if (changed) { + await this.saveAddon(addon); + } + + return changed; + } + + private async migrateSyncAddon(addon: Addon, scannedAddons: Addon[]): Promise { + const scannedAddon = scannedAddons.find( + (sa) => sa.externalId === addon.externalId && addon.providerName === sa.providerName, + ); + + if (!scannedAddon) { + console.log(`[MigrateAddon] '${addon.name}' No scanned addon found`); + return; + } + + addon.installedExternalReleaseId = scannedAddon.externalLatestReleaseId; + addon.externalChannel = scannedAddon.externalChannel; + + // Fill in any addons where this is missing + if (!addon.installedFolderList) { + addon.installedFolderList = scannedAddon.installedFolderList; + } + + await this.saveAddon(addon); + } + + public async setInstallationAutoUpdate(installation: WowInstallation): Promise { + const addons = await this._addonStorage.getAllForInstallationIdAsync(installation.id); + if (addons.length === 0) { + console.log(`No addons were found to set auto update: ${installation.location}`); + return; + } + + console.log(`Setting ${addons.length} addons to auto update: ${installation.defaultAutoUpdate.toString()}`); + + for (const addon of addons) { + addon.autoUpdateEnabled = installation.defaultAutoUpdate; + } + + await this._addonStorage.saveAll(addons); + console.log(`Auto update set complete`); + } + + private async scanAddons(installation: WowInstallation, currentAddons?: Addon[]): Promise { + const addonList: Addon[] = []; + + if (!installation) { + return []; + } + + this._scanUpdateSrc.next({ + type: ScanUpdateType.Start, + }); + + try { + const defaultAddonChannel = installation.defaultAddonChannelType; + + const useSymlinkMode = await this._wowUpService.getUseSymlinkMode(); + const addonFolders = await this._warcraftService.listAddons(installation, useSymlinkMode); + + if (addonFolders.length === 0) { + return []; + } + + await this.removeGitFolders(addonFolders); + + if (Array.isArray(currentAddons)) { + const skippedAddons = this.removeNonRescanFolders(addonFolders, currentAddons); + addonList.push(...skippedAddons); + } + + // Get all the fingerprints we might need + await this._addonFingerprintService.getFingerprints(addonFolders); + + this._scanUpdateSrc.next({ + type: ScanUpdateType.Update, + currentCount: 0, + totalCount: addonFolders.length, + }); + + const enabledProviders = this._addonProviderService.getEnabledAddonProviders(); + for (const provider of enabledProviders) { + try { + const validFolders = addonFolders.filter((af) => !af.ignoreReason && !af.matchingAddon && af.tocs.length > 0); + + await provider.scan(installation, defaultAddonChannel, validFolders); + } catch (e) { + console.error("scan failed: " + provider.name); + console.error(e); + this._scanErrorSrc.next( + new AddonScanError({ + providerName: provider.name, + innerError: e, + }), + ); + } + } + + const matchedAddonFolders = addonFolders.filter((addonFolder) => !!addonFolder.matchingAddon); + const matchedAddonFolderNames = matchedAddonFolders.map((mf) => mf.name); + + matchedAddonFolders.forEach((maf) => { + if (maf.matchingAddon === undefined) { + console.warn("matching adding undefined"); + return; + } + + const targetToc = this._tocService.getTocForGameType2(maf.name, maf.tocs, installation.clientType); + if (targetToc === undefined) { + console.warn("toc file undefined", maf, installation.clientType); + // maf.matchingAddon.warningType = AddonWarningType.GameVersionTocMissing; + return; + } + + if (!targetToc.fileName.startsWith(maf.name)) { + console.warn("TOC NAME MISMATCH", maf.name, targetToc.fileName); + maf.matchingAddon.warningType = AddonWarningType.TocNameMismatch; + } + + this.setExternalIds(maf.matchingAddon, targetToc); + }); + + const matchedGroups = _.groupBy( + matchedAddonFolders, + (addonFolder) => + `${addonFolder.matchingAddon?.providerName ?? ""}${addonFolder.matchingAddon?.externalId ?? ""}`, + ); + + console.debug("matchedGroups", matchedGroups); + + for (const value of Object.values(matchedGroups)) { + const ordered = _.orderBy(value, (v) => v.matchingAddon?.externalIds?.length ?? 0).reverse(); + const first = ordered[0]; + if (first.matchingAddon) { + addonList.push(first.matchingAddon); + } + } + + const unmatchedFolders = addonFolders.filter((af) => + this.isAddonFolderUnmatched(matchedAddonFolderNames, af, installation), + ); + + for (const uf of unmatchedFolders) { + const unmatchedAddon = await this.createUnmatchedAddon(uf, installation, matchedAddonFolderNames); + addonList.push(unmatchedAddon); + } + + //Clear the changelogs since they wont always be latest + addonList.forEach((addon) => { + if (!addon) { + return; + } + + addon.latestChangelog = undefined; + addon.latestChangelogVersion = undefined; + addon.channelType = installation.defaultAddonChannelType; + }); + + console.debug(addonList); + + return addonList; + } finally { + this._scanUpdateSrc.next({ + type: ScanUpdateType.Complete, + }); + } + } + + private setExternalIds(addon: Addon, toc: Toc) { + if (!toc) { + return; + } + + const externalIds: AddonExternalId[] = []; + for (const [key, value] of Object.entries(ADDON_PROVIDER_TOC_EXTERNAL_ID_MAP)) { + this.insertExternalId(externalIds, key, toc[value] as string); + } + + //If the addon does not include the current external id add it + if (!this.containsOwnExternalId(addon, externalIds)) { + if (!addon.providerName || !addon.externalId) { + return; + } + + this.insertExternalId(externalIds, addon.providerName, addon.externalId); + } + + addon.externalIds = externalIds; + } + + /** + * This should verify that a folder that did not have a match, is actually unmatched + * This will happen for any sub folders of TukUI or WowInterface addons + */ + private isAddonFolderUnmatched( + matchedFolderNames: string[], + addonFolder: AddonFolder, + installation: WowInstallation, + ) { + if (addonFolder.matchingAddon) { + return false; + } + + const targetToc = this._tocService.getTocForGameType2(addonFolder.name, addonFolder.tocs, installation.clientType); + + // if the folder is load on demand, it 'should' be a sub folder + const isLoadOnDemand = targetToc?.loadOnDemand === "1"; + if (isLoadOnDemand && this.allItemsMatch(targetToc.dependencyList, matchedFolderNames)) { + return false; + } + + return true; + } + + /** Check if all primitives in subset are in the superset (strings, ints) */ + private allItemsMatch(subset: any[], superset: any[]) { + return _.difference(subset, superset).length === 0; + } + + public insertExternalId(externalIds: AddonExternalId[], providerName: string, addonId?: string): void { + if (!addonId || [ADDON_PROVIDER_RAIDERIO, ADDON_PROVIDER_WOWUP_COMPANION].includes(providerName)) { + return; + } + + const exists = externalIds.findIndex((extId) => extId.id === addonId && extId.providerName === providerName) !== -1; + + if (exists) { + console.debug(`External id exists ${providerName}|${addonId}`); + return; + } + + if (this._addonProviderService.getProvider(providerName)?.isValidAddonId(addonId) ?? false) { + externalIds.push({ + id: addonId, + providerName: providerName, + }); + } else { + console.warn(`Invalid provider id ${providerName}|${addonId}`); + console.warn(externalIds); + } + } + + public async setProvider( + addon: Addon | undefined, + externalId: string, + providerName: string, + installation: WowInstallation, + ): Promise { + if (addon === undefined) { + throw new Error("Invalid addon"); + } + + const provider = this._addonProviderService.getProvider(providerName); + if (!provider) { + throw new Error(`Provider not found: ${providerName}`); + } + + const isInstalled = await this.isInstalled(externalId, providerName, installation); + if (isInstalled) { + throw new Error(ERROR_ADDON_ALREADY_INSTALLED); + } + + const externalAddon = await firstValueFrom(this.getAddon(externalId, providerName, installation)); + if (!externalAddon) { + throw new Error(`External addon not found: ${providerName}|${externalId}`); + } + + await this.saveAddon(externalAddon); + + if (!externalAddon.id) { + throw new Error(`External addon had no id`); + } + + await this.installAddon(externalAddon, undefined, addon); + await this.removeAddon(addon, false, false); + } + + public async reconcileOrphanAddons(installations: WowInstallation[]): Promise { + const addons = await this._addonStorage.getAll(); + + for (const addon of addons) { + if (!addon.installationId) { + console.debug( + `Removing detached legacy addon [${getEnumName(WowClientType, addon.clientType)}]: ${addon.name}`, + ); + await this.removeAddon(addon, false, false); + continue; + } + + const installation = installations.find((installation) => installation.id === addon.installationId); + if (installation) { + continue; + } + + console.debug(`Removing orphaned addon [${getEnumName(WowClientType, addon.clientType)}]: ${addon.name}`); + await this.removeAddon(addon, false, false); + } + } + + public getFeaturedAddons(installation: WowInstallation): Observable { + return forkJoin( + this._addonProviderService.getEnabledAddonProviders().map(async (p) => { + try { + return await p.getFeaturedAddons(installation); + } catch (e) { + console.error(`Failed to get featured addons: ${p.name}`, e); + this._searchErrorSrc.next(new GenericProviderError(e as Error, p.name)); + return []; + } + }), + ).pipe( + map((results) => { + return _.orderBy(results.flat(1), ["downloadCount"]).reverse(); + }), + ); + } + + public async getByExternalId( + externalId: string, + providerName: string, + installationId: string, + ): Promise { + return await this._addonStorage.getByExternalIdAsync(externalId, providerName, installationId); + } + + public async isInstalled(externalId: string, providerName: string, installation: WowInstallation): Promise { + const addon = await this.getByExternalId(externalId, providerName, installation.id); + return !!addon; + } + + // TODO move this to a different service + public setProviderEnabled(providerName: string, enabled: boolean): void { + const provider = this._addonProviderService.getProvider(providerName); + if (provider) { + provider.enabled = enabled; + } + + this._addonProviderChangeSrc.next(provider); + } + + public async backfillAddons(): Promise { + const installations = await this._warcraftInstallationService.getWowInstallationsAsync(); + + for (const installation of installations) { + const addons = await this._addonStorage.getAllForInstallationIdAsync(installation.id); + for (const addon of addons) { + await this.backfillAddon(addon); + await this.backfillAddonInstalledFolderList(addon); + } + } + } + + private async backfillAddonInstalledFolderList(addon: Addon): Promise { + if (addon.installedFolderList) { + return; + } + + addon.installedFolderList = addon.installedFolders?.split(",") ?? []; + await this.saveAddon(addon); + } + + public async backfillAddon(addon: Addon): Promise { + if (addon.externalIds && this.containsOwnExternalId(addon)) { + return; + } + + try { + const tocPaths = await this.getTocPaths(addon); + const tocFiles = await Promise.all(tocPaths.map((tocPath) => this._tocService.parse(tocPath))); + const orderedTocFiles = _.orderBy(tocFiles, ["wowInterfaceId", "loadOnDemand"], ["desc", "asc"]); + const primaryToc = _.first(orderedTocFiles); + if (!primaryToc) { + throw new Error("Could not find primary toc"); + } + + this.setExternalIds(addon, primaryToc); + await this.saveAddon(addon); + } catch (e) { + console.error(e); + } + } + + public containsOwnExternalId(addon: Addon, array?: AddonExternalId[]): boolean { + const arr = array || addon.externalIds; + const result = + Array.isArray(arr) && !!arr.find((ext) => ext.id === addon.externalId && ext.providerName === addon.providerName); + return result; + } + + public async getTocPaths(addon: Addon): Promise { + if (!addon.installationId) { + return []; + } + + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (!installation) { + return []; + } + + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + + const addonTocs = await this._tocService.getAllTocs( + addonFolderPath, + addon.installedFolderList, + installation.clientType, + ); + + const tocPaths = addonTocs.map((toc) => toc.filePath); + return tocPaths; + } + + private createAddon( + searchResult: AddonSearchResult, + latestFile: AddonSearchResultFile | undefined, + installation: WowInstallation, + ): Addon | undefined { + if (!latestFile) { + return undefined; + } + + const dependencies = Array.isArray(latestFile.dependencies) + ? latestFile.dependencies.map(this.createAddonDependency) + : []; + + const fundingLinks = Array.isArray(searchResult.fundingLinks) ? [...searchResult.fundingLinks] : []; + + return { + id: uuidv4(), + name: searchResult.name, + thumbnailUrl: searchResult.thumbnailUrl, + latestVersion: latestFile.version, + clientType: installation.clientType, + externalId: searchResult.externalId.toString(), + gameVersion: getGameVersionList([latestFile.gameVersion]), + author: searchResult.author, + downloadUrl: latestFile.downloadUrl, + externalUrl: searchResult.externalUrl, + providerName: searchResult.providerName, + channelType: installation.defaultAddonChannelType, + isIgnored: false, + autoUpdateEnabled: installation.defaultAutoUpdate, + autoUpdateNotificationsEnabled: installation.defaultAutoUpdate, + releasedAt: latestFile.releaseDate, + summary: searchResult.summary, + screenshotUrls: searchResult.screenshotUrls, + dependencies, + externalChannel: getEnumName(AddonChannelType, latestFile.channelType), + isLoadOnDemand: false, + externalLatestReleaseId: latestFile.externalId, + fundingLinks, + latestChangelog: latestFile.changelog, + latestChangelogVersion: latestFile.version, + installationId: installation.id, + installedFolderList: [], + }; + } + + private hasValidTocTitle(toc: Toc) { + return toc?.title && /[a-zA-Z]/g.test(toc.title); + } + + private async createUnmatchedAddon( + addonFolder: AddonFolder, + installation: WowInstallation, + matchedAddonFolderNames: string[], + ): Promise { + const targetToc = this._tocService.getTocForGameType2(addonFolder.name, addonFolder.tocs, installation.clientType); + const tocMissingDependencies = _.difference(targetToc?.dependencyList, matchedAddonFolderNames); + const lastUpdatedAt = await this._fileService.getLatestDirUpdateTime(addonFolder.path); + + return { + id: uuidv4(), + name: this.hasValidTocTitle(targetToc) ? targetToc.title ?? addonFolder.name : addonFolder.name, + thumbnailUrl: "", + latestVersion: targetToc?.version || "", + installedVersion: targetToc?.version || "", + clientType: installation.clientType, + externalId: "", + gameVersion: getGameVersionList(targetToc?.interface ?? []), + author: targetToc?.author || "", + downloadUrl: "", + externalUrl: "", + providerName: ADDON_PROVIDER_UNKNOWN, + channelType: AddonChannelType.Stable, + isIgnored: true, + autoUpdateEnabled: false, + autoUpdateNotificationsEnabled: false, + releasedAt: new Date(lastUpdatedAt), + installedAt: addonFolder.fileStats?.mtime || new Date(), + installedFolders: addonFolder.name, + installedFolderList: [addonFolder.name], + summary: "", + screenshotUrls: [], + isLoadOnDemand: targetToc?.loadOnDemand === "1", + externalChannel: getEnumName(AddonChannelType, AddonChannelType.Stable), + missingDependencies: tocMissingDependencies, + ignoreReason: addonFolder.ignoreReason, + installationId: installation.id, + }; + } + + private createAddonDependency = (dependency: AddonSearchResultDependency): AddonDependency => { + return { + externalAddonId: dependency.externalAddonId, + type: dependency.type, + }; + }; + + private trackInstallAction(installType: InstallType, addon: Addon) { + this._analyticsService.trackAction(USER_ACTION_ADDON_INSTALL, { + clientType: getEnumName(WowClientType, addon.clientType), + provider: addon.providerName, + addon: addon.name, + addonId: addon.externalId, + installType, + }); + } + + private async areAnyAddonsAvailableForUpdate(): Promise { + const addons = await this.getAllAddonsAvailableForUpdate(); + return addons.length > 0; + } + + private getWowInstallation(addon: Addon): WowInstallation { + const installation = this._warcraftInstallationService.getWowInstallation(addon.installationId); + if (installation === undefined) { + throw new Error(`installation not found: ${addon.installationId ?? ""}`); + } + return installation; + } + + private getAddonProvider(addon: Addon): AddonProvider { + const addonProvider = this._addonProviderService.getProvider(addon.providerName ?? ""); + if (addonProvider === undefined) { + throw new Error(`Provider not found: ${addon.providerName ?? ""}`); + } + + return addonProvider; + } +} diff --git a/WowUp/wowup-electron/src/app/services/analytics/analytics.service.ts b/WowUp/wowup-electron/src/app/services/analytics/analytics.service.ts new file mode 100644 index 0000000..4f0e15d --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/analytics/analytics.service.ts @@ -0,0 +1,123 @@ +import { Injectable } from "@angular/core"; +import { ApplicationInsights } from "@microsoft/applicationinsights-web"; +import { BehaviorSubject } from "rxjs"; +import { v4 as uuidV4 } from "uuid"; +import { AppConfig } from "../../../environments/environment"; +import { PreferenceStorageService } from "../storage/preference-storage.service"; +import { TELEMETRY_ENABLED_KEY } from "../../../common/constants"; +import { ElectronService } from ".."; +import { WowUpService } from "../wowup/wowup.service"; + +@Injectable({ + providedIn: "root", +}) +export class AnalyticsService { + private readonly installIdPreferenceKey = "install_id"; + private readonly _installId: string; + private readonly _telemetryEnabledSrc = new BehaviorSubject(false); + + private _insights?: ApplicationInsights; + + public readonly telemetryEnabled$ = this._telemetryEnabledSrc.asObservable(); + + private get installId() { + return this._installId; + } + + public async shouldPromptTelemetry(): Promise { + return (await this._preferenceStorageService.getAsync(TELEMETRY_ENABLED_KEY)) === undefined; + } + + public async getTelemetryEnabled(): Promise { + const enabled = await this._preferenceStorageService.getBool(TELEMETRY_ENABLED_KEY); + this.configureAppInsights(enabled); + return enabled; + } + + public async setTelemetryEnabled(value: boolean) { + if (this._insights) { + this._insights.appInsights.config.disableTelemetry = value; + } + + await this._preferenceStorageService.setAsync(TELEMETRY_ENABLED_KEY, value); + this._telemetryEnabledSrc.next(value); + } + + public constructor( + private _preferenceStorageService: PreferenceStorageService, + private _electronService: ElectronService, + private _wowUpService: WowUpService + ) { + this._installId = this.loadInstallId(); + + this.getTelemetryEnabled() + .then((enabled) => { + this._telemetryEnabledSrc.next(enabled); + }) + .catch(console.error); + } + + public async trackStartup(): Promise { + const systemLocale = await this._electronService.getLocale(); + const uiLocale = await this._wowUpService.getCurrentLanguage(); + this.track("app-startup", { + systemLocale, + uiLocale, + }); + } + + public trackError(error: Error): void { + if (!this._telemetryEnabledSrc.value) { + return; + } + + this._insights?.trackException({ exception: error }); + } + + private track(name: string, properties = undefined) { + if (!this._telemetryEnabledSrc.value) { + return; + } + + this._insights?.trackEvent({ name, properties }); + } + + public trackAction(name: string, properties: any = undefined): void { + this.track(name, properties); + } + + private loadInstallId() { + let installId = this._preferenceStorageService.getSync(this.installIdPreferenceKey); + if (installId) { + return installId; + } + + installId = uuidV4(); + this._preferenceStorageService.setAsync(this.installIdPreferenceKey, installId).catch(console.error); + + return installId; + } + + private configureAppInsights(enable: boolean) { + if (!enable || this._insights) { + return; + } + + this._insights = new ApplicationInsights({ + config: { + instrumentationKey: AppConfig.azure.applicationInsightsKey, + disableAjaxTracking: true, + disableFetchTracking: true, + }, + }); + this._insights.loadAppInsights(); + this._insights.trackPageView(); + + // If telemetry is off, don't let it track anything + this._insights.addTelemetryInitializer(() => { + if (!this._telemetryEnabledSrc.value) { + return false; + } + }); + } +} diff --git a/WowUp/wowup-electron/src/app/services/caching/caching-service.ts b/WowUp/wowup-electron/src/app/services/caching/caching-service.ts new file mode 100644 index 0000000..5b1ab68 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/caching/caching-service.ts @@ -0,0 +1,32 @@ +import { Injectable } from "@angular/core"; +import * as NodeCache from "node-cache"; + +@Injectable({ + providedIn: "root", +}) +export class CachingService { + private readonly _cache = new NodeCache(); + + public get(key: string): T | undefined { + return this._cache.get(key); + } + + public set(key: string, value: T, ttlSec = 600): boolean { + return this._cache.set(key, value, ttlSec); + } + + public async transaction(key: string, missingAction: () => Promise, ttlSec = 600): Promise { + const cached = this.get(key); + if (cached !== undefined && cached !== null) { + return cached; + } + + const result = await missingAction?.call(this); + + if (result !== undefined && result !== null) { + this.set(key, result, ttlSec); + } + + return result; + } +} diff --git a/WowUp/wowup-electron/src/app/services/dialog/dialog.factory.ts b/WowUp/wowup-electron/src/app/services/dialog/dialog.factory.ts new file mode 100644 index 0000000..10acfd3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/dialog/dialog.factory.ts @@ -0,0 +1,67 @@ +import { ComponentType } from "@angular/cdk/portal"; +import { Injectable } from "@angular/core"; +import { MatDialog, MatDialogConfig, MatDialogRef } from "@angular/material/dialog"; +import { AddonChannelType, AddonSearchResult } from "wowup-lib-core"; + +import { AddonViewModel } from "../../business-objects/addon-view-model"; +import { AddonDetailComponent, AddonDetailModel } from "../../components/addons/addon-detail/addon-detail.component"; +import { AlertDialogComponent, AlertDialogData } from "../../components/common/alert-dialog/alert-dialog.component"; +import { ConfirmDialogComponent } from "../../components/common/confirm-dialog/confirm-dialog.component"; + +@Injectable({ + providedIn: "root", +}) +export class DialogFactory { + public constructor(private _dialog: MatDialog) {} + + public getConfirmDialog(title: string, message: string): MatDialogRef { + return this._dialog.open(ConfirmDialogComponent, { + data: { + title, + message, + }, + }); + } + + public getAlertDialog(data: AlertDialogData): MatDialogRef { + return this._dialog.open(AlertDialogComponent, { + minWidth: 250, + data: { ...data }, + }); + } + + public getErrorDialog(title: string, message: string): MatDialogRef { + return this.getAlertDialog({ + title, + message, + }); + } + + public getPotentialAddonDetailsDialog( + searchResult: AddonSearchResult, + channelType: AddonChannelType, + ): MatDialogRef { + const data: AddonDetailModel = { + searchResult, + channelType, + }; + + return this._dialog.open(AddonDetailComponent, { + data, + }); + } + + public getAddonDetailsDialog(listItem: AddonViewModel): MatDialogRef | undefined { + const data: AddonDetailModel = { + listItem: listItem.clone(), + }; + + return this._dialog.open(AddonDetailComponent, { + data, + }); + } + + public getDialog(component: ComponentType, config?: MatDialogConfig): MatDialogRef { + return this._dialog.open(component, config); + } +} diff --git a/WowUp/wowup-electron/src/app/services/download/download.service.ts b/WowUp/wowup-electron/src/app/services/download/download.service.ts new file mode 100644 index 0000000..b93926d --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/download/download.service.ts @@ -0,0 +1,64 @@ +import { v4 as uuidv4 } from "uuid"; + +import { Injectable } from "@angular/core"; + +import { IPC_DOWNLOAD_FILE_CHANNEL } from "../../../common/constants"; +import { DownloadRequest } from "../../../common/models/download-request"; +import { DownloadStatus } from "../../../common/models/download-status"; +import { DownloadStatusType } from "../../../common/models/download-status-type"; +import { ElectronService } from "../electron/electron.service"; +import { DownloadAuth } from "wowup-lib-core"; + +export interface DownloadOptions { + auth?: DownloadAuth; + fileName: string; + outputFolder: string; + url: string; + onProgress?: (progress: number) => void; +} + +@Injectable({ + providedIn: "root", +}) +export class DownloadService { + public constructor(private _electronService: ElectronService) {} + + /** + * Downloads a file URL to the specified folder, prepends UUID so there are no collisions + * @returns Saved file path + */ + public downloadZipFile(downloadOptions: DownloadOptions): Promise { + return new Promise((resolve, reject) => { + const request: DownloadRequest = { + auth: downloadOptions.auth, + fileName: downloadOptions.fileName, + outputFolder: downloadOptions.outputFolder, + responseKey: uuidv4(), + url: downloadOptions.url, + }; + + const eventHandler = (_evt: any, arg: DownloadStatus) => { + if (arg.type !== DownloadStatusType.Progress) { + this._electronService.off(request.responseKey, eventHandler); + } + + switch (arg.type) { + case DownloadStatusType.Complete: + resolve(arg.savePath ?? ""); + break; + case DownloadStatusType.Error: + reject(arg.error); + break; + case DownloadStatusType.Progress: + downloadOptions.onProgress?.call(null, arg.progress ?? 0); + break; + default: + break; + } + }; + + this._electronService.on(request.responseKey, eventHandler); + this._electronService.send(IPC_DOWNLOAD_FILE_CHANNEL, request); + }); + } +} diff --git a/WowUp/wowup-electron/src/app/services/electron/electron.service.spec.ts b/WowUp/wowup-electron/src/app/services/electron/electron.service.spec.ts new file mode 100644 index 0000000..4077a59 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/electron/electron.service.spec.ts @@ -0,0 +1,16 @@ +import { ElectronService } from "./electron.service"; + +class StubbedElectronService extends ElectronService { + public get isElectron(): boolean { + return false; + } +} + +describe("ElectronService", () => { + beforeEach(() => {}); + + it("should be created", () => { + const service: ElectronService = new StubbedElectronService(); + expect(service).toBeTruthy(); + }); +}); diff --git a/WowUp/wowup-electron/src/app/services/electron/electron.service.ts b/WowUp/wowup-electron/src/app/services/electron/electron.service.ts new file mode 100644 index 0000000..e412644 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/electron/electron.service.ts @@ -0,0 +1,366 @@ +// If you import a module but never use any of the imported values other than as TypeScript types, +// the resulting javascript file will look as if you never imported the module at all. +import { IpcRendererEvent, OpenDialogOptions, OpenDialogReturnValue, OpenExternalOptions, Settings } from "electron"; +import { LoginItemSettings } from "electron/main"; +import { find } from "lodash"; +import * as minimist from "minimist"; +import { BehaviorSubject, ReplaySubject, Subject } from "rxjs"; +import { v4 as uuidv4 } from "uuid"; + +import { Injectable } from "@angular/core"; + +import { + IPC_APP_UPDATE_STATE, + IPC_CLOSE_WINDOW, + IPC_CUSTOM_PROTOCOL_RECEIVED, + IPC_FOCUS_WINDOW, + IPC_GET_APP_VERSION, + IPC_GET_LAUNCH_ARGS, + IPC_GET_LOCALE, + IPC_GET_LOGIN_ITEM_SETTINGS, + IPC_GET_PENDING_OPEN_URLS, + IPC_IS_DEFAULT_PROTOCOL_CLIENT, + IPC_MAXIMIZE_WINDOW, + IPC_MINIMIZE_WINDOW, + IPC_POWER_MONITOR_LOCK, + IPC_POWER_MONITOR_RESUME, + IPC_POWER_MONITOR_SUSPEND, + IPC_POWER_MONITOR_UNLOCK, + IPC_QUIT_APP, + IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, + IPC_RESTART_APP, + IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, + IPC_SET_LOGIN_ITEM_SETTINGS, + IPC_SET_ZOOM_LIMITS, + IPC_SHOW_OPEN_DIALOG, + IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT, + IPC_WINDOW_LEAVE_FULLSCREEN, + IPC_WINDOW_MAXIMIZED, + IPC_WINDOW_MINIMIZED, + IPC_WINDOW_RESUME, + IPC_WINDOW_UNMAXIMIZED, +} from "../../../common/constants"; +import { IpcRequest } from "../../../common/models/ipc-request"; +import { IpcResponse } from "../../../common/models/ipc-response"; +import { ValueRequest } from "../../../common/models/value-request"; +import { ValueResponse } from "../../../common/models/value-response"; +import { MainChannels } from "../../../common/wowup"; +import { AppOptions, AppUpdateEvent } from "../../../common/wowup/models"; +import { isProtocol } from "../../utils/string.utils"; + +@Injectable({ + providedIn: "root", +}) +export class ElectronService { + private readonly _windowMaximizedSrc = new BehaviorSubject(false); + private readonly _windowMinimizedSrc = new BehaviorSubject(false); + private readonly _powerMonitorSrc = new BehaviorSubject(""); + private readonly _customProtocolSrc = new BehaviorSubject(""); + private readonly _appUpdateSrc = new ReplaySubject(); + private readonly _windowResumedSrc = new Subject(); + private readonly _windowFocusedSrc = new BehaviorSubject(true); + + private _appVersion = ""; + private _opts!: AppOptions; + + public readonly windowMaximized$ = this._windowMaximizedSrc.asObservable(); + public readonly windowMinimized$ = this._windowMinimizedSrc.asObservable(); + public readonly powerMonitor$ = this._powerMonitorSrc.asObservable(); + public readonly customProtocol$ = this._customProtocolSrc.asObservable(); + public readonly appUpdate$ = this._appUpdateSrc.asObservable(); + public readonly windowResumed$ = this._windowResumedSrc.asObservable(); + public readonly windowFocused$ = this._windowFocusedSrc.asObservable(); + public readonly isWin = process.platform === "win32"; + public readonly isMac = process.platform === "darwin"; + public readonly isLinux = process.platform === "linux"; + public readonly isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR; + + public get isElectron(): boolean { + return !!(window && window.process && window.process.type); + } + + public get platform(): string { + return process.platform; + } + + public constructor() { + // Conditional imports + if (!this.isElectron) { + return; + } + + console.log("Platform", process.platform, this.isLinux); + + window.addEventListener("online", this.onWindowOnline); + window.addEventListener("offline", this.onWindowOffline); + + this.invoke(IPC_GET_APP_VERSION) + .then((version) => { + this._appVersion = version; + }) + .catch((e) => { + console.error("Failed to get app version", e); + }); + + this.onRendererEvent(IPC_APP_UPDATE_STATE, (evt, updateEvt: AppUpdateEvent) => { + console.log("IPC_APP_UPDATE_STATE", IPC_APP_UPDATE_STATE); + console.log(updateEvt); + this._appUpdateSrc.next(updateEvt); + }); + + this.onRendererEvent(IPC_WINDOW_MINIMIZED, () => { + this._windowMinimizedSrc.next(true); + }); + + this.onRendererEvent(IPC_WINDOW_RESUME, () => { + this._windowResumedSrc.next(undefined); + }); + + this.onRendererEvent(IPC_WINDOW_MAXIMIZED, () => { + this._windowMaximizedSrc.next(true); + }); + + this.onRendererEvent(IPC_WINDOW_UNMAXIMIZED, () => { + this._windowMaximizedSrc.next(false); + }); + + this.onRendererEvent("blur", () => { + this._windowFocusedSrc.next(false); + }); + + this.onRendererEvent("focus", () => { + this._windowFocusedSrc.next(true); + }); + + this.onRendererEvent(IPC_CUSTOM_PROTOCOL_RECEIVED, (evt, protocol: string) => { + this._customProtocolSrc.next(protocol); + }); + + this.onRendererEvent(IPC_POWER_MONITOR_LOCK, () => { + console.log("POWER_MONITOR_LOCK received", `navigator.onLine: ${navigator.onLine.toString()}`); + this._powerMonitorSrc.next(IPC_POWER_MONITOR_LOCK); + }); + + this.onRendererEvent(IPC_POWER_MONITOR_UNLOCK, () => { + console.log("POWER_MONITOR_UNLOCK received", `navigator.onLine: ${navigator.onLine.toString()}`); + this._powerMonitorSrc.next(IPC_POWER_MONITOR_UNLOCK); + }); + + this.onRendererEvent(IPC_POWER_MONITOR_SUSPEND, () => { + console.log("POWER_MONITOR_SUSPEND received", `navigator.onLine: ${navigator.onLine.toString()}`); + this._powerMonitorSrc.next(IPC_POWER_MONITOR_SUSPEND); + }); + + this.onRendererEvent(IPC_POWER_MONITOR_RESUME, () => { + console.log("POWER_MONITOR_RESUME received", `navigator.onLine: ${navigator.onLine.toString()}`); + this._powerMonitorSrc.next(IPC_POWER_MONITOR_RESUME); + }); + + this.invoke(IPC_SET_ZOOM_LIMITS, 1, 1).catch((e) => { + console.error("Failed to set zoom limits", e); + }); + + this.isWindowFocused() + .then((focused) => { + this._windowFocusedSrc.next(focused); + }) + .catch(console.error); + } + + private onWindowOnline = () => { + console.log("Window online..."); + }; + + private onWindowOffline = () => { + console.warn("Window offline..."); + }; + + public getLoginItemSettings(): Promise { + return this.invoke(IPC_GET_LOGIN_ITEM_SETTINGS); + } + + public setLoginItemSettings(settings: Settings): Promise { + return this.invoke(IPC_SET_LOGIN_ITEM_SETTINGS, settings); + } + + public isDefaultProtocolClient(protocol: string): Promise { + return this.invoke(IPC_IS_DEFAULT_PROTOCOL_CLIENT, protocol); + } + + public setAsDefaultProtocolClient(protocol: string): Promise { + return this.invoke(IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, protocol); + } + + public async removeAsDefaultProtocolClient(protocol: string): Promise { + return this.invoke(IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, protocol); + } + + // Check for any URLs that were available at app launch on Mac + public async processPendingOpenUrls(): Promise { + const pendingUrls: string[] = await this.invoke(IPC_GET_PENDING_OPEN_URLS); + for (const pendingUrl of pendingUrls) { + if (isProtocol(pendingUrl)) { + // If we did get a custom protocol notify the app + this._customProtocolSrc.next(pendingUrl); + } + } + } + + public async getAppOptions(): Promise { + if (!this._opts) { + console.debug("getAppOptions"); + // TODO check protocols here + const launchArgs = await this.invoke(IPC_GET_LAUNCH_ARGS); + this._opts = (minimist(launchArgs.slice(1), { + boolean: ["hidden", "quit"], + string: ["install"], + })) as AppOptions; + + // Find the first protocol arg if any exist + const customProtocol = find(launchArgs, (arg) => isProtocol(arg)); + if (customProtocol) { + // If we did get a custom protocol notify the app + this._customProtocolSrc.next(customProtocol); + } + } + + return this._opts; + } + + public onRendererEvent(channel: MainChannels, listener: (event: IpcRendererEvent, ...args: any[]) => void): void { + window.wowup?.onRendererEvent(channel, listener); + } + + public async getLocale(): Promise { + const locale = await this.invoke(IPC_GET_LOCALE); + return locale.split("-")[0]; + } + + public getVersionNumber(): Promise { + return this.invoke(IPC_GET_APP_VERSION); + } + + public async minimizeWindow(): Promise { + await this.invoke(IPC_MINIMIZE_WINDOW); + } + + public async maximizeWindow(): Promise { + await this.invoke(IPC_MAXIMIZE_WINDOW); + } + + public async unmaximizeWindow(): Promise { + await this.invoke(IPC_MAXIMIZE_WINDOW); + } + + public async restartApplication(): Promise { + await this.invoke(IPC_RESTART_APP); + } + + public async quitApplication(): Promise { + await this.invoke(IPC_QUIT_APP); + } + + public async closeWindow(): Promise { + await this.invoke(IPC_CLOSE_WINDOW); + } + + public async leaveFullScreen(): Promise { + await this.invoke(IPC_WINDOW_LEAVE_FULLSCREEN); + } + + public async isWindowFocused(): Promise { + return await this.invoke("get-focus"); + } + + public async readClipboardText(): Promise { + return await this.invoke("clipboard-read-text"); + } + + public showNotification(title: string, options?: NotificationOptions): Notification { + return new Notification(title, options); + } + + public async showOpenDialog(options: OpenDialogOptions): Promise { + return await this.invoke(IPC_SHOW_OPEN_DIALOG, options); + } + + public async showItemInFolder(path: string): Promise { + return await this.invoke("show-item-in-folder", path); + } + + public async getUserDefaultSystemPreference( + key: string, + type: "string" | "boolean" | "integer" | "float" | "double" | "url" | "array" | "dictionary" + ): Promise { + return await this.invoke(IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT, key, type); + } + + public async sendIpcValueMessage(channel: string, value: TIN): Promise { + const request: ValueRequest = { + value, + responseKey: uuidv4(), + }; + + const response = await this.sendIPCMessage, ValueResponse>(channel, request); + + return response.value; + } + + public focusWindow(): Promise { + return this.invoke(IPC_FOCUS_WINDOW); + } + + public sendIPCMessage( + channel: string, + request: TIN + ): Promise { + return new Promise((resolve, reject) => { + window.wowup.onceRendererEvent(request.responseKey, (_evt: any, arg: TOUT) => { + if (arg.error) { + return reject(arg.error); + } + resolve(arg); + }); + window.wowup.rendererSend(channel, request); + }); + } + + public async invoke(channel: string, ...args: any[]): Promise { + try { + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + return await window.wowup.rendererInvoke(channel, ...args); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ + } catch (e) { + console.error("Invoke failed", e); + throw e; + } + } + + public sendSync(channel: string, ...args: any[]): T { + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + return window.wowup.rendererSendSync(channel, ...args) as T; + /* eslint-enable @typescript-eslint/no-unsafe-argument */ + } + + public on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void { + window.wowup.rendererOn(channel, listener); + } + + public off(channel: string, listener: (...args: any[]) => void): void { + window.wowup.rendererOff(channel, listener); + } + + public send(channel: string, ...args: any[]): void { + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + window.wowup.rendererSend(channel, ...args); + /* eslint-enable @typescript-eslint/no-unsafe-argument */ + } + + public openExternal(url: string, options?: OpenExternalOptions): Promise { + return window.wowup.openExternal(url, options); + } + + public openPath(path: string): Promise { + return window.wowup.openPath(path); + } +} diff --git a/WowUp/wowup-electron/src/app/services/files/file.service.ts b/WowUp/wowup-electron/src/app/services/files/file.service.ts new file mode 100644 index 0000000..b30e3e1 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/files/file.service.ts @@ -0,0 +1,197 @@ +import { Injectable } from "@angular/core"; +import { v4 as uuidv4 } from "uuid"; +import { + IPC_COPY_FILE_CHANNEL, + IPC_CREATE_DIRECTORY_CHANNEL, + IPC_DELETE_DIRECTORY_CHANNEL, + IPC_GET_HOME_DIR, + IPC_GET_ASSET_FILE_PATH, + IPC_LIST_DIRECTORIES_CHANNEL, + IPC_PATH_EXISTS_CHANNEL, + IPC_READ_FILE_CHANNEL, + IPC_WRITE_FILE_CHANNEL, + IPC_SHOW_DIRECTORY, + IPC_STAT_FILES_CHANNEL, + IPC_UNZIP_FILE_CHANNEL, + IPC_LIST_ENTRIES, + IPC_LIST_FILES_CHANNEL, + IPC_READDIR, + IPC_READ_FILE_BUFFER_CHANNEL, + IPC_GET_LATEST_DIR_UPDATE_TIME, + IPC_LIST_DIR_RECURSIVE, + IPC_GET_DIRECTORY_TREE, + DEFAULT_FILE_MODE, +} from "../../../common/constants"; +import { CopyFileRequest } from "../../../common/models/copy-file-request"; +import { UnzipRequest } from "../../../common/models/unzip-request"; +import { FsDirent, TreeNode } from "../../../common/models/ipc-events"; +import { ElectronService } from "../electron/electron.service"; +import { GetDirectoryTreeOptions, GetDirectoryTreeRequest } from "../../../common/models/ipc-request"; +import { ZipEntry } from "../../../common/models/ipc-response"; +import { FsStats } from "wowup-lib-core"; + +@Injectable({ + providedIn: "root", +}) +export class FileService { + public constructor(private _electronService: ElectronService) {} + + public getHomeDir(): Promise { + return this._electronService.invoke(IPC_GET_HOME_DIR); + } + + public getAssetFilePath(fileName: string): Promise { + return this._electronService.invoke(IPC_GET_ASSET_FILE_PATH, fileName); + } + + public createDirectory(directoryPath: string): Promise { + return this._electronService.invoke(IPC_CREATE_DIRECTORY_CHANNEL, directoryPath); + } + + public showDirectory(sourceDir: string): Promise { + return this._electronService.invoke(IPC_SHOW_DIRECTORY, sourceDir); + } + + public pathExists(sourcePath: string): Promise { + return this._electronService.invoke(IPC_PATH_EXISTS_CHANNEL, sourcePath); + } + + /** + * Delete a file or directory + */ + public async remove(sourcePath: string): Promise { + if (!sourcePath) { + throw new Error("remove sourcePath required"); + } + + return await this._electronService.invoke(IPC_DELETE_DIRECTORY_CHANNEL, sourcePath); + } + + public async removeAll(...sourcePaths: string[]): Promise { + if (!Array.isArray(sourcePaths) || !sourcePaths.length) { + return false; + } + + const results = await Promise.all( + sourcePaths.map((sp) => { + console.log(`[RemovePath]: ${sp}`); + return this._electronService.invoke(IPC_DELETE_DIRECTORY_CHANNEL, sp); + }) + ); + + return results.every((r) => r === true); + } + + public async removeAllSafe(...sourcePaths: string[]): Promise { + try { + return await this.removeAll(...sourcePaths); + } catch (e) { + console.error(`Failed to remove all`, sourcePaths, e); + return false; + } + } + + /** + * Copy a file or folder + */ + public async copy( + sourceFilePath: string, + destinationFilePath: string, + destinationFileChmod: string | number = DEFAULT_FILE_MODE + ): Promise { + const request: CopyFileRequest = { + sourceFilePath, + destinationFilePath, + destinationFileChmod, + responseKey: uuidv4(), + }; + + await this._electronService.invoke(IPC_COPY_FILE_CHANNEL, request); + + return destinationFilePath; + } + + public async deleteIfExists(filePath: string): Promise { + const pathExists = await this.pathExists(filePath); + if (pathExists) { + await this.remove(filePath); + } + } + + public async readFile(sourcePath: string): Promise { + return await this._electronService.invoke(IPC_READ_FILE_CHANNEL, sourcePath); + } + + public async readFileBuffer(sourcePath: string): Promise { + return await this._electronService.invoke(IPC_READ_FILE_BUFFER_CHANNEL, sourcePath); + } + + /** Returns the time in ms of the last updated file in a folder */ + public async getLatestDirUpdateTime(dirPath: string): Promise { + return await this._electronService.invoke(IPC_GET_LATEST_DIR_UPDATE_TIME, dirPath); + } + + public async listDirectoryRecursive(dirPath: string): Promise { + return await this._electronService.invoke(IPC_LIST_DIR_RECURSIVE, dirPath); + } + + public async getDirectoryTree(dirPath: string, opts?: GetDirectoryTreeOptions): Promise { + const request: GetDirectoryTreeRequest = { + dirPath, + opts, + }; + return await this._electronService.invoke(IPC_GET_DIRECTORY_TREE, request); + } + + public async writeFile(sourcePath: string, contents: string): Promise { + return await this._electronService.invoke(IPC_WRITE_FILE_CHANNEL, sourcePath, contents); + } + + public async listDirectories(sourcePath: string, scanSymlinks = false): Promise { + return await this._electronService.invoke(IPC_LIST_DIRECTORIES_CHANNEL, sourcePath, scanSymlinks); + } + + public readdir(dirPath: string): Promise { + return this._electronService.invoke(IPC_READDIR, dirPath); + } + + public statFiles(filePaths: string[]): Promise<{ [path: string]: FsStats }> { + return this._electronService.invoke(IPC_STAT_FILES_CHANNEL, filePaths); + } + + public listEntries(sourcePath: string, filter: string): Promise { + return this._electronService.invoke(IPC_LIST_ENTRIES, sourcePath, filter); + } + + public listFiles(sourcePath: string, filter: string): Promise { + return this._electronService.invoke(IPC_LIST_FILES_CHANNEL, sourcePath, filter); + } + + public listZipFiles(sourcePath: string, filter: string): Promise { + return this._electronService.invoke("zip-list-files", sourcePath, filter); + } + + public readFileInZip(zipPath: string, filePath: string): Promise { + return this._electronService.invoke("zip-read-file", zipPath, filePath); + } + + public async unzipFile(zipFilePath: string, outputFolder: string): Promise { + console.log("unzipFile", zipFilePath); + + const request: UnzipRequest = { + outputFolder, + zipFilePath, + responseKey: uuidv4(), + }; + + return await this._electronService.invoke(IPC_UNZIP_FILE_CHANNEL, request); + } + + public async zipFile(srcPath: string, destPath: string): Promise { + await this._electronService.invoke("zip-file", srcPath, destPath); + } + + public async renameFile(srcPath: string, destPath: string): Promise { + await this._electronService.invoke("rename-file", srcPath, destPath); + } +} diff --git a/WowUp/wowup-electron/src/app/services/icons/icon.service.ts b/WowUp/wowup-electron/src/app/services/icons/icon.service.ts new file mode 100644 index 0000000..cd5f3df --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/icons/icon.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from "@angular/core"; +import { MatIconRegistry } from "@angular/material/icon"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { DomSanitizer } from "@angular/platform-browser"; +import { + faAngleDoubleDown, + faArrowUp, + faSyncAlt, + faTimes, + faExternalLinkAlt, + faPlay, + faBug, + faLink, + faInfoCircle, + faCodeBranch, + faCaretDown, + faExclamation, + faExclamationTriangle, + faCode, + faCoins, + faCompressArrowsAlt, + faPencilAlt, + faArrowDown, + faCheckCircle, + faDiceD6, + faSearch, + faNewspaper, + faCog, + faAngleUp, + faAngleDown, + faChevronDown, + faChevronRight, + faUserCircle, + faEllipsisV, + faCopy, + faTrash, + faHistory, + faMinimize, + faUpRightFromSquare, +} from "@fortawesome/free-solid-svg-icons"; +import { + faQuestionCircle, + faClock, + faCheckCircle as farCheckCircle, + faCaretSquareRight, + faCaretSquareLeft, +} from "@fortawesome/free-regular-svg-icons"; +import { faDiscord, faGithub, faPatreon } from "@fortawesome/free-brands-svg-icons"; + +@Injectable({ + providedIn: "root", +}) +export class IconService { + public constructor(private _matIconRegistry: MatIconRegistry, private _sanitizer: DomSanitizer) { + this.addSvg(faAngleDoubleDown); + this.addSvg(faArrowUp); + this.addSvg(faArrowDown); + this.addSvg(faSyncAlt); + this.addSvg(faTimes); + this.addSvg(faExternalLinkAlt); + this.addSvg(faQuestionCircle); + this.addSvg(faPlay); + this.addSvg(faClock); + this.addSvg(faBug); + this.addSvg(faLink); + this.addSvg(faDiscord); + this.addSvg(faGithub); + this.addSvg(faInfoCircle); + this.addSvg(faCodeBranch); + this.addSvg(faCaretDown); + this.addSvg(faExclamationTriangle); + this.addSvg(faCode); + this.addSvg(faPatreon); + this.addSvg(faCoins); + this.addSvg(faCompressArrowsAlt); + this.addSvg(faPencilAlt); + this.addSvg(faCheckCircle); + this.addSvg(faDiceD6); + this.addSvg(faSearch); + this.addSvg(faInfoCircle); + this.addSvg(faNewspaper); + this.addSvg(faCog); + this.addSvg(faAngleUp); + this.addSvg(faAngleDown); + this.addSvg(faChevronDown); + this.addSvg(faChevronRight); + this.addSvg(faUserCircle); + this.addSvg(faEllipsisV); + this.addSvg(faCopy); + this.addSvg(farCheckCircle); + this.addSvg(faExclamation); + this.addSvg(faTrash); + this.addSvg(faHistory); + this.addSvg(faCaretSquareRight); + this.addSvg(faCaretSquareLeft); + this.addSvg(faMinimize); + this.addSvg(faUpRightFromSquare); + } + + private addSvg(icon: IconDefinition): void { + const svg = ``; + + this._matIconRegistry.addSvgIconLiteralInNamespace( + icon.prefix, + icon.iconName, + this._sanitizer.bypassSecurityTrustHtml(svg) + ); + } +} diff --git a/WowUp/wowup-electron/src/app/services/index.ts b/WowUp/wowup-electron/src/app/services/index.ts new file mode 100644 index 0000000..0f8cf3d --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/index.ts @@ -0,0 +1 @@ +export * from "./electron/electron.service"; diff --git a/WowUp/wowup-electron/src/app/services/links/link.service.ts b/WowUp/wowup-electron/src/app/services/links/link.service.ts new file mode 100644 index 0000000..172fdfb --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/links/link.service.ts @@ -0,0 +1,87 @@ +import { from, Observable, of } from "rxjs"; +import { catchError, first, map, switchMap } from "rxjs/operators"; + +import { Injectable } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { TranslateService } from "@ngx-translate/core"; + +import { + DialogResult, + ExternalUrlConfirmationDialogComponent, +} from "../../components/common/external-url-confirmation-dialog/external-url-confirmation-dialog.component"; +import { WowUpService } from "../wowup/wowup.service"; +import { AnalyticsService } from "../analytics/analytics.service"; +import { USER_ACTION_OPEN_LINK } from "../../../common/constants"; +import { ElectronService } from "../electron/electron.service"; + +@Injectable({ + providedIn: "root", +}) +export class LinkService { + public constructor( + private _dialog: MatDialog, + private _analyticsService: AnalyticsService, + private _electronService: ElectronService, + private _wowUpService: WowUpService, + private _translateService: TranslateService + ) {} + + public async openExternalLink(url: string): Promise { + this._analyticsService.trackAction(USER_ACTION_OPEN_LINK, { + link: url, + }); + await this._electronService.openExternal(url); + } + + public confirmLinkNavigation(href: string): Observable { + return from(this._wowUpService.getTrustedDomains()).pipe( + first(), + switchMap((domains) => + from(this._wowUpService.isTrustedDomain(href, domains)).pipe(map((isTrusted) => ({ isTrusted, domains }))) + ), + switchMap(({ isTrusted, domains }) => { + if (isTrusted) { + return from(this.openExternalLink(href)); + } else { + return this.showLinkNavigationDialog(href, domains); + } + }), + catchError((e) => { + console.error(e); + return of(undefined); + }) + ); + } + + private showLinkNavigationDialog(href: string, domains: string[]): Observable { + const dialogRef = this._dialog.open(ExternalUrlConfirmationDialogComponent, { + data: { + title: this._translateService.instant("APP.LINK_NAVIGATION.TITLE"), + message: this._translateService.instant("APP.LINK_NAVIGATION.MESSAGE", { url: href }), + url: href, + domains, + }, + }); + + return dialogRef.afterClosed().pipe( + first(), + switchMap((result: DialogResult) => { + if (result === undefined || !result.success) { + return of(undefined); + } + + if (result.trustDomain !== "") { + return from(this._wowUpService.trustDomain(result.trustDomain)).pipe( + switchMap(() => from(this.openExternalLink(href))) + ); + } else { + return from(this.openExternalLink(href)); + } + }), + catchError((e) => { + console.error("failed to open external link", e); + return of(undefined); + }) + ); + } +} diff --git a/WowUp/wowup-electron/src/app/services/network/network.service.ts b/WowUp/wowup-electron/src/app/services/network/network.service.ts new file mode 100644 index 0000000..01b771a --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/network/network.service.ts @@ -0,0 +1,188 @@ +import * as CircuitBreaker from "opossum"; +import { firstValueFrom, Subject } from "rxjs"; +import { first, timeout } from "rxjs/operators"; + +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; + +import { AppConfig } from "../../../environments/environment"; + +export interface CircuitBreakerChangeEvent { + state: "open" | "closed"; +} + +const CACHE_CONTROL_HEADERS = { "Cache-Control": "no-cache", Pragma: "no-cache" }; + +export class CircuitBreakerWrapper { + private readonly _name: string; + private readonly _cb: CircuitBreaker; + private readonly _httpClient: HttpClient; + private readonly _defaultTimeoutMs: number; + + private _state = "closed"; + + public constructor( + name: string, + httpClient: HttpClient, + resetTimeoutMs = AppConfig.defaultHttpResetTimeoutMs, + httpTimeoutMs = AppConfig.defaultHttpTimeoutMs + ) { + this._name = name; + this._httpClient = httpClient; + this._defaultTimeoutMs = httpTimeoutMs; + this._cb = new CircuitBreaker(this.internalAction, { + timeout: httpTimeoutMs, + resetTimeout: resetTimeoutMs, + errorFilter: (err) => { + // Don't trip the breaker on a 404 + return err.status === 404; + }, + }); + this._cb.on("open", () => { + console.log(`${name} circuit breaker open`); + this._state = "open"; + }); + this._cb.on("close", () => { + console.log(`${name} circuit breaker close`); + this._state = "closed"; + }); + } + + public isOpen() { + return this._state === "open"; + } + + public enable() { + this._cb.enable(); + } + + public close() { + this._cb.close(); + } + + public async fire(action: () => Promise): Promise { + return (await this._cb.fire(action)) as TOUT; + } + + public getJson( + url: URL | string, + headers: { + [header: string]: string | string[]; + } = {}, + timeoutMs?: number + ): Promise { + return this.fire(() => + firstValueFrom( + this._httpClient + .get(url.toString(), { headers: { ...CACHE_CONTROL_HEADERS, ...headers } }) + .pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs)) + ) + ); + } + + public getText( + url: URL | string, + timeoutMs?: number, + headers: { + [header: string]: string | string[]; + } = {} + ): Promise { + return this.fire(() => + firstValueFrom( + this._httpClient + .get(url.toString(), { responseType: "text", headers: { ...CACHE_CONTROL_HEADERS, ...headers } }) + .pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs)) + ) + ); + } + + public postJson( + url: URL | string, + body: unknown, + headers: { + [header: string]: string | string[]; + } = {}, + timeoutMs?: number + ): Promise { + const cheaders = headers || {}; + const ctimeout = timeoutMs ?? this._defaultTimeoutMs; + + return this.fire(() => + firstValueFrom( + this._httpClient.post(url.toString(), body, { headers: { ...cheaders } }).pipe( + first(), + // switchMap((r) => mockTimeout(r, 30000)), + timeout(ctimeout) + ) + ) + ); + } + + public deleteJson( + url: URL | string, + headers: { + [header: string]: string | string[]; + } = {}, + timeoutMs?: number + ): Promise { + const cheaders = headers || {}; + return this.fire(() => + firstValueFrom( + this._httpClient + .delete(url.toString(), { headers: { ...cheaders } }) + .pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs)) + ) + ); + } + + private internalAction = (action: () => Promise) => { + return action?.call(this); + }; +} +/** Useful when wanting to test HTTP timeout conditions */ +// function mockTimeout(data?: T, timeout = 10000): Observable { +// console.debug("mockTimeout", timeout); +// const prom = new Promise((resolve, reject) => { +// setTimeout(() => { +// resolve(data); +// }, timeout); +// }); + +// return from(prom); +// } + +@Injectable({ + providedIn: "root", +}) +export class NetworkService { + private _breakerChangedSrc = new Subject(); + + public breakerChanged$ = this._breakerChangedSrc.asObservable(); + + public constructor(private _httpClient: HttpClient) {} + + public getCircuitBreaker( + name: string, + resetTimeoutMs: number = AppConfig.defaultHttpResetTimeoutMs, + httpTimeoutMs: number = AppConfig.defaultHttpTimeoutMs + ): CircuitBreakerWrapper { + console.debug("Create circuit breaker", name, resetTimeoutMs, httpTimeoutMs); + return new CircuitBreakerWrapper(name, this._httpClient, resetTimeoutMs, httpTimeoutMs); + } + + public getJson(url: URL | string, timeoutMs?: number): Promise { + return firstValueFrom( + this._httpClient + .get(url.toString(), { headers: { ...CACHE_CONTROL_HEADERS } }) + .pipe(first(), timeout(timeoutMs ?? AppConfig.defaultHttpTimeoutMs)) + ); + } + + public getText(url: URL | string, timeoutMs?: number): Promise { + return firstValueFrom( + this._httpClient + .get(url.toString(), { responseType: "text", headers: { ...CACHE_CONTROL_HEADERS } }) + .pipe(first(), timeout(timeoutMs ?? AppConfig.defaultHttpTimeoutMs)) + ); + } +} diff --git a/WowUp/wowup-electron/src/app/services/news/news.service.ts b/WowUp/wowup-electron/src/app/services/news/news.service.ts new file mode 100644 index 0000000..95925b2 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/news/news.service.ts @@ -0,0 +1,99 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; +import { AppConfig } from "../../../environments/environment"; +import { NetworkService } from "../network/network.service"; + +declare type NewsFeedType = "xml"; + +interface NewsFeed { + url: string; + type: NewsFeedType; + process: () => Promise; +} + +export interface NewsItem { + id: string; + title: string; + link: string; + description: string; + publishedAt: Date; + publishedBy: string; + thumbnail: string; +} + +class WowTavernFeed implements NewsFeed { + public url = AppConfig.warcraftTavernNewsFeedUrl; + + public type: NewsFeedType = "xml"; + + public constructor(public networkService: NetworkService) {} + + public async process(): Promise { + const newsItems: NewsItem[] = []; + + const xmlStr = await this.networkService.getText(this.url); + const parser = new DOMParser(); + const dom = parser.parseFromString(xmlStr, "application/xml"); + const channels = dom.getElementsByTagName("channel"); + + for (const channel of Array.from(channels)) { + for (const item of Array.from(channel.getElementsByTagName("item"))) { + newsItems.push(this.getNewsItem(item)); + } + } + + return newsItems; + } + + private getNewsItem(item: Element): NewsItem { + return { + id: this.getText(item, "dc:identifier"), + title: this.getText(item, "title"), + description: this.getText(item, "description"), + link: this.getText(item, "link"), + publishedAt: new Date(this.getText(item, "pubDate")), + publishedBy: this.getText(item, "dc:creator"), + thumbnail: this.getUrl(item, "media:content"), + }; + } + + private getText(item: Element, name: string): string { + return item.getElementsByTagName(name)[0]?.textContent ?? ""; + } + + private getUrl(item: Element, name: string): string { + return item.getElementsByTagName(name)[0]?.getAttribute("url") ?? ""; + } +} + +@Injectable({ + providedIn: "root", +}) +export class NewsService { + private _lastFetchedAt = 0; + private _newsFeeds: NewsFeed[] = [new WowTavernFeed(this._networkService)]; + + public newsItems$ = new BehaviorSubject([]); + + public get lastFetchedAt(): number { + return this._lastFetchedAt; + } + + public constructor(private _networkService: NetworkService) {} + + public async loadFeeds(): Promise { + let newsItems: NewsItem[] = []; + for (const feed of this._newsFeeds) { + try { + const feedItems = await feed.process(); + newsItems = newsItems.concat(...feedItems); + } catch (e) { + console.error(e); + } + } + + this._lastFetchedAt = Date.now(); + this.newsItems$.next(newsItems); + return newsItems; + } +} diff --git a/WowUp/wowup-electron/src/app/services/push/push.service.ts b/WowUp/wowup-electron/src/app/services/push/push.service.ts new file mode 100644 index 0000000..ba9ddc3 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/push/push.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from "@angular/core"; +import { Subject } from "rxjs"; +import { ElectronService } from ".."; +import { IPC_PUSH_NOTIFICATION } from "../../../common/constants"; +import { AddonUpdatePushNotification, PushNotification } from "../../../common/wowup/models"; + +@Injectable({ + providedIn: "root", +}) +export class PushService { + private readonly _addonUpdateSrc = new Subject(); + + public readonly addonUpdate$ = this._addonUpdateSrc.asObservable(); + + public constructor(private _electronService: ElectronService) { + this._electronService.onRendererEvent(IPC_PUSH_NOTIFICATION, (evt, data: PushNotification) => { + try { + this.parsePushNotification(data); + } catch (e) { + console.error("Failed to handle push notification", e); + } + }); + } + + private parsePushNotification(data: PushNotification) { + switch (data.action) { + case "addon-update": + this.parseAddonUpdateNotification(data as PushNotification); + break; + default: + console.warn("Unhandled push notification", data.action); + } + } + + private parseAddonUpdateNotification(note: PushNotification) { + if (typeof note.message === "string") { + note.message = JSON.parse(note.message) as AddonUpdatePushNotification[]; + } + + console.debug("parseAddonUpdateNotification", note); + this._addonUpdateSrc.next(note.message); + } +} diff --git a/WowUp/wowup-electron/src/app/services/session/session.service.ts b/WowUp/wowup-electron/src/app/services/session/session.service.ts new file mode 100644 index 0000000..7dc7872 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/session/session.service.ts @@ -0,0 +1,222 @@ +import * as _ from "lodash"; +import { BehaviorSubject, combineLatest, from, Subject } from "rxjs"; + +import { Injectable } from "@angular/core"; + +import { CURRENT_THEME_KEY, SELECTED_DETAILS_TAB_KEY, TAB_INDEX_SETTINGS } from "../../../common/constants"; +import { PreferenceStorageService } from "../storage/preference-storage.service"; +import { WarcraftInstallationService } from "../warcraft/warcraft-installation.service"; +import { ColumnState } from "../../models/wowup/column-state"; +import { map, switchMap } from "rxjs/operators"; +import { WowUpAccountService } from "../wowup/wowup-account.service"; +import { AddonService } from "../addons/addon.service"; +import { AddonProviderFactory } from "../addons/addon.provider.factory"; +import { WowUpService } from "../wowup/wowup.service"; +import { WowInstallation } from "wowup-lib-core"; + +@Injectable({ + providedIn: "root", +}) +export class SessionService { + private readonly _selectedWowInstallationSrc = new BehaviorSubject(undefined); + private readonly _pageContextTextSrc = new BehaviorSubject(""); // right side bar text, context to the screen + private readonly _statusTextSrc = new BehaviorSubject(""); // left side bar text, context to the app + private readonly _selectedHomeTabSrc = new BehaviorSubject(0); + private readonly _autoUpdateCompleteSrc = new BehaviorSubject(0); + private readonly _addonsChangedSrc = new Subject(); + private readonly _myAddonsColumnsSrc = new BehaviorSubject([]); + private readonly _targetFileInstallCompleteSrc = new Subject(); + private readonly _myAddonsCompactVersionSrc = new BehaviorSubject(false); + private readonly _adSpaceSrc = new BehaviorSubject(false); + private readonly _enableControlsSrc = new BehaviorSubject(false); + private readonly _getAddonsColumnsSrc = new Subject(); + private readonly _currentThemeSrc = new BehaviorSubject("default-theme"); + private readonly _rescanCompleteSrc = new Subject(); + + private _selectedDetailTabType: DetailsTabType; + + public readonly selectedWowInstallation$ = this._selectedWowInstallationSrc.asObservable(); + public readonly statusText$ = this._statusTextSrc.asObservable(); + public readonly selectedHomeTab$ = this._selectedHomeTabSrc.asObservable(); + public readonly pageContextText$ = this._pageContextTextSrc.asObservable(); + public readonly autoUpdateComplete$ = this._autoUpdateCompleteSrc.asObservable(); + public readonly addonsChanged$ = this._addonsChangedSrc.asObservable(); + public readonly myAddonsHiddenColumns$ = this._myAddonsColumnsSrc.asObservable(); + public readonly getAddonsHiddenColumns$ = this._getAddonsColumnsSrc.asObservable(); + public readonly targetFileInstallComplete$ = this._targetFileInstallCompleteSrc.asObservable(); + public readonly editingWowInstallationId$ = new BehaviorSubject(""); + public readonly wowUpAuthToken$ = this._wowUpAccountService.wowUpAuthTokenSrc.asObservable(); + public readonly wowUpAccount$ = this._wowUpAccountService.wowUpAccountSrc.asObservable(); + public readonly wowUpAccountPushEnabled$ = this._wowUpAccountService.accountPushSrc.asObservable(); + public readonly myAddonsCompactVersion$ = this._myAddonsCompactVersionSrc.asObservable(); + public readonly adSpace$ = this._adSpaceSrc.asObservable(); // TODO this should be driven by the enabled providers + public readonly enableControls$ = combineLatest([this._enableControlsSrc, this._addonService.syncing$]).pipe( + map(([enable, syncing]) => enable && !syncing) + ); + public readonly debugAdFrame$ = new Subject(); + public readonly currentTheme$ = this._currentThemeSrc.asObservable(); + public readonly rescanComplete$ = this._rescanCompleteSrc.asObservable(); + + public readonly wowUpAuthenticated$ = this.wowUpAccount$.pipe(map((account) => account !== undefined)); + + public set myAddonsCompactVersion(val: boolean) { + this._myAddonsCompactVersionSrc.next(val); + } + + public didPromptCfMigration = true; + + public constructor( + private _warcraftInstallationService: WarcraftInstallationService, + private _preferenceStorageService: PreferenceStorageService, + private _wowUpAccountService: WowUpAccountService, + private _wowUpService: WowUpService, + private _addonService: AddonService, + private _addonProviderService: AddonProviderFactory + ) { + this._preferenceStorageService + .getObjectAsync(SELECTED_DETAILS_TAB_KEY) + .then((obj) => { + this._selectedDetailTabType = obj || "description"; + }) + .catch((e) => console.error(e)); + + this._warcraftInstallationService.wowInstallations$ + .pipe(switchMap((installations) => from(this.onWowInstallationsChange(installations)))) + .subscribe(); + + this._wowUpService.preferenceChange$.subscribe((change) => { + if (change.key === CURRENT_THEME_KEY) { + this._currentThemeSrc.next(change.value); + } + }); + + this._wowUpService + .getCurrentTheme() + .then((theme) => { + this._currentThemeSrc.next(theme); + }) + .catch(console.error); + + this._addonProviderService.addonProviderChange$.subscribe(() => { + this.updateAdSpace(); + }); + + this.updateAdSpace(); + } + + private updateAdSpace() { + const allProviders = this._addonProviderService.getEnabledAddonProviders(); + this._adSpaceSrc.next(allProviders.findIndex((p) => p.adRequired) !== -1); + } + + public get wowUpAuthToken(): string { + return this._wowUpAccountService.wowUpAuthTokenSrc.value; + } + + public get currentTheme(): string { + return this._currentThemeSrc.value; + } + + public login(): void { + this._wowUpAccountService.login(); + } + + public logout(): void { + this._wowUpAccountService.logout(); + } + + public setEnableControls(enabled: boolean): void { + this._enableControlsSrc.next(enabled); + } + + public async toggleAccountPush(enabled: boolean): Promise { + return await this._wowUpAccountService.toggleAccountPush(enabled); + } + + public isAuthenticated(): boolean { + return false; + } + + public notifyTargetFileInstallComplete(): void { + this._targetFileInstallCompleteSrc.next(true); + } + + public notifyAddonsChanged(): void { + this._addonsChangedSrc.next(true); + } + + public getSelectedDetailsTab(): DetailsTabType { + return this._selectedDetailTabType; + } + + public async setSelectedDetailsTab(tabType: DetailsTabType) { + this._selectedDetailTabType = tabType; + await this._preferenceStorageService.setAsync(SELECTED_DETAILS_TAB_KEY, tabType); + } + + public async onWowInstallationsChange(wowInstallations: WowInstallation[]): Promise { + if (wowInstallations.length === 0) { + this._selectedHomeTabSrc.next(TAB_INDEX_SETTINGS); + return; + } + + let selectedInstall = _.find(wowInstallations, (installation) => installation.selected); + if (!selectedInstall) { + selectedInstall = _.first(wowInstallations); + if (selectedInstall) { + await this.setSelectedWowInstallation(selectedInstall.id); + } + } + + if (selectedInstall) { + this._selectedWowInstallationSrc.next(selectedInstall); + } + } + + public autoUpdateComplete(): void { + this._autoUpdateCompleteSrc.next(Date.now()); + } + + public setContextText(tabIndex: number, text: string): void { + if (tabIndex !== this._selectedHomeTabSrc.value) { + return; + } + + this._pageContextTextSrc.next(text); + } + + public set statusText(text: string) { + this._statusTextSrc.next(text); + } + + public getSelectedHomeTab(): number { + return this._selectedHomeTabSrc.value; + } + + public set selectedHomeTab(tabIndex: number) { + this._pageContextTextSrc.next(""); + this._selectedHomeTabSrc.next(tabIndex); + } + + public async setSelectedWowInstallation(installationId: string): Promise { + if (!installationId) { + return; + } + + const installation = this._warcraftInstallationService.getWowInstallation(installationId); + if (!installation) { + return; + } + + await this._warcraftInstallationService.setSelectedWowInstallation(installation); + this._selectedWowInstallationSrc.next(installation); + } + + public getSelectedWowInstallation(): WowInstallation | undefined { + return this._selectedWowInstallationSrc.value; + } + + public rescanCompleted() { + this._rescanCompleteSrc.next(true); + } +} diff --git a/WowUp/wowup-electron/src/app/services/snackbar/snackbar.service.ts b/WowUp/wowup-electron/src/app/services/snackbar/snackbar.service.ts new file mode 100644 index 0000000..3e68d74 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/snackbar/snackbar.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from "@angular/core"; +import { MatSnackBar, MatSnackBarRef } from "@angular/material/snack-bar"; +import { TranslateService } from "@ngx-translate/core"; +import { + CenteredSnackbarComponent, + CenteredSnackbarComponentData, +} from "../../components/common/centered-snackbar/centered-snackbar.component"; + +export interface SnackbarConfig { + timeout?: number; + classes?: string[]; + localeArgs?: any; +} + +@Injectable({ + providedIn: "root", +}) +export class SnackbarService { + public constructor(private _translateService: TranslateService, private _snackBar: MatSnackBar) {} + + public showSuccessSnackbar(localeKey: string, config?: SnackbarConfig): MatSnackBarRef { + return this.showSnackbar(localeKey, { + ...config, + classes: [...(config?.classes ?? []), "snackbar-success"], + }); + } + + public showErrorSnackbar(localeKey: string, config?: SnackbarConfig): MatSnackBarRef { + return this.showSnackbar(localeKey, { + ...config, + classes: [...(config?.classes ?? []), "snackbar-error"], + }); + } + + public showSnackbar(localeKey: string, config?: SnackbarConfig): MatSnackBarRef { + const message: string = this._translateService.instant(localeKey, config?.localeArgs as unknown); + const data: CenteredSnackbarComponentData = { + message, + }; + return this._snackBar.openFromComponent(CenteredSnackbarComponent, { + duration: config?.timeout ?? 5000, + panelClass: ["wowup-snackbar", "text-1", ...(config?.classes ?? [])], + data, + }); + } +} diff --git a/WowUp/wowup-electron/src/app/services/storage/addon-storage.service.ts b/WowUp/wowup-electron/src/app/services/storage/addon-storage.service.ts new file mode 100644 index 0000000..840c3e7 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/storage/addon-storage.service.ts @@ -0,0 +1,128 @@ +import { Injectable } from "@angular/core"; +import { Addon } from "wowup-lib-core"; + +import { + IPC_ADDONS_SAVE_ALL, + ADDON_STORE_NAME, + IPC_STORE_SET_OBJECT, + IPC_STORE_GET_OBJECT, + IPC_STORE_REMOVE_OBJECT, + IPC_STORE_GET_ALL, +} from "../../../common/constants"; +import { ElectronService } from "../electron/electron.service"; + +@Injectable({ + providedIn: "root", +}) +export class AddonStorageService { + public constructor(private _electronService: ElectronService) {} + private async getStore(): Promise { + return await this._electronService.invoke(IPC_STORE_GET_ALL, ADDON_STORE_NAME); + } + + public async queryAsync(action: (store: Addon[]) => T): Promise { + const store = await this.getStore(); + return action(store); + } + + public async queryAllAsync(action: (item: Addon) => boolean): Promise { + const addons: Addon[] = []; + const store = await this.getStore(); + for (const addon of store) { + if (action(addon)) { + addons.push(addon); + } + } + + return addons; + } + + public async saveAll(addons: Addon[]): Promise { + console.debug(`[addon-storage] save all: ${addons?.length ?? 0}`); + await this._electronService.invoke(IPC_ADDONS_SAVE_ALL, addons); + } + + public setAsync(key: string | undefined, value: Addon): Promise { + if (!key) { + return Promise.resolve(undefined); + } + + return this._electronService.invoke(IPC_STORE_SET_OBJECT, ADDON_STORE_NAME, key, value); + } + + public get(key: string): Promise { + return this._electronService.invoke(IPC_STORE_GET_OBJECT, ADDON_STORE_NAME, key); + } + + public async removeAllAsync(...addons: Addon[]): Promise { + for (const addon of addons) { + await this.removeAsync(addon); + } + } + + public async removeAsync(addon: Addon): Promise { + if (addon.id) { + await this._electronService.invoke(IPC_STORE_REMOVE_OBJECT, ADDON_STORE_NAME, addon.id); + } + } + + public async removeAllForInstallationAsync(installationId: string): Promise { + const addons = await this.getAllForInstallationIdAsync(installationId); + await this.removeAllAsync(...addons); + } + + public async getByExternalIdAsync( + externalId: string, + providerName: string, + installationId: string + ): Promise { + const addons: Addon[] = []; + const store = await this.getStore(); + + for (const addon of store) { + if ( + addon.installationId === installationId && + addon.externalId === externalId && + addon.providerName === providerName + ) { + addons.push(addon); + break; + } + } + + return addons[0]; + } + + public async getAll(): Promise { + return await this.getStore(); + } + + public async getAllForInstallationIdAsync( + installationId: string, + validator?: (addon: Addon) => boolean + ): Promise { + const addons: Addon[] = []; + const store = await this.getStore(); + + for (const addon of store) { + if (addon.installationId === installationId && (!validator || validator(addon))) { + addons.push(addon); + } + } + + return addons; + } + + public async getAllForProviderAsync(providerName: string, validator?: (addon: Addon) => boolean): Promise { + const addons: Addon[] = []; + const store = await this.getStore(); + + for (const addon of store) { + if (addon.providerName === providerName && (!validator || validator(addon))) { + addons.push(addon); + } + } + + return addons; + } +} diff --git a/WowUp/wowup-electron/src/app/services/storage/preference-storage.service.ts b/WowUp/wowup-electron/src/app/services/storage/preference-storage.service.ts new file mode 100644 index 0000000..4f8176a --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/storage/preference-storage.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from "@angular/core"; + +import { PREFERENCE_STORE_NAME } from "../../../common/constants"; +import { StorageService } from "./storage.service"; +import { ElectronService } from "../electron/electron.service"; + +@Injectable({ + providedIn: "root", +}) +export class PreferenceStorageService extends StorageService { + protected readonly storageName = PREFERENCE_STORE_NAME; + + public constructor(electronService: ElectronService) { + super(electronService); + } +} + diff --git a/WowUp/wowup-electron/src/app/services/storage/sensitive-storage.service.ts b/WowUp/wowup-electron/src/app/services/storage/sensitive-storage.service.ts new file mode 100644 index 0000000..b30dbdd --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/storage/sensitive-storage.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from "@angular/core"; + +import { SENSITIVE_STORE_NAME } from "../../../common/constants"; +import { StorageService } from "./storage.service"; +import { ElectronService } from "../electron/electron.service"; + +@Injectable({ + providedIn: "root", +}) +export class SensitiveStorageService extends StorageService { + protected readonly storageName = SENSITIVE_STORE_NAME; + + public constructor(electronService: ElectronService) { + super(electronService); + } +} diff --git a/WowUp/wowup-electron/src/app/services/storage/storage.service.ts b/WowUp/wowup-electron/src/app/services/storage/storage.service.ts new file mode 100644 index 0000000..e774908 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/storage/storage.service.ts @@ -0,0 +1,50 @@ +import { Subject } from "rxjs"; +import { + IPC_STORE_GET_OBJECT, + IPC_STORE_GET_OBJECT_SYNC, + IPC_STORE_SET_OBJECT, + TRUE_STR, +} from "../../../common/constants"; +import { ElectronService } from "../electron/electron.service"; + +export interface StorageChangeEvent { + key: string; + value: T; +} + +export abstract class StorageService { + protected readonly _changeSrc = new Subject(); + protected abstract readonly storageName: string; + + public change$ = this._changeSrc.asObservable(); + + protected constructor(private _electronService: ElectronService) {} + + public async getBool(key: string): Promise { + const val = await this.getAsync(key); + return val === TRUE_STR; + } + + public getAsync(key: string): Promise { + return this._electronService.invoke(IPC_STORE_GET_OBJECT, this.storageName, key); + } + + public getSync(key: string): T { + return this._electronService.sendSync(IPC_STORE_GET_OBJECT_SYNC, this.storageName, key); + } + + public async setAsync(key: string, value: unknown): Promise { + try { + const result = await this._electronService.invoke(IPC_STORE_SET_OBJECT, this.storageName, key, value); + this._changeSrc.next({ key, value: result }); + return result; + } catch (e) { + console.error(`setAsync failed: ${key}`); + throw e; + } + } + + public getObjectAsync(key: string): Promise { + return this._electronService.invoke(IPC_STORE_GET_OBJECT, this.storageName, key); + } +} diff --git a/WowUp/wowup-electron/src/app/services/toc/toc.service.ts b/WowUp/wowup-electron/src/app/services/toc/toc.service.ts new file mode 100644 index 0000000..d83c41d --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/toc/toc.service.ts @@ -0,0 +1,164 @@ +import { Injectable } from "@angular/core"; +import * as path from "path"; +import { WowClientType, getTocForGameType } from "wowup-lib-core"; + +import * as tocModels from "wowup-lib-core"; +import { removeExtension } from "../../utils/string.utils"; +import { FileService } from "../files/file.service"; +import { uniq } from "lodash"; + +@Injectable({ + providedIn: "root", +}) +export class TocService { + public constructor(private _fileService: FileService) {} + + public async parse(tocPath: string): Promise { + const fileName = path.basename(tocPath); + let tocText = await this._fileService.readFile(tocPath); + tocText = tocText.trim(); + + const dependencies = + this.getValue(tocModels.TOC_DEPENDENCIES, tocText) || this.getValue(tocModels.TOC_REQUIRED_DEPS, tocText); + + const dependencyList: string[] = this.getDependencyList(tocText); + + return { + fileName, + filePath: tocPath, + author: this.getValue(tocModels.TOC_AUTHOR, tocText), + curseProjectId: this.getValue(tocModels.TOC_X_CURSE_PROJECT_ID, tocText), + interface: this.getValueArray(tocModels.TOC_INTERFACE, tocText), + title: this.getValue(tocModels.TOC_TITLE, tocText), + website: this.getWebsite(tocText), + version: this.getValue(tocModels.TOC_VERSION, tocText), + partOf: this.getValue(tocModels.TOC_X_PART_OF, tocText), + category: this.getValue(tocModels.TOC_X_CATEGORY, tocText), + localizations: this.getValue(tocModels.TOC_X_LOCALIZATIONS, tocText), + wowInterfaceId: this.getValue(tocModels.TOC_X_WOWI_ID, tocText), + wagoAddonId: this.getValue(tocModels.TOC_X_WAGO_ID, tocText), + dependencies, + dependencyList, + tukUiProjectId: this.getValue(tocModels.TOC_X_TUKUI_PROJECTID, tocText), + tukUiProjectFolders: this.getValue(tocModels.TOC_X_TUKUI_PROJECTFOLDERS, tocText), + loadOnDemand: this.getValue(tocModels.TOC_X_LOADONDEMAND, tocText), + addonProvider: this.getValue(tocModels.TOC_X_ADDON_PROVIDER, tocText), + notes: this.getValue(tocModels.TOC_NOTES, tocText), + }; + } + + public stripColorCode(str: string): string { + if (str.indexOf("|c") === -1) { + return str; + } + + const regex = /(\|c[a-z0-9]{8})|(\|r)/gi; + + return str.replace(regex, "").trim(); + } + + public stripTextureCode(str: string): string { + if (str.indexOf("|T") === -1) { + return str; + } + + const regex = /(\|T.*\|t)/g; + + return str.replace(regex, "").trim(); + } + + /** + * Return all valid tocs from a given base directory combined with the installation folders for the given client type + */ + public async getAllTocs( + baseDir: string, + installedFolders: string[], + clientType: WowClientType, + ): Promise { + const tocs: tocModels.Toc[] = []; + + for (const dir of installedFolders) { + const dirPath = path.join(baseDir, dir); + + const tocFiles = await this._fileService.listFiles(dirPath, "*.toc"); + const allTocs = await Promise.all( + tocFiles.map((tf) => { + const tocPath = path.join(dirPath, tf); + return this.parse(tocPath); + }), + ); + + const tf = this.getTocForGameType2(dir, allTocs, clientType); + if (tf !== undefined) { + tocs.push(tf); + } + } + + return tocs; + } + + public getTocForGameType2( + folderName: string, + tocs: tocModels.Toc[], + clientType: WowClientType, + ): tocModels.Toc | undefined { + let matchedToc = ""; + + const tocFileNames = tocs.map((toc) => toc.fileName); + matchedToc = getTocForGameType(tocFileNames, clientType); + + // If we still have no match, we need to return the toc that matches the folder name if it exists + // Example: All the things for TBC (ATT-Classic) + if (matchedToc === "") { + return tocs.find((toc) => removeExtension(toc.fileName).toLowerCase() === folderName.toLowerCase()); + } + + return tocs.find((toc) => toc.fileName === matchedToc); + } + + private getWebsite(tocText: string) { + return this.getValue(tocModels.TOC_WEBSITE, tocText) || this.getValue(tocModels.TOC_X_WEBSITE, tocText); + } + + private getDependencyList(tocText: string) { + const dependencies = this.getValue(tocModels.TOC_DEPENDENCIES, tocText); + const requiredDeps = this.getValue(tocModels.TOC_REQUIRED_DEPS, tocText); + + const deps = [...dependencies.split(","), ...requiredDeps.split(",")].filter((dep) => !!dep); + + return deps; + } + + public async parseMetaData(tocPath: string): Promise { + const tocText = await this._fileService.readFile(tocPath); + + return tocText.split("\n").filter((line) => line.trim().startsWith("## ")); + } + + private getValueArray(key: string, tocText: string): string[] { + const value = this.getValue(key, tocText); + return uniq(value.split(",").map((x) => x.trim())); + } + + private getValue(key: string, tocText: string): string { + const match = new RegExp(`^## ${key}:(.*?)$`, "m").exec(tocText); + + if (!match || match.length !== 2) { + return ""; + } + + return this.stripEncodedChars(match[1].trim()); + } + + private stripEncodedChars(value: string) { + let str = this.stripColorCode(value); + str = this.stripTextureCode(str); + str = this.stripNewLineChars(str); + + return str; + } + + private stripNewLineChars(value: string) { + return value.replace(/\|r/g, ""); + } +} diff --git a/WowUp/wowup-electron/src/app/services/ui-message/ui-message.service.ts b/WowUp/wowup-electron/src/app/services/ui-message/ui-message.service.ts new file mode 100644 index 0000000..3fc7244 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/ui-message/ui-message.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from "@angular/core"; +import { Subject } from "rxjs"; + +declare type UiMessageAction = "ad-frame-reload"; + +export interface UiMessage { + action: UiMessageAction; + data?: T; +} + +@Injectable({ + providedIn: "root", +}) +export class UiMessageService { + private readonly _messageSenderSrc = new Subject>(); + + public readonly message$ = this._messageSenderSrc.asObservable(); + + public sendMessage(action: UiMessageAction, data?: any) { + this._messageSenderSrc.next({ + action, + data, + }); + } +} diff --git a/WowUp/wowup-electron/src/app/services/warcraft/warcraft-installation.service.ts b/WowUp/wowup-electron/src/app/services/warcraft/warcraft-installation.service.ts new file mode 100644 index 0000000..e56b93d --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/warcraft/warcraft-installation.service.ts @@ -0,0 +1,310 @@ +import * as _ from "lodash"; +import * as path from "path"; +import { firstValueFrom, from, ReplaySubject, Subject } from "rxjs"; +import { map, tap } from "rxjs/operators"; +import { v4 as uuidv4 } from "uuid"; + +import { Injectable } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +import { WOW_INSTALLATIONS_KEY } from "../../../common/constants"; +import { getWowClientFolderName } from "../../../common/warcraft"; +import { getEnumName, getWowClientGroupForType } from "wowup-lib-core"; +import { ElectronService } from "../electron/electron.service"; +import { FileService } from "../files/file.service"; +import { PreferenceStorageService } from "../storage/preference-storage.service"; +import { WarcraftService } from "./warcraft.service"; +import { AddonChannelType, WowClientGroup, WowClientType } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +const DEFAULT_NAME_TOKEN = "{defaultName}"; + +@Injectable({ + providedIn: "root", +}) +export class WarcraftInstallationService { + private readonly _wowInstallationsSrc = new ReplaySubject(1); + private readonly _legacyInstallationSrc = new Subject(); + + private _wowInstallations: WowInstallation[] = []; + private _blizzardAgentPath = ""; + + public readonly wowInstallations$ = this._wowInstallationsSrc.asObservable(); + public readonly legacyInstallationSrc$ = this._legacyInstallationSrc.asObservable(); + + public get blizzardAgentPath(): string { + return `${this._blizzardAgentPath}`; + } + + public constructor( + private _preferenceStorageService: PreferenceStorageService, + private _warcraftService: WarcraftService, + private _translateService: TranslateService, + private _fileService: FileService, + private _electronService: ElectronService, + ) { + this._wowInstallationsSrc.subscribe((installations) => { + for (const installation of installations) { + // Saved display names can be unreliable (e.g. language change, + // expansion release). Always regenerate them. + const typeName = getEnumName(WowClientType, installation.clientType); + this.getDisplayName(installation.label, typeName) + .then((displayName) => (installation.displayName = displayName)) + .catch(() => {}); + } + this._wowInstallations = installations; + }); + + from(this._warcraftService.getBlizzardAgentPath()) + .pipe( + tap((blizzardAgentPath) => { + this._blizzardAgentPath = blizzardAgentPath; + }), + // switchMap((blizzardAgentPath) => this.migrateAllLegacyInstallations(blizzardAgentPath)), + map((blizzardAgentPath) => this.importWowInstallations(blizzardAgentPath)), + ) + .subscribe(); + } + + public async reOrderInstallation(installationId: string, direction: number): Promise { + const originIndex = this._wowInstallations.findIndex((installation) => installation.id === installationId); + if (originIndex === -1) { + console.warn("Installation not found to re-order", installationId); + return; + } + + const newIndex = originIndex + direction; + if (newIndex < 0 || newIndex >= this._wowInstallations.length) { + console.warn("New index was out of bounds"); + return; + } + + const installationCpy = [...this._wowInstallations]; + + [installationCpy[newIndex], installationCpy[originIndex]] = [ + installationCpy[originIndex], + installationCpy[newIndex], + ]; + + await this.setWowInstallations(installationCpy); + this._wowInstallationsSrc.next(installationCpy); + } + + public async getWowInstallationsAsync(): Promise { + const results = await this._preferenceStorageService.getObjectAsync(WOW_INSTALLATIONS_KEY); + return results || []; + } + + public getWowInstallation(installationId: string | undefined): WowInstallation | undefined { + if (!installationId) { + console.warn("getWowInstallation invalid installationId"); + return undefined; + } + + return this._wowInstallations.find((installation) => installation.id === installationId); + } + + public async getWowInstallationsByClientType(clientType: WowClientType): Promise { + const installations = await this.getWowInstallationsAsync(); + return _.filter(installations, (installation) => installation.clientType === clientType); + } + + public async getWowInstallationsByClientTypes(clientTypes: WowClientType[]): Promise { + const installations = await this.getWowInstallationsAsync(); + return _.filter(installations, (installation) => clientTypes.includes(installation.clientType)); + } + + public async getWowInstallationsByClientGroups(clientGroups: WowClientGroup[]): Promise { + const installations = await this.getWowInstallationsAsync(); + return _.filter(installations, (installation) => { + const clientGroup = getWowClientGroupForType(installation.clientType); + return clientGroups.includes(clientGroup); + }); + } + + public async setWowInstallations(wowInstallations: WowInstallation[]): Promise { + console.log(`Setting wow installations: ${wowInstallations.length}`); + await this._preferenceStorageService.setAsync(WOW_INSTALLATIONS_KEY, wowInstallations); + } + + public async setSelectedWowInstallation(wowInstallation: WowInstallation): Promise { + const allInstallations = await this.getWowInstallationsAsync(); + _.forEach(allInstallations, (installation) => { + installation.selected = installation.id === wowInstallation.id; + }); + + await this.setWowInstallations(allInstallations); + } + + public async getInstallationDisplayName(wowInstallation: WowInstallation): Promise { + const typeName = getEnumName(WowClientType, wowInstallation.clientType); + return await this.getDisplayName(wowInstallation.label, typeName); + } + + public async updateWowInstallation(wowInstallation: WowInstallation): Promise { + const typeName = getEnumName(WowClientType, wowInstallation.clientType); + wowInstallation.displayName = await this.getDisplayName(wowInstallation.label, typeName); + + const storedInstallations = await this.getWowInstallationsAsync(); + const matchIndex = _.findIndex(storedInstallations, (installation) => installation.id === wowInstallation.id); + + if (matchIndex === -1) { + throw new Error("No installation to update"); + } + + storedInstallations.splice(matchIndex, 1, wowInstallation); + + await this.setWowInstallations(storedInstallations); + this._wowInstallationsSrc.next(storedInstallations); + } + + public async selectWowClientPath(): Promise { + const selectionName = this._translateService.instant("COMMON.WOW_EXE_SELECTION_NAME"); + const extensionFilter = this._warcraftService.getExecutableExtension(); + let dialogResult: Electron.OpenDialogReturnValue; + if (extensionFilter) { + dialogResult = await this._electronService.showOpenDialog({ + filters: [{ extensions: [extensionFilter], name: selectionName }], + properties: ["openFile"], + }); + } else { + // platforms like linux don't really fit the rule + dialogResult = await this._electronService.showOpenDialog({ + properties: ["openFile"], + }); + } + + if (dialogResult.canceled) { + return ""; + } + + const selectedPath = _.first(dialogResult.filePaths); + if (!selectedPath || !selectedPath.endsWith("")) { + console.warn("No path selected"); + return ""; + } + + return selectedPath; + } + + public async addInstallation(installation: WowInstallation, notify = true): Promise { + const existingInstallations = await this.getWowInstallationsAsync(); + const exists = _.findIndex(existingInstallations, (inst) => inst.location === installation.location) !== -1; + if (exists) { + throw new Error(`Installation already exists: ${installation.location}`); + } + + existingInstallations.push(installation); + + await this.setWowInstallations(existingInstallations); + + if (notify) { + this._wowInstallationsSrc.next(existingInstallations); + } + } + + public async removeWowInstallation(installation: WowInstallation): Promise { + const installations = await this.getWowInstallationsAsync(); + const installationExists = _.findIndex(installations, (inst) => inst.id === installation.id) !== -1; + if (!installationExists) { + throw new Error(`Installation does not exist: ${installation.id}`); + } + + _.remove(installations, (inst) => inst.id === installation.id); + + await this.setWowInstallations(installations); + this._wowInstallationsSrc.next(installations); + } + + public async createWowInstallationForPath(applicationPath: string): Promise { + const clientType = this._warcraftService.getClientTypeForBinary(applicationPath); + const typeName = getEnumName(WowClientType, clientType); + const currentInstallations = await this.getWowInstallationsByClientType(clientType); + + const label = currentInstallations.length + ? `${DEFAULT_NAME_TOKEN} ${currentInstallations.length + 1}` + : DEFAULT_NAME_TOKEN; + + const displayName = await this.getDisplayName(label, typeName); + + const installation: WowInstallation = { + id: uuidv4(), + clientType: clientType, + defaultAddonChannelType: AddonChannelType.Stable, + defaultAutoUpdate: false, + label: label, + displayName: displayName, + location: applicationPath, + selected: false, + }; + + return installation; + } + + public async importWowInstallations(blizzardAgentPath: string): Promise { + if (!blizzardAgentPath) { + console.log(`Cannot import wow installations, no agent path`); + const installations = await this.getWowInstallationsAsync(); + this._wowInstallationsSrc.next(installations); + return; + } + + const installedProducts = await this._warcraftService.getInstalledProducts(blizzardAgentPath); + + for (const product of Array.from(installedProducts.values())) { + const typeName = getEnumName(WowClientType, product.clientType); + const currentInstallations = await this.getWowInstallationsByClientType(product.clientType); + + const label = currentInstallations.length + ? `${DEFAULT_NAME_TOKEN} ${currentInstallations.length + 1}` + : DEFAULT_NAME_TOKEN; + const displayName = await this.getDisplayName(label, typeName); + + const fullProductPath = this.getFullProductPath(product.location, product.clientType); + + if (currentInstallations.some((inst) => inst.location === fullProductPath)) { + continue; + } + + const wowInstallation: WowInstallation = { + id: uuidv4(), + clientType: product.clientType, + label, + displayName, + location: fullProductPath, + selected: false, + defaultAddonChannelType: AddonChannelType.Stable, + defaultAutoUpdate: false, + }; + + try { + await this.addInstallation(wowInstallation, false); + } catch (e) { + // Ignore duplicate error + } + } + + const wowInstallations = await this.getWowInstallationsAsync(); + this._wowInstallationsSrc.next(wowInstallations); + } + + private async getDisplayName(label: string, typeName: string): Promise { + const defaultName: string = await firstValueFrom( + this._translateService.get(`COMMON.CLIENT_TYPES.${typeName.toUpperCase()}`), + ); + console.debug("getDisplayName", defaultName, label, typeName); + const finalLabel = label.replace(DEFAULT_NAME_TOKEN, defaultName); + + if (finalLabel.includes(DEFAULT_NAME_TOKEN)) { + throw new Error(`Default token in name: ${label} => ${typeName} `); + } + + return finalLabel; + } + + private getFullProductPath(location: string, clientType: WowClientType): string { + const clientFolderName = getWowClientFolderName(clientType); + const executableName = this._warcraftService.getExecutableName(clientType); + return path.join(location, clientFolderName, executableName); + } +} diff --git a/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.impl.ts b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.impl.ts new file mode 100644 index 0000000..537901c --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.impl.ts @@ -0,0 +1,11 @@ +import { WowClientType } from "wowup-lib-core"; +import { InstalledProduct } from "wowup-lib-core"; + +export interface WarcraftServiceImpl { + getExecutableExtension(): string; + isWowApplication(appName: string): boolean; + getBlizzardAgentPath(): Promise; + getExecutableName(clientType: WowClientType): string; + getClientType(binaryPath: string): WowClientType; + resolveProducts(decodedProducts: InstalledProduct[], agentPath: string): InstalledProduct[]; +} diff --git a/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.linux.ts b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.linux.ts new file mode 100644 index 0000000..66d6ee0 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.linux.ts @@ -0,0 +1,177 @@ +import * as path from "path"; +import { WowClientType } from "wowup-lib-core"; +import { InstalledProduct } from "wowup-lib-core"; +import { WOW_CLASSIC_ERA_FOLDER, WOW_CLASSIC_ERA_PTR_FOLDER, WOW_RETAIL_XPTR_FOLDER } from "../../../common/constants"; +import { ElectronService } from "../electron/electron.service"; +import { FileService } from "../files/file.service"; +import { WarcraftServiceImpl } from "./warcraft.service.impl"; + +const WOW_RETAIL_NAME = "Wow.exe"; +const WOW_RETAIL_PTR_NAME = "WowT.exe"; +const WOW_RETAIL_BETA_NAME = "WowB.exe"; +const WOW_CLASSIC_NAME = "WowClassic.exe"; +const WOW_CLASSIC_PTR_NAME = "WowClassicT.exe"; +const WOW_CLASSIC_BETA_NAME = "WowClassicB.exe"; + +const WOW_APP_NAMES = [ + WOW_RETAIL_NAME, + WOW_RETAIL_PTR_NAME, + WOW_RETAIL_BETA_NAME, + WOW_CLASSIC_NAME, + WOW_CLASSIC_PTR_NAME, + WOW_CLASSIC_BETA_NAME, +]; + +const LUTRIS_CONFIG_PATH = "/.config/lutris/system.yml"; +// Search in this order until products are found on one. +// All WoW products can be found under any or all of them, +// since each of them are essentially just Battle.net +// launchers with a different install path. +const LUTRIS_WOW_DIRS = ["battlenet/drive_c", "world-of-warcraft/drive_c", "world-of-warcraft-classic/drive_c"]; + +// BLIZZARD STRINGS +const WINDOWS_BLIZZARD_AGENT_PATH = "ProgramData/Battle.net/Agent"; +const BLIZZARD_PRODUCT_DB_NAME = "product.db"; + +export class WarcraftServiceLinux implements WarcraftServiceImpl { + public constructor(private _electronService: ElectronService, private _fileService: FileService) {} + + public getExecutableExtension(): string { + return "exe"; + } + + public isWowApplication(appName: string): boolean { + return WOW_APP_NAMES.includes(appName); + } + + /** + * On Linux players could be using Lutris to install the Battle.net launcher or WoW + */ + public async getBlizzardAgentPath(): Promise { + try { + const lutrisLibraryPath = await this.getLutrisWowPath(); + if (lutrisLibraryPath.length === 0) { + throw new Error("Lutris library not found"); + } + + const agentPath = path.join(lutrisLibraryPath, WINDOWS_BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME); + const agentPathExists = await this._fileService.pathExists(agentPath); + + if (agentPathExists) { + console.log(`Found WoW products at ${agentPath}`); + return agentPath; + } + } catch (e) { + console.error("Failed to search for blizzard products", e); + } + + return ""; + } + + public resolveProducts(decodedProducts: InstalledProduct[], agentPath: string): InstalledProduct[] { + const resolvedProducts: InstalledProduct[] = []; + const agentPathPrefixRegex = new RegExp(`(.*drive_c)`); + for (const product of decodedProducts) { + console.log(`location: ${location.toString()} agentPath: ${agentPath}`); + const regexResults = agentPathPrefixRegex.exec(agentPath); + if (regexResults === null) { + console.warn("No agentPath match found"); + continue; + } + + const agentPathPrefix = regexResults[1].trim(); + resolvedProducts.push({ + ...product, + location: path.join(agentPathPrefix, product.location.substr(3)), + } as InstalledProduct); + } + return resolvedProducts; + } + + public async getLutrisWowPath(): Promise { + const homeDir = await this._fileService.getHomeDir(); + const resolvedPath = path.join(homeDir, LUTRIS_CONFIG_PATH); + try { + const lutrisConfigExists = await this._fileService.pathExists(resolvedPath); + if (lutrisConfigExists) { + const lutrisConfig = await this._fileService.readFile(resolvedPath); + const libraryPathRegex = new RegExp(`game_path: (.*)`); + const regexResults = libraryPathRegex.exec(lutrisConfig); + if (regexResults === null) { + throw new Error("No matching game_path found"); + } + + const libraryPath = regexResults[1].trim(); + const libraryPathExists = await this._fileService.pathExists(libraryPath); + if (libraryPathExists) { + for (const wowDir of LUTRIS_WOW_DIRS) { + const productPath = path.join(libraryPath, wowDir); + const productPathExists = await this._fileService.pathExists(productPath); + if (productPathExists) { + console.log(`Found WoW product in Lutris library at ${productPath}`); + return productPath; + } + } + } + } + throw new Error(); + } catch (e) { + console.error("Failed to search for Lutris library location", e); + } + return ""; + } + + public getExecutableName(clientType: WowClientType): string { + switch (clientType) { + case WowClientType.Retail: + return WOW_RETAIL_NAME; + case WowClientType.ClassicEra: + case WowClientType.Classic: + return WOW_CLASSIC_NAME; + case WowClientType.RetailPtr: + case WowClientType.RetailXPtr: + return WOW_RETAIL_PTR_NAME; + case WowClientType.ClassicPtr: + case WowClientType.ClassicEraPtr: + return WOW_CLASSIC_PTR_NAME; + case WowClientType.Beta: + return WOW_RETAIL_BETA_NAME; + case WowClientType.ClassicBeta: + return WOW_CLASSIC_BETA_NAME; + default: + return ""; + } + } + + public getClientType(binaryPath: string): WowClientType { + const binaryName = path.basename(binaryPath); + switch (binaryName) { + case WOW_RETAIL_NAME: + return WowClientType.Retail; + case WOW_CLASSIC_NAME: + if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_FOLDER)) { + return WowClientType.ClassicEra; + } else { + return WowClientType.Classic; + } + case WOW_RETAIL_PTR_NAME: + if (binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)) { + return WowClientType.RetailXPtr; + } else { + return WowClientType.RetailPtr; + } + case WOW_CLASSIC_PTR_NAME: + if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)) { + return WowClientType.ClassicEraPtr; + } else { + return WowClientType.ClassicPtr; + } + case WOW_RETAIL_BETA_NAME: + return WowClientType.Beta; + case WOW_CLASSIC_BETA_NAME: + return WowClientType.ClassicBeta; + default: + return WowClientType.None; + } + } +} diff --git a/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.mac.ts b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.mac.ts new file mode 100644 index 0000000..27068ee --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.mac.ts @@ -0,0 +1,105 @@ +import * as path from "path"; +import { WowClientType } from "wowup-lib-core"; +import { InstalledProduct } from "wowup-lib-core"; + +import { WOW_CLASSIC_ERA_FOLDER, WOW_CLASSIC_ERA_PTR_FOLDER, WOW_RETAIL_XPTR_FOLDER } from "../../../common/constants"; +import { FileService } from "../files/file.service"; +import { WarcraftServiceImpl } from "./warcraft.service.impl"; + +const WOW_RETAIL_NAME = "World of Warcraft.app"; +const WOW_RETAIL_PTR_NAME = "World of Warcraft Test.app"; +const WOW_RETAIL_BETA_NAME = "World of Warcraft Beta.app"; +const WOW_CLASSIC_NAME = "World of Warcraft Classic.app"; +const WOW_CLASSIC_PTR_NAME = "World of Warcraft Classic Test.app"; +const WOW_CLASSIC_BETA_NAME = "World of Warcraft Classic Beta.app"; + +const WOW_APP_NAMES = [ + WOW_RETAIL_NAME, + WOW_RETAIL_PTR_NAME, + WOW_CLASSIC_NAME, + WOW_CLASSIC_PTR_NAME, + WOW_RETAIL_BETA_NAME, + WOW_CLASSIC_BETA_NAME, +]; + +const BLIZZARD_AGENT_PATH = "/Users/Shared/Battle.net/Agent"; +const BLIZZARD_PRODUCT_DB_NAME = "product.db"; + +export class WarcraftServiceMac implements WarcraftServiceImpl { + public constructor(private _fileService: FileService) {} + + public getExecutableExtension(): string { + return "app"; + } + + public isWowApplication(appName: string): boolean { + return WOW_APP_NAMES.includes(appName); + } + + /** + * Attempt to figure out where the blizzard agent was installed at + */ + public async getBlizzardAgentPath(): Promise { + const agentPath = path.join(BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME); + const exists = await this._fileService.pathExists(agentPath); + return exists ? agentPath : ""; + } + + public getExecutableName(clientType: WowClientType): string { + switch (clientType) { + case WowClientType.Retail: + return WOW_RETAIL_NAME; + case WowClientType.ClassicEra: + case WowClientType.Classic: + return WOW_CLASSIC_NAME; + case WowClientType.RetailPtr: + case WowClientType.RetailXPtr: + return WOW_RETAIL_NAME; + case WowClientType.ClassicPtr: + case WowClientType.ClassicEraPtr: + return WOW_CLASSIC_PTR_NAME; + case WowClientType.Beta: + return WOW_RETAIL_BETA_NAME; + case WowClientType.ClassicBeta: + return WOW_CLASSIC_BETA_NAME; + default: + return ""; + } + } + + public getClientType(binaryPath: string): WowClientType { + const binaryName = path.basename(binaryPath); + switch (binaryName) { + case WOW_RETAIL_NAME: + return WowClientType.Retail; + case WOW_CLASSIC_NAME: + if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_FOLDER)) { + return WowClientType.ClassicEra; + } else { + return WowClientType.Classic; + } + case WOW_RETAIL_PTR_NAME: + if (binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)) { + return WowClientType.RetailXPtr; + } else { + return WowClientType.RetailPtr; + } + case WOW_CLASSIC_PTR_NAME: + if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)) { + return WowClientType.ClassicEraPtr; + } else { + return WowClientType.ClassicPtr; + } + case WOW_RETAIL_BETA_NAME: + return WowClientType.Beta; + case WOW_CLASSIC_BETA_NAME: + return WowClientType.ClassicBeta; + default: + return WowClientType.None; + } + } + + public resolveProducts(decodedProducts: InstalledProduct[]): InstalledProduct[] { + return decodedProducts; + } +} diff --git a/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.ts b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.ts new file mode 100644 index 0000000..e517973 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.ts @@ -0,0 +1,291 @@ +import * as path from "path"; +import { BehaviorSubject } from "rxjs"; +import { filter, map } from "rxjs/operators"; + +import { Injectable } from "@angular/core"; + +import { ElectronService } from "../electron/electron.service"; +import * as constants from "../../../common/constants"; +import { SelectItem } from "../../models/wowup/select-item"; +import { getEnumName, getEnumList } from "wowup-lib-core"; +import { FileService } from "../files/file.service"; +import { PreferenceStorageService } from "../storage/preference-storage.service"; +import { TocService } from "../toc/toc.service"; +import { WarcraftServiceImpl } from "./warcraft.service.impl"; +import { WarcraftServiceLinux } from "./warcraft.service.linux"; +import { WarcraftServiceMac } from "./warcraft.service.mac"; +import { WarcraftServiceWin } from "./warcraft.service.win"; +import { ProductDb } from "../../../common/wowup/product-db"; +import { AddonFolder, Toc, WowClientType } from "wowup-lib-core"; +import { InstalledProduct, WowInstallation } from "wowup-lib-core"; + +@Injectable({ + providedIn: "root", +}) +export class WarcraftService { + private readonly _impl: WarcraftServiceImpl; + private readonly _productsSrc = new BehaviorSubject([]); + private readonly _installedClientTypesSrc = new BehaviorSubject(undefined); + private readonly _allClientTypes = getEnumList(WowClientType).filter( + (clientType) => clientType !== WowClientType.None + ); + + public readonly products$ = this._productsSrc.asObservable(); + public readonly productsReady$ = this.products$.pipe(filter((products) => Array.isArray(products))); + public readonly installedClientTypes$ = this._installedClientTypesSrc.asObservable(); + + // Map the client types so that we can localize them + public installedClientTypesSelectItems$ = this._installedClientTypesSrc.pipe( + filter((clientTypes) => clientTypes !== undefined), + map((clientTypes) => { + if (clientTypes === undefined) { + return []; + } + + return clientTypes.map((ct): SelectItem => { + const clientTypeName = getEnumName(WowClientType, ct).toUpperCase(); + return { + display: `COMMON.CLIENT_TYPES.${clientTypeName}`, + value: ct, + }; + }); + }) + ); + + public constructor( + private _electronService: ElectronService, + private _fileService: FileService, + private _preferenceStorageService: PreferenceStorageService, + private _tocService: TocService + ) { + this._impl = this.getImplementation(); + } + + public getExecutableName(clientType: WowClientType): string { + return this._impl.getExecutableName(clientType); + } + + public getExecutableExtension(): string { + return this._impl.getExecutableExtension(); + } + + public async isWowApplication(appPath: string): Promise { + const pathExists = await this._fileService.pathExists(appPath); + if (!pathExists) { + return false; + } + + const fileName = path.basename(appPath); + return this._impl.isWowApplication(fileName); + } + + public getAllClientTypes(): WowClientType[] { + return [...this._allClientTypes]; + } + + public getProductLocation( + clientType: WowClientType, + installedProducts: Map + ): string { + const clientLocation = installedProducts.get(clientType); + return clientLocation?.location ?? ""; + } + + /** + * Scan the local blizzard product db for install WoW instances + */ + public async getInstalledProducts(blizzardAgentPath: string): Promise> { + const decodedProducts = await this.decodeProducts(blizzardAgentPath); + const resolvedProducts = this._impl.resolveProducts(decodedProducts, blizzardAgentPath); + const dictionary = new Map(); + + for (const product of resolvedProducts) { + dictionary.set(product.clientType, product); + } + + return dictionary; + } + + public getAddonFolderPath(installation: WowInstallation): string { + const installDir = path.dirname(installation.location); + return path.join(installDir, constants.WOW_INTERFACE_FOLDER_NAME, constants.WOW_ADDON_FOLDER_NAME); + } + + public async listAddons(installation: WowInstallation, scanSymlinks = false): Promise { + const addonFolders: AddonFolder[] = []; + if (!installation) { + return addonFolders; + } + + const addonFolderPath = this.getAddonFolderPath(installation); + + // Folder may not exist if no addons have been installed + const addonFolderExists = await this._fileService.pathExists(addonFolderPath); + if (!addonFolderExists) { + return addonFolders; + } + + const directories = await this._fileService.listDirectories(addonFolderPath, scanSymlinks); + const dirPaths = directories.map((dir) => path.join(addonFolderPath, dir)); + const dirStats = await this._fileService.statFiles(dirPaths); + + for (let i = 0; i < directories.length; i += 1) { + const dir = directories[i]; + const addonFolder = await this.getAddonFolder(addonFolderPath, dir); + if (!addonFolder) { + console.warn(`Failed to get addonFolder, no toc found: ${dir}`); + continue; + } + + addonFolder.fileStats = dirStats[path.join(addonFolderPath, dir)]; + if (addonFolder) { + addonFolders.push(addonFolder); + } + } + + return addonFolders; + } + + public async getAddonFolder(addonFolderPath: string, dir: string): Promise { + try { + const dirPath = path.join(addonFolderPath, dir); + const dirFiles = await this._fileService.readdir(dirPath); + const tocFiles = dirFiles.filter((f) => path.extname(f) === ".toc"); + if (tocFiles.length === 0) { + return undefined; + } + + const tocs: Toc[] = []; + for (const tocFile of tocFiles) { + const tocPath = path.join(dirPath, tocFile); + const toc = await this._tocService.parse(tocPath); + tocs.push(toc); + } + + return { + name: dir, + path: dirPath, + status: "Pending", + tocs: tocs, + }; + } catch (e) { + console.error(e); + return undefined; + } + } + + public async getBlizzardAgentPath(): Promise { + const storedAgentPath = await this._preferenceStorageService.getAsync(constants.BLIZZARD_AGENT_PATH_KEY); + if (storedAgentPath) { + return storedAgentPath; + } + + const agentPath = await this._impl.getBlizzardAgentPath(); + await this._preferenceStorageService.setAsync(constants.BLIZZARD_AGENT_PATH_KEY, agentPath); + + return agentPath; + } + + public getClientTypeForBinary(binaryPath: string): WowClientType { + return this._impl.getClientType(binaryPath); + } + + /** + * Get the old style preference key for a WoW client type + * @deprecated + */ + public getLegacyClientLocationKey(clientType: WowClientType): string { + switch (clientType) { + case WowClientType.Retail: + return constants.RETAIL_LOCATION_KEY; + case WowClientType.Classic: + return constants.CLASSIC_LOCATION_KEY; + case WowClientType.ClassicEra: + return constants.CLASSIC_LOCATION_KEY; + case WowClientType.RetailPtr: + return constants.RETAIL_PTR_LOCATION_KEY; + case WowClientType.ClassicPtr: + return constants.CLASSIC_PTR_LOCATION_KEY; + case WowClientType.Beta: + return constants.BETA_LOCATION_KEY; + default: + throw new Error(`Failed to get client location key: ${clientType}, ${getEnumName(WowClientType, clientType)}`); + } + } + + private async decodeProducts(productDbPath: string) { + if (!productDbPath) { + return []; + } + + try { + const productDb = await this._electronService.invoke("decode-product-db", productDbPath); + console.log("productDb", productDb); + + let wowProducts: InstalledProduct[] = productDb.products + .filter((p) => p.family === "wow") + .map((p) => ({ + location: p.client.location, + name: p.client.name, + clientType: this.getClientTypeForFolderName(p.client.name), + })); + + wowProducts = wowProducts.filter((wp) => { + const hasClientType = wp.clientType != WowClientType.None; + if (!hasClientType) { + console.warn("Invalid client type detected", wp); + } + return hasClientType; + }); + + console.log("wowProducts", wowProducts); + + return wowProducts; + } catch (e) { + console.error(`failed to decode product db at ${productDbPath}`); + console.error(e); + return []; + } + } + + private getImplementation(): WarcraftServiceImpl { + if (this._electronService.isWin) { + return new WarcraftServiceWin(this._electronService, this._fileService); + } + + if (this._electronService.isMac) { + return new WarcraftServiceMac(this._fileService); + } + + if (this._electronService.isLinux) { + return new WarcraftServiceLinux(this._electronService, this._fileService); + } + + throw new Error("No warcraft service implementation found"); + } + + private getClientTypeForFolderName(folderName: string): WowClientType { + switch (folderName) { + case constants.WOW_RETAIL_FOLDER: + return WowClientType.Retail; + case constants.WOW_RETAIL_PTR_FOLDER: + return WowClientType.RetailPtr; + case constants.WOW_RETAIL_XPTR_FOLDER: + return WowClientType.RetailXPtr; + case constants.WOW_CLASSIC_ERA_FOLDER: + return WowClientType.ClassicEra; + case constants.WOW_CLASSIC_FOLDER: + return WowClientType.Classic; + case constants.WOW_CLASSIC_PTR_FOLDER: + return WowClientType.ClassicPtr; + case constants.WOW_BETA_FOLDER: + return WowClientType.Beta; + case constants.WOW_CLASSIC_BETA_FOLDER: + return WowClientType.ClassicBeta; + case constants.WOW_CLASSIC_ERA_PTR_FOLDER: + return WowClientType.ClassicEraPtr; + default: + return WowClientType.None; + } + } +} diff --git a/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.win.ts b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.win.ts new file mode 100644 index 0000000..36542ce --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/warcraft/warcraft.service.win.ts @@ -0,0 +1,129 @@ +import * as path from "path"; +import { ElectronService } from "../electron/electron.service"; +import { FileService } from "../files/file.service"; +import { WarcraftServiceImpl } from "./warcraft.service.impl"; +import { + IPC_LIST_DISKS_WIN32, + WOW_CLASSIC_ERA_FOLDER, + WOW_CLASSIC_ERA_PTR_FOLDER, + WOW_RETAIL_XPTR_FOLDER, +} from "../../../common/constants"; +import { WowClientType } from "wowup-lib-core"; +import { InstalledProduct } from "wowup-lib-core"; + +const WOW_RETAIL_NAME = "Wow.exe"; +const WOW_RETAIL_PTR_NAME = "WowT.exe"; +const WOW_RETAIL_BETA_NAME = "WowB.exe"; +const WOW_CLASSIC_NAME = "WowClassic.exe"; +const WOW_CLASSIC_PTR_NAME = "WowClassicT.exe"; +const WOW_CLASSIC_BETA_NAME = "WowClassicB.exe"; + +const WOW_APP_NAMES = [ + WOW_RETAIL_NAME, + WOW_RETAIL_PTR_NAME, + WOW_RETAIL_BETA_NAME, + WOW_CLASSIC_NAME, + WOW_CLASSIC_PTR_NAME, + WOW_CLASSIC_BETA_NAME, +]; + +// BLIZZARD STRINGS +const WINDOWS_BLIZZARD_AGENT_PATH = "ProgramData/Battle.net/Agent"; +const BLIZZARD_PRODUCT_DB_NAME = "product.db"; + +export class WarcraftServiceWin implements WarcraftServiceImpl { + public constructor(private _electronService: ElectronService, private _fileService: FileService) {} + + public getExecutableExtension(): string { + return "exe"; + } + + public isWowApplication(appName: string): boolean { + return WOW_APP_NAMES.includes(appName); + } + + /** + * Attempt to figure out where the blizzard agent was installed at + */ + public async getBlizzardAgentPath(): Promise { + try { + const diskInfo = await this._electronService.invoke(IPC_LIST_DISKS_WIN32); + console.debug("diskInfo", diskInfo); + const driveNames: string[] = diskInfo.map((i) => i.mounted); + + for (const name of driveNames) { + const agentPath = path.join(name, WINDOWS_BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME); + const exists = await this._fileService.pathExists(agentPath); + + if (exists) { + console.log(`Found products at ${agentPath}`); + return agentPath; + } + } + } catch (e) { + console.error("Failed to search for blizzard products", e); + } + + return ""; + } + + public getExecutableName(clientType: WowClientType): string { + switch (clientType) { + case WowClientType.Retail: + return WOW_RETAIL_NAME; + case WowClientType.ClassicEra: + case WowClientType.Classic: + return WOW_CLASSIC_NAME; + case WowClientType.RetailPtr: + case WowClientType.RetailXPtr: + return WOW_RETAIL_PTR_NAME; + case WowClientType.ClassicPtr: + case WowClientType.ClassicEraPtr: + return WOW_CLASSIC_PTR_NAME; + case WowClientType.Beta: + return WOW_RETAIL_BETA_NAME; + case WowClientType.ClassicBeta: + return WOW_CLASSIC_BETA_NAME; + default: + return ""; + } + } + + public getClientType(binaryPath: string): WowClientType { + const binaryName = path.basename(binaryPath); + switch (binaryName) { + case WOW_RETAIL_NAME: + return WowClientType.Retail; + case WOW_CLASSIC_NAME: + if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_FOLDER)) { + return WowClientType.ClassicEra; + } else { + return WowClientType.Classic; + } + case WOW_RETAIL_PTR_NAME: + if (binaryPath.toLowerCase().includes(WOW_RETAIL_XPTR_FOLDER)) { + return WowClientType.RetailXPtr; + } else { + return WowClientType.RetailPtr; + } + case WOW_RETAIL_XPTR_FOLDER: + return WowClientType.RetailXPtr; + case WOW_CLASSIC_PTR_NAME: + if (binaryPath.toLowerCase().includes(WOW_CLASSIC_ERA_PTR_FOLDER)) { + return WowClientType.ClassicEraPtr; + } else { + return WowClientType.ClassicPtr; + } + case WOW_RETAIL_BETA_NAME: + return WowClientType.Beta; + case WOW_CLASSIC_BETA_NAME: + return WowClientType.ClassicBeta; + default: + return WowClientType.None; + } + } + + public resolveProducts(decodedProducts: InstalledProduct[]): InstalledProduct[] { + return decodedProducts; + } +} diff --git a/WowUp/wowup-electron/src/app/services/wowup-api/wowup-api.service.ts b/WowUp/wowup-electron/src/app/services/wowup-api/wowup-api.service.ts new file mode 100644 index 0000000..df5a22a --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/wowup-api/wowup-api.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from "@angular/core"; +import { WowUpGetAccountResponse } from "wowup-lib-core"; +import { AppConfig } from "../../../environments/environment"; +import { CachingService } from "../caching/caching-service"; +import { CircuitBreakerWrapper, NetworkService } from "../network/network.service"; + +const API_URL = AppConfig.wowUpApiUrl; + +@Injectable({ + providedIn: "root", +}) +export class WowUpApiService { + private readonly _circuitBreaker: CircuitBreakerWrapper; + + public constructor(private _networkService: NetworkService, private _cacheService: CachingService) { + this._circuitBreaker = this._networkService.getCircuitBreaker(`WowUpApiService_main`); + } + + public async getAccount(authToken: string): Promise { + const url = new URL(`${API_URL}/account`); + return await this._circuitBreaker.getJson( + url, + this.getAuthorizationHeader(authToken), + 5000 + ); + } + + public async registerPushToken(authToken: string, pushToken: string, deviceType: string): Promise { + const url = new URL(`${API_URL}/account/push`); + url.searchParams.set("push_token", pushToken); + url.searchParams.set("os", deviceType); + + return this._circuitBreaker.postJson(url, {}, this.getAuthorizationHeader(authToken)); + } + + public async removePushToken(authToken: string, pushToken: string): Promise { + const url = new URL(`${API_URL}/account/push/${pushToken}`); + return this._circuitBreaker.deleteJson(url, this.getAuthorizationHeader(authToken)); + } + + private getAuthorizationHeader(authToken: string): { Authorization: string } { + return { + Authorization: `Bearer ${authToken}`, + }; + } +} diff --git a/WowUp/wowup-electron/src/app/services/wowup/patch-notes.service.ts b/WowUp/wowup-electron/src/app/services/wowup/patch-notes.service.ts new file mode 100644 index 0000000..378ea4f --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/wowup/patch-notes.service.ts @@ -0,0 +1,713 @@ +import { Injectable } from "@angular/core"; + +import ChangeLogJson from "../../../assets/changelog.json"; +import { ChangeLog } from "../../models/wowup/change-log"; + +@Injectable({ + providedIn: "root", +}) +export class PatchNotesService { + public changeLogs: ChangeLog[] = []; + + public constructor() { + this.changeLogs = [...CHANGELOGS, ...ChangeLogJson.ChangeLogs]; + } +} + +const CHANGELOGS: ChangeLog[] = [ + { + Version: "2.12.0", + html: ` +

Features

+
    +
  • Add Cataclysm
  • +
+

Changes

+
    +
  • Remove minor warning for folders not containing exact interface match
  • +
+ `, + }, + { + Version: "2.11.1", + html: ` +

Changes

+
    +
  • Update for better Linux support
  • +
+ `, + }, + { + Version: "2.11.0", + html: ` +

Changes

+
    +
  • German locale updates (Glow)
  • +
  • Support new WoW Dragonflight 10.2 PTR
  • +
  • Update to latest OW framework
  • +
  • Update to latest UI framework
  • +
+ `, + }, + { + Version: "2.10.0", + html: ` +

Changes

+
    +
  • Support new TukUI API
  • +
  • New retry logic when downloading an addon update
  • +
+

Fixes

+
    +
  • Fix XPTR client breaking Retail install
  • +
  • Fix WTF explorer
  • +
  • Fix External link handling
  • +
  • If downloading an update fails button should no longer stay in error state forever
  • +
+ `, + }, + { + Version: "2.9.4", + html: ` +

New Features

+
    +
  • Support new TukUI API
  • +
+ `, + }, + { + Version: "2.9.3", + html: ` +

Fixes

+
    +
  • Fix Wago key issue when downloading (KDederichs)
  • +
+ `, + }, + { + Version: "2.9.2", + html: ` +

New Features

+
    +
  • Dynamic WoW client names (CyanoHao)
  • +
+

Changes

+
    +
  • Spanish locale updates (SkollVargr)
  • +
  • Norwegian locale updates (espenja)
  • +
  • German locale updates (maestrohdude)
  • +
  • Russian locale updates (Valdemar)
  • +
  • Improved WowInterface API usage for some addons
  • +
  • Improved private GitHub repo support
  • +
  • Disable the ability to shift + click columns
  • +
  • Update a bunch of dependencies
  • +
+

Fixes

+
    +
  • Searching on CurseForge should work more as expected
  • +
  • Fix an issue with My Addon columns appearing squished
  • +
  • Rendering lists should no longer fail due to malformed game version
  • +
  • Fix an issue with missing client types when installing from CF page link
  • +
  • Audio should now be muted just in case of an annoying ad
  • +
  • Fix an issue with window not showing at startup on Mac
  • +
  • Improve some error handling when installing addons
  • +
+ `, + }, + { + Version: "2.9.1", + html: ` +

New Features

+
    +
  • Added support for CurseForge API
  • +
  • Added support handling CurseForge protocol
  • +
  • Added support for custom Wago API keys (Linaori)
  • +
  • Added support Dragonflight
  • +
+

Changes

+
    +
  • Spanish locale updates (SkollVargr)
  • +
  • Attempt to fix the weird diagonal line bug
  • +
  • Fix issues with 10.x interface version detection/sorting
  • +
  • Update a bunch of dependencies
  • +
+ `, + }, + { + Version: "2.9.0", + html: ` +

New Features

+
    +
  • Add new support for the Wrath of the Lich King Classic Beta client
  • +
  • Add new logo for the Dragonflight Beta client
  • +
+

Changes

+
    +
  • Chinese locale updates (CyanoHao)
  • +
+ `, + }, + { + Version: "2.8.3", + html: ` +

New Features

+
    +
  • App bundle now much smaller (CyanoHao)
  • +
  • GitHub personal access tokens now supported
  • +
+

Changes

+
    +
  • Allow removal of addons that had their dirs removed prior to delete (Linaori)
  • +
  • Curse addon provider removed
  • +
  • CurseV2 addon provider removed
  • +
  • GitHub update check should now attempt to use release.json
  • +
  • GitHub should now respect release channels better
  • +
  • Spanish locale updates (SkollVargr)
  • +
  • Polish locale updates (nydas3k)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • German locale updates (Glow)
  • +
  • Hardware media keys no longer captured from media ads
  • +
+

Fixes

+
    +
  • Allow removing of addons with no directories (Linaori)
  • +
  • Fix some text wrapping issues on the addon context menu
  • +
  • Fix an issue with some GitHub repos
  • +
  • Fix an issue with saved window position breaking things
  • +
  • Fix an issue showing wrath Github releases as retail
  • +
  • Add a confirmation prompt every time Wago is enabled
  • +
+ `, + }, + { + Version: "2.7.1", + html: ` +

Important CurseForge Changes

+

+ CurseForge will soon end the ability for WowUp to show or update their addons.
+ Read more about how to migrate your addons as best we can on our website.
+ Read more here +

+

New Features

+
    +
  • New Application option to remember the last selected details tab or not. (EpicDenmos)
  • +
+

Changes

+
    +
  • German locale updates (Glow)
  • +
  • Russian locale updates (Medok, Valdemar)
  • +
  • Spanish locale updates (SkollVargr)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • Polish locale updates (nydas3k)
  • +
+

Fixes

+
    +
  • Get Addons search text now behaves in a more normal feeling way
  • +
  • CurseForge V2 is now disabled by default
  • +
  • Addon details install button now shows unavailable when addon is blocked
  • +
  • Wago ad frame has a progressive backoff for error catching
  • +
`, + }, + { + Version: "2.7.0", + html: ` +

New Features

+
    +
  • New Polish language support (nydas3k)
  • +
  • New support for installing from the WowUp website
  • +
  • New CurseForge V2 addon provider
  • +
  • New copy news link button
  • +
  • GitHub/Zip addons should now survive a re-scan
  • +
+

Changes

+
    +
  • German locale updates (Glow)
  • +
  • Portuguese locale updates (TheLastDarkthorne)
  • +
  • Russian locale updates (Medok)
  • +
  • Spanish locale updates (SkollVargr)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • Wago is now a trusted domain by default (Artemis)
  • +
  • Installing via GitHub URL is now more aggressive
  • +
  • Addon detail/changelog text is now selectable
  • +
  • Re-scanning addons is now faster
  • +
  • Addons with folder name/toc mismatches will now be marked with a warning
  • +
  • Middle clicking a website link will no longer open the link in a WowUp window
  • +
+

Fixes

+
    +
  • Light theme can be read again
  • +
  • Fixed an issue with Wago provider not working properly on Mac
  • +
  • Fix an issue with caching Wago scan results
  • +
  • Wago addon IDs should now work as expected when swapping providers
  • +
  • Fix an issue with showing the wrong folder name for some TukUI addons
  • +
  • Fix an issue with desktop notifications appearing if they were disabled
  • +
  • Fix an issue with install direct zip URL not working
  • +
  • Fix an issue with unmatched addons breaking the re-scan process
  • +
  • Github install supports _classic, _vanilla, _tbc, _bc, and _bcc zip files
  • +
  • Github install supports octet stream content type zips
  • +
  • Spinner is now shown during scan at app startup
  • +
  • My Addons spinner is now centered
  • +
  • Updated a bunch of dependencies
  • +
`, + }, + { + Version: "2.6.2", + html: ` +

Fixes

+
    +
  • Fixed an issue with Wago addons updating incorrectly
  • +
`, + }, + { + Version: "2.6.1", + html: ` +

Changes

+
    +
  • Chinese locale updates (CyanoHao)
  • +
  • Russian locale updates (Medok)
  • +
  • Spanish locale updates (SkollVargr)
  • +
  • Added the ability to collapse the sidebar if no ad is required
  • +
+

Fixes

+
    +
  • Fixed an issue with having no clients locking up the app
  • +
+ `, + }, + { + Version: "2.6.0", + html: ` +

New Features

+
    +
  • Added the ability to automatically discover WoW installs for Lutris users (fultonm)
  • +
  • +
    Added the new Wago.io addon provider
    + + + +
  • +
  • +
    New app layout courtesy of our friends at Warcraft Tavern.
    + + + +
  • +
  • Added the ability to switch to Beta build release channel from the app
  • +
+

Changes

+
    +
  • Spanish locale updates (SkollVargr)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • Russian locale updates (Medok)
  • +
  • German locale updates (Glow)
  • +
  • Updated At text on the My Addons page should now keep itself up to date
  • +
  • Modified the GitHub install by URL feature to be more flexible for multi-toc addons
  • +
  • App now starts up much faster
  • +
  • Increased the timeout for the CurseForge provider
  • +
  • Simplify Re-Scan logic
  • +
+

Fixes

+
    +
  • Fixed an issue with some addon IDs colliding between addon providers
  • +
  • Fixed an issue with a provider being able to block update checks
  • +
  • Fixed an issue with restoring the window from a pinned shortcut on the taskbar
  • +
  • Try to fix the issue with duplicated addons
  • +
`, + }, + { + Version: "2.5.2", + html: ` +

Hotfix

+
    +
  • Fix an issue with the guide url going to the wrong domain
  • +
`, + }, + { + Version: "2.5.1", + html: ` +

Hotfix

+
    +
  • Update the Windows signing cert
  • +
  • Fix an issue with the app locking up on startup sometimes
  • +
  • Fix an issue with the import dialog failing to create an import string
  • +
+

New Features

+
    +
  • Added the ability to turn off system notifications for individual addons (tellier-dev)
  • +
  • Added the ability to remove addons from the details dialog on Get Addons tab (tellier-dev)
  • +
  • +
    Added the ability to import/export a list of addons for a WoW client. The first step in backing up your list of addons or sharing them with your friends!
    + +
  • +
  • +
    Added the ability to create and restore backups, this is a work in progress.
    + +
  • +
  • Added a button to open the client folder for a WoW install
  • +
  • Added WowUpHub category support
  • +
  • Added WowUpHub preview support
  • +
  • Added Mac M1 builds
  • +
  • Added support for more multi-toc suffixes
  • +
  • Added support for Classic Era PTR client
  • +
  • Added some badges to indicate number of updates and which client needs them
  • +
+

Fixes

+
    +
  • Fix an issue with addons being ignored by TukUI too early during a scan
  • +
  • Fix an issue with certain addons not being able to be re-scanned
  • +
  • Fix an issue with updating the same addon in multiple WoW clients at the same time
  • +
+

Changes

+
    +
  • Spanish locale updates (SkollVargr)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • Russian locale updates (Medok)
  • +
  • German locale updates (Glow)
  • +
  • Tweak the tabs some more
  • +
  • Performance improvements for My Addons page
  • +
  • Images details tab is now Previews
  • +
  • Use a different lightbox library for previews
  • +
  • Expanded system proxy support
  • +
`, + }, + { + Version: "2.5.0", + html: ` +
+

New Features

+
    +
  • Added the ability to turn off system notifications for individual addons (tellier-dev)
  • +
  • Added the ability to remove addons from the details dialog on Get Addons tab (tellier-dev)
  • +
  • +
    Added the ability to import/export a list of addons for a WoW client. The first step in backing up your list of addons or sharing them with your friends!
    + +
  • +
  • +
    Added the ability to create and restore backups, this is a work in progress.
    + +
  • +
  • Added a button to open the client folder for a WoW install
  • +
  • Added WowUpHub category support
  • +
  • Added WowUpHub preview support
  • +
  • Added Mac M1 builds
  • +
  • Added support for more multi-toc suffixes
  • +
  • Added support for Classic Era PTR client
  • +
  • Added some badges to indicate number of updates and which client needs them
  • +
+

Fixes

+
    +
  • Fix an issue with addons being ignored by TukUI too early during a scan
  • +
  • Fix an issue with certain addons not being able to be re-scanned
  • +
  • Fix an issue with updating the same addon in multiple WoW clients at the same time
  • +
+

Changes

+
    +
  • Spanish locale updates (SkollVargr)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • Russian locale updates (Medok)
  • +
  • German locale updates (Glow)
  • +
  • Tweak the tabs some more
  • +
  • Performance improvements for My Addons page
  • +
  • Images details tab is now Previews
  • +
  • Use a different lightbox library for previews
  • +
  • Expanded system proxy support
  • +
`, + }, + { + Version: "2.4.7", + html: ` +
+

New Features

+
    +
  • Add support for automatic detection of new Classic Era PTR client
  • +
+

Changes

+
    +
  • Russian locale updates (Medok)
  • +
  • Spanish locale updates (SkollVargr)
  • +
  • Chinese locale updates (CyanoHao)
  • +
+
`, + }, + { + Version: "2.4.6", + html: ` +
+

Fixes

+ +
`, + }, + { + Version: "2.4.5", + html: ` +
+

Fixes

+
    +
  • Fix an issue with Google Drive preventing files from being deleted
  • +
  • Fix an issue with checking addon owners against the block list
  • +
+
`, + }, + { + Version: "2.4.4", + html: ` +
+

Fixes

+
    +
  • Fix an issue with single CurseForge addons failing, causing the whole batch of addons to be marked "Unknown"
  • +
+
`, + }, + { + Version: "2.4.3", + html: ` +
+

Fixes

+
    +
  • Fix an issue with auto updates failing with the same addon installed in multiple WoW clients
  • +
  • Fix an caching issue with CurseForge featured addons
  • +
+
`, + }, + { + Version: "2.4.2", + html: ` +
+

Fixes

+
    +
  • Updating the app badge should now properly check the setting
  • +
+

Changes

+
    +
  • Reduce the cache time of featured addons responses
  • +
+
`, + }, + { + Version: "2.4.1", + html: ` +
+

Fixes

+
    +
  • The app badge toggle option is now visible for all operating systems
  • +
  • When changing the Ignore state on an addon the app badge should update
  • +
+
`, + }, + { + Version: "2.4.0", + html: `
+

New Features

+
    +
  • +
    Added a news section powered by
    + + + +
  • +
  • New Images tab in the addon details dialog
  • +
  • You can now change the order of your WoW installations
  • +
  • Added an optional app badge notification with the count of available updates for Mac and Linux users
  • +
  • Get Addons tab will now include a list of recently updated addons from WowUpHub
  • +
  • When opening an external link you can now trust the domain to avoid being prompted again
  • +
+

Changes

+
    +
  • Russian locale updates (Medok)
  • +
  • German locale updates (Glow)
  • +
  • Spanish locale updates (SkollVargr)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • Italian locale updates (Bito)
  • +
  • Revamped UI
  • +
  • WowUp updates will now download automatically
  • +
  • Optimize update checks for WowUpHub and CurseForge providers, now faster
  • +
  • When starting with 0 installs found, user should go to installations page
  • +
  • Update Angular and Electron to latest
  • +
+

Fixes

+
    +
  • Fixed an issue with table header font in addon details
  • +
  • Fixed an issue with odd table de-select behavior
  • +
  • Fixed an issue with the client selector overlapping the top toolbar
  • +
  • Fixed an issue with re-scanning not showing the correct game version
  • +
  • Fixed an issue with Update All button sometimes getting stuck enabled
  • +
  • Fixed an issue with CurseForge categories throwing an error
  • +
  • Fixed an issue with addons showing up with a blank version from WowUpHub in Get Addons
  • +
  • Fixed several issues with light themes
  • +
  • Reworked the app update flow
  • +
+
`, + }, + { + Version: "2.3.4", + html: `
+

Changes

+
    +
  • Fix the numpad zoom in/out shortcuts
  • +
+
`, + }, + { + Version: "2.3.3", + html: `
+

Changes

+
    +
  • Undo the CurseForge tweak, caused too many issues
  • +
+
`, + }, + { + Version: "2.3.2", + html: `
+

New Features

+
    +
  • Numpad keys can now be used to control zoom
  • +
+

Changes

+
    +
  • Russian locale updates (Medok)
  • +
  • Scanning should now be more accurate
  • +
  • Tweak some CurseForge matching
  • +
  • Reduce the time that searches are cached
  • +
  • Sorting by columns other than name should now fall back to case insensitive names
  • +
+
`, + }, + { + Version: "2.3.1", + html: `
+

New Features

+
    +
  • Installing from a GitHub URL now supports the new BigWigs metadata file
  • +
  • Installing from a GitHub URL now supports -bcc zip files when a Burning Crusade client is selected
  • +
  • The Get Addons tab will now also list Recently Added addons from CurseForge, just sort by the 'Released At' column
  • +
+

Changes

+
    +
  • German locale updates (Glow)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • Spanish locale updates (SkollVargr)
  • +
  • Using the WowUp Addon should no longer cause the Battle.net app to say "Updating" for all clients (Linaori)
  • +
  • When re-scanning the "Released At" date should be more accurate
  • +
  • Can no longer drag columns on "My Addons" and "Get Addons" off the screen
  • +
  • Sorting by name is no longer case sensitive
  • +
  • Sorting by name on "My Addons" and "Get Addons" is no longer case sensitive
  • +
  • Long author names will now attempt to wrap all text
  • +
  • WowUp addon companion data now has it's own provider name
  • +
  • The WowUp addon companion data row should now be kept up to date without a re-scan
  • +
  • A success toast will now be shown when an addon is successfully removed
  • +
  • Optimize fetching of WowInterface data
  • +
+
`, + }, + { + Version: "2.3.0", + html: `
+

New Features

+
    +
  • Support for new multiple toc file addons from the WowUpHub
  • +
+

Changes

+
    +
  • Addons with warnings and ignored will be weighted at the bottom again
  • +
  • Addons from the WowUpHub will now show author names from their respective toc files where applicable
  • +
  • Addons that are no longer supported by a client sourced from the WowUpHub will now be marked with a warning, not throw an error
  • +
  • Fix a display issue with selecting the latest game version when installing an addon (right click > re-install)
  • +
  • Fix an issue with the interface version being displayed in the Game Version column after a re-scan
  • +
  • Fix an issue with curse matches being ignored when they have no matching client type releases during a re-scan
  • +
  • Fix an issue with over aggressive TukUI name matching during a re-scan
  • +
  • Fix an issue Classic Era being incorrectly identified when adding it manually
  • +
+
`, + }, + { + Version: "2.2.2", + html: `
+

Changes

+
    +
  • Fix some issues related to addons migrating from classic to TBC / Multiple toc files
  • +
  • Improve TukUI searching
  • +
+
`, + }, + { + Version: "2.2.1", + html: `
+

New Features

+
    +
  • +
    WowUp companion data addon name updated (Linaori)
    + +
  • +
  • Classic PTR client is now treated like a Burning Crusade client
  • +
  • Addons in Warning state will now be at the top when sorting by status on My Addons tab
  • +
  • Author names lists will now wrap on My Addons tab
  • +
  • +
    Added the installation name to the sync error toast
    + +
  • +
  • Tweaked the automatic column widths for My Addons tab
  • +
  • When adding/removing an addon the count text should update again
  • +
  • When performing a re-scan the latest addon info should be fetched once again
  • +
  • Http circuit breaker will no longer trip for 404s
  • + +
+
`, + }, + { + Version: "2.2.0", + html: `
+

Locale Updates

+
    +
  • Korean locale updates (Jaehyuk-Lee)
  • +
  • German locale updates (Glow/maestrohdude)
  • +
  • Spanish locale updates (SkollVargr)
  • +
  • Chinese locale updates (CyanoHao)
  • +
  • Russian locale updates (Medok)
  • +
+

New Features

+
    +
  • +
    Add category browsing support for CurseForge and TukUI (Strayge)
    + +
  • +
  • Add new logging for addon updating (Linaori)
  • +
  • +
    Add support for CurseForge install links via app settings (Noxis)
    + +
  • +
  • Add a button to remove an addon via the addon details dialog
  • +
  • +
    Add new detection for addons that have been removed by their provider
    + +
  • +
  • +
    All new handling of World of Warcraft client installations
    + +
  • +
  • Add an advanced feature for symlinks in your addon folder via app settings
  • +
  • +
    Add initial support for Burning Crusade beta client
    + +
  • +
+

Bug Fixes/Tweaks

+
    +
  • Clicking the desktop notification will now focus the app
  • +
  • Improve formatting for changelogs and descriptions
  • +
  • Rework the sorting of the Get Addons tab
  • +
  • Switch to a new grid library, improved performance for your lists
  • +
  • System bar icon will now change per system theme setting on Mac
  • +
  • Fix an issue with unzipped folders permissions on Mac
  • +
  • Fix an issue with update all clients button being disabled when selected client had no updates
  • +
  • Fix an issue with not rolling back an addon update when unzipping fails
  • +
  • Fix an issue with rotating download links not being updated in the json store
  • +
  • Fix an issue with Check Updates not checking all installations
  • +
  • Fix an issue with Scanning ignoring disabled addon providers
  • +
  • Fix a memory leak issue when checking for updates
  • +
+
`, + }, +]; diff --git a/WowUp/wowup-electron/src/app/services/wowup/wowup-account.service.ts b/WowUp/wowup-electron/src/app/services/wowup/wowup-account.service.ts new file mode 100644 index 0000000..910e957 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/wowup/wowup-account.service.ts @@ -0,0 +1,207 @@ +import { BehaviorSubject, from } from "rxjs"; +import { filter, map, switchMap } from "rxjs/operators"; + +import { Injectable } from "@angular/core"; + +import { + ACCT_FEATURE_KEYS, + ACCT_PUSH_ENABLED_KEY, + APP_PROTOCOL_NAME, + FEATURE_ACCOUNTS_ENABLED, + IPC_PUSH_INIT, + IPC_PUSH_REGISTER, + IPC_PUSH_SUBSCRIBE, + IPC_PUSH_UNREGISTER, + STORAGE_WOWUP_AUTH_TOKEN, +} from "../../../common/constants"; +import { AppConfig } from "../../../environments/environment"; +import { getProtocol } from "../../utils/string.utils"; +import { ElectronService } from "../electron/electron.service"; +import { LinkService } from "../links/link.service"; +import { PreferenceStorageService } from "../storage/preference-storage.service"; +import { WowUpApiService } from "../wowup-api/wowup-api.service"; +import { HttpErrorResponse } from "@angular/common/http"; +import { WowUpGetAccountResponse } from "wowup-lib-core"; + +@Injectable({ + providedIn: "root", +}) +export class WowUpAccountService { + public readonly wowUpAuthTokenSrc = new BehaviorSubject(""); + public readonly wowUpAccountSrc = new BehaviorSubject(undefined); + public readonly accountPushSrc = new BehaviorSubject(false); + + public async getAccountPushEnabled(): Promise { + const pref = await this._preferenceStorageService.getAsync(ACCT_PUSH_ENABLED_KEY); + return pref === "true"; + } + + public async setAccountPushEnabled(enabled: boolean) { + await this._preferenceStorageService.setAsync(ACCT_PUSH_ENABLED_KEY, enabled); + } + + public get authToken(): string { + return this.wowUpAuthTokenSrc.value; + } + + public get account(): WowUpGetAccountResponse | undefined { + return this.wowUpAccountSrc.value; + } + + public constructor( + private _electronService: ElectronService, + private _linkService: LinkService, + private _preferenceStorageService: PreferenceStorageService, + private _wowUpApiService: WowUpApiService + ) { + if (!FEATURE_ACCOUNTS_ENABLED) { + return; + } + + this.wowUpAuthTokenSrc + .pipe( + filter((token) => !!token && token.length > 10), + switchMap((token) => from(this.onAuthTokenChanged(token))) + ) + .subscribe(); + + this._electronService.customProtocol$ + .pipe( + filter((protocol) => !!protocol), + map((protocol) => this.handleLoginProtocol(protocol)) + ) + .subscribe(); + + this.loadAuthToken(); + + this.getAccountPushEnabled() + .then((pushEnabled) => { + console.debug("accountPushEnabled", pushEnabled); + if (pushEnabled) { + this.initializePush().catch((e) => console.error(e)); + this.accountPushSrc.next(true); + } + }) + .catch(console.error); + } + + public login(): void { + this._linkService + .openExternalLink(`${AppConfig.wowUpWebsiteUrl}/login?client=desktop`) + .catch((e) => console.error(e)); + } + + public logout(): void { + this.clearAuthToken(); + this.resetAccountPreferences().catch(console.error); + } + + private onAuthTokenChanged = async (token: string) => { + try { + const account = await this._wowUpApiService.getAccount(token); + console.debug("Account", account); + this.wowUpAccountSrc.next(account); + + const pushEnabled = await this.getAccountPushEnabled(); + if (pushEnabled) { + await this.toggleAccountPush(true); + } + } catch (e) { + console.error(e); + + // Check if user is no longer authorized + if (e instanceof HttpErrorResponse && [403, 401].includes(e.status)) { + this.logout(); + } + } + }; + + private clearAuthToken(): void { + window.localStorage.removeItem(STORAGE_WOWUP_AUTH_TOKEN); + this.wowUpAuthTokenSrc.next(""); + this.wowUpAccountSrc.next(undefined); + } + + private loadAuthToken(): void { + const storedToken = window.localStorage.getItem(STORAGE_WOWUP_AUTH_TOKEN); + if (storedToken) { + console.debug("loaded auth token", storedToken); + this.wowUpAuthTokenSrc.next(storedToken); + } + } + + /** + * Handle the post login protocol message + * wowup://login/desktop/#{token} + */ + private handleLoginProtocol = (protocol: string): void => { + const protocolName = getProtocol(protocol); + if (protocolName !== APP_PROTOCOL_NAME) { + return; + } + + const parts = protocol.split("/"); + if (parts[2] !== "login" || parts[3] !== "desktop") { + return; + } + + const token = parts[4]; + if (typeof token !== "string" || token.length < 10) { + console.warn("Invalid auth token", protocol); + return; + } + + console.debug("GOT WOWUP PROTOCOL", protocol); + window.localStorage.setItem(STORAGE_WOWUP_AUTH_TOKEN, token); + this.wowUpAuthTokenSrc.next(token); + }; + + // ACCOUNT FEATURES + public async toggleAccountPush(enabled: boolean): Promise { + try { + if (enabled) { + await this.initializePush(); + await this.registerForPush(this.authToken, this.account.config.pushAppId); + + if (this.account.config?.pushChannels?.addonUpdates) { + await this.subscribe(this.account.config.pushChannels.addonUpdates); + } + } else { + await this.unregisterForPush(this.authToken, this.account.config.pushAppId); + } + + await this.setAccountPushEnabled(enabled); + } catch (e) { + console.error("Failed to toggle account push", e); + throw e; + } + } + + // LOCAL PREFERENCES + private async resetAccountPreferences(): Promise { + for (const key of ACCT_FEATURE_KEYS) { + await this._preferenceStorageService.setAsync(key, false); + } + } + + // PUSH + public async initializePush(): Promise { + return await this._electronService.invoke(IPC_PUSH_INIT); + } + + public async registerForPush(authToken: string, pushAppId: string): Promise { + const pushToken = await this._electronService.invoke(IPC_PUSH_REGISTER, pushAppId); + await this._wowUpApiService.registerPushToken(authToken, pushToken, this._electronService.platform); + return pushToken; + } + + public async unregisterForPush(authToken: string, pushAppId: string): Promise { + const pushToken = await this._electronService.invoke(IPC_PUSH_REGISTER, pushAppId); + await this._electronService.invoke(IPC_PUSH_UNREGISTER); + await this._wowUpApiService.removePushToken(authToken, pushToken); + } + + private async subscribe(channel: string): Promise { + await this._electronService.invoke(IPC_PUSH_SUBSCRIBE, channel); + } +} diff --git a/WowUp/wowup-electron/src/app/services/wowup/wowup-addon.service.ts b/WowUp/wowup-electron/src/app/services/wowup/wowup-addon.service.ts new file mode 100644 index 0000000..2876ed0 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/wowup/wowup-addon.service.ts @@ -0,0 +1,240 @@ +import * as path from "path"; +import { filter } from "rxjs/operators"; + +import { Injectable } from "@angular/core"; + +import { + WOWUP_ADDON_FOLDER_NAME, + WOWUP_ASSET_FOLDER_NAME, + WOWUP_DATA_ADDON_FOLDER_NAME, +} from "../../../common/constants"; +import { AddonInstallState } from "../../models/wowup/addon-install-state"; +import { toInterfaceVersion } from "../../utils/addon.utils"; +import { AddonProviderFactory } from "../addons/addon.provider.factory"; +import { AddonService } from "../addons/addon.service"; +import { FileService } from "../files/file.service"; +import { WarcraftInstallationService } from "../warcraft/warcraft-installation.service"; +import { Addon, AddonChannelType } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; +import { WarcraftService } from "../warcraft/warcraft.service"; +import { first } from "lodash"; + +enum WowUpAddonFileType { + Raw, + HandlebarsTemplate, +} + +interface WowUpAddonVersion { + name: string; + currentVersion: string; + newVersion: string; +} + +interface WowUpAddonData { + updatesAvailable: WowUpAddonVersion[]; + generatedAt: string; + interfaceVersion: string; + wowUpAddonName: string; + wowUpAddonVersion: string; +} + +interface WowUpAddonFileProcessing { + type: WowUpAddonFileType; + filename: string; +} + +@Injectable({ + providedIn: "root", +}) +export class WowUpAddonService { + public readonly files: WowUpAddonFileProcessing[] = [ + { + filename: "data.lua", + type: WowUpAddonFileType.HandlebarsTemplate, + }, + { + filename: "wowup_data_addon.toc", + type: WowUpAddonFileType.HandlebarsTemplate, + }, + { + filename: "ldbicon.tga", + type: WowUpAddonFileType.Raw, + }, + ]; + private compiledFiles = {}; + + public constructor( + private _addonService: AddonService, + private _addonProviderFactory: AddonProviderFactory, + private _fileService: FileService, + private _warcraftInstallationService: WarcraftInstallationService, + private _warcraftService: WarcraftService, + ) { + _addonService.addonInstalled$ + .pipe(filter((update) => update.installState === AddonInstallState.Complete)) + .subscribe((update) => { + const installation = this._warcraftInstallationService.getWowInstallation(update.addon.installationId ?? ""); + if (!installation) { + return; + } + this.updateForInstallation(installation).catch((e) => console.error(e)); + }); + + _addonService.addonRemoved$.subscribe(() => { + this.updateForAllClientTypes().catch((e) => console.error(e)); + }); + } + + public async updateForAllClientTypes(): Promise { + const installations = await this._warcraftInstallationService.getWowInstallationsAsync(); + + for (const installation of installations) { + try { + await this.updateForInstallation(installation); + } catch (e) { + console.error(e); + } + } + } + + public async updateForInstallation(installation: WowInstallation): Promise { + const addons = await this._addonService.getAllAddons(installation); + if (addons.length === 0) { + console.log(`WowUpAddonService: No addons to sync ${installation.displayName}`); + return; + } + + await this.persistUpdateInformationToWowUpAddon(installation, addons); + await this.syncCompanionAddon(addons, installation); + } + + private async syncCompanionAddon(addons: Addon[], installation: WowInstallation): Promise { + const companionAddon = this.getCompanionAddon(addons); + if (!companionAddon) { + console.debug(`No wow companion found: ${installation.displayName}`); + return; + } + + const addonFolderPath = this._warcraftService.getAddonFolderPath(installation); + const addonFolder = await this._warcraftService.getAddonFolder(addonFolderPath, WOWUP_DATA_ADDON_FOLDER_NAME); + if (addonFolder === undefined) { + console.warn("Could not find addon folder", addonFolderPath); + return; + } + + const provider = this._addonProviderFactory.createWowUpCompanionAddonProvider(); + await provider.scan(installation, AddonChannelType.Stable, [addonFolder]); + + if (companionAddon) { + const updatedCompanion: Addon = { ...companionAddon }; + await this._addonService.saveAddon(updatedCompanion); + } + } + + private async persistUpdateInformationToWowUpAddon(installation: WowInstallation, addons: Addon[]) { + const wowUpAddon = this.findAddonByFolderName(addons, WOWUP_ADDON_FOLDER_NAME); + if (!wowUpAddon) { + console.debug(`WowUp Addon not found: ${installation.displayName}`); + return; + } + + console.log("Found the WowUp addon notification addon, trying to sync updates available to wowup_data_addon"); + try { + const availableUpdates = addons.filter(this.canUpdateAddon).map(this.getAddonVersion); + + let interfaceVersion = ""; + + try { + interfaceVersion = toInterfaceVersion(first(wowUpAddon.gameVersion) || ""); + } catch (e) { + console.error(e); + } + + const wowUpAddonData: WowUpAddonData = { + updatesAvailable: availableUpdates, + generatedAt: new Date().toString(), + interfaceVersion, + wowUpAddonName: wowUpAddon.installedFolders ?? "", + wowUpAddonVersion: wowUpAddon.installedVersion ?? "", + }; + + const dataAddonPath = path.join( + this._warcraftService.getAddonFolderPath(installation), + WOWUP_DATA_ADDON_FOLDER_NAME, + ); + + const pathExists = await this._fileService.pathExists(dataAddonPath); + if (!pathExists) { + await this._fileService.createDirectory(dataAddonPath); + } + + for (const file of this.files) { + const designatedPath = path.join(dataAddonPath, file.filename); + switch (file.type) { + case WowUpAddonFileType.HandlebarsTemplate: + await this.handleHandlebarsTemplateFileType(file, designatedPath, wowUpAddonData); + break; + case WowUpAddonFileType.Raw: + await this.handleRawFileType(file, designatedPath); + break; + default: + break; + } + } + + console.log("Available update data synced to wowup_data_addon/{data.lua,wowup_data_addon.toc}"); + } catch (e) { + console.error(e); + } + } + + private getAddonVersion = (addon: Addon): WowUpAddonVersion => { + return { + name: addon.name, + currentVersion: addon.installedVersion ?? "", + newVersion: addon.latestVersion ?? "", + }; + }; + + private canUpdateAddon = (addon: Addon) => { + return !addon.isIgnored && !addon.autoUpdateEnabled && addon.latestVersion !== addon.installedVersion; + }; + + private async handleHandlebarsTemplateFileType( + file: WowUpAddonFileProcessing, + designatedPath: string, + wowUpAddonData: WowUpAddonData, + ) { + const assetPath = path.join(WOWUP_ASSET_FOLDER_NAME, `${file.filename}.hbs`); + const templatePath = await this._fileService.getAssetFilePath(assetPath); + const templateContents = await this._fileService.readFile(templatePath); + + if (!this.compiledFiles[file.filename]) { + this.compiledFiles[file.filename] = window.libs.handlebars.compile(templateContents); + } + + const fileData: string = this.compiledFiles[file.filename](wowUpAddonData).toString(); + await this._fileService.writeFile(designatedPath, fileData); + } + + private async handleRawFileType(file: WowUpAddonFileProcessing, designatedPath: string) { + const assetPath = path.join(WOWUP_ASSET_FOLDER_NAME, file.filename); + const filePath = await this._fileService.getAssetFilePath(assetPath); + const exists = await this._fileService.pathExists(designatedPath); + if (exists) { + console.log(`File exists, skipping copy: ${designatedPath}`); + } else { + await this._fileService.copy(filePath, designatedPath); + } + } + + private getCompanionAddon(addons: Addon[]): Addon | undefined { + return this.findAddonByFolderName(addons, WOWUP_DATA_ADDON_FOLDER_NAME); + } + + private findAddonByFolderName(addons: Addon[], folderName: string): Addon | undefined { + return addons.find( + (addon: Addon) => Array.isArray(addon.installedFolderList) && addon.installedFolderList.includes(folderName), + ); + } +} diff --git a/WowUp/wowup-electron/src/app/services/wowup/wowup-protocol.service.ts b/WowUp/wowup-electron/src/app/services/wowup/wowup-protocol.service.ts new file mode 100644 index 0000000..4948777 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/wowup/wowup-protocol.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from "@angular/core"; +import { catchError, filter, first, of, switchMap, tap } from "rxjs"; +import { APP_PROTOCOL_NAME } from "../../../common/constants"; +import { InstallFromProtocolDialogComponent } from "../../components/addons/install-from-protocol-dialog/install-from-protocol-dialog.component"; +import { getProtocol, getProtocolParts } from "../../utils/string.utils"; +import { DialogFactory } from "../dialog/dialog.factory"; +import { ElectronService } from "../electron/electron.service"; + +@Injectable({ + providedIn: "root", +}) +export class WowUpProtocolService { + public constructor(private _dialogFactory: DialogFactory, private _electronService: ElectronService) {} + + public initialize() { + this._electronService.customProtocol$ + .pipe( + tap((prt) => console.log("WowUpProtocolService", prt)), + filter((prt) => getProtocol(prt) === APP_PROTOCOL_NAME && this.isInstallAction(prt)), + switchMap((prt) => this.onInstallProtocol(prt)), + catchError((e) => { + console.error(e); + return of(undefined); + }) + ) + .subscribe(); + } + + public isInstallAction(protocol: string) { + return getProtocolParts(protocol)[0] === "install"; + } + + public onInstallProtocol(protocol: string) { + console.log("onInstallProtocol", protocol); + + const dialog = this._dialogFactory.getDialog(InstallFromProtocolDialogComponent, { + disableClose: true, + data: { + protocol, + }, + }); + + return dialog.afterClosed().pipe(first()); + } +} diff --git a/WowUp/wowup-electron/src/app/services/wowup/wowup.service.ts b/WowUp/wowup-electron/src/app/services/wowup/wowup.service.ts new file mode 100644 index 0000000..acf08c5 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/wowup/wowup.service.ts @@ -0,0 +1,457 @@ +import { UpdateCheckResult } from "electron-updater"; +import * as _ from "lodash"; +import { join } from "path"; +import { Subject } from "rxjs"; + +import { Injectable } from "@angular/core"; +import { TranslateService } from "@ngx-translate/core"; + +import { + ADDON_MIGRATION_VERSION_KEY, + ADDON_PROVIDERS_KEY, + COLLAPSE_TO_TRAY_PREFERENCE_KEY, + CURRENT_THEME_KEY, + DEFAULT_AUTO_UPDATE_PREFERENCE_KEY_SUFFIX, + DEFAULT_CHANNEL_PREFERENCE_KEY_SUFFIX, + DEFAULT_THEME, + DEFAULT_TRUSTED_DOMAINS, + ENABLE_APP_BADGE_KEY, + ENABLE_SYSTEM_NOTIFICATIONS_PREFERENCE_KEY, + GET_ADDONS_HIDDEN_COLUMNS_KEY, + GET_ADDONS_SORT_ORDER, + IPC_APP_CHECK_UPDATE, + IPC_APP_INSTALL_UPDATE, + IPC_GET_APP_VERSION, + IPC_UPDATE_APP_BADGE, + KEEP_ADDON_DETAIL_TAB_PREFERENCE_KEY, + MY_ADDONS_HIDDEN_COLUMNS_KEY, + MY_ADDONS_SORT_ORDER, + SELECTED_LANGUAGE_PREFERENCE_KEY, + START_MINIMIZED_PREFERENCE_KEY, + START_WITH_SYSTEM_PREFERENCE_KEY, + TRUSTED_DOMAINS_KEY, + UPDATE_NOTES_POPUP_VERSION_KEY, + USE_HARDWARE_ACCELERATION_PREFERENCE_KEY, + USE_SYMLINK_MODE_PREFERENCE_KEY, + WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY, +} from "../../../common/constants"; +import { AddonProviderState } from "../../models/wowup/addon-provider-state"; +import { ColumnState } from "../../models/wowup/column-state"; +import { PreferenceChange } from "../../models/wowup/preference-change"; +import { SortOrder } from "../../models/wowup/sort-order"; +import { getEnumName, getEnumList } from "wowup-lib-core"; +import { ElectronService } from "../electron/electron.service"; +import { FileService } from "../files/file.service"; +import { PreferenceStorageService } from "../storage/preference-storage.service"; +import { WowUpReleaseChannelType } from "../../../common/wowup/wowup-release-channel-type"; +import { AddonChannelType, AddonProviderType, WowClientType } from "wowup-lib-core"; + +@Injectable({ + providedIn: "root", +}) +export class WowUpService { + private readonly _preferenceChangeSrc = new Subject(); + + private _availableVersion = ""; + + public readonly updaterName = "WowUpUpdater.exe"; + public readonly applicationFolderPath: string = window.userDataPath ?? ""; + public readonly applicationLogsFolderPath: string = window.logPath ?? ""; + public readonly applicationDownloadsFolderPath: string = join(this.applicationFolderPath, "downloads"); + public readonly applicationUpdaterPath: string = join(this.applicationFolderPath, this.updaterName); + public readonly wtfBackupFolder: string = join(this.applicationFolderPath, "wtf_backups"); + + public readonly preferenceChange$ = this._preferenceChangeSrc.asObservable(); + + public constructor( + private _preferenceStorageService: PreferenceStorageService, + private _electronService: ElectronService, + private _fileService: FileService, + private _translateService: TranslateService + ) { + this.setDefaultClientPreferences().catch(console.error); + + this.createDownloadDirectory() + .then(() => this.cleanupDownloads()) + // .then(() => console.debug("createDownloadDirectory complete")) + .catch((e) => console.error("Failed to create download directory", e)); + + this.setAutoStartup() + .then(() => console.log("loginItemSettings", this._electronService.getLoginItemSettings())) + .catch((e) => console.error(e)); + } + + public async getApplicationVersion(): Promise { + const appVersion = await this._electronService.invoke(IPC_GET_APP_VERSION); + return `${appVersion}${this._electronService.isPortable ? " (portable)" : ""}`; + } + + public async isBetaBuild(): Promise { + const appVersion = await this.getApplicationVersion(); + return appVersion.toLowerCase().indexOf("beta") != -1; + } + + /** + * This is called before the app component is initialized in order to catch issues + * with unsupported languages + */ + public async initializeLanguage(): Promise { + console.log("Language setup start"); + const currentLang = await this.getCurrentLanguage(); + const langCode = currentLang || (await this._electronService.getLocale()); + + this._translateService.setDefaultLang("en"); + try { + await this._translateService.use(langCode).toPromise(); + console.log(`using locale ${langCode}`); + await this.setCurrentLanguage(langCode); + } catch (e) { + console.warn(`Language ${langCode} not found defaulting to english`); + await this.setCurrentLanguage("en"); + await this._translateService.use("en").toPromise(); + } + + console.log("Language setup complete"); + } + + public get availableVersion(): string { + return this._availableVersion; + } + + public async getCollapseToTray(): Promise { + return (await this._preferenceStorageService.getAsync(COLLAPSE_TO_TRAY_PREFERENCE_KEY)) === "true"; + } + + public async setCollapseToTray(value: boolean): Promise { + const key = COLLAPSE_TO_TRAY_PREFERENCE_KEY; + await this._preferenceStorageService.setAsync(key, value); + this._preferenceChangeSrc.next({ key, value: value.toString() }); + } + + public async getCurrentTheme(): Promise { + const theme = await this._preferenceStorageService.getAsync(CURRENT_THEME_KEY); + return theme || DEFAULT_THEME; + } + + public async setCurrentTheme(value: string): Promise { + const key = CURRENT_THEME_KEY; + await this._preferenceStorageService.setAsync(key, value); + this._preferenceChangeSrc.next({ key, value: value }); + } + + public async getUseHardwareAcceleration(): Promise { + const preference = await this._preferenceStorageService.getAsync(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY); + return preference === "true"; + } + + public async setUseHardwareAcceleration(value: boolean) { + const key = USE_HARDWARE_ACCELERATION_PREFERENCE_KEY; + await this._preferenceStorageService.setAsync(key, value); + this._preferenceChangeSrc.next({ key, value: value.toString() }); + } + + public async getUseSymlinkMode(): Promise { + const preference = await this._preferenceStorageService.getAsync(USE_SYMLINK_MODE_PREFERENCE_KEY); + return preference === "true"; + } + + public async setUseSymlinkMode(value: boolean): Promise { + const key = USE_SYMLINK_MODE_PREFERENCE_KEY; + await this._preferenceStorageService.setAsync(key, value); + this._preferenceChangeSrc.next({ key, value: value.toString() }); + } + + public async getCurrentLanguage(): Promise { + const preference = await this._preferenceStorageService.getAsync(SELECTED_LANGUAGE_PREFERENCE_KEY); + console.log("Set Language Preference: " + preference); + return preference; + } + + public async setCurrentLanguage(value: string): Promise { + const key = SELECTED_LANGUAGE_PREFERENCE_KEY; + await this._preferenceStorageService.setAsync(key, value); + this._preferenceChangeSrc.next({ key, value: value.toString() }); + } + + public async getStartWithSystem(): Promise { + const preference = await this._preferenceStorageService.getAsync(START_WITH_SYSTEM_PREFERENCE_KEY); + return preference === "true"; + } + + public async setStartWithSystem(value: boolean): Promise { + const key = START_WITH_SYSTEM_PREFERENCE_KEY; + await this._preferenceStorageService.setAsync(key, value); + this._preferenceChangeSrc.next({ key, value: value.toString() }); + + await this.setAutoStartup(); + } + + public async getStartMinimized(): Promise { + const preference = await this._preferenceStorageService.getAsync(START_MINIMIZED_PREFERENCE_KEY); + console.log("getStartMinimized", typeof preference, preference); + return preference === "true"; + } + + public async setStartMinimized(value: boolean): Promise { + const key = START_MINIMIZED_PREFERENCE_KEY; + await this._preferenceStorageService.setAsync(key, value); + this._preferenceChangeSrc.next({ key, value: value.toString() }); + + await this.setAutoStartup(); + } + + public async getWowUpReleaseChannel(): Promise { + const preference = await this._preferenceStorageService.getAsync(WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY); + return parseInt(preference, 10) as WowUpReleaseChannelType; + } + + public async setWowUpReleaseChannel(releaseChannel: WowUpReleaseChannelType): Promise { + try { + await this._electronService.invoke("set-release-channel", releaseChannel); + return this._preferenceStorageService.setAsync(WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY, releaseChannel); + } catch (e) { + console.error(e); + } + } + + public async getKeepLastAddonDetailTab(): Promise { + const preference = await this._preferenceStorageService.getAsync(KEEP_ADDON_DETAIL_TAB_PREFERENCE_KEY); + return preference === "true"; + } + + public async setKeepLastAddonDetailTab(value: boolean): Promise { + const key = KEEP_ADDON_DETAIL_TAB_PREFERENCE_KEY; + await this._preferenceStorageService.setAsync(key, value); + this._preferenceChangeSrc.next({ key, value: value.toString() }); + } + + public async getAddonProviderStates(): Promise { + const obj = await this._preferenceStorageService.getObjectAsync(ADDON_PROVIDERS_KEY); + return obj || []; + } + + public async getAddonProviderState(providerName: string): Promise { + const preference = await this.getAddonProviderStates(); + return _.find(preference, (pref) => pref.providerName === providerName.toLowerCase()); + } + + public async setAddonProviderState(state: AddonProviderState): Promise { + const key = ADDON_PROVIDERS_KEY; + const stateCpy = { ...state }; + stateCpy.providerName = stateCpy.providerName.toLowerCase() as AddonProviderType; + + const preference = await this.getAddonProviderStates(); + const stateIndex = _.findIndex(preference, (pref) => pref.providerName === stateCpy.providerName); + + if (stateIndex === -1) { + preference.push(stateCpy); + } else { + preference[stateIndex] = stateCpy; + } + + await this._preferenceStorageService.setAsync(key, preference); + this._preferenceChangeSrc.next({ key, value: preference.toString() }); + } + + public async getEnableSystemNotifications(): Promise { + return await this._preferenceStorageService.getBool(ENABLE_SYSTEM_NOTIFICATIONS_PREFERENCE_KEY); + } + + public async setEnableSystemNotifications(enabled: boolean): Promise { + await this._preferenceStorageService.setAsync(ENABLE_SYSTEM_NOTIFICATIONS_PREFERENCE_KEY, enabled); + } + + public async getEnableAppBadge(): Promise { + return await this._preferenceStorageService.getBool(ENABLE_APP_BADGE_KEY); + } + + public async setEnableAppBadge(enabled: boolean): Promise { + await this._preferenceStorageService.setAsync(ENABLE_APP_BADGE_KEY, enabled); + } + + public async updateAppBadgeCount(count: number): Promise { + const badgeEnabled = await this.getEnableAppBadge(); + if (count > 0 && !badgeEnabled) { + console.debug("app badge disabled"); + return; + } + + console.debug("Update app badge", count); + await this._electronService.invoke(IPC_UPDATE_APP_BADGE, count); + } + + public async getMyAddonsHiddenColumns(): Promise { + const obj = await this._preferenceStorageService.getObjectAsync(MY_ADDONS_HIDDEN_COLUMNS_KEY); + return obj || []; + } + + public async setMyAddonsHiddenColumns(columnStates: ColumnState[]): Promise { + await this._preferenceStorageService.setAsync(MY_ADDONS_HIDDEN_COLUMNS_KEY, columnStates); + } + + public async getMyAddonsSortOrder(): Promise { + const obj = await this._preferenceStorageService.getObjectAsync(MY_ADDONS_SORT_ORDER); + return obj ?? []; + } + + public async setMyAddonsSortOrder(sortOrder: SortOrder[]): Promise { + await this._preferenceStorageService.setAsync(MY_ADDONS_SORT_ORDER, sortOrder); + } + + public async getGetAddonsHiddenColumns(): Promise { + return (await this._preferenceStorageService.getObjectAsync(GET_ADDONS_HIDDEN_COLUMNS_KEY)) || []; + } + + public async setGetAddonsHiddenColumns(columnStates: ColumnState[]): Promise { + await this._preferenceStorageService.setAsync(GET_ADDONS_HIDDEN_COLUMNS_KEY, columnStates); + } + + public async getAddonsSortOrder(): Promise { + return await this._preferenceStorageService.getObjectAsync(GET_ADDONS_SORT_ORDER); + } + + public getClientDefaultAddonChannelKey(clientType: WowClientType): string { + const typeName = getEnumName(WowClientType, clientType); + return `${typeName}${DEFAULT_CHANNEL_PREFERENCE_KEY_SUFFIX}`.toLowerCase(); + } + + public async shouldShowNewVersionNotes(): Promise { + const popupVersion = await this._preferenceStorageService.getAsync(UPDATE_NOTES_POPUP_VERSION_KEY); + return popupVersion !== (await this._electronService.getVersionNumber()); + } + + public async setNewVersionNotes(): Promise { + const versionNumber = await this._electronService.getVersionNumber(); + await this._preferenceStorageService.setAsync(UPDATE_NOTES_POPUP_VERSION_KEY, versionNumber); + } + + public async shouldMigrateAddons(): Promise { + const migrateVersion = await this._preferenceStorageService.getAsync(ADDON_MIGRATION_VERSION_KEY); + return migrateVersion !== (await this._electronService.getVersionNumber()); + } + + public async setMigrationVersion(): Promise { + const versionNumber = await this._electronService.getVersionNumber(); + await this._preferenceStorageService.setAsync(ADDON_MIGRATION_VERSION_KEY, versionNumber); + } + + public async showLogsFolder(): Promise { + await this._fileService.showDirectory(this.applicationLogsFolderPath); + } + + public async showConfigFolder(): Promise { + await this._fileService.showDirectory(this.applicationFolderPath); + } + + public checkForAppUpdate(): void { + this._electronService.send(IPC_APP_CHECK_UPDATE); + } + + public async isSameVersion(updateCheckResult: UpdateCheckResult): Promise { + const appVersion = await this._electronService.getVersionNumber(); + return updateCheckResult && updateCheckResult.updateInfo?.version === appVersion; + } + + public installUpdate(): void { + return this._electronService.send(IPC_APP_INSTALL_UPDATE); + } + + public async getTrustedDomains(): Promise { + const trustedDomains = await this._preferenceStorageService.getObjectAsync(TRUSTED_DOMAINS_KEY); + return trustedDomains ?? []; + } + + public async isTrustedDomain(href: string | URL, domains?: string[]): Promise { + const url = href instanceof URL ? href : new URL(href); + if (DEFAULT_TRUSTED_DOMAINS.includes(url.hostname)) { + return true; + } + + const trustedDomains = domains || (await this.getTrustedDomains()); + return trustedDomains.includes(url.hostname); + } + + public async trustDomain(domain: string): Promise { + let trustedDomains = await this._preferenceStorageService.getObjectAsync(TRUSTED_DOMAINS_KEY); + trustedDomains = _.uniq([...trustedDomains, domain]); + + await this._preferenceStorageService.setAsync(TRUSTED_DOMAINS_KEY, trustedDomains); + } + + private async setDefaultPreference(key: string, defaultValue: any): Promise { + const pref = await this._preferenceStorageService.getAsync(key); + if (pref === null || pref === undefined) { + if (Array.isArray(defaultValue)) { + await this._preferenceStorageService.setAsync(key, defaultValue); + } else { + await this._preferenceStorageService.setAsync(key, defaultValue.toString()); + } + } + } + + private getClientDefaultAutoUpdateKey(clientType: WowClientType): string { + const typeName = getEnumName(WowClientType, clientType); + return `${typeName}${DEFAULT_AUTO_UPDATE_PREFERENCE_KEY_SUFFIX}`.toLowerCase(); + } + + private async setDefaultClientPreferences(): Promise { + const keys = getEnumList(WowClientType).filter((key) => key !== WowClientType.None); + for (const key of keys) { + const preferenceKey = this.getClientDefaultAddonChannelKey(key); + await this.setDefaultPreference(preferenceKey, AddonChannelType.Stable); + + const autoUpdateKey = this.getClientDefaultAutoUpdateKey(key); + await this.setDefaultPreference(autoUpdateKey, false); + } + } + + private async getDefaultReleaseChannel() { + const isBetaBuild = await this.isBetaBuild(); + return isBetaBuild ? WowUpReleaseChannelType.Beta : WowUpReleaseChannelType.Stable; + } + + /** + * Clean up lost downloads in the download folder + */ + private async cleanupDownloads() { + const downloadFiles = await this._fileService.listEntries(this.applicationDownloadsFolderPath, "*"); + + for (const entry of downloadFiles) { + const path = join(this.applicationDownloadsFolderPath, entry.name); + try { + await this._fileService.remove(path); + } catch (e) { + console.error("Failed to delete download entry", path); + console.error(e); + } + } + } + + private async createDownloadDirectory() { + await this._fileService.createDirectory(this.applicationDownloadsFolderPath); + } + + private async setAutoStartup(): Promise { + const startMinimized = await this.getStartMinimized(); + const startWithSystem = await this.getStartWithSystem(); + + if (this._electronService.isLinux) { + const autoLauncher = new window.libs.autoLaunch({ + name: "WowUp", + isHidden: startMinimized, + }); + + if (startWithSystem) { + autoLauncher.enable(); + } else { + autoLauncher.disable(); + } + } else { + await this._electronService.setLoginItemSettings({ + openAtLogin: startWithSystem, + openAsHidden: this._electronService.isMac ? startMinimized : false, + args: this._electronService.isWin ? (startMinimized ? ["--hidden"] : []) : [], + }); + } + } +} diff --git a/WowUp/wowup-electron/src/app/services/wtf/wtf.service.ts b/WowUp/wowup-electron/src/app/services/wtf/wtf.service.ts new file mode 100644 index 0000000..e893ea2 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/wtf/wtf.service.ts @@ -0,0 +1,436 @@ +import { Injectable } from "@angular/core"; +import * as path from "path"; +import { FsStats } from "wowup-lib-core"; +import { WowInstallation } from "wowup-lib-core"; + +import { TreeNode } from "../../../common/models/ipc-events"; +import { FileService } from "../files/file.service"; +import { WowUpService } from "../wowup/wowup.service"; + +const WTF_FOLDER = "WTF"; +const ACCOUNT_FOLDER = "Account"; +const SAVED_VARIABLES_FOLDER = "SavedVariables"; +const BACKUP_META_FILENAME = "wowup-meta.json"; + +export interface FileStats { + name: string; + path: string; + stats: FsStats; +} + +export interface WtfEntry { + name: string; + children: WtfEntry[]; +} + +export interface WtfNode extends TreeNode { + isLua: boolean; + ignore: boolean; + children: WtfNode[]; +} + +export interface WtfBackupMetadataFile { + createdBy: string; + createdAt: number; + contents: WtfBackupMeta[]; +} + +export interface WtfBackupMeta { + path: string; + isDirectory: boolean; + size: number; + hash?: string; +} + +export interface WtfBackup { + location: string; + fileName: string; + size: number; + birthtimeMs: number; + error?: string; + metadata?: WtfBackupMetadataFile; +} + +@Injectable({ + providedIn: "root", +}) +export class WtfService { + public constructor(private _fileService: FileService, private _wowUpService: WowUpService) {} + + /** + * Get a nested tree of nodes for every file within the WTF structure + * including the hash will make this operation much slower + */ + public async getWtfContents(installation: WowInstallation, includeHash = false): Promise { + console.time("getWtfContents"); + try { + const wtfPath = this.getWtfPath(installation); + const tree = await this._fileService.getDirectoryTree(wtfPath, { includeHash }); + return this.getWtfNode(tree); + } finally { + console.timeEnd("getWtfContents"); + } + } + + public getWtfNode(treeNode: TreeNode): WtfNode { + const wtfNode: WtfNode = { + ...treeNode, + isLua: path.extname(treeNode.name) === ".lua", + ignore: this.shouldIgnoreFile(treeNode.name), + children: treeNode.children.map((tn) => this.getWtfNode(tn)), + }; + + return wtfNode; + } + + private shouldIgnoreFile(fileName: string): boolean { + const canonName = fileName.toLowerCase(); + return canonName.endsWith(".lua.bak") || canonName.startsWith("blizzard_"); + } + + public getWtfPath(installation: WowInstallation): string { + return path.join(path.dirname(installation.location), WTF_FOLDER); + } + + public getAccountsPath(installation: WowInstallation): string { + return path.join(this.getWtfPath(installation), ACCOUNT_FOLDER); + } + + public async listAccountPaths(installations: WowInstallation): Promise { + return await this._fileService.listDirectories(this.getAccountsPath(installations)); + } + + public async getAccounts(installation: WowInstallation): Promise { + const accountFolders = await this.listAccountPaths(installation); + return accountFolders.filter((folder) => folder !== SAVED_VARIABLES_FOLDER); + } + + public async getServers(installation: WowInstallation, account: string): Promise { + const accountPath = path.join(this.getAccountsPath(installation), account); + const entries = await this._fileService.listDirectories(accountPath); + + return entries.filter((entry) => entry !== SAVED_VARIABLES_FOLDER); + } + + public async getCharacters( + installation: WowInstallation, + account: string, + server: string + ): Promise { + const serverPath = path.join(this.getAccountsPath(installation), account, server); + const entries = await this._fileService.listDirectories(serverPath); + return entries; + } + + public async getCharacterVariables( + installation: WowInstallation, + account: string, + server: string, + character: string + ): Promise { + try { + const characterPath = path.join( + this.getAccountsPath(installation), + account, + server, + character, + SAVED_VARIABLES_FOLDER + ); + const entries = await this._fileService.listFiles(characterPath, "*.lua"); + + const entryPaths = entries.map((entry) => path.join(characterPath, entry)); + const fileSizes = await this._fileService.statFiles(entryPaths); + + const fsStats: FileStats[] = Object.keys(fileSizes).map((key) => { + return { + name: path.basename(key), + path: key, + stats: fileSizes[key], + }; + }); + + return fsStats; + } catch (e) { + console.error(e); + return []; + } + } + + public async getGlobalVariables(installation: WowInstallation, account: string): Promise { + const accountPath = path.join(this.getAccountsPath(installation), account, SAVED_VARIABLES_FOLDER); + const entries = await this._fileService.listFiles(accountPath, "*.lua"); + + const entryPaths = entries.map((entry) => path.join(accountPath, entry)); + const fileSizes = await this._fileService.statFiles(entryPaths); + + const fsStats: FileStats[] = Object.keys(fileSizes).map((key) => { + return { + name: path.basename(key), + path: key, + stats: fileSizes[key], + }; + }); + + return fsStats; + } + + public async getBackupList(installation: WowInstallation): Promise { + console.time("getBackupList"); + try { + const wtfBackups: WtfBackup[] = []; + + const backupZipFiles = await this.listBackupFiles(installation); + const fsStats = await this._fileService.statFiles(backupZipFiles); + + for (let i = 0; i < backupZipFiles.length; i++) { + const zipFile = backupZipFiles[i]; + const stat = fsStats[zipFile]; + + const wtfBackup: WtfBackup = { + location: zipFile, + fileName: path.basename(zipFile), + size: stat.size, + birthtimeMs: stat.birthtimeMs, + }; + + try { + const zipMetaTxt = await this._fileService.readFileInZip(zipFile, BACKUP_META_FILENAME); + const zipMetaData: WtfBackupMetadataFile = JSON.parse(zipMetaTxt); + + if (!Array.isArray(zipMetaData.contents)) { + wtfBackup.error = "INVALID_CONTENTS"; + } else if (typeof zipMetaData.createdAt !== "number") { + wtfBackup.error = "INVALID_CREATED_AT"; + } else if (typeof zipMetaData.createdBy !== "string") { + wtfBackup.error = "INVALID_CREATED_BY"; + } + } catch (e) { + console.error("Failed to process backup metadata", zipFile, e); + wtfBackup.error = "GENERIC_ERROR"; + } finally { + wtfBackups.push(wtfBackup); + } + } + + return wtfBackups; + } finally { + console.timeEnd("getBackupList"); + } + } + + /** + * Prepare the backup folder, create some back metadata for the backup, zip the wtf contents into a zip file + */ + public async createBackup(installation: WowInstallation, status?: (int) => void): Promise { + // ensure we have a directory to save our backup zip to + await this.createBackupDirectory(installation); + + // delete any pre-existing meta file so it does not affect the overall hash + await this.deleteBackupMetadataFile(installation); + + // create the hash output for the file tree + const [metadataFilePath, metadataFile] = await this.createBackupMetadataFile(installation); + + status?.call(this, metadataFile.contents.length); + + try { + await this.createBackupZip(installation); + } finally { + // always delete the metadata file + await this._fileService.remove(metadataFilePath); + } + } + + /** + * Delete a backup zip file based on the given installation + */ + public async deleteBackup(fileName: string, installation: WowInstallation): Promise { + const backupPath = this.getBackupPath(installation); + const fullPath = path.join(backupPath, fileName); + + const pathExists = await this._fileService.pathExists(fullPath); + if (!pathExists) { + throw new Error("path not found"); + } + + await this._fileService.remove(fullPath); + } + + /** + * Use the given backup zip file to carefully replace the user's existing WTF folder with what was inside the backup zip + * This operation should support a rollback feature up on failures + */ + public async applyBackup(fileName: string, installation: WowInstallation): Promise { + console.time("applyBackup"); + try { + if (!fileName.endsWith(".zip")) { + throw new Error(`Invalid backup file: ${fileName}`); + } + + const backupPath = this.getBackupPath(installation); + const wtfPath = this.getWtfPath(installation); + const fullPath = path.join(backupPath, fileName); + + console.log(`Check backup zip location: ${fullPath}`); + const pathExists = await this._fileService.pathExists(fullPath); + if (!pathExists) { + throw new Error("path not found"); + } + + const wtfPathExists = await this._fileService.pathExists(wtfPath); + const wtfBackupPath = `${wtfPath}-wowup`; + if (wtfPathExists) { + console.log(`Rename current wtf folder: ${wtfPath}`); + await this._fileService.renameFile(wtfPath, wtfBackupPath); + } + + console.log(`Unzip backup file: ${wtfPath}`); + try { + await this._fileService.unzipFile(fullPath, wtfPath); + + // remove the metadata file, so that hashes will match + await this.deleteBackupMetadataFile(installation); + + // validate that what we unzipped matches what was expected + console.log(`Validate backup result`); + await this.isBackupApplicationValid(fullPath, installation); + } catch (e) { + // Roll back the changes we made + console.log(`Rolling back changes`); + await this._fileService.deleteIfExists(wtfPath); + await this._fileService.renameFile(wtfBackupPath, wtfPath); + throw e; + } + + console.log(`Removing soft backup: ${wtfBackupPath}`); + await this._fileService.deleteIfExists(wtfBackupPath); + } finally { + console.timeEnd("applyBackup"); + } + } + + /** + * Cross check the unzipped results against the metadata we have stored in the source zip file + */ + private async isBackupApplicationValid(zipFile: string, installation: WowInstallation) { + const srcMetaTxt = await this._fileService.readFileInZip(zipFile, BACKUP_META_FILENAME); + + const srcMeta: WtfBackupMetadataFile = JSON.parse(srcMetaTxt); + let newMeta = await this.getBackupMetaList(installation); + + // since the metadata file is not considered when hashing the contents, ignore it + srcMeta.contents = srcMeta.contents.filter((sm) => !sm.path.endsWith(BACKUP_META_FILENAME)); + newMeta = newMeta.filter((nm) => !nm.path.endsWith(BACKUP_META_FILENAME)); + + if (srcMeta.contents.length !== newMeta.length) { + throw new Error("Backup content count did not match"); + } + + for (const sm of srcMeta.contents) { + const nm = newMeta.find((n) => n.path === sm.path); + if (!nm) { + console.warn(`Matching path not found" ${sm.path}`); + continue; + } + + if (nm.hash !== sm.hash) { + console.warn(`Hash mismatch found: ${sm.path} : ${nm.path}`); + continue; + } + } + } + + private async createBackupZip(installation: WowInstallation): Promise { + console.time("createBackupZip"); + try { + const wtfPath = this.getWtfPath(installation); + const zipPath = path.join(this.getBackupPath(installation), `wtf_${Date.now()}.zip`); + await this._fileService.zipFile(wtfPath, zipPath); + } finally { + console.timeEnd("createBackupZip"); + } + } + + private async createBackupDirectory(installation: WowInstallation): Promise { + const backupPath = this.getBackupPath(installation); + await this._fileService.createDirectory(backupPath); + } + + private async createBackupMetadataFile( + installation: WowInstallation + ): Promise<[string, WtfBackupMetadataFile]> { + const backupMetaList = await this.getBackupMetaList(installation); + const backupMetadata: WtfBackupMetadataFile = { + contents: backupMetaList, + createdAt: Date.now(), + createdBy: "manual", + }; + + return [await this.writeWtfMetadataFile(backupMetadata, installation), backupMetadata]; + } + + private async deleteBackupMetadataFile(installation: WowInstallation) { + const wtfPath = this.getWtfPath(installation); + const metaPath = path.join(wtfPath, BACKUP_META_FILENAME); + await this._fileService.deleteIfExists(metaPath); + } + + private async getBackupMetaList(installation: WowInstallation): Promise { + const wtfTree = await this.getWtfContents(installation, true); + const wtfList = this.flattenTree([wtfTree]); + + return this.toBackupMeta(wtfList, installation); + } + + private async writeWtfMetadataFile( + backupMetadata: WtfBackupMetadataFile, + installation: WowInstallation + ): Promise { + const wtfPath = this.getWtfPath(installation); + + const metaPath = path.join(wtfPath, BACKUP_META_FILENAME); + await this._fileService.writeFile(metaPath, JSON.stringify(backupMetadata, null, 2)); + + return metaPath; + } + + /** + * Get a list of all the zip files in our internal backup folder + */ + private async listBackupFiles(installation: WowInstallation) { + const backupPath = this.getBackupPath(installation); + const zipFiles = await this._fileService.listFiles(backupPath, "*.zip"); + return zipFiles.map((f) => path.join(backupPath, f)); + } + + /** + * Get the path to our internal backup folder + */ + public getBackupPath(installation: WowInstallation): string { + return path.join(this._wowUpService.wtfBackupFolder, installation.id); + } + + /** + * Convert a tree structure to a flat array + */ + private flattenTree(nodes: WtfNode[]): WtfNode[] { + return Array.prototype.concat.apply( + nodes, + nodes.map((n) => this.flattenTree(n.children)) + ); + } + + private toBackupMeta(nodes: WtfNode[], installation: WowInstallation): WtfBackupMeta[] { + const wtfPath = this.getWtfPath(installation); + + return nodes.map((n) => { + const nodeBase = n.path.replace(wtfPath, ""); + return { + hash: n.hash, + isDirectory: n.isDirectory, + path: nodeBase, + size: n.size, + }; + }); + } +} diff --git a/WowUp/wowup-electron/src/app/services/zoom/zoom.service.ts b/WowUp/wowup-electron/src/app/services/zoom/zoom.service.ts new file mode 100644 index 0000000..9b68a92 --- /dev/null +++ b/WowUp/wowup-electron/src/app/services/zoom/zoom.service.ts @@ -0,0 +1,88 @@ +import { BehaviorSubject } from "rxjs"; + +import { Injectable } from "@angular/core"; + +import { IPC_GET_ZOOM_FACTOR, IPC_SET_ZOOM_FACTOR } from "../../../common/constants"; +import { ZOOM_SCALE, ZoomDirection } from "../../utils/zoom.utils"; +import { ElectronService } from "../electron/electron.service"; +import { PreferenceStorageService } from "../storage/preference-storage.service"; + +@Injectable({ + providedIn: "root", +}) +export class ZoomService { + private readonly _zoomFactorChangeSrc = new BehaviorSubject(1.0); + + public readonly zoomFactor$ = this._zoomFactorChangeSrc.asObservable(); + + public constructor( + private _electronService: ElectronService, + private _preferenceStorageService: PreferenceStorageService + ) { + this.getZoomFactor() + .then((zoom) => this._zoomFactorChangeSrc.next(zoom)) + .catch(() => console.error("Failed to set initial zoom")); + + window.wowup.onRendererEvent("zoom-changed", (_evt, zoomDirection: string) => { + this.onWindowZoomChanged(zoomDirection).catch((e) => console.error(e)); + }); + } + + public getZoomFactor(): Promise { + return this._electronService.invoke(IPC_GET_ZOOM_FACTOR); + } + + public setZoomFactor = async (zoomFactor: number): Promise => { + await this._electronService.invoke(IPC_SET_ZOOM_FACTOR, zoomFactor); + this._zoomFactorChangeSrc.next(zoomFactor); + }; + + private async onWindowZoomChanged(zoomDirection: string) { + if (zoomDirection === "in") { + const factor = await this.getNextZoomInFactor(); + await this.setZoomFactor(factor); + } else if (zoomDirection === "out") { + const factor = await this.getNextZoomOutFactor(); + await this.setZoomFactor(factor); + } + } + + public applyZoom = async (zoomDirection: ZoomDirection): Promise => { + switch (zoomDirection) { + case ZoomDirection.ZoomIn: + await this.setZoomFactor(await this.getNextZoomInFactor()); + break; + case ZoomDirection.ZoomOut: + await this.setZoomFactor(await this.getNextZoomOutFactor()); + break; + case ZoomDirection.ZoomReset: + await this.setZoomFactor(1.0); + break; + case ZoomDirection.ZoomUnknown: + default: + break; + } + }; + + private async getNextZoomInFactor(): Promise { + const windowZoomFactor = await this.getZoomFactor(); + const zoomFactor = Math.round(windowZoomFactor * 100) / 100; + let zoomIndex = ZOOM_SCALE.indexOf(zoomFactor); + if (zoomIndex == -1) { + return 1.0; + } + zoomIndex = Math.min(zoomIndex + 1, ZOOM_SCALE.length - 1); + return ZOOM_SCALE[zoomIndex]; + } + + private async getNextZoomOutFactor(): Promise { + const windowZoomFactor = await this.getZoomFactor(); + const zoomFactor = Math.round(windowZoomFactor * 100) / 100; + let zoomIndex = ZOOM_SCALE.indexOf(zoomFactor); + if (zoomIndex == -1) { + return 1.0; + } + zoomIndex = Math.max(zoomIndex - 1, 0); + return ZOOM_SCALE[zoomIndex]; + } +} diff --git a/WowUp/wowup-electron/src/app/tests/test-helpers.ts b/WowUp/wowup-electron/src/app/tests/test-helpers.ts new file mode 100644 index 0000000..6dcb8af --- /dev/null +++ b/WowUp/wowup-electron/src/app/tests/test-helpers.ts @@ -0,0 +1,44 @@ +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; +import { MatModule } from "../modules/mat-module"; + +export function mockPreload(): void { + window.wowup = { + onRendererEvent: () => {}, + onceRendererEvent: () => {}, + openExternal: async () => {}, + openPath: () => Promise.resolve(""), + rendererInvoke: () => Promise.resolve(undefined), + rendererSendSync: () => undefined, + rendererOff: () => {}, + rendererOn: () => {}, + rendererSend: () => {}, + }; +} + +// AoT requires an exported function for factories +export function httpLoaderFactoryTest(http: HttpClient): TranslateHttpLoader { + return new TranslateHttpLoader(http, "./assets/i18n/", ".json"); +} + +export function getStandardImports(): any[] { + return [ + MatModule, + HttpClientModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: httpLoaderFactoryTest, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }), + ]; +} diff --git a/WowUp/wowup-electron/src/app/utils/addon.utils.ts b/WowUp/wowup-electron/src/app/utils/addon.utils.ts new file mode 100644 index 0000000..aa74ce6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/addon.utils.ts @@ -0,0 +1,95 @@ +import { orderBy, filter } from "lodash"; +import { Addon, AddonDependency, AddonDependencyType, AddonExternalId } from "wowup-lib-core"; + +export function getAllProviders(addon: Addon): AddonExternalId[] { + return orderBy(addon.externalIds, ["providerName"], ["asc"]); +} + +export function getProviders(addon: Addon): AddonExternalId[] { + return filter(getAllProviders(addon), (extId) => extId.providerName !== addon.providerName); +} + +export function hasMultipleProviders(addon: Addon): boolean { + return getProviders(addon).length > 0; +} + +export function getAddonDependencies( + addon: Addon, + dependencyType: AddonDependencyType | undefined = undefined +): AddonDependency[] { + if (dependencyType === undefined) { + return addon.dependencies ?? []; + } + + return filter(addon.dependencies, (dep) => dep.type === dependencyType); +} + +export function needsUpdate(addon: Addon | undefined): boolean { + if (addon.isIgnored) { + return false; + } + + // Sometimes authors push out new builds without changing the toc version. + if (addon.externalLatestReleaseId && addon.externalLatestReleaseId !== addon.installedExternalReleaseId) { + return true; + } + + return !!addon.installedVersion && addon.installedVersion !== addon.latestVersion; +} + +export function needsInstall(addon: Addon): boolean { + return !addon.installedVersion; +} + +// export function getGameVersion(interfaceStr: string | undefined): string { +// if (typeof interfaceStr !== "string" || interfaceStr.length === 0) { +// return Array(3).fill("0").join("."); +// } + +// if (interfaceStr.toString().indexOf(".") !== -1) { +// return interfaceStr; +// } + +// // split the long interface into 3 chunks, major minor patch +// const chunks = [ +// interfaceStr.substring(0, interfaceStr.length - 4), +// interfaceStr.substring(interfaceStr.length - 4, interfaceStr.length - 2), +// interfaceStr.substring(interfaceStr.length - 2), +// ]; +// return chunks.map((c) => parseInt(c, 10)).join("."); +// } + +/** + * Accepts n semver (10.0.0) and formats it as an interface version (100000) + * if the format is invalid or missing throw error + */ +export function toInterfaceVersion(version: string): string { + if (!version) { + throw new Error("interface version empty or undefined"); + } + + if (version.indexOf(".") === -1) { + return version; + } + + const parts = version.split("."); + if (parts.length != 3) { + console.warn(`invalid part length: ${parts.length} - ${version}`); + while (parts.length < 3) { + parts.push("0"); + } + console.warn(`guessing version: ${parts.join(".")}`); + } + + const paddedParts = parts.map((part, idx) => padInterfacePart(part, idx)); + + return paddedParts.join(""); +} + +function padInterfacePart(part: string, idx: number) { + const num = parseInt(part, 10); + if (idx === 0) { + return num; + } + return num >= 10 ? num : `0${num}`; +} diff --git a/WowUp/wowup-electron/src/app/utils/array.utils.ts b/WowUp/wowup-electron/src/app/utils/array.utils.ts new file mode 100644 index 0000000..67ff8a0 --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/array.utils.ts @@ -0,0 +1,19 @@ +export function strictFilter(arr: (T | undefined)[]): T[] { + const filtered: T[] = []; + for (const item of arr) { + if (item !== undefined) { + filtered.push(item); + } + } + return filtered; +} + +export function strictFilterBy(arr: (T | undefined)[], predicate: (val: T) => boolean): T[] { + const filtered: T[] = []; + for (const item of arr) { + if (item !== undefined && predicate.call(null, item)) { + filtered.push(item); + } + } + return filtered; +} diff --git a/WowUp/wowup-electron/src/app/utils/dom.utils.ts b/WowUp/wowup-electron/src/app/utils/dom.utils.ts new file mode 100644 index 0000000..1e270bb --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/dom.utils.ts @@ -0,0 +1,59 @@ +export function formatDynamicLinks(container: HTMLElement, onClick: (element: HTMLAnchorElement) => boolean): void { + if (!container) { + return; + } + + const aTags = container.querySelectorAll("a"); + const tagArr = Array.from(aTags); + for (const tag of tagArr) { + if (tag.getAttribute("clk")) { + continue; + } + + if (tag.href.toLowerCase().indexOf("http") === -1 || tag.href.toLowerCase().indexOf("localhost") !== -1) { + tag.classList.add("no-link"); + } + + tag.setAttribute("clk", "1"); + tag.addEventListener( + "click", + (e: MouseEvent) => { + const anchor = onOpenLink(e); + if (anchor === undefined) { + return; + } + + onClick.call(null, anchor); + }, + false + ); + } +} + +function onOpenLink(e: MouseEvent): HTMLAnchorElement | undefined { + e.preventDefault(); + + // Go up the call chain to find the tag + const path = e.composedPath() as HTMLElement[]; + let anchor: HTMLAnchorElement | undefined = undefined; + for (const element of path) { + if (element.tagName !== "A") { + continue; + } + + anchor = element as HTMLAnchorElement; + break; + } + + if (!anchor) { + console.warn("No anchor in path"); + return undefined; + } + + if (anchor.href.toLowerCase().indexOf("http") !== 0 || anchor.href.toLowerCase().indexOf("localhost") !== -1) { + console.warn(`Unhandled relative path: ${anchor.href}`); + return undefined; + } + + return anchor; +} diff --git a/WowUp/wowup-electron/src/app/utils/markdown.utlils.ts b/WowUp/wowup-electron/src/app/utils/markdown.utlils.ts new file mode 100644 index 0000000..0b2d99f --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/markdown.utlils.ts @@ -0,0 +1,13 @@ +import * as MarkdownIt from "markdown-it"; + +export function convertMarkdown(markdown: string): string { + const md = new MarkdownIt({ + html: true, + breaks: false, + }); + + let html = md.render(markdown?.trim() ?? ""); + html = html.replace(/(?:\r\n|\r|\n)/g, ""); + + return html; +} diff --git a/WowUp/wowup-electron/src/app/utils/number.utils.ts b/WowUp/wowup-electron/src/app/utils/number.utils.ts new file mode 100644 index 0000000..3b82e34 --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/number.utils.ts @@ -0,0 +1,39 @@ +export function shortenDownloadCount(value: number, nDigit: number): string { + if (value < 10) { + return value.toString(); + } + const exponent = Math.log10(value); + const nGroups = Math.floor(exponent / nDigit); + const shortValue = value / Math.pow(10, nGroups * nDigit); + return shortValue.toFixed(0); +} + +export function formatSize(size: number): string { + if (size < 1024) { + return `${size} bytes`; + } + + const sizeKb = Math.round(size / 1024); + if (sizeKb < 1024) { + return `${sizeKb} kb`; + } + + const sizeMb = Math.round(size / 1024 / 1024); + return `${sizeMb} mb`; +} + +// This is a horrifying way to round to the nearest tens place +export function roundDownloadCount(value: number): number { + if (value < 10) { + return value; + } + const exp = value.toExponential(); + const numberMatch = /(\d*\.?\d*)e\+(\d+)/.exec(exp); + if (numberMatch === null) { + throw new Error("failed to get number match"); + } + + const number = Math.ceil(parseFloat(numberMatch[1]) * 10); + const exponent = new Array(parseInt(numberMatch[2] ?? "0") - 1).fill(0); + return parseInt(`${number}${exponent.join("")}`, 10); +} diff --git a/WowUp/wowup-electron/src/app/utils/search-result.utils.ts b/WowUp/wowup-electron/src/app/utils/search-result.utils.ts new file mode 100644 index 0000000..8d5f8e6 --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/search-result.utils.ts @@ -0,0 +1,48 @@ +import * as _ from "lodash"; +import { + AddonChannelType, + AddonDependencyType, + AddonSearchResult, + AddonSearchResultDependency, + AddonSearchResultFile, +} from "wowup-lib-core"; + +export function getLatestFile( + searchResult: AddonSearchResult | undefined, + channel: AddonChannelType +): AddonSearchResultFile | undefined { + if (!searchResult?.files) { + console.warn( + `Search result had no files: [${searchResult?.providerName ?? ""}:${searchResult?.externalId ?? ""}] ${ + searchResult?.name ?? "" + }` + ); + return undefined; + } + + let files = _.filter(searchResult.files, (f) => f.channelType <= channel); + files = _.orderBy(files, "releaseDate", "desc"); + let latestFile = _.first(files); + + // In the event that there are no matching files for the desired channel, return the latest regardless of channel + if (!latestFile) { + latestFile = _.first(_.orderBy(searchResult.files, "releaseDate", "desc")); + } + + return latestFile; +} + +export function getDependencies( + searchResult: AddonSearchResult, + channel: AddonChannelType +): AddonSearchResultDependency[] { + return getLatestFile(searchResult, channel)?.dependencies || []; +} + +export function getDependencyType( + searchResult: AddonSearchResult, + channel: AddonChannelType, + dependencyType: AddonDependencyType +): AddonSearchResultDependency[] { + return _.filter(getDependencies(searchResult, channel), (dep) => dep.type === dependencyType); +} diff --git a/WowUp/wowup-electron/src/app/utils/string.utils.ts b/WowUp/wowup-electron/src/app/utils/string.utils.ts new file mode 100644 index 0000000..90925f2 --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/string.utils.ts @@ -0,0 +1,106 @@ +import { createHash } from "crypto"; +import { DAY_SECONDS, HOUR_SECONDS, MONTH_SECONDS, YEAR_SECONDS } from "../../common/constants"; + +export function strIsNotNullOrEmpty(value?: string): boolean{ + return typeof value === 'string' && value.length > 0; +} + +export function stringIncludes(value: string | undefined, search: string): boolean { + if (!value) { + return false; + } + return value.trim().toLowerCase().indexOf(search.trim().toLowerCase()) >= 0; +} + +export function removeExtension(str: string): string { + return str.replace(/\.[^/.]+$/, ""); +} + +export function camelToSnakeCase(str: string): string { + // if the string is all caps, ignore it + if (str.toUpperCase() === str) { + return str; + } + + const originalStart = str.charAt(0); + return originalStart + str.slice(1).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} + +/** + * Get the sha1 hash of a string + */ +export function getSha1Hash(str: string): string { + return createHash("sha1").update(str).digest("hex"); +} + +export function capitalizeString(str: string): string { + return str.charAt(0).toUpperCase() + str.toLowerCase().slice(1); +} + +export function isProtocol(arg: string): boolean { + return getProtocol(arg) != null; +} + +export function getProtocol(arg: string): string | null { + const match = /^([a-z][a-z0-9+\-.]*):/.exec(arg); + return match !== null && match.length > 1 ? match[1] : null; +} + +export function getProtocolParts(protocol: string) { + return new URL(protocol).pathname + .split("/") + .filter((part) => !!part) + .map((part) => part.toLowerCase()); +} + +export function getRelativeDateFormat(value: string): [string, object | undefined] { + if (!value) { + return ["", undefined]; + } + + let then: Date; + try { + then = new Date(value); + } catch (error) { + return ["", undefined]; + } + + if (isNaN(then.getTime())) { + return ["", undefined]; + } + + const deltaMs = new Date().getTime() - then.getTime(); + + let tempSec = Math.floor(deltaMs / 1000); + + const years = Math.floor(tempSec / YEAR_SECONDS); + if (years) { + return ["COMMON.DATES.YEARS_AGO", { count: years }]; + } + + const months = Math.floor((tempSec %= YEAR_SECONDS) / MONTH_SECONDS); + if (months) { + return ["COMMON.DATES.MONTHS_AGO", { count: months }]; + } + + const days = Math.floor((tempSec %= MONTH_SECONDS) / DAY_SECONDS); + if (days > 1) { + return ["COMMON.DATES.DAYS_AGO", { count: days }]; + } + + if (days) { + return ["COMMON.DATES.YESTERDAY", undefined]; + } + + const hours = Math.floor((tempSec %= DAY_SECONDS) / HOUR_SECONDS); + if (hours) { + return ["COMMON.DATES.HOURS_AGO", { count: hours }]; + } + + // const minutes = Math.floor((tempSec %= HOUR_SECONDS) / MINUTE_SECONDS); + // if (minutes) { + // return [`${tempSec} ago`, undefined]; + // } + + return ["COMMON.DATES.JUST_NOW", undefined]; +} diff --git a/WowUp/wowup-electron/src/app/utils/test.utils.ts b/WowUp/wowup-electron/src/app/utils/test.utils.ts new file mode 100644 index 0000000..750a05b --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/test.utils.ts @@ -0,0 +1,27 @@ +import { HttpClient, HttpClientModule } from "@angular/common/http"; +import { ModuleWithProviders } from "@angular/core"; +import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core"; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; +import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler"; + +export function testHttpLoaderFactory(http: HttpClient): TranslateHttpLoader { + return new TranslateHttpLoader(http, "./assets/i18n/", ".json"); +} + +export function getStandardTestImports(): any[] { + return [HttpClientModule, createTranslateModule()]; +} + +export function createTranslateModule(): ModuleWithProviders { + return TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: testHttpLoaderFactory, + deps: [HttpClient], + }, + compiler: { + provide: TranslateCompiler, + useClass: TranslateMessageFormatCompiler, + }, + }); +} diff --git a/WowUp/wowup-electron/src/app/utils/tests/addon.utils.spec.ts b/WowUp/wowup-electron/src/app/utils/tests/addon.utils.spec.ts new file mode 100644 index 0000000..0c366fc --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/tests/addon.utils.spec.ts @@ -0,0 +1,81 @@ +import { getGameVersion } from "wowup-lib-core"; +import * as AddonUtils from "../addon.utils"; + +describe("AddonUtils", () => { + it("Should convert 9.1.2", () => { + const gameVersion = getGameVersion("90102"); + expect(gameVersion).toEqual("9.1.2"); + }); + + it("Should convert 9.11.22", () => { + const gameVersion = getGameVersion("91122"); + expect(gameVersion).toEqual("9.11.22"); + }); + + it("Should accept 9.11.22", () => { + const gameVersion = getGameVersion("9.11.22"); + expect(gameVersion).toEqual("9.11.22"); + }); + + it("Should accept empty str", () => { + const gameVersion = getGameVersion(""); + expect(gameVersion).toEqual("0.0.0"); + }); + + it("Should accept undefined", () => { + const gameVersion = getGameVersion(undefined); + expect(gameVersion).toEqual("0.0.0"); + }); + + it("Should convert 10.1.2", () => { + const gameVersion = getGameVersion("100102"); + expect(gameVersion).toEqual("10.1.2"); + }); + + it("Should convert 10.11.22", () => { + const gameVersion = getGameVersion("101122"); + expect(gameVersion).toEqual("10.11.22"); + }); + + it("Should interface 9.1.2", () => { + const gameVersion = AddonUtils.toInterfaceVersion("9.1.2"); + expect(gameVersion).toEqual("90102"); + }); + + + it("Should interface 10.0", () => { + const gameVersion = AddonUtils.toInterfaceVersion("10.0"); + expect(gameVersion).toEqual("100000"); + }); + + it("Should interface 9.11.22", () => { + const gameVersion = AddonUtils.toInterfaceVersion("9.11.22"); + expect(gameVersion).toEqual("91122"); + }); + + it("Should interface 90102", () => { + const gameVersion = AddonUtils.toInterfaceVersion("90102"); + expect(gameVersion).toEqual("90102"); + }); + + it("Should throw interface empty str", () => { + expect(() => AddonUtils.toInterfaceVersion("")).toThrow(new Error("interface version empty or undefined")); + }); + + it("Should throw interface undefined", () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect(() => AddonUtils.toInterfaceVersion(undefined as any)).toThrow( + new Error("interface version empty or undefined"), + ); + }); + + it("Should interface 10.1.2", () => { + const gameVersion = AddonUtils.toInterfaceVersion("10.1.2"); + expect(gameVersion).toEqual("100102"); + }); + + it("Should Interface 10.11.22", () => { + const gameVersion = AddonUtils.toInterfaceVersion("10.11.22"); + expect(gameVersion).toEqual("101122"); + }); +}); diff --git a/WowUp/wowup-electron/src/app/utils/time.utils.ts b/WowUp/wowup-electron/src/app/utils/time.utils.ts new file mode 100644 index 0000000..c679379 --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/time.utils.ts @@ -0,0 +1,7 @@ +export function delayMs(timeMs: number): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, timeMs); + }); +} diff --git a/WowUp/wowup-electron/src/app/utils/zoom.utils.ts b/WowUp/wowup-electron/src/app/utils/zoom.utils.ts new file mode 100644 index 0000000..8f59e99 --- /dev/null +++ b/WowUp/wowup-electron/src/app/utils/zoom.utils.ts @@ -0,0 +1,37 @@ +// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values +const ZOOM_IN_CODE = "Equal"; +const ZOOM_OUT_CODE = "Minus"; +const ZOOM_RESET_CODE = "Digit0"; + +export const ZOOM_SCALE = [0.5, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]; + +export enum ZoomDirection { + ZoomIn, + ZoomOut, + ZoomReset, + ZoomUnknown, +} + +export function isZoomInShortcut(event: KeyboardEvent): boolean { + return event.ctrlKey && event.code === ZOOM_IN_CODE; +} + +export function isZoomOutShortcut(event: KeyboardEvent): boolean { + return event.ctrlKey && event.code === ZOOM_OUT_CODE; +} + +export function isZoomResetShortcut(event: KeyboardEvent): boolean { + return event.ctrlKey && event.code === ZOOM_RESET_CODE; +} + +export function getZoomDirection(event: KeyboardEvent): ZoomDirection { + if (isZoomInShortcut(event)) { + return ZoomDirection.ZoomIn; + } else if (isZoomOutShortcut(event)) { + return ZoomDirection.ZoomOut; + } else if (isZoomResetShortcut(event)) { + return ZoomDirection.ZoomReset; + } + + return ZoomDirection.ZoomUnknown; +} diff --git a/WowUp/wowup-electron/src/assets/.gitkeep b/WowUp/wowup-electron/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/WowUp/wowup-electron/src/assets/Digital-Patreon-Wordmark_FieryCoral.png b/WowUp/wowup-electron/src/assets/Digital-Patreon-Wordmark_FieryCoral.png new file mode 100644 index 0000000..8ba51a2 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/Digital-Patreon-Wordmark_FieryCoral.png differ diff --git a/WowUp/wowup-electron/src/assets/changelog.json b/WowUp/wowup-electron/src/assets/changelog.json new file mode 100644 index 0000000..9727422 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/changelog.json @@ -0,0 +1,75 @@ +{ + "ChangeLogs": [ + { + "Version": "2.1.3", + "changes": ["Handle a change Townlong Yak made"] + }, + { + "Version": "2.1.2", + "changes": ["Always backfill install folder list"] + }, + { + "Version": "2.1.1", + "changes": ["Fix an issue with fresh TukUI/ElvUI installs"] + }, + { + "Version": "2.1.0", + "changes": [ + "German locale updates (Glow)", + "Russian locale updates (Medok)", + "Italian locale updates (Bito)", + "French locale updates (Zazou)", + "Spanish locale updates (SkollVargr)", + "Chinese locale updates (CyanoHao)", + "Portuguese locale updates (danielschmitz)", + "Added Czech translations (pkejval)", + "Added view on provider link added to context menu (stevietv)", + "Added the ability to turn providers on and off (Linaori)", + "Added support for the latest version of the WowUp addon (Linaori)", + "Added the new Hub provider", + "Added support for full descriptions for all providers.", + "Added support for changelogs for most providers.", + "Added the showing of unmatched addon folders to 'My Addons'", + "Added support for ignoring development directories", + "Added auto updated addon names to the notification when possible", + "Added the ability to install a zip file URL directly", + "Added support for identifying Raider.io addon updated by their own app", + "Added a new error descriptions when importing from URL", + "Added better error handling for install/update errors", + "Added a new app icon", + "Added localization support to the system menus on mac", + "Added indicator for entering fullscreen (f11) mode", + "Added new app update button and update notification toast", + "Standardize the zoom scale (strayge)", + "Change the default 'No' button in confirm boxes to match theme (jar349)", + "Show folder context menu item will now list all associated folders", + "Mac app is now notarized", + "Improved scanning speed", + "Improved http caching strategy", + "Fix an issue with sorting some columns by date incorrectly", + "Fix some issues with wow install path scanning", + "Fix issues with the background auto update loop", + "Fix an issue with Ctr + r or view > refresh or view > force reload breaking the app", + "Fix an issue with 'Ignore' context menu item not being clickable the full width of the menu", + "Fix an issue with 'Auto Update' context menu item not being clickable the full width of the menu", + "Many more bug fixes" + ] + }, + { + "Version": "2.0.3", + "changes": ["Fix an issue with legacy TukUI/ElvUI external IDs saved as ints"] + }, + { + "Version": "2.0.2", + "changes": ["Fix an issue with a hardcoded hub URL"] + }, + { + "Version": "2.0.1", + "changes": ["Use the new TukUI mirror to fix timeout issues"] + }, + { + "Version": "2.0.0", + "changes": ["You are not prepared!"] + } + ] +} diff --git a/WowUp/wowup-electron/src/assets/chrome-close.svg b/WowUp/wowup-electron/src/assets/chrome-close.svg new file mode 100644 index 0000000..a52356d --- /dev/null +++ b/WowUp/wowup-electron/src/assets/chrome-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/WowUp/wowup-electron/src/assets/chrome-maximize.svg b/WowUp/wowup-electron/src/assets/chrome-maximize.svg new file mode 100644 index 0000000..a20fb4e --- /dev/null +++ b/WowUp/wowup-electron/src/assets/chrome-maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/WowUp/wowup-electron/src/assets/chrome-minimize.svg b/WowUp/wowup-electron/src/assets/chrome-minimize.svg new file mode 100644 index 0000000..1ae87f9 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/chrome-minimize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/WowUp/wowup-electron/src/assets/chrome-restore.svg b/WowUp/wowup-electron/src/assets/chrome-restore.svg new file mode 100644 index 0000000..da4d030 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/chrome-restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/WowUp/wowup-electron/src/assets/i18n/cs.json b/WowUp/wowup-electron/src/assets/i18n/cs.json new file mode 100644 index 0000000..b7468c1 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/cs.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Active addons: {count}", + "ADDED_BADGE_TOOLTIP": "We will attempt to install this addon", + "CONFLICT_BADGE_TOOLTIP": "Addons in conflict will not be modified", + "COPY_BUTTON": "Copy", + "DIALOG_TITLE": "Import/Export Addons: {clientType}", + "EXPORT_STRING_COPIED": "Export string copied to clipboard", + "EXPORT_STRING_PASTED": "Inserted clipboard contents", + "EXPORT_TAB_LABEL": "Export", + "EXPORT_TEXT_LABEL": "Addon Export Data", + "GENERIC_IMPORT_ERROR": "An error occurred during import", + "IGNORED_ADDON_COUNT": "Ignored addons: {count}", + "IMPORT_ADDED_COUNT": "{count} added", + "IMPORT_BADGE_ADDED": "New", + "IMPORT_BADGE_CONFLICT": "Conflict", + "IMPORT_BADGE_NO_CHANGE": "No Change", + "IMPORT_BUTTON": "Import", + "IMPORT_CONFLICT_COUNT": "{count} in conflict", + "IMPORT_NO_CHANGE_COUNT": "{count} unchanged", + "IMPORT_STRING_INVALID": "Import string was invalid", + "IMPORT_TAB_LABEL": "Import", + "IMPORT_TEXT_INSTRUCTIONS": "Paste WowUp addon export data into the field below to get started", + "IMPORT_TEXT_LABEL": "Import data", + "IMPORT_TOTAL_COUNT": "Importing {count} {count, plural, =1{addon} other{addons}}", + "INSTALL_BUTTON": "Install", + "INVALID_CLIENT_TYPE": "Import string did not match your selected client type", + "NO_CHANGE_BADGE_TOOLTIP": "You already have this addon installed", + "PASTE_BUTTON": "Paste", + "PROVIDER_MISMATCH": "Addon provider does not match", + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Copy", + "CUT": "Cut", + "LABEL": "Upravit", + "PASTE": "Paste", + "REDO": "Redo", + "SELECT_ALL": "SelectAll", + "UNDO": "Undo" + }, + "QUIT": "Quit", + "VIEW": { + "FORCE_RELOAD": "Force Reload", + "LABEL": "Zobrazit", + "RELOAD": "Reload", + "TOGGLE_DEV_TOOLS": "Toggle Dev Tools", + "TOGGLE_FULL_SCREEN": "Toggle Full Screen", + "ZOOM_IN": "Přiblížit", + "ZOOM_OUT": "Oddálit", + "ZOOM_RESET": "Resetovat přiblížení" + }, + "WINDOW": { + "CLOSE": "Close", + "LABEL": "Window" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Automaticky aktualizovány\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "{count} {count, plural, =1{addon byl aktualizován} =2{addony byly aktualizovány} =3{addony byly aktualizovány} =4{addony byly aktualizovány} other{addonů bylo aktualizováno}}.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Automatické aktualizace", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Leave full screen mode", + "FULLSCREEN_SNACKBAR": { + "MAC": "Press ^⌘F to exit full screen", + "WINDOWS": "Press F11 to exit full screen" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nAre you sure you want to open this 3rd party page in your default browser?", + "TITLE": "You are about to leave WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Neznámý" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Hledání addonů dokončeno...", + "ADDON_SCAN_STARTED": "Hledám addony...", + "ADDON_SCAN_UPDATE": "Prohledávám {count} {count, plural, =1{složku} =2{složky} =3{složky} =4{složky} other{složek}}..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Zjistit aktualizace...", + "QUIT_ACTION": "Konec", + "SHOW_ACTION": "Ukázat" + }, + "THEME": { + "ALLIANCE": "Alliance", + "DEFAULT": "WowUp", + "GROUP_DARK": "Tmavý", + "GROUP_LIGHT": "Světlý", + "HORDE": "Horda" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Full Screen", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Checking for update", + "DOWNLOADED_TOOLTIP": "Nainstalovat aktualizaci WowUp", + "DOWNLOADING_UPDATE": "Downloading update", + "INSTALL_MESSAGE": "Chcete restartovat WowUp and nainstalovat aktualizaci?", + "INSTALL_TITLE": "Aktualizace WowUp je připravena", + "NOT_AVAILABLE": "Nejnovější verze WowUp je již nainstalována", + "PORTABLE_DOWNLOAD_MESSAGE": "Chcete ručně stáhnout nejnovější přenosnou (portable) verzi?\n\nMusíte vypnout aplikaci a ručně nakopírovat novou verzi.", + "PORTABLE_DOWNLOAD_TITLE": "Vyžadováno ruční stažení", + "SNACKBAR_ACTION": "Aktualizovat & Restart", + "SNACKBAR_TEXT": "Nová verze WowUp je k dispozici", + "TOOLTIP": "Aktualizace WowUp je k dospozici", + "UPDATE_AVAILABLE": "Starting download", + "UPDATE_ERROR": "Nepodařilo se stáhnout aktualizaci WowUp" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Achievements", + "ACTION_BARS": "Action Bars", + "ALL_ADDONS": "All Addons", + "AUCTION_ECONOMY": "Auction & Economy", + "BAGS_INVENTORY": "Bags & Inventory", + "BOSS_ENCOUNTERS": "Boss Encounters", + "BUFFS_DEBUFFS": "Buffs & Debuffs", + "BUNDLES": "Bundles", + "CHAT_COMMUNICATION": "Chat & Communication", + "CLASS": "Class", + "COMBAT": "Combat", + "COMPANIONS": "Companions", + "DATA_EXPORT": "Data Export", + "DEVELOPMENT_TOOLS": "Development Tools", + "GUILD": "Guild", + "LIBRARIES": "Libraries", + "MAIL": "Mail", + "MAP_MINIMAP": "Map & Minimap", + "MISCELLANEOUS": "Miscellaneous", + "MISSIONS": "Missions", + "PLUGINS": "Plugins", + "PROFESSIONS": "Professions", + "PVP": "PVP", + "QUESTS_LEVELING": "Quests & Leveling", + "ROLEPLAY": "Roleplay", + "TOOLTIPS": "Tooltips", + "UNIT_FRAMES": "Unit Frames" + }, + "ADDON_STATE": { + "IGNORED": "Ignorován", + "INSTALL": "Instalovat", + "PENDING": "Pending", + "UNAVAILABLE": "Unavailable", + "UNAVAILABLE_TOOLTIP": "This author or provider has made this addon unavailable", + "UNINSTALL": "Odinstalovat", + "UNKNOWN": "", + "UPDATE": "Aktualizovat", + "UPTODATE": "Aktuální", + "WARNING": "Warning" + }, + "ADDON_STATUS": { + "BACKINGUP": "Zálohuji", + "COMPLETE": "Nainstalován", + "DOWNLOADING": "Stahuji", + "ERROR": "Chyba", + "INSTALLING": "Instaluji", + "PENDING": "Čekající", + "RETRY": "Retrying...", + "UNINSTALLING": "Odinstalovávaný", + "UPDATING": "Aktualizuji..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "We have detected an issue with this addon. We are no longer able to update this addon or provide details.", + "GENERIC_TOOLTIP": "We have detected an issue with this addon", + "MISSING_ON_PROVIDER_DESCRIPTION": "This addon appears to have been removed by either the author or the addon provider.
We are no longer able to update this addon or provide details.", + "MISSING_ON_PROVIDER_TOOLTIP": "This addon appears to have been removed by the provider", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} properly returned this addon, however, it did not have any files matching this game version.
Once there is an update matching this game version this warning should go away.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} did not have any matching game version files", + "TOC_NAME_MISMATCH_DESCRIPTION": "This addon's folder name did not match with the expected toc file, you may experience issues with this addon in-game.", + "TOC_NAME_MISMATCH_TOOLTIP": "This addon's folder does not match the toc" + }, + "CLIENT_TYPES": { + "BETA": "Dragonflight Beta", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Public Test Realm (Wrath Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "Před {count} {count, plural, =1{dnem} other{dny}}", + "HOURS_AGO": "Před {count} {count, plural, =1{hodinou} other{hodinami}}", + "JUST_NOW": "Právě teď", + "MONTHS_AGO": "Před {count} {count, plural, =1{měsícem} other{měsíci}}", + "YEARS_AGO": "Před {count} {count, plural, =1{rokem} other{roky}}", + "YESTERDAY": "Včera" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} {dependencyCount, plural, =1{závislost vyžadována} other{závislostí vyžadováno}}" + }, + "DOWNLOAD_COUNT": { + "e+0": "{count}", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count} tisíc", + "e+4": "{count} tisíc", + "e+5": "{count} tisíc", + "e+6": "{count} miliónů", + "e+7": "{count} miliónů", + "e+8": "{count} miliónů", + "e+9": "{count} miliard" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alpha", + "BETA": "Beta", + "STABLE": "Stabilní" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Failed to toggle instant updates for your account. Please try again later, or reach out on Discord.", + "ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.", + "ADDON_SCAN_ERROR": "Vyskytla se chyba při párování vaší složky addonů s poskytovatelem {providerName}, zkuste to prosím později.", + "ADDON_SYNC_ERROR": "Vyskytla se chyba při zjišťování aktualizací od poskytovatele {providerName}, zkuste to prosím později.", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: An error occurred checking for updates to {addonName} from {providerName}, please try again later.", + "CHANGE_PROVIDER_ERROR": "Nepodařilo se změnit poskytovatele {providerName} pro addon {addonName}", + "GITHUB_LIMIT_ERROR": "Dosáhli jste limitu {max} požadavků na GitHub API.\nVyčkejte prosím do {reset} a pak to zkuste znovu.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Failed to check updates for {addonName}.\nPlease verify that the repository is correct or set this addon to be ignored." + }, + "PROGRESS_SPINNER": { + "LOADING": "Nahrávám..." + }, + "PROVIDER_ERROR": "Error contacting {providerName}", + "SEARCH": { + "NO_ADDONS": "Žádné addony nebyly nalezeny" + }, + "WOW_EXE_SELECTION_NAME": "WoW Executable" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "ID addonu:", + "BY_AUTHOR": "Od {authorName}", + "CHANGELOG_TAB": "Změny", + "COPY_ADDON_ID_SNACKBAR": "ID addonu zkopírováno do schránky", + "COPY_ADDON_ID_TOOLTIP": "Zkopíruje ID addonu do schránky", + "DEPENDENCY_TEXT": "Tento addon má {dependencyCount} {dependencyCount, plural, =1{závislost} =2{závislosti} =3{závislosti} =4{závislosti} other{závislostí}}", + "DESCRIPTION_NOT_FOUND": "No description found", + "DESCRIPTION_TAB": "Popis", + "FUNDING_LINK_TITLE": "Podpořte autora", + "IMAGES_TAB": "Previews", + "MISSING_DEPENDENCIES": "Chybějící závislosti", + "NO_CHANGELOG_TEXT": "No changelog available", + "VIEW_IN_BROWSER_BUTTON": "Zobrazit v prohlížeči", + "VIEW_ON_PROVIDER_PREFIX": "Zobrazit na" + }, + "ALERT": { + "ERROR_TITLE": "Chyba", + "POSITIVE_BUTTON": "OK" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "Ne", + "POSITIVE_BUTTON": "Ano" + }, + "CURSE_MIGRATION": { + "MESSAGE": "The CurseForge API has been shutdown so WowUp can no longer fetch addon information from it.
Unfortunately, this means that the CurseForge provider has been removed and your addons from CurseForge will no longer be updated.
It is recommended that you perform a re-scan in order to see what addons can be found on other providers.
The Re-Scan process may take a while.
Read more here.", + "NEGATIVE_BUTTON": "Manually Re-Scan", + "POSITIVE_BUTTON": "Automatically Re-Scan", + "RE_SCAN_ERROR": "The automatic Re-Scan failed, please try again manually.", + "RE_SCAN_SUCCESS": "The automatic Re-Scan has finished you're ready to go.", + "TITLE": "CurseForge Migration" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Addon is installed!", + "ADDON_INSTALLING": "Installing addon", + "CANCEL_BUTTON": "Close", + "ERRORS": { + "ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}", + "GENERIC": "Error fetching data for protocol: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}" + }, + "INSTALL_BUTTON": "Install", + "TITLE": "Install Addon From {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "URL adresa addonu", + "ADDON_URL_INPUT_PLACEHOLDER": "GitHub nebo WowInterface URL adresa", + "CLOSE_BUTTON": "Zavřít", + "DESCRIPTION": "Pokud chcete instalovat addon přímo z URL adresy, vložte ji sem.", + "DOWNLOAD_COUNT": "{textCount} stažení z {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "Nebyl nalezen žádný soubor ke stažení {message}.\n\nPlatný zip soubor musí být k dispozici, aby ho WowUp mohl stáhnout.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-bc' is required in order for WowUp to download it.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "Nebyl nalezen žádný soubor ke stažení {message}.\n\nPlatný zip soubor s názvem končícím s '-classic' musí být k dispozici, aby ho WowUp mohl stáhnout.", + "FAILED_TO_CONNECT": "Nemohu se připojit k API. Chvíli počkejte a pak to zkuste znovu.", + "INSTALL_FAILED": "Něco se nepovedlo při instalaci addonu, zkuste to prosím znovu.\n\nPokud se tato zpráva opakuje, kontaktujte nás na Discordu na kanále #help-me.", + "INVALID_URL": "Zadaná hodnota není platná URL adresa. Příklady platných URL adres jsou:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "Žádný addon na zadané URL adrese nebyl nalezen. Ujistěte se, že URL adresa odkazuje na správnou stránku.\n\nPři instalaci z GitHubu se ujistěte, že repozitář obsahuje release tag s zipem obsahujícím addon.", + "NO_RELEASE_FOUND": "Nebyl nalezen žádný soubor ke stažení {message}.\n\nPlatný zip soubor musí být k dispozici, aby ho WowUp mohl stáhnout.", + "NO_SEARCH_RESULTS": "Hledání neodpovídá žádný addon.", + "TITLE": "Instalace addonu se nepodařila", + "WRATH_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-wrath' is required in order for WowUp to download it." + }, + "IMPORT_ASSET_WARNING": "We were unable to verify if the latest release of this addon is compatible with your selected client.\n\nBut we did find a zip file \"{zipName}\".\n\nInstall at your own risk.", + "IMPORT_BUTTON": "Importovat", + "IMPORT_WARNING_TITLE": "Addon Import Warning", + "INSTALL_BUTTON": "Instalovat", + "INSTALL_SUCCESS_LABEL": "Úspěšně nainstalováno!", + "SUPPORTED_SOURCES": "Podporuje WowInterface a GitHub", + "TITLE": "URL adresa pro instalaci addonu" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Patch Notes {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Confirm", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, + "TITLE": "WowUp Permissions Setup", + "WAGO": { + "DESCRIPTION": "Enabled the Wago.io addon provider, this will display an ad required to use their service.\nThe ads directly benefit the authors of your favorite addons!", + "TOGGLE_LABEL": "Enable Wago.io Provider" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "This does not appear to be a valid World of Warcraft application:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Chcete nám pomoci dále vylepšovat WowUp posíláním anonymních dat o instalaci a případných chybách?", + "NEGATIVE_BUTTON": "Ne. Děkuji.", + "POSITIVE_BUTTON": "Jasně!", + "TITLE": "Telemetrie WowUp" + }, + "TRUST_DOMAIN_CHECKBOX": "Trust this domain and do not ask me in the future" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Atributy", + "CHANGE_LOG_SECTION_LABEL": "Změny", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Navštivte naše webové stránky!" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Login Now!", + "LOGOUT_BUTTON": "Logout", + "LOGOUT_CONFIRMATION_MESSAGE": "Are you sure you want to log out? All of your local account data will be removed, until you login again.", + "LOGOUT_CONFIRMATION_TITLE": "Logout?", + "MANAGE_ACCOUNT_BUTTON": "Manage Account", + "TITLE": "Account" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Categories", + "ADDON_CATEGORIES_MENU_TITLE": "Addon Categories", + "ADDON_CATEGORIES_SELECTED_TITLE": "Category: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Browse various categories", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Instalovat z URL adresy", + "INSTALL_FROM_URL_TOOLTIP": "Install and addon from a URL", + "REFRESH_BUTTON": "Aktualizovat", + "REFRESH_TOOLTIP": "Refresh addon results", + "RESET_CATEGORY_TOOLTIP": "Reset Category", + "SEARCH_LABEL": "Hledat", + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "AUTHOR_COLUMN_HEADER": "Autor(ři)", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Stažení", + "PROVIDER_COLUMN_HEADER": "Poskytovatel", + "RELEASED_AT_COLUMN_HEADER": "Vydáno", + "STATUS_COLUMN_HEADER": "Stav" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "O programu", + "ACCOUNT_TAB_TITLE": "Account", + "COLLAPSE_BUTTON_TITLE": "Collapse", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Expand", + "GET_ADDONS_TAB_TITLE": "Stáhnout nové addony", + "GUIDE_TAB_TITLE": "Guide", + "MIGRATING_ADDONS": "Migruji addony...", + "MY_ADDONS_TAB_TITLE": "Moje addony", + "NEWS_TAB_TITLE": "News", + "OPTIONS_TAB_TITLE": "Nastavení" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count} {count, plural, =1{addon vybrán} =2{addony vybrány} =3{addony vybrány} =4{addony vybrány} other{addonů vybráno}}", + "ALPHA_ADDON_CHANNEL": "Alpha", + "AUTO_UPDATE_ADDON_BUTTON": "Automaticky aktualizovat", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Notifications Enabled", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "Kanál aktualizací", + "IGNORE_ADDON_BUTTON": "Ignorovat", + "PROVIDER_SUBMENU_TITLE": "Poskytovatelé", + "REINSTALL_ADDON_BUTTON": "Přeinstalovat", + "REMOVE_ADDON_BUTTON": "Odebrat", + "SHOW_FOLDER": "Zobrazit složku", + "STABLE_ADDON_CHANNEL": "Stabilní" + }, + "ADDON_IS_CODE_REPOSITORY": "Jedná se o addon z repozitáře kódu", + "ADDON_REMOVED_SNACKBAR": "Successfully removed: {addonName} ", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Opravdu chcete změnit poskytovatele pro {addonName} na {providerName}? Tato operace odinstaluje existující addon a nainstaluje verzi od nového poskytovatele.", + "TITLE": "Změnit poskytovatele addonu?" + }, + "CHECK_UPDATES_BUTTON": "Zkontrolovat aktualizace", + "CHECK_UPDATES_BUTTON_TOOLTIP": "WowUp zkontroluje, zda neexistují novější verze addonů", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{update} other{updates}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Zobraz sloupce" + }, + "ERROR_SNACKBAR": "An error occurred", + "FILTER_LABEL": "Filtrovat", + "FUNDING_TOOLTIP": { + "CUSTOM": "Podpořte autora", + "GENERIC": "Podpořte autora na {platform}", + "GITHUB": "Podpořte autora na GitHubu", + "PATREON": "Podpořte autora na Patreonu", + "PAYPAL": "Podpořte autora pomocí PayPalu" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Import/Export Addons", + "MULTIPLE_PROVIDERS_TOOLTIP": "Tento addon má více poskytovatelů", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{addon} =2{addony} =3{addony} =4{addony} other{addonů}}", + "JOIN_DISCORD": "Pokecejte s námi na Discordu", + "PATREON_SUPPORT": "Podpořte WowUp na Patreonu", + "SEARCH_RESULTS": "{count} {count, plural, =1{výsledek} =2{výsledky} =3{výsledky} =4{výsledky} other{výsledků}}", + "VIEW_GITHUB": "Koukněte na kód na GitHubu", + "VIEW_GUIDE": "Koukněte na našeho průvodce funkcemi WowUp" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Požadovaná závislost chybí", + "RESCAN_FOLDERS_BUTTON": "Prohledat složky", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Prohledá složku WoW clienta pro nalezení aktuálně instalovaných addonů", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "Prohledáním složky může dojít k resetování informací o addonech. Používejte tuto funkci pouze v případech, kdy nejsou některé addony správně načteny v seznamu, případně když nejsou zobrazeny správné verze addonů. Prohledání složky nikdy nevymaže instalované addony, jen zaktualizuje informace, které má WowUp uložené o instalovaných addonech.\n\nProhledání může trvat delší dobu.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Spustit prohledání?", + "SPINNER": { + "GATHERING_ADDONS": "Zjišťuji informace o addonech...", + "UPDATING": "Aktualizuji {updateCount}/{addonCount}", + "UPDATING_WITH_ADDON_NAME": "Aktualizuji {updateCount}/{addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "ADDON_INSTALL_BUTTON": "Instalovat", + "ADDON_UPDATE_BUTTON": "Aktualizovat", + "AUTHOR_COLUMN_HEADER": "Autor(ři)", + "AUTO_UPDATE_ICON_TOOLTIP": "Automatická aktualizace povolena", + "GAME_VERSION_COLUMN_HEADER": "Verze hry", + "LATEST_VERSION_COLUMN_HEADER": "Nejnovější verze", + "PROVIDER_COLUMN_HEADER": "Poskytovatel", + "PROVIDER_RELEASE_CHANNEL": "Kanál poskytovatele", + "RELEASED_AT_COLUMN_HEADER": "Vydáno", + "STATUS_COLUMN_HEADER": "Stav", + "UPDATED_AT_COLUMN_HEADER": "Aktualizováno" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Odstraněním addonu ve WowUp dojde i k odstranění addonu z herního clienta (Interface/Addons). Uživatelské nastavení ve složce WTF zůstane zachováno.", + "CONFIRMATION_LESS_THAN_THREE": "Opravdu chcete odebrat {count} {count, plural, =1{addon} =2{addony} =3{addony} =4{addony} other{addonů}}?", + "CONFIRMATION_MORE_THAN_THREE": "Opravdu chcete odebrat {count} {count, plural, =1{addon} =2{addony} =3{addony} =4{addony} other{addonů}}?", + "CONFIRMATION_ONE": "Opravdu chcete odebrat {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} má {dependencyCount} {dependencyCount, plural, =1{závislost} =2{závislosti} =3{závislosti} =4{závislosti} other{závislostí}}. Chcete je také odebrat?", + "DEPENDENCY_TITLE": "Odebrat závislosti addonu?", + "TITLE": "Odebrat {count, plural, =1{addon} =2{addony} =3{addony} =4{addony} other{addonů}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "The installed addon did not match with any of the configured providers", + "UPDATE_ALL_BUTTON": "Aktualizovat vše", + "UPDATE_ALL_BUTTON_TOOLTIP": "Zaktualizuje všechny addony vybraného clienta", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Aktualizovat všechny clienty", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Aktualizovat Retail/Classic" + }, + "WTF_BACKUP_BUTTON": "Interface Settings Backup" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "News link copied to clipboard", + "NEWS_LINK_COPY_TOOLTIP": "Copy news link", + "PAGE_CONTEXT_FOOTER": "{count} news stories", + "REFRESH_TOOLTIP": "Refresh news feed" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Ad or access key required", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "If you have requested a CurseForge API key you can input it here to connect to their API.", + "API_KEY_TITLE": "CurseForge API Key", + "PROVIDER_NOTE": "API Key Required" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Vyberte poskytovatele addonů, kteří budou použiti pro vyhledávání a instalaci addonů", + "FIELD_LABEL": "Povolení poskytovatelé addonů", + "INPUT_LABEL": "Poskytovatelé addonů" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "A personal access token will increase the amount of GitHub API calls you can make, it is freely available. Learn More", + "PLACEHOLDER": "Personal Access Token", + "TITLE": "GitHub Personal Access Token" + }, + "TITLE": "Addony", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Switching to the Beta channel will allow you to receive experimental builds that contain bug fixes, as well as new and upcoming features. you may only go back to the current stable version by uninstalling your existing app and re-installing from wowup.io.\n\nWhile the Beta channel is functional, use at your own risk.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Switching to the Stable channel for the app will prevent you from receiving more Beta builds, the next update will be the next Stable release.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "Jazyk", + "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", + "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Show a badge on the app icon with the number of addons with available updates.", + "ENABLE_APP_BADGE_LABEL": "Enable app badge notification", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Povolí různé systémové notifikace, např. po automatické aktualizaci addonů.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Povolit systémové notifikace", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Minimalizovat do lišty při uzavírání WowUp okna.", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Minimalizovat do lišty při uzavírání WowUp okna.", + "MINIMIZE_ON_CLOSE_LABEL": "Minimalizovat místo vypnutí", + "PROTOCOL_DESCRIPTION": "WowUp will register a custom URI protocol in your system and handle its incoming inquries", + "PROTOCOL_LABEL": "Allow WowUp to handle wowup:// URI", + "SCALE_DESCRIPTION": "Změní % přiblížení pro celou aplikaci.", + "SCALE_LABEL": "% přiblížení", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Aby se změna jazyku projevila, je třeba restartovat WowUp.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Nastavuji nový výchozí jazyk aplikace", + "SET_LANGUAGE_DESCRIPTION": "Vyberte jazyk", + "SET_LANGUAGE_LABEL": "Nastavit výchozí jazyk", + "START_MINIMIZED_DESCRIPTION": "WowUp se spustí minimalizovaně v systrayi", + "START_MINIMIZED_LABEL": "Spustit WowUp minimalizovaný", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp se spustí automaticky po spuštění systému.", + "START_WITH_SYSTEM_LABEL": "Spustit WowUp po spuštění", + "TELEMETRY_DESCRIPTION": "Pomozte nám vylepšit WowUp odesíláním anonymních informací o instalaci a chybách.", + "TELEMETRY_LABEL": "Telemetrie", + "THEME_DESCRIPTION": "Přizpůsobte si barevné schéma", + "THEME_LABEL": "Barevné schéma", + "TITLE": "Aplikace", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp can set itself as the default handler for CurseForge download links. This may cause issues if you try to use the CursForge app, are you sure you want to continue?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Handle CurseForge Downloads?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Chcete restartovat aplikaci?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "Vypnutím hardwarové akcelerace můžete vyřešit problémy s FPS či případné problémy s vykreslením WowUp. Změna tohoto nastavení vyžaduje restartování aplikace.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Vypnutí hardwarové akcelerace vyžaduje restartování aplikace.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Zaputí hardwarové akcelerace vyžaduje restartování aplikace.", + "USE_HARDWARE_ACCELERATION_LABEL": "Zapnout hardwarovou akceleraci", + "USE_SYMLINK_SUPPORT": "Enable Symlink Support", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Enabling symlink support will allow WowUp to recognize symlinks when performing a re-scan. Warning: If you do not know what a symlink is, you do not need this. When updating symlinks will currently be replaced with an actual folder and the link lost.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Enable symlink support?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Allow WowUp to scan symlink folders in your addon folder. Warning: they will be replaced when updating/installing." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Uložit debugovací data", + "DEBUG_DATA_DESCRIPTION": "Zaloguje debugovací data do logovacího souboru pro případnou diagnostiku problémů s aplikací. Pro zvědavce: Tato data naleznete v nejnovějším logovacím souboru.", + "DEBUG_DATA_LABEL": "Debug data", + "LOG_FILES_BUTTON": "Zobraz logovací soubory", + "LOG_FILES_DESCRIPTION": "Otevře složku s logovacími soubory.", + "LOG_FILES_LABEL": "Logovací soubory", + "TITLE": "Debug aplikace" + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "Addony", + "APPLICATION": "Aplikace", + "CLIENTS": "WoW klienti", + "CURSEFORGE": "CurseForge", + "DEBUG": "Debug aplikace", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Add New", + "AUTO_UPDATE_DESCRIPTION": "Nově instalované addony budou automaticky označeny k automatické aktualizaci pomocí WowUp.", + "AUTO_UPDATE_LABEL": "Automatické aktualizace", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Cancel", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Opravdu chcete vyčistit složku pro instalaci addonů v klientu {clientName}? Toto vymaže pouze informace o addonech, které má uložené WowUp.\n\nAddony nebudou vymazány.", + "TITLE": "Vyčistit složku?" + }, + "CLIENT_TYPE_INPUT_HINT": "Složka, která obsahuje složku \"{clientFolderName}\" pro {clientTypeName} klienta", + "CLIENT_TYPE_PATH_LABEL": "Cesta k {clientTypeName} klientu", + "DEFAULT_ADDON_CHANNEL_LABEL": "Výchozí kanál aktualizací addonů", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Kanál aktualizací addonů", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Edit", + "MOVE_DOWN_BUTTON": "Move Down", + "MOVE_UP_BUTTON": "Move Up", + "NO_CLIENTS_FOUND_TEXT": "No World of Warcraft installations found, please make sure your Battle.net client is up to date or add a client manually", + "OPEN_FOLDER_BUTTON": "Open Folder", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Vybrat", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Remove", + "RESCAN_CLIENTS_BUTTON": "Znovu vyhledat", + "RESCAN_CLIENTS_LABEL": "Pokusí se znovu vyhledat všechny instalace hry World of Warcraft", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Save", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Are you sure you want to apply this backup to your interface settings?\n\nMake sure that the World of Warcraft game is not running before you apply a backup.\n\nThis operation cannot be undone.", + "TITLE": "Apply WTF Backup?" + }, + "BACKUP_APPLY_SUCCESS": "Successfully applied backup: {name}", + "BACKUP_COUNT_TEXT": "Found {count} {count, plural, =1{backup} other{backups}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Applying backup...", + "CREATING_BACKUP": "Creating backup from {count} files...", + "LOADING_BACKUPS": "Loading backups...", + "REMOVING_BACKUP": "Removing backup..." + }, + "CREATE_BACKUP_BUTTON": "Create Backup", + "DELETE_CONFIRMATION": { + "MESSAGE": "Are you sure you want to delete backup {name}?\nThis cannot be undone.", + "TITLE": "Delete WTF Backup?" + }, + "DIALOG_TITLE": "WTF Settings Backup: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Failed to apply backup: {name}", + "FAILED_TO_DELETE": "Failed to delete backup: {name}", + "GENERIC_ERROR": "There was an issue processing this backup", + "INVALID_CONTENTS": "There was an issue processing this backup", + "INVALID_CREATED_AT": "There was an issue processing this backup", + "INVALID_CREATED_BY": "There was an issue processing this backup" + }, + "SHOW_FOLDER_BUTTON": "Show Folder", + "TOOL_TIP": { + "APPLY_BUTTON": "Apply this backup", + "DELETE_BUTTON": "Delete this backup" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/de.json b/WowUp/wowup-electron/src/assets/i18n/de.json new file mode 100644 index 0000000..4c13ab4 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/de.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Aktive Addons: {count}", + "ADDED_BADGE_TOOLTIP": "Wir werden versuchen, dieses Addon zu installieren", + "CONFLICT_BADGE_TOOLTIP": "Addons in Konflikt werden nicht geändert", + "COPY_BUTTON": "Kopieren", + "DIALOG_TITLE": "Import/Export Addons: {clientType}", + "EXPORT_STRING_COPIED": "Export Zeichenfolge in die Zwischenablage kopiert", + "EXPORT_STRING_PASTED": "Inhalt der Zwischenablage eingefügt", + "EXPORT_TAB_LABEL": "Export", + "EXPORT_TEXT_LABEL": "Addon Export Daten", + "GENERIC_IMPORT_ERROR": "Beim Import ist ein Fehler aufgetreten", + "IGNORED_ADDON_COUNT": "Ignorierte Addons: {count}", + "IMPORT_ADDED_COUNT": "{count} hinzugefügt", + "IMPORT_BADGE_ADDED": "Neu", + "IMPORT_BADGE_CONFLICT": "Konflikt", + "IMPORT_BADGE_NO_CHANGE": "Keine Änderung", + "IMPORT_BUTTON": "Import", + "IMPORT_CONFLICT_COUNT": "{count} im Konflikt", + "IMPORT_NO_CHANGE_COUNT": "{count} ungeändert", + "IMPORT_STRING_INVALID": "Import Zeichenfolge war ungültig", + "IMPORT_TAB_LABEL": "Import", + "IMPORT_TEXT_INSTRUCTIONS": "Füge die Wowup Addon-Exportdaten in das Feld unten ein, um zu beginnen", + "IMPORT_TEXT_LABEL": "Importdaten", + "IMPORT_TOTAL_COUNT": "Importiere {count} {count, plural, =1{Addon} other{Addons}}", + "INSTALL_BUTTON": "Installieren", + "INVALID_CLIENT_TYPE": "Import Zeichenfolge stimmt nicht mit dem ausgewählten Client-Typen überein", + "NO_CHANGE_BADGE_TOOLTIP": "Du hast diesen Addon bereits installiert", + "PASTE_BUTTON": "Einfügen", + "PROVIDER_MISMATCH": "Addon Anbieter stimmen nicht überein", + "RESET_BUTTON": "Zurücksetzen", + "VERSION_MISMATCH": "Versionen stimmen nicht überein" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Warum sehe ich diese Werbung?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "Um wago.io / CurseForge als Addon-Anbieter zu nutzen und seine Autoren für ihre harte Arbeit an deinen bevorzugten Addons zu unterstützen, müssen wir diese Werbung zeigen.\n\nWenn du diese Werbung nicht sehen möchtest, kannst du wago.io / CurseForge jederzeit als Addon-Anbieter in den Einstellungen deaktivieren.", + "TITLE": "Warum sehe ich diese Werbung?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Kopieren", + "CUT": "Ausschneiden", + "LABEL": "Bearbeiten", + "PASTE": "Einfügen", + "REDO": "Wiederholen", + "SELECT_ALL": "Alles auswählen", + "UNDO": "Rückgängig" + }, + "QUIT": "Beenden", + "VIEW": { + "FORCE_RELOAD": "Neuladen erzwingen", + "LABEL": "Ansicht", + "RELOAD": "Neu laden", + "TOGGLE_DEV_TOOLS": "Entwicklungs Werkzeug umschalten", + "TOGGLE_FULL_SCREEN": "Vollbild umschalten", + "ZOOM_IN": "Hineinzoomen", + "ZOOM_OUT": "Rauszoomen", + "ZOOM_RESET": "Zoom zurücksetzen" + }, + "WINDOW": { + "CLOSE": "Schließen", + "LABEL": "Fenster" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Automatisch aktualisiert\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "{count} {count, plural, =1{Addon} other{Addons}} automatisch aktualisiert.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Automatische Updates", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Vollbildmodus verlassen", + "FULLSCREEN_SNACKBAR": { + "MAC": "Drücke ^⌘F um den Vollbildmodus zu verlassen", + "WINDOWS": "Drücke F11 um den Vollbildmodus zu verlassen" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nMöchtest Du diese Seite eines Drittanbieters wirklich in deinem Standardbrowser öffnen?", + "TITLE": "Du bist dabei WowUp zu verlassen" + }, + "PROVIDERS": { + "UNKNOWN": "Unbekannt" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Addon-Scan abgeschlossen...", + "ADDON_SCAN_STARTED": "Addon-Scan gestartet...", + "ADDON_SCAN_UPDATE": "{count} Ordner werden gescannt..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Auf Updates prüfen...", + "QUIT_ACTION": "Beenden", + "SHOW_ACTION": "Öffnen" + }, + "THEME": { + "ALLIANCE": "Allianz", + "DEFAULT": "Standard", + "GROUP_DARK": "Dunkel", + "GROUP_LIGHT": "Hell", + "HORDE": "Horde" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Vollbild", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Nach Updates suchen", + "DOWNLOADED_TOOLTIP": "WowUp Update installieren", + "DOWNLOADING_UPDATE": "Update wird heruntergeladen", + "INSTALL_MESSAGE": "Möchtest Du WowUp neu starten, um das Update zu installieren?", + "INSTALL_TITLE": "WowUp-Update bereit", + "NOT_AVAILABLE": "Die aktuellste Version von WowUp ist bereits installiert", + "PORTABLE_DOWNLOAD_MESSAGE": "Möchtest Du die letzte portable Version manuell herunterladen?\n\nDu musst die Anwendung manuell schließen und die neue Version über die Alte kopieren.", + "PORTABLE_DOWNLOAD_TITLE": "Manuelles Herunterladen erforderlich", + "SNACKBAR_ACTION": "Updaten und neustarten", + "SNACKBAR_TEXT": "Eine neue Version von WowUp ist verfügbar", + "TOOLTIP": "WowUp Update verfügbar", + "UPDATE_AVAILABLE": "Download wird gestartet", + "UPDATE_ERROR": "WowUp konnte nicht nach Updates suchen" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Erfolge", + "ACTION_BARS": "Aktionsleisten", + "ALL_ADDONS": "Alle Addons", + "AUCTION_ECONOMY": "Auktion & Wirtschaft", + "BAGS_INVENTORY": "Taschen & Inventar", + "BOSS_ENCOUNTERS": "Boss Begegnungen", + "BUFFS_DEBUFFS": "Stärkungs- & Schwächungszauber", + "BUNDLES": "Sammlungen", + "CHAT_COMMUNICATION": "Chat & Kommunikation", + "CLASS": "Klassen", + "COMBAT": "Kampf", + "COMPANIONS": "Begleiter", + "DATA_EXPORT": "Datenexport", + "DEVELOPMENT_TOOLS": "Entwicklungswerkzeuge", + "GUILD": "Gilde", + "LIBRARIES": "Bibliotheken", + "MAIL": "Mail", + "MAP_MINIMAP": "Karte & Minikarte", + "MISCELLANEOUS": "Sonstiges", + "MISSIONS": "Missionen", + "PLUGINS": "Plugins", + "PROFESSIONS": "Berufe", + "PVP": "PvP", + "QUESTS_LEVELING": "Quests & Leveling", + "ROLEPLAY": "Rollenspiel", + "TOOLTIPS": "Tooltips", + "UNIT_FRAMES": "Einheitenfenster" + }, + "ADDON_STATE": { + "IGNORED": "Ignoriert", + "INSTALL": "Installieren", + "PENDING": "Ausstehend", + "UNAVAILABLE": "Nicht verfügbar", + "UNAVAILABLE_TOOLTIP": "Dieser Autor oder Anbieter hat dieses Addon nicht verfügbar gemacht", + "UNINSTALL": "Deinstallieren", + "UNKNOWN": "Unbekannt", + "UPDATE": "Aktualisieren", + "UPTODATE": "Aktuell", + "WARNING": "Warnung" + }, + "ADDON_STATUS": { + "BACKINGUP": "Backup wird erstellt", + "COMPLETE": "Installiert", + "DOWNLOADING": "Herunterladen...", + "ERROR": "Fehler", + "INSTALLING": "Installieren...", + "PENDING": "Ausstehend", + "RETRY": "Retrying...", + "UNINSTALLING": "Deinstallieren...", + "UPDATING": "Aktualisieren..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "Wir haben ein Problem mit diesem Addon festgestellt. Wir können dieses Addon nicht mehr aktualisieren oder Details bereitstellen.", + "GENERIC_TOOLTIP": "Wir haben ein Problem mit diesem Addon festgestellt", + "MISSING_ON_PROVIDER_DESCRIPTION": "Dieses Addon scheint entweder vom Autor oder vom Addon-Anbieter entfernt worden zu sein.
Wir können dieses Addon nicht mehr aktualisieren oder Details bereitstellen.", + "MISSING_ON_PROVIDER_TOOLTIP": "Dieses Addon scheint vom Anbieter entfernt worden zu sein", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} hat dieses Addon ordnungsgemäß zurückgegeben, es gab jedoch keine Dateien, die mit dieser Spielversion übereinstimmen.
Sobald es ein Update gibt, das zu dieser Spielversion passt, sollte diese Warnung verschwinden.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} hatte keine passenden Spielversionsdateien", + "TOC_NAME_MISMATCH_DESCRIPTION": "Der Ordnername dieses Addons stimmte nicht mit der erwarteten TOC-Datei überein, es können Probleme mit diesem Addon im Spiel auftreten.", + "TOC_NAME_MISMATCH_TOOLTIP": "Der Ordner dieses Addons stimmt nicht mit der TOC-Datei überein" + }, + "CLIENT_TYPES": { + "BETA": "Beta von Dragonflight", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Öffentlicher Testrealm (Wrath Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "Vor {count} {count, plural, one{Tag} other{Tagen}}", + "HOURS_AGO": "Vor {count} {count, plural, one{Stunde} other{Stunden}}", + "JUST_NOW": "Gerade eben", + "MONTHS_AGO": "Vor {count} {count, plural, one{Monat} other{Monaten}}", + "YEARS_AGO": "Vor {count} {count, plural, one{Jahr} other{Jahren}}", + "YESTERDAY": "Gestern" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} {dependencyCount, plural, one{Abhängigkeit} other{Abhängigkeiten}} erforderlich" + }, + "DOWNLOAD_COUNT": { + "e+0": "{count}", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count} Tausend", + "e+4": "{count} Tausend", + "e+5": "{count} Tausend", + "e+6": "{count} Millionen", + "e+7": "{count} Millionen", + "e+8": "{count} Millionen", + "e+9": "{count} Milliarden" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alpha", + "BETA": "Beta", + "STABLE": "Stabil" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Sofortige Updates für deinen Account konnten nicht eingeschaltet werden. Bitte versuche es später erneut oder schreibe uns auf Discord.", + "ADDON_INSTALL_ERROR": "Addon '{addonName}' konnte nicht installiert werden. Versuche es später erneut.", + "ADDON_SCAN_ERROR": "Beim Abgleichen deiner Addon-Ordner mit {providerName} ist ein Fehler aufgetreten. Versuche es später erneut.", + "ADDON_SYNC_ERROR": "Beim Überprüfen auf Aktualisierungen von '{providerName}' ist ein Fehler aufgetreten.", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: Beim Überprüfen auf Aktualisierungen von {addonName} von {providerName} ist ein Fehler aufgetreten. Versuche es später erneut.", + "CHANGE_PROVIDER_ERROR": "Anbieterwechsel von {addonName} nach {providerName} fehlgeschlagen", + "GITHUB_LIMIT_ERROR": "Du hast dein GitHub-API-Limit von {max} Anforderungen erreicht.\nBitte warten bis {reset} und versuche es erneut.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Aktualisierungen für {addonName} konnten nicht überprüft werden.\nBitte überprüfen, ob das Repository korrekt ist, oder stelle das Addon auf 'Ignoriert' ein." + }, + "PROGRESS_SPINNER": { + "LOADING": "Laden..." + }, + "PROVIDER_ERROR": "Fehler beim Verbinden mit {providerName}", + "SEARCH": { + "NO_ADDONS": "Keine Addons gefunden" + }, + "WOW_EXE_SELECTION_NAME": "WoW App (ausführbar)" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "Addon ID:", + "BY_AUTHOR": "Von {authorName}", + "CHANGELOG_TAB": "Änderungsverlauf", + "COPY_ADDON_ID_SNACKBAR": "Addon ID in die Zwischenablage kopiert", + "COPY_ADDON_ID_TOOLTIP": "Addon ID in die Zwischenablage kopieren", + "DEPENDENCY_TEXT": "Dieses Addon besitzt {dependencyCount} {dependencyCount, plural, one{Abhängigkeit} other{Abhängigkeiten}}", + "DESCRIPTION_NOT_FOUND": "Keine Beschreibung gefunden", + "DESCRIPTION_TAB": "Beschreibung", + "FUNDING_LINK_TITLE": "Unterstütze diesen Autor", + "IMAGES_TAB": "Vorschau", + "MISSING_DEPENDENCIES": "Fehlende Abhängigkeiten", + "NO_CHANGELOG_TEXT": "Kein Änderungsverlauf verfügbar", + "VIEW_IN_BROWSER_BUTTON": "Im Browser anzeigen", + "VIEW_ON_PROVIDER_PREFIX": "Anzeigen auf" + }, + "ALERT": { + "ERROR_TITLE": "Fehler", + "POSITIVE_BUTTON": "Okay" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "Nein", + "POSITIVE_BUTTON": "Ja" + }, + "CURSE_MIGRATION": { + "MESSAGE": "Die CurseForge-API wurde heruntergefahren, sodass WowUp keine Addon-Informationen mehr von dieser abrufen kann.
Leider bedeutet dies, dass der CurseForge-Anbieter entfernt wurde und deine Addons von CurseForge nicht mehr aktualisiert werden.
Es wird empfohlen, einen erneuten Scan durchzuführen, um zu sehen, welche Addons bei anderen Anbietern zu finden sind.
Der Re-Scan-Vorgang kann eine Weile dauern.
Lese hier mehr.", + "NEGATIVE_BUTTON": "Manueller Re-Scan", + "POSITIVE_BUTTON": "Automatischer Re-Scan", + "RE_SCAN_ERROR": "Der automatische Re-Scan ist fehlgeschlagen, bitte versuche es erneut manuell.", + "RE_SCAN_SUCCESS": "Der automatische Re-Scan ist beendet, du kannst loslegen.", + "TITLE": "CurseForge-Migration" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Addon installiert!", + "ADDON_INSTALLING": "Addon wird installiert", + "CANCEL_BUTTON": "Schließen", + "ERRORS": { + "ADDON_NOT_FOUND": "Für das Protokoll '{protocol}' wurde kein Addon gefunden", + "GENERIC": "Fehler beim Abrufen der Daten für das Protokoll: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "Kein WoW-Client für Protokoll '{protocol}' installiert" + }, + "INSTALL_BUTTON": "Installieren", + "TITLE": "Das Addon von {providerName} installieren" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "Addon-URL", + "ADDON_URL_INPUT_PLACEHOLDER": "z.B. GitHub- oder WoWInterface-URL", + "CLOSE_BUTTON": "Schließen", + "DESCRIPTION": "Möchtest du ein Addon über eine URL installieren, füge diese unten ein.", + "DOWNLOAD_COUNT": "{textCount} {count, plural, one{Download} other{Downloads}} auf {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "Es wurde kein Asset zum Herunterladen von {message} gefunden.\n\nEine gültige Zip-Datei muss in dem Release enthalten sein, damit WowUp diese herunterladen kann.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "Es wurde kein Asset zum Herunterladen von {message} gefunden.\n\nEine gültige Zip-Datei mit der Endung '-bc' muss in dem Release enthalten sein, damit WowUp diese herunterladen kann.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "Es wurde kein Asset zum Herunterladen von {message} gefunden.\n\nEine gültige Zip-Datei mit der Endung '-classic' muss in dem Release enthalten sein, damit WowUp diese herunterladen kann.", + "FAILED_TO_CONNECT": "Keine Verbindung mit der API möglich, warte ein wenig und versuche es erneut.", + "INSTALL_FAILED": "Beim Versuch das Addon zu installieren ist etwas schief gelaufen, bitte versuche es erneut.\n\nWenn diese Nachricht weiterhin erscheint, kannst Du auf unserem Discord im Kanal #help-me nach Hilfe fragen.", + "INVALID_URL": "Die eingetragene URL ist ungültig. Beispiele für gültige URLs sind:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "Es wurde kein Addon gefunden. Stelle sicher, dass die URL zur richtigen Website führt.\n\nSolltest Du versuchen ein Addon von GitHub zu installieren, stelle sicher, dass das Repository Release-Tags mit einem ZIP-Archiv, welches das Addon enthält, besitzt.", + "NO_RELEASE_FOUND": "Es wurde kein Release für {message} gefunden.\n\nEin gültiges Release mit Zip-Datei-Assets ist erforderlich, damit WowUp sie herunterladen kann.", + "NO_SEARCH_RESULTS": "Es wurden keine Suchergebnisse gefunden.", + "TITLE": "Installation fehlgeschlagen", + "WRATH_ASSET_NOT_FOUND": "Es wurde keine Datei gefunden, die von {message} heruntergeladen werden kann.\n\nFür den Download durch WowUp ist eine gültige ZIP-Datei erforderlich, die mit '-wrath' endet." + }, + "IMPORT_ASSET_WARNING": "Wir konnten nicht überprüfen, ob die neueste Version dieses Add-ons mit deinem ausgewählten Client kompatibel ist.\n\nAber wir haben eine ZIP-Datei \"{zipName}\" gefunden.\n\nDie Installation erfolgt auf eigene Gefahr.", + "IMPORT_BUTTON": "Importieren", + "IMPORT_WARNING_TITLE": "Addon Import Warnung", + "INSTALL_BUTTON": "Installieren", + "INSTALL_SUCCESS_LABEL": "Installiert!", + "SUPPORTED_SOURCES": "Unterstützt WowInterface und GitHub", + "TITLE": "Installation eines Addons über eine externe URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Neu in Version {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "Werbepartner", + "DESCRIPTION_BOTTOM": ". Klicke auf die Schaltfläche 'Verwalten', um deine Einwilligungen zu steuern oder Widerspruch gegen die Verarbeitung deiner Daten einzulegen. Du kannst deine Einstellungen jederzeit über das Einstellungsmenü ändern.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Verwalten", + "TITLE": "CurseForge" + }, + "MESSAGE": "Bevor wir beginnen, müssen wir einige Berechtigungen für die App einrichten.", + "POSITIVE_BUTTON": "Bestätigen", + "TELEMETRY": { + "DESCRIPTION": "Möchest Du helfen, WowUp zu verbessern, indem anonyme Installationsdaten und/oder Fehler gesendet werden?", + "TOGGLE_LABEL": "Telemetrie zulassen" + }, + "TITLE": "Einrichtung der WowUp-Berechtigungen", + "WAGO": { + "DESCRIPTION": "Wenn der Addon-Anbieter Wago.io aktiviert ist, wird eine Werbung angezeigt, die für die Nutzung deren Dienste erforderlich ist.\nDie Anzeigen kommen direkt den Autoren deiner Lieblings-Addons zugute!", + "TOGGLE_LABEL": "Aktiviere Wago.io Addon-Anbieter" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "Dies scheint keine gültige World of Warcraft-Anwendung zu sein:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Möchest Du helfen, WowUp zu verbessern, indem anonyme Installationsdaten und/oder Fehler gesendet werden?", + "NEGATIVE_BUTTON": "Nein, Danke", + "POSITIVE_BUTTON": "Ja", + "TITLE": "WowUp Telemetrie" + }, + "TRUST_DOMAIN_CHECKBOX": "Vertraue dieser Domain und frage mich in Zukunft nicht mehr" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Zuschreibungen", + "CHANGE_LOG_SECTION_LABEL": "Änderungsverlauf", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Schau dir die Website an!" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Jetzt Einloggen!", + "LOGOUT_BUTTON": "Ausloggen", + "LOGOUT_CONFIRMATION_MESSAGE": "Bist du dir sicher, dass du dich ausloggen möchten? Alle deine lokalen Kontodaten werden entfernt, bis du dich erneut einloggst.", + "LOGOUT_CONFIRMATION_TITLE": "Ausloggen?", + "MANAGE_ACCOUNT_BUTTON": "Konto verwalten", + "TITLE": "Konto" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Kategorien", + "ADDON_CATEGORIES_MENU_TITLE": "Addon Kategorien", + "ADDON_CATEGORIES_SELECTED_TITLE": "Kategorie: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Durchsuche verschiedene Kategorien", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Mit URL installieren", + "INSTALL_FROM_URL_TOOLTIP": "Installiere ein Addon von einer URL", + "REFRESH_BUTTON": "Aktualisieren", + "REFRESH_TOOLTIP": "Addon-Ergebnisse aktualisieren", + "RESET_CATEGORY_TOOLTIP": "Kategorie zurücksetzen", + "SEARCH_LABEL": "Suchen", + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "AUTHOR_COLUMN_HEADER": "Autor", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Anzahl Downloads", + "PROVIDER_COLUMN_HEADER": "Anbieter", + "RELEASED_AT_COLUMN_HEADER": "Veröffentlicht", + "STATUS_COLUMN_HEADER": "Status" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "Über WowUp", + "ACCOUNT_TAB_TITLE": "Konto", + "COLLAPSE_BUTTON_TITLE": "Reduzieren", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Erweitern", + "GET_ADDONS_TAB_TITLE": "Addons installieren", + "GUIDE_TAB_TITLE": "Guide", + "MIGRATING_ADDONS": "Addons werden migriert...", + "MY_ADDONS_TAB_TITLE": "Meine Addons", + "NEWS_TAB_TITLE": "News", + "OPTIONS_TAB_TITLE": "Einstellungen" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count} {count, plural, =1{Addon} other{Addons}} ausgewählt", + "ALPHA_ADDON_CHANNEL": "Alpha", + "AUTO_UPDATE_ADDON_BUTTON": "Automatisch aktualisieren", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Benachrichtigungen aktiviert", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "Kanal", + "IGNORE_ADDON_BUTTON": "Ignorieren", + "PROVIDER_SUBMENU_TITLE": "Anbieter", + "REINSTALL_ADDON_BUTTON": "Neu installieren", + "REMOVE_ADDON_BUTTON": "Entfernen", + "SHOW_FOLDER": "Dateiordner anzeigen", + "STABLE_ADDON_CHANNEL": "Stabil" + }, + "ADDON_IS_CODE_REPOSITORY": "Addon scheint ein Code-Repository zu sein", + "ADDON_REMOVED_SNACKBAR": "{addonName} erfolgreich entfernt", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Möchtest Du den Anbieter für {addonName} auf {providerName} festlegen? Dies wird das aktuelle Addon deinstallieren und durch eine Version vom neuen Anbieter ersetzen.", + "TITLE": "Addon-Anbieter wechseln?" + }, + "CHECK_UPDATES_BUTTON": "Auf Updates prüfen", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Auf neue Addon-Updates prüfen", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{update} other{updates}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Spalten anzeigen" + }, + "ERROR_SNACKBAR": "Ein Fehler ist aufgetreten", + "FILTER_LABEL": "Filter", + "FUNDING_TOOLTIP": { + "CUSTOM": "Unterstütze den Autor", + "GENERIC": "Unterstütze den Autor auf {platform}", + "GITHUB": "Unterstütze den Autor auf GitHub", + "PATREON": "Unterstütze den Autor auf Patreon", + "PAYPAL": "Unterstütze den Autor auf PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Addons importieren/exportieren", + "MULTIPLE_PROVIDERS_TOOLTIP": "Dieses Addon hat mehrere Anbieter", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{Addon} other{Addons}}", + "JOIN_DISCORD": "Rede mit uns auf Discord", + "PATREON_SUPPORT": "Unterstütze WowUp auf Patreon", + "SEARCH_RESULTS": "{count} {count, plural, =1{Treffer} other{Treffer}}", + "VIEW_GITHUB": "Schau Dir den Code auf GitHub an", + "VIEW_GUIDE": "Schau Dir unseren Guide an, um zu sehen, was WowUp kann" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Erforderliche Abhängigkeiten fehlen", + "RESCAN_FOLDERS_BUTTON": "Ordner erneut scannen", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Scanne Deinen Client-Ordner nach installierten Addons", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "Ein neuer Scan wird nach bereits installierten Addons suchen. Dies kann bekannte Addon-Informationen zurücksetzen. Dieser Vorgang braucht einen Moment.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Neuen Scan starten?", + "SPINNER": { + "GATHERING_ADDONS": "Addons werden gesammelt...", + "UPDATING": "{updateCount}/{addonCount} aktualisiert", + "UPDATING_WITH_ADDON_NAME": "{updateCount}/{addonCount} aktualisiert\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "ADDON_INSTALL_BUTTON": "Installieren", + "ADDON_UPDATE_BUTTON": "Aktualisieren", + "AUTHOR_COLUMN_HEADER": "Autor", + "AUTO_UPDATE_ICON_TOOLTIP": "Automatisches Update aktiviert", + "GAME_VERSION_COLUMN_HEADER": "Spielversion", + "LATEST_VERSION_COLUMN_HEADER": "Aktuelle Version", + "PROVIDER_COLUMN_HEADER": "Anbieter", + "PROVIDER_RELEASE_CHANNEL": "Anbieter-Kanal", + "RELEASED_AT_COLUMN_HEADER": "Veröffentlicht", + "STATUS_COLUMN_HEADER": "Status", + "UPDATED_AT_COLUMN_HEADER": "Aktualisiert" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Dadurch werden alle zugehörigen Ordner aus dem World of Warcraft Interface-Ordner entfernt. Die Charakter-Einstellungen des Addons werden nicht entfernt.", + "CONFIRMATION_LESS_THAN_THREE": "Bist Du sicher, dass Du die folgenden {count} Addons entfernen möchtest?", + "CONFIRMATION_MORE_THAN_THREE": "Bist Du sicher, dass Du die ausgewählten {count} Addons entfernen möchtest?", + "CONFIRMATION_ONE": "Bist Du sicher, dass Du {addonName} entfernen möchtest?", + "DEPENDENCY_MESSAGE": "{addonName} hat {dependencyCount} {dependencyCount, plural, one{Abhängigkeit} other{Abhängigkeiten}}. Möchtest Du diese ebenfalls entfernen?", + "DEPENDENCY_TITLE": "Addon-Abhängigkeiten entfernen?", + "TITLE": "Deinstalliere {count, plural, =1{Addon} other{Addons}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "Das installierte Addon stimmte mit keinem der konfigurierten Anbieter überein", + "UPDATE_ALL_BUTTON": "Alle aktualisieren", + "UPDATE_ALL_BUTTON_TOOLTIP": "Alle Addons für diesen Client aktualisieren", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Alle Clients aktualisieren", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Retail/Classic aktualisieren" + }, + "WTF_BACKUP_BUTTON": "Backup der Interface Einstellungen" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "Nachrichtenlink in die Zwischenablage kopiert", + "NEWS_LINK_COPY_TOOLTIP": "Nachrichtenlink kopieren", + "PAGE_CONTEXT_FOOTER": "{count} News", + "REFRESH_TOOLTIP": "Newsfeed aktualisieren" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Werbung oder API-Schlüssel erforderlich", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "Wenn du einen CurseForge-API-Schlüssel angefordert hast, kannst du ihn hier eingeben, um dich mit deren API zu verbinden.", + "API_KEY_TITLE": "CurseForge API-Schlüssel", + "PROVIDER_NOTE": "API-Schlüssel erforderlich" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Wähle aus, welche Addon-Anbieter zur Suche und Installation neuer Addons genutzt werden können", + "FIELD_LABEL": "Aktivierte Addon-Anbieter", + "INPUT_LABEL": "Anbieter" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "Ein persönliches Zugriffstoken erhöht die Anzahl der GitHub-API-Aufrufe, die durchgeführt werden können, dieser ist frei verfügbar. Mehr erfahren", + "PLACEHOLDER": "Persönliches Zugriffstoken", + "TITLE": "Persönliches GitHub-Zugriffstoken" + }, + "TITLE": "Addons", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Wenn du zum Beta-Kanal wechseln, erhälst du experimentelle Builds, die Fehlerbehebungen sowie neue und kommende Funktionen enthalten. Du kannst nur zur aktuellen stabilen Version zurückkehren, indem du deine vorhandene Anwendung deinstallierst und von wowup.io neu installierst.\n\nSolange der Beta-Kanal funktionsfähig ist, verwende ihn auf eigene Gefahr.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Wenn du zum Stabilen-Kanal der Anwendung wechselst, erhältst du keine weiteren Beta-Builds. Das nächste Update wird die nächste Stabile-Version sein.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Einstellen des App-Release-Kanals", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Ja ich verstehe", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Wechseln zwischen der Beta- und Stabilen-Versionen der Anwendung", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Kanal", + "APP_RELEASE_CHANNEL_LABEL": "App-Release-Kanal", + "CURRENT_LANGUAGE_LABEL": "Aktuelle Sprache", + "CURSE_PROTOCOL_DESCRIPTION": "Beim Herunterladen von Addons von der CurseForge-Website übernimmt WowUp die Installation", + "CURSE_PROTOCOL_LABEL": "CurseForge-Download-Links verarbeiten", + "ENABLE_APP_BADGE_DESCRIPTION": "Zeigt auf dem App-Symbol einen Zähler, mit der Anzahl an verfügbaren Updates an.", + "ENABLE_APP_BADGE_LABEL": "App-Badge-Benachrichtigung aktivieren", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Aktivieren/Deaktivieren verschiedener Systembenachrichtigungen", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Systembenachrichtigungen aktivieren", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "Beim Öffnen einer AddOn-Detailansicht wird automatisch der zuletzt geöffnete Tab ausgewählt", + "KEEP_LAST_OPENED_TAB_LABEL": "Zuletzt geöffneten Tab beibehalten", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Beim Schließen WowUp in die Menüleiste minimieren", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Beim Schließen des WowUp-Fensters in den Benachrichtigungsbereich der Taskleiste minimieren.", + "MINIMIZE_ON_CLOSE_LABEL": "Minimieren beim Schließen", + "PROTOCOL_DESCRIPTION": "WowUp registriert ein benutzerdefiniertes URI-Protokoll in deinem System und verarbeitet die eingehenden Anfragen", + "PROTOCOL_LABEL": "Erlaube WowUp die 'wowup:// URI' zu verarbeiten", + "SCALE_DESCRIPTION": "Den Zoomfaktor für die ganze Anwendung verändern.", + "SCALE_LABEL": "Skalierung", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Zum Ändern der Standardsprache muss die Anwendung neu gestartet werden.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Festlegen einer neuen Standardsprache", + "SET_LANGUAGE_DESCRIPTION": "Wähle eine Sprache aus, in die Du wechseln möchtest", + "SET_LANGUAGE_LABEL": "Sprache einstellen", + "START_MINIMIZED_DESCRIPTION": "...und wird nicht auf dem Bildschirm angezeigt", + "START_MINIMIZED_LABEL": "Starte WowUp minimiert", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp wird direkt gestartet, nachdem dein Betriebssystem geladen wurde...", + "START_WITH_SYSTEM_LABEL": "Starte WowUp mit dem System", + "TELEMETRY_DESCRIPTION": "Hilf mit, WowUp zu verbessern, indem Du anonyme Installationsdaten und/oder Fehler sendest.", + "TELEMETRY_LABEL": "Telemetrie", + "THEME_DESCRIPTION": "Ändere das Farbschema nach deinen Wünschen", + "THEME_LABEL": "Farbschema", + "TITLE": "Anwendung", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp kann sich selbst als Standardhandler für CurseForge-Download-Links festlegen. Dies kann zu Problemen führen, wenn Du versuchst, die CurseForge-App zu verwenden. Bist Du sicher, dass Du fortfahren möchtest?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Mit CurseForge-Downloads umgehen?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Möchtest Du die Anwendung neu starten?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "Das Deaktivieren der Hardwarebeschleunigung kann FPS-Probleme und Probleme bei der Darstellung beheben. Eine Änderung dieser Einstellung benötigt einen Neustart der Anwendung.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Beim Deaktivieren der Hardwarebeschleunigung benötigt die Anwendung einen Neustart.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Beim Aktivieren der Hardwarebeschleunigung benötigt die Anwendung einen Neustart.", + "USE_HARDWARE_ACCELERATION_LABEL": "Hardwarebeschleunigung aktivieren", + "USE_SYMLINK_SUPPORT": "Symlink-Unterstützung aktivieren", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Durch das Aktivieren der Symlink-Unterstützung kann WowUp Symlinks erkennen, wenn ein erneuter Scan durchgeführt wird. Warnung: Wenn Du nicht weißt, was ein Symlink ist, brauchst Du dies nicht. Beim Aktualisieren werden Symlinks derzeit durch einen tatsächlichen Ordner ersetzt und der Link geht verloren.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Symlink-Unterstützung aktivieren?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Erlaubt WowUp Symlink-Ordner in deinem Addon-Ordner zu scannen. Warnung: Diese werden beim Aktualisieren/Installieren ersetzt." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "Sehe ein und verwalte, wie CurseForge-Werbetreibende deine Daten für Werbepersonalisierung nutzen können.", + "ADS_OPTION_LABEL": "Werbepersonalisierung und Daten", + "ADS_OPTION_MANAGE": "Verwalten", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Konfigurationsdateien anzeigen", + "CONFIG_FILES_DESCRIPTION": "Öffne den Ordner, in dem beispielsweise deine addons.json und preferences.json gespeichert sind.", + "CONFIG_FILES_LABEL": "Konfigurationsdateien", + "DEBUG_DATA_BUTTON": "Debug-Daten speichern", + "DEBUG_DATA_DESCRIPTION": "Protokolliere Debug-Daten, um mögliche Probleme zu diagnostizieren. Dies findest Du in Deiner aktuellen Protokolldatei (für Neugierige).", + "DEBUG_DATA_LABEL": "Debug-Daten", + "LOG_FILES_BUTTON": "Log-Dateien anzeigen", + "LOG_FILES_DESCRIPTION": "Den Ordner öffnen, der Deine letzten Log-Dateien enthält.", + "LOG_FILES_LABEL": "Log-Dateien", + "TITLE": "Debuggen" + }, + "TABS": { + "ABOUT": "Über", + "ADDONS": "Addons", + "APPLICATION": "Anwendung", + "CLIENTS": "Clients", + "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Neuen hinzufügen", + "AUTO_UPDATE_DESCRIPTION": "Neu installierte Addons werden standardmäßig auf 'Automatisches Update' eingestellt", + "AUTO_UPDATE_LABEL": "Automatisch aktualisieren", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Abbrechen", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Bist Du sicher, dass Du den Installationspfad für {clientName} löschen möchtest? Dies wird alle gespeicherten Addon-Informationen für diesen Client entfernen.\n\nDeine Addon-Ordner werden nicht entfernt.", + "TITLE": "Installationspfad löschen?" + }, + "CLIENT_TYPE_INPUT_HINT": "Der Ordner, der den {clientTypeName} Client-Ordner \"{clientFolderName}\" enthält", + "CLIENT_TYPE_PATH_LABEL": "{clientTypeName} Pfad", + "DEFAULT_ADDON_CHANNEL_LABEL": "Standard Addon-Kanal", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Addon-Kanal", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Bearbeiten", + "MOVE_DOWN_BUTTON": "Nach unten bewegen", + "MOVE_UP_BUTTON": "Nach oben bewegen", + "NO_CLIENTS_FOUND_TEXT": "Keine World of Warcraft Installation gefunden. Stelle sicher, dass dein Battle.net-Client auf dem neusten Stand ist oder fügen einen Client manuell hinzu", + "OPEN_FOLDER_BUTTON": "Ordner öffnen", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Auswählen", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Entfernen", + "RESCAN_CLIENTS_BUTTON": "Neu scannen", + "RESCAN_CLIENTS_LABEL": "Installierte World of Warcraft-Produkte erneut durchsuchen", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Speichern", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Ordner: ", + "PAGE_EXPLANATION": "Der WTF Explorer ermöglicht dir, alle Daten zu inspizieren, die deine Add-Ons gespeichert haben.\nDateien in Grau sind in der Regel Dinge, die du ignorieren kannst, wie beispielsweise Sicherungskopien.\nDateien in Rot sollten zu Add-Ons gehören, die du nicht mehr installiert hast.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Möchtest du diese Sicherung wirklich auf deine Interface Einstellungen anwenden?\n\nStelle sicher, dass World of Warcraft nicht ausgeführt wird, bevor du ein Backup anwendest.\n\nDieser Vorgang kann nicht rückgängig gemacht werden.", + "TITLE": "WTF-Backup anwenden?" + }, + "BACKUP_APPLY_SUCCESS": "Sicherung erfolgreich angewendet: {name}", + "BACKUP_COUNT_TEXT": "{count} {count, plural, =1{Backup} other{Backups}} gefunden", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Backup wir angewendet...", + "CREATING_BACKUP": "Erstelle Backup von {count} Dateien...", + "LOADING_BACKUPS": "Backups werden geladen...", + "REMOVING_BACKUP": "Backup wird gelöscht..." + }, + "CREATE_BACKUP_BUTTON": "Backup erstellen", + "DELETE_CONFIRMATION": { + "MESSAGE": "Bist du sicher, dass du das Backup {name} löschen möchtest?\nDas kann nicht rückgängig gemacht werden.", + "TITLE": "WTF-Backup löschen?" + }, + "DIALOG_TITLE": "WTF Einstellungs Backup: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Sicherung konnte nicht angewendet werden: {name}", + "FAILED_TO_DELETE": "Sicherung konnte nicht gelöscht werden: {name}", + "GENERIC_ERROR": "Bei der Verarbeitung dieses Backups ist ein Problem aufgetreten", + "INVALID_CONTENTS": "Bei der Verarbeitung dieses Backups ist ein Problem aufgetreten", + "INVALID_CREATED_AT": "Bei der Verarbeitung dieses Backups ist ein Problem aufgetreten", + "INVALID_CREATED_BY": "Bei der Verarbeitung dieses Backups ist ein Problem aufgetreten" + }, + "SHOW_FOLDER_BUTTON": "Ordner anzeigen", + "TOOL_TIP": { + "APPLY_BUTTON": "Dieses Backup anwenden", + "DELETE_BUTTON": "Dieses Backup löschen" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/en.json b/WowUp/wowup-electron/src/assets/i18n/en.json new file mode 100644 index 0000000..2e4b63b --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/en.json @@ -0,0 +1,663 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Active addons: {count}", + "ADDED_BADGE_TOOLTIP": "We will attempt to install this addon", + "CONFLICT_BADGE_TOOLTIP": "Addons in conflict will not be modified", + "COPY_BUTTON": "Copy", + "DIALOG_TITLE": "Import/Export Addons: {clientType}", + "EXPORT_STRING_COPIED": "Export string copied to clipboard", + "EXPORT_STRING_PASTED": "Inserted clipboard contents", + "EXPORT_TAB_LABEL": "Export", + "EXPORT_TEXT_LABEL": "Addon Export Data", + "GENERIC_IMPORT_ERROR": "An error occurred during import", + "IGNORED_ADDON_COUNT": "Ignored addons: {count}", + "IMPORT_ADDED_COUNT": "{count} added", + "IMPORT_BADGE_ADDED": "New", + "IMPORT_BADGE_CONFLICT": "Conflict", + "IMPORT_BADGE_NO_CHANGE": "No Change", + "IMPORT_BUTTON": "Import", + "IMPORT_CONFLICT_COUNT": "{count} in conflict", + "IMPORT_NO_CHANGE_COUNT": "{count} unchanged", + "IMPORT_STRING_INVALID": "Import string was invalid", + "IMPORT_TAB_LABEL": "Import", + "IMPORT_TEXT_INSTRUCTIONS": "Paste WowUp addon export data into the field below to get started", + "IMPORT_TEXT_LABEL": "Import data", + "IMPORT_TOTAL_COUNT": "Importing {count} {count, plural, =1{addon} other{addons}}", + "INSTALL_BUTTON": "Install", + "INVALID_CLIENT_TYPE": "Import string did not match your selected client type", + "NO_CHANGE_BADGE_TOOLTIP": "You already have this addon installed", + "PASTE_BUTTON": "Paste", + "PROVIDER_MISMATCH": "This addon is already installed, but the provider does not match", + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "This addon is already installed, but the versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and help support authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable Wago / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Copy", + "CUT": "Cut", + "LABEL": "Edit", + "PASTE": "Paste", + "REDO": "Redo", + "SELECT_ALL": "SelectAll", + "UNDO": "Undo" + }, + "QUIT": "Quit", + "VIEW": { + "FORCE_RELOAD": "Force Reload", + "LABEL": "View", + "RELOAD": "Reload", + "TOGGLE_DEV_TOOLS": "Toggle Dev Tools", + "TOGGLE_FULL_SCREEN": "Toggle Full Screen", + "ZOOM_IN": "Zoom In", + "ZOOM_OUT": "Zoom Out", + "ZOOM_RESET": "Reset Zoom" + }, + "WINDOW": { + "CLOSE": "Close", + "LABEL": "Window" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Automatically updated\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "Automatically updated {count} {count, plural, =1{addon} other{addons}}.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Auto Updates", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Leave full screen mode", + "FULLSCREEN_SNACKBAR": { + "MAC": "Press ^⌘F to exit full screen", + "WINDOWS": "Press F11 to exit full screen" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nAre you sure you want to open this 3rd party page in your default browser?", + "TITLE": "You are about to leave WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Unknown" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Addon scan completed...", + "ADDON_SCAN_STARTED": "Addon scan started...", + "ADDON_SCAN_UPDATE": "Scanning {count} folders..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Check for Updates...", + "QUIT_ACTION": "Quit", + "SHOW_ACTION": "Show" + }, + "THEME": { + "ALLIANCE": "Alliance", + "DEFAULT": "WowUp", + "GROUP_DARK": "Dark", + "GROUP_LIGHT": "Light", + "HORDE": "Horde" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Full Screen", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Checking for update", + "DOWNLOADED_TOOLTIP": "Install WowUp update", + "DOWNLOADING_UPDATE": "Downloading update", + "INSTALL_MESSAGE": "Do you want to restart WowUp and install the update?", + "INSTALL_TITLE": "WowUp Update Ready", + "NOT_AVAILABLE": "Latest version of WowUp is already installed", + "PORTABLE_DOWNLOAD_MESSAGE": "Do you want to manually download the latest portable version?\n\nYou will need to close the app manually and copy over the new version.", + "PORTABLE_DOWNLOAD_TITLE": "Manual Download Required", + "SNACKBAR_ACTION": "Update & Restart", + "SNACKBAR_TEXT": "A new version of WowUp is available", + "TOOLTIP": "WowUp update available", + "UPDATE_AVAILABLE": "Starting download", + "UPDATE_ERROR": "Failed to get WowUp update" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Achievements", + "ACTION_BARS": "Action Bars", + "ALL_ADDONS": "All Addons", + "AUCTION_ECONOMY": "Auction & Economy", + "BAGS_INVENTORY": "Bags & Inventory", + "BOSS_ENCOUNTERS": "Boss Encounters", + "BUFFS_DEBUFFS": "Buffs & Debuffs", + "BUNDLES": "Bundles", + "CHAT_COMMUNICATION": "Chat & Communication", + "CLASS": "Class", + "COMBAT": "Combat", + "COMPANIONS": "Companions", + "DATA_EXPORT": "Data Export", + "DEVELOPMENT_TOOLS": "Development Tools", + "GUILD": "Guild", + "LIBRARIES": "Libraries", + "MAIL": "Mail", + "MAP_MINIMAP": "Map & Minimap", + "MISCELLANEOUS": "Miscellaneous", + "MISSIONS": "Missions", + "PLUGINS": "Plugins", + "PROFESSIONS": "Professions", + "PVP": "PVP", + "QUESTS_LEVELING": "Quests & Leveling", + "ROLEPLAY": "Roleplay", + "TOOLTIPS": "Tooltips", + "UNIT_FRAMES": "Unit Frames" + }, + "ADDON_STATE": { + "IGNORED": "Ignored", + "INSTALL": "Install", + "PENDING": "Pending", + "UNAVAILABLE": "Unavailable", + "UNAVAILABLE_TOOLTIP": "This author or provider has made this addon unavailable", + "UNINSTALL": "Uninstall", + "UNKNOWN": "", + "UPDATE": "Update", + "UPTODATE": "Up to date", + "WARNING": "Warning" + }, + "ADDON_STATUS": { + "BACKINGUP": "Backing Up", + "COMPLETE": "Installed", + "DOWNLOADING": "Downloading", + "ERROR": "Error", + "INSTALLING": "Installing", + "PENDING": "Pending", + "RETRY": "Retrying...", + "UNINSTALLING": "Uninstalling", + "UPDATING": "Updating..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "We have detected an issue with this addon. We are no longer able to update this addon or provide details.", + "GENERIC_TOOLTIP": "We have detected an issue with this addon", + "MISSING_ON_PROVIDER_DESCRIPTION": "{providerName} did not return the addon when we requested it.
We are no longer able to update this addon or provide details until {providerName} fixes it.", + "MISSING_ON_PROVIDER_TOOLTIP": "{providerName} is not returning this addon as expected", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} properly returned this addon, however, it did not have any files matching this game version.
Once there is an update matching this game version this warning should go away.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} did not have any matching game version files", + "TOC_NAME_MISMATCH_DESCRIPTION": "This addon's folder name did not match with the expected toc file, you may experience issues with this addon in-game.", + "TOC_NAME_MISMATCH_TOOLTIP": "This addon's folder does not match the toc" + }, + "CLIENT_TYPES": { + "BETA": "Dragonflight Beta", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Public Test Realm (Wrath Classic)", + "RETAIL": "World of Warcraft", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count} {count, plural, =1{day} other{days}} ago", + "HOURS_AGO": "{count} {count, plural, =1{hour} other{hours}} ago", + "JUST_NOW": "Just now", + "MONTHS_AGO": "{count} {count, plural, =1{month} other{months}} ago", + "YEARS_AGO": "{count} {count, plural, =1{year} other{years}} ago", + "YESTERDAY": "Yesterday" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} required {dependencyCount, plural, =1{dependency} other{dependencies}}" + }, + "DOWNLOAD_COUNT": { + "e+0": "few", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count} thousand", + "e+4": "{count} thousand", + "e+5": "{count} thousand", + "e+6": "{count} million", + "e+7": "{count} million", + "e+8": "{count} million", + "e+9": "{count} billion" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alpha", + "BETA": "Beta", + "STABLE": "Stable" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Failed to toggle instant updates for your account. Please try again later, or reach out on Discord.", + "ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.", + "ADDON_SCAN_ERROR": "An error occurred matching your addon folders with {providerName}, please try again later.", + "ADDON_SYNC_ERROR": "An error occurred checking for updates from {providerName}, please try again later.", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: An error occurred checking for updates to {addonName} from {providerName}, please try again later.", + "CHANGE_PROVIDER_ERROR": "Failed to change provider for {addonName} to {providerName}", + "GITHUB_LIMIT_ERROR": "You have reached your GitHub API limit of {max} requests.\nPlease wait until {reset} and try again.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Failed to check updates for {addonName}.\nPlease verify that the repository is correct or set this addon to be ignored." + }, + "PROGRESS_SPINNER": { + "LOADING": "Loading..." + }, + "PROVIDER_ERROR": "Error contacting {providerName}", + "SEARCH": { + "NO_ADDONS": "No addons found" + }, + "WOW_EXE_SELECTION_NAME": "WoW Executable" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "Addon ID:", + "BY_AUTHOR": "By {authorName}", + "CHANGELOG_TAB": "Changelog", + "COPY_ADDON_ID_SNACKBAR": "Addon ID copied to clipboard", + "COPY_ADDON_ID_TOOLTIP": "Copy addon ID to clipboard", + "DEPENDENCY_TEXT": "This addon has {dependencyCount} required {dependencyCount, plural, =1{dependency} other{dependencies}}", + "DESCRIPTION_NOT_FOUND": "No description found", + "DESCRIPTION_TAB": "Description", + "FUNDING_LINK_TITLE": "Support this author", + "IMAGES_TAB": "Previews", + "MISSING_DEPENDENCIES": "Missing dependencies", + "NO_CHANGELOG_TEXT": "No changelog available", + "VIEW_IN_BROWSER_BUTTON": "View in browser", + "VIEW_ON_PROVIDER_PREFIX": "View on" + }, + "ALERT": { + "ERROR_TITLE": "Error", + "POSITIVE_BUTTON": "Okay" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "No", + "POSITIVE_BUTTON": "Yes" + }, + "CURSE_MIGRATION": { + "MESSAGE": "The CurseForge API has been shutdown so WowUp can no longer fetch addon information from it.
Unfortunately, this means that the CurseForge provider has been removed and your addons from CurseForge will no longer be updated.
It is recommended that you perform a re-scan in order to see what addons can be found on other providers.
The Re-Scan process may take a while.
Read more here.", + "NEGATIVE_BUTTON": "Manually Re-Scan", + "POSITIVE_BUTTON": "Automatically Re-Scan", + "RE_SCAN_ERROR": "The automatic Re-Scan failed, please try again manually.", + "RE_SCAN_SUCCESS": "The automatic Re-Scan has finished you're ready to go.", + "TITLE": "CurseForge Migration" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Addon is installed!", + "ADDON_INSTALLING": "Installing addon", + "CANCEL_BUTTON": "Close", + "ERRORS": { + "ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}", + "GENERIC": "Error fetching data for protocol: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}" + }, + "INSTALL_BUTTON": "Install", + "TITLE": "Install Addon From {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "Addon URL", + "ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub or WowInterface URL", + "CLOSE_BUTTON": "Close", + "DESCRIPTION": "If you want to install an addon directly from a URL paste it below to get started.", + "DOWNLOAD_COUNT": "{textCount} {count, plural, =1{download} other{downloads}} on {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "No asset was found to download {message}.\n\nA valid zip file is required to be in the release in order for WowUp to download it.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-bc' is required in order for WowUp to download it.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-classic' is required in order for WowUp to download it.", + "FAILED_TO_CONNECT": "Cannot connect to API, please wait a bit and try again.", + "INSTALL_FAILED": "Something went wrong when trying to install the addon, please try again.\n\nIf this message keeps showing up, you can get help on Discord in the #help-me channel.", + "INVALID_URL": "The given value is not a valid URL. Examples of valid addon URLs are:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "No addon was found, make sure your URL is pointing to the correct page.\n\nWhen installing from github, please make sure the repository has a release tag with a zip archive containing the addon.", + "NO_RELEASE_FOUND": "No releases were found for {message}.\n\nValid release with zip file assets are required for WowUp to download it.", + "NO_SEARCH_RESULTS": "No search results were found.", + "TITLE": "Addon Installation Failed", + "WRATH_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-wrath' is required in order for WowUp to download it." + }, + "IMPORT_ASSET_WARNING": "We were unable to verify if the latest release of this addon is compatible with your selected client.\n\nBut we did find a zip file \"{zipName}\".\n\nInstall at your own risk.", + "IMPORT_BUTTON": "Import", + "IMPORT_WARNING_TITLE": "Addon Import Warning", + "INSTALL_BUTTON": "Install", + "INSTALL_SUCCESS_LABEL": "Installed!", + "SUPPORTED_SOURCES": "Supports WowInterface and GitHub", + "TITLE": "Install Addon URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Patch Notes {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Accept & Continue", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, + "TITLE": "WowUp Permissions Setup", + "WAGO": { + "DESCRIPTION": "Enable the Wago.io addon provider to help support the authors of your favorite addons! This will display a promotional panel required to use their service.\nBy opting in you agree to their Terms of Service and Data Consent.", + "TOGGLE_LABEL": "Enable Wago.io Provider" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "This does not appear to be a valid World of Warcraft application:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "NEGATIVE_BUTTON": "No Thanks", + "POSITIVE_BUTTON": "Sure!", + "TITLE": "WowUp Telemetry" + }, + "TRUST_DOMAIN_CHECKBOX": "Trust this domain and do not ask me in the future" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Attributions", + "CHANGE_LOG_SECTION_LABEL": "Change Log", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Check out the website!" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Login Now!", + "LOGOUT_BUTTON": "Logout", + "LOGOUT_CONFIRMATION_MESSAGE": "Are you sure you want to log out? All of your local account data will be removed, until you login again.", + "LOGOUT_CONFIRMATION_TITLE": "Logout?", + "MANAGE_ACCOUNT_BUTTON": "Manage Account", + "TITLE": "Account" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Categories", + "ADDON_CATEGORIES_MENU_TITLE": "Addon Categories", + "ADDON_CATEGORIES_SELECTED_TITLE": "Category: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Browse various categories", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Install from URL", + "INSTALL_FROM_URL_TOOLTIP": "Install an addon from a URL", + "REFRESH_BUTTON": "Refresh", + "REFRESH_TOOLTIP": "Refresh addon results", + "RESET_CATEGORY_TOOLTIP": "Reset Category", + "SEARCH_LABEL": "Search", + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "AUTHOR_COLUMN_HEADER": "Author(s)", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Downloads", + "PROVIDER_COLUMN_HEADER": "Provider", + "RELEASED_AT_COLUMN_HEADER": "Released At", + "STATUS_COLUMN_HEADER": "Status" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "About", + "ACCOUNT_TAB_TITLE": "Account", + "COLLAPSE_BUTTON_TITLE": "Collapse", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Expand", + "GET_ADDONS_TAB_TITLE": "Get Addons", + "GUIDE_TAB_TITLE": "Guide", + "MIGRATING_ADDONS": "Migrating addons...", + "MY_ADDONS_TAB_TITLE": "My Addons", + "NEWS_TAB_TITLE": "News", + "OPTIONS_TAB_TITLE": "Options" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count} {count, plural, =1{addon} other{addons}} selected", + "ALPHA_ADDON_CHANNEL": "Alpha", + "AUTO_UPDATE_ADDON_BUTTON": "Auto Update", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Notifications Enabled", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "Channel", + "IGNORE_ADDON_BUTTON": "Ignore", + "PROVIDER_SUBMENU_TITLE": "Providers", + "REINSTALL_ADDON_BUTTON": "Re-Install", + "REMOVE_ADDON_BUTTON": "Remove", + "SHOW_FOLDER": "Show Folder", + "STABLE_ADDON_CHANNEL": "Stable" + }, + "ADDON_IS_CODE_REPOSITORY": "Addon appears to be a code repository", + "ADDON_REMOVED_SNACKBAR": "Successfully removed: {addonName} ", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Do you want to change the addon provider for {addonName} to {providerName}? This operation will uninstall your existing addon and replace it with a copy from the new provider.", + "TITLE": "Change Addon Provider?" + }, + "CHECK_UPDATES_BUTTON": "Check Updates", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Check for latest addon updates", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{update} other{updates}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Show Columns" + }, + "ERROR_SNACKBAR": "An error occurred", + "FILTER_LABEL": "Filter", + "FUNDING_TOOLTIP": { + "CUSTOM": "Support this author", + "GENERIC": "Support this author on {platform}", + "GITHUB": "Support this author on GitHub", + "PATREON": "Support this author on Patreon", + "PAYPAL": "Support this author on PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Import/Export Addons", + "MULTIPLE_PROVIDERS_TOOLTIP": "This addon has multiple providers", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{addon} other{addons}}", + "JOIN_DISCORD": "Chat with us on Discord", + "PATREON_SUPPORT": "Support WowUp on Patreon", + "SEARCH_RESULTS": "{count} {count, plural, =1{result} other{results}}", + "VIEW_GITHUB": "Check out the code on GitHub", + "VIEW_GUIDE": "Check out our guide to see what WowUp can do" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Required dependency missing", + "RESCAN_FOLDERS_BUTTON": "Re-Scan Folders", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Scan your client folder for installed addons", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "Doing a re-scan will attempt to guess which addons you currently have installed, doing so may reset known addon information. Use this function when certain addons are not recognized or addon versions are not being shown correctly. This scan will never delete your installed addons, only what WowUp knows about them.\n\nScanning may take a few moments.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Start re-scan?", + "SPINNER": { + "GATHERING_ADDONS": "Gathering addons...", + "UPDATING": "Updating {updateCount}/{addonCount}", + "UPDATING_WITH_ADDON_NAME": "Updating {updateCount}/{addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "ADDON_INSTALL_BUTTON": "Install", + "ADDON_UPDATE_BUTTON": "Update", + "AUTHOR_COLUMN_HEADER": "Author(s)", + "AUTO_UPDATE_ICON_TOOLTIP": "Auto update enabled", + "GAME_VERSION_COLUMN_HEADER": "Game Version", + "LATEST_VERSION_COLUMN_HEADER": "Latest Version", + "PROVIDER_COLUMN_HEADER": "Provider", + "PROVIDER_RELEASE_CHANNEL": "Provider Channel", + "RELEASED_AT_COLUMN_HEADER": "Released At", + "STATUS_COLUMN_HEADER": "Status", + "UPDATED_AT_COLUMN_HEADER": "Updated At" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Removing an addon via WowUp will remove the selected addon from your interface/addons directory. The character settings for this addon will not be removed.", + "CONFIRMATION_LESS_THAN_THREE": "Are you sure you want to remove the following {count} addons?", + "CONFIRMATION_MORE_THAN_THREE": "Are you sure you want to remove the selected {count} addons?", + "CONFIRMATION_ONE": "Are you sure you want to remove {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} has {dependencyCount} {dependencyCount, plural, =1{dependency} other{dependencies}}. Do you want to remove them also?", + "DEPENDENCY_TITLE": "Remove Addon Dependencies?", + "TITLE": "Uninstall {count, plural, =1{Addon} other{Addons}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "The installed addon did not match with any of the configured providers", + "UPDATE_ALL_BUTTON": "Update All", + "UPDATE_ALL_BUTTON_TOOLTIP": "Update all addons for this client", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Update All Clients", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Update Retail/Classic" + }, + "WTF_BACKUP_BUTTON": "Interface Settings Backup" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "News link copied to clipboard", + "NEWS_LINK_COPY_TOOLTIP": "Copy news link", + "PAGE_CONTEXT_FOOTER": "{count} news stories", + "REFRESH_TOOLTIP": "Refresh news feed" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Ad or access key required", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "If you have requested a CurseForge API key you can input it here to connect to their API.", + "API_KEY_TITLE": "CurseForge API Key", + "INSERT_API_KEY": "Insert Default CurseForge API Key", + "PROVIDER_NOTE": "API Key Required" + }, + "CURSE_MIGRATION_BUTTON": "CurseForge Migration", + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Select which providers may be used to search for, and install new addons", + "FIELD_LABEL": "Enabled Addon Providers", + "INPUT_LABEL": "Providers" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "A personal access token will increase the amount of GitHub API calls you can make, it is freely available. Learn More", + "PLACEHOLDER": "Personal Access Token", + "TITLE": "GitHub Personal Access Token" + }, + "TITLE": "Addons", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Switching to the Beta channel will allow you to receive experimental builds that contain bug fixes, as well as new and upcoming features. You may only go back to the current stable version by uninstalling your existing app and re-installing from wowup.io.\n\nWhile the Beta channel is functional, use at your own risk.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Switching to the Stable channel for the app will prevent you from receiving more Beta builds, the next update will be the next Stable release.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "Current Language", + "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", + "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Show a badge on the app icon with the number of addons with available updates.", + "ENABLE_APP_BADGE_LABEL": "Enable App Badge Notification", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Enable various system notification popups, such as auto updated addons.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Enable System Notifications", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "When closing the WowUp window, minimize to the menu bar.", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "When closing the WowUp window, minimize to the taskbar notification area.", + "MINIMIZE_ON_CLOSE_LABEL": "Minimize on Close", + "PROTOCOL_DESCRIPTION": "WowUp will register a custom URI protocol in your system and handle its incoming inquiries", + "PROTOCOL_LABEL": "Enable the wowup:// URI protocol", + "SCALE_DESCRIPTION": "Change zoom factor for entire app.", + "SCALE_LABEL": "Scale", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Changing the default language requires the application to restart.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Setting a new default language", + "SET_LANGUAGE_DESCRIPTION": "Select a language to change to", + "SET_LANGUAGE_LABEL": "Set Language", + "START_MINIMIZED_DESCRIPTION": "WowUp will start minimized and not show up.", + "START_MINIMIZED_LABEL": "Launch WowUp minimized", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp will be started automatically when you start up your system.", + "START_WITH_SYSTEM_LABEL": "Launch WowUp with system", + "TELEMETRY_DESCRIPTION": "Help improve WowUp by sending anonymous install data and/or errors.", + "TELEMETRY_LABEL": "Telemetry", + "THEME_DESCRIPTION": "Change the color theme to whatever you like", + "THEME_LABEL": "Color Theme", + "TITLE": "Application", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp can set itself as the default handler for CurseForge download links. This may cause issues if you try to use the CurseForge app, are you sure you want to continue?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Handle CurseForge Downloads?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Do you want to restart?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "Disabling hardware acceleration might solve FPS issues and fix other rendering issues in this app.
Changing this setting requires a restart.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Disabling hardware acceleration requires the application to restart.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Enabling hardware acceleration requires the application to restart.", + "USE_HARDWARE_ACCELERATION_LABEL": "Enable Hardware Acceleration", + "USE_SYMLINK_SUPPORT": "Enable Symlink Support", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Enabling symlink support will allow WowUp to recognize symlinks when performing a re-scan. Warning: If you do not know what a symlink is, you do not need this. When updating symlinks will currently be replaced with an actual folder and the link lost.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Enable symlink support?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Allow WowUp to scan symlink folders in your addon folder. Warning: they will be replaced when updating/installing." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Dump Debug Data", + "DEBUG_DATA_DESCRIPTION": "Log debug data to help with diagnosing potential issues. This can be found in your latest log file for the curious.", + "DEBUG_DATA_LABEL": "Debug Data", + "LOG_FILES_BUTTON": "Show Log Files", + "LOG_FILES_DESCRIPTION": "Open the folder that contains your last couple of log files.", + "LOG_FILES_LABEL": "Log Files", + "TITLE": "Debug" + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "Addons", + "APPLICATION": "Application", + "CLIENTS": "Clients", + "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Add New", + "AUTO_UPDATE_DESCRIPTION": "All existing and newly installed addons will be set to auto update by default", + "AUTO_UPDATE_LABEL": "Auto Update", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Cancel", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Are you sure you want to remove the installation at \"{location}\"? This will remove all stored addon information for this client.\n\nYour addon folders will not be removed.", + "TITLE": "Remove World of Warcraft Installation?" + }, + "CLIENT_TYPE_INPUT_HINT": "Select the {clientTypeName} application \"{clientFolderName}\"", + "CLIENT_TYPE_PATH_LABEL": "{clientTypeName} path", + "DEFAULT_ADDON_CHANNEL_LABEL": "Default Addon Channel", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Addon Channel", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Edit", + "MOVE_DOWN_BUTTON": "Move Down", + "MOVE_UP_BUTTON": "Move Up", + "NO_CLIENTS_FOUND_TEXT": "No World of Warcraft installations found, please make sure your Battle.net client is up to date or add a client manually", + "OPEN_FOLDER_BUTTON": "Open Folder", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Select", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Remove", + "RESCAN_CLIENTS_BUTTON": "Re-Scan", + "RESCAN_CLIENTS_LABEL": "Rescan installed World of Warcraft products", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Save", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Are you sure you want to apply this backup to your interface settings?\n\nMake sure that the World of Warcraft game is not running before you apply a backup.\n\nThis operation cannot be undone.", + "TITLE": "Apply WTF Backup?" + }, + "BACKUP_APPLY_SUCCESS": "Successfully applied backup: {name}", + "BACKUP_COUNT_TEXT": "Found {count} {count, plural, =1{backup} other{backups}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Applying backup...", + "CREATING_BACKUP": "Creating backup from {count} files...", + "LOADING_BACKUPS": "Loading backups...", + "REMOVING_BACKUP": "Removing backup..." + }, + "CREATE_BACKUP_BUTTON": "Create Backup", + "DELETE_CONFIRMATION": { + "MESSAGE": "Are you sure you want to delete backup {name}?\nThis cannot be undone.", + "TITLE": "Delete WTF Backup?" + }, + "DIALOG_TITLE": "WTF Settings Backup: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Failed to apply backup: {name}", + "FAILED_TO_DELETE": "Failed to delete backup: {name}", + "GENERIC_ERROR": "There was an issue processing this backup", + "INVALID_CONTENTS": "There was an issue processing this backup", + "INVALID_CREATED_AT": "There was an issue processing this backup", + "INVALID_CREATED_BY": "There was an issue processing this backup" + }, + "SHOW_FOLDER_BUTTON": "Show Folder", + "TOOL_TIP": { + "APPLY_BUTTON": "Apply this backup", + "DELETE_BUTTON": "Delete this backup" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/es.json b/WowUp/wowup-electron/src/assets/i18n/es.json new file mode 100644 index 0000000..b6f7240 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/es.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Addons activos: {count}", + "ADDED_BADGE_TOOLTIP": "Intentaremos instalar este addon", + "CONFLICT_BADGE_TOOLTIP": "Los addons en conflicto no serán modificados", + "COPY_BUTTON": "Copiar", + "DIALOG_TITLE": "Importar/Exportar addons: {clientType}", + "EXPORT_STRING_COPIED": "Datos de exportación copiados al portapapeles", + "EXPORT_STRING_PASTED": "Se ha insertado el contenido del portapapeles", + "EXPORT_TAB_LABEL": "Exportar", + "EXPORT_TEXT_LABEL": "Datos de exportación de addons", + "GENERIC_IMPORT_ERROR": "Ocurrió un error durante la importación", + "IGNORED_ADDON_COUNT": "Addons ignorados: {count}", + "IMPORT_ADDED_COUNT": "{count} añadidos", + "IMPORT_BADGE_ADDED": "Nuevo", + "IMPORT_BADGE_CONFLICT": "En conflicto", + "IMPORT_BADGE_NO_CHANGE": "Sin cambios", + "IMPORT_BUTTON": "Importar", + "IMPORT_CONFLICT_COUNT": "{count} en conflicto", + "IMPORT_NO_CHANGE_COUNT": "{count} sin cambios", + "IMPORT_STRING_INVALID": "La cadena de importación no es válida", + "IMPORT_TAB_LABEL": "Importar", + "IMPORT_TEXT_INSTRUCTIONS": "Pegue los datos de exportación de WowUp en el campo inferior para comenzar", + "IMPORT_TEXT_LABEL": "Datos para la importación", + "IMPORT_TOTAL_COUNT": "Importando {count} {count, plural, =1{addon} other{addons}}", + "INSTALL_BUTTON": "Instalar", + "INVALID_CLIENT_TYPE": "La cadena de importación no concuerda con el tipo de cliente seleccionado", + "NO_CHANGE_BADGE_TOOLTIP": "Este addon ya está instalado", + "PASTE_BUTTON": "Pegar", + "PROVIDER_MISMATCH": "El proveedor del addon no concuerda", + "RESET_BUTTON": "Reiniciar", + "VERSION_MISMATCH": "Las versiones no coinciden" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "¿Por qué estoy viendo este anuncio?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "Para poder usar wago.io / CurseForge como proveedor de addons y apoyar a sus autores por su duro trabajo, se nos requiere mostrar esta publicidad.\n\nSi no quiere ver estos anuncios, puede desactivar el proveedor wago.io / CurseForge en la pestaña de Opciones.", + "TITLE": "¿Por qué estoy viendo este anuncio?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Copiar", + "CUT": "Cortar", + "LABEL": "Edición", + "PASTE": "Pegar", + "REDO": "Rehacer", + "SELECT_ALL": "Seleccionar todo", + "UNDO": "Deshacer" + }, + "QUIT": "Salir", + "VIEW": { + "FORCE_RELOAD": "Forzar recarga", + "LABEL": "Ver", + "RELOAD": "Recargar", + "TOGGLE_DEV_TOOLS": "Alternar herramientas desarrollo", + "TOGGLE_FULL_SCREEN": "Alternar pantalla completa", + "ZOOM_IN": "Ampliar", + "ZOOM_OUT": "Reducir", + "ZOOM_RESET": "Restablecer zoom" + }, + "WINDOW": { + "CLOSE": "Cerrar", + "LABEL": "Ventana" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "{count} {count, plural, =1{addon actualizado} other{addons actualizados}} automáticamente.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Actualizaciones automáticas", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Abandonar modo de pantalla completa", + "FULLSCREEN_SNACKBAR": { + "MAC": "Pulse ^⌘F para salir del modo de pantalla completa", + "WINDOWS": "Pulse F11 para salir del modo de pantalla completa" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\n¿Quiere abrir este enlace a una web de terceros en su navegador predeterminado?", + "TITLE": "Está a punto de abandonar WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Desconocido" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Escaneo de addons completado.", + "ADDON_SCAN_STARTED": "Iniciando escaneo de addons...", + "ADDON_SCAN_UPDATE": "Escaneando {count} carpetas..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Buscar actualizaciones de WowUp", + "QUIT_ACTION": "Salir", + "SHOW_ACTION": "Mostrar" + }, + "THEME": { + "ALLIANCE": "Alianza", + "DEFAULT": "WowUp", + "GROUP_DARK": "Oscuro", + "GROUP_LIGHT": "Claro", + "HORDE": "Horda" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Pantalla completa", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Buscando actualización", + "DOWNLOADED_TOOLTIP": "Instalar actualización de WowUp", + "DOWNLOADING_UPDATE": "Descargando actualización", + "INSTALL_MESSAGE": "¿Quiere reiniciar WowUp para instalar la actualización?", + "INSTALL_TITLE": "Actualización de WowUp preparada", + "NOT_AVAILABLE": "La versión más reciente de WowUp ya se encuentra instalada", + "PORTABLE_DOWNLOAD_MESSAGE": "¿Quiere descargar manualmente la última versión portable?\n\nTendrá que cerrar la aplicación y copiar los archivos de la nueva versión manualmente.", + "PORTABLE_DOWNLOAD_TITLE": "Descarga manual requerida", + "SNACKBAR_ACTION": "Actualizar y reiniciar", + "SNACKBAR_TEXT": "Una nueva versión de WowUp está disponible", + "TOOLTIP": "Actualización de WowUp disponible", + "UPDATE_AVAILABLE": "Iniciando descarga", + "UPDATE_ERROR": "Error al obtener la actualización de WowUp" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Logros", + "ACTION_BARS": "Barras de acción", + "ALL_ADDONS": "Todos los addons", + "AUCTION_ECONOMY": "Subasta y economía", + "BAGS_INVENTORY": "Bolsas e inventario", + "BOSS_ENCOUNTERS": "Encuentros de jefe", + "BUFFS_DEBUFFS": "Beneficios y perjuicios", + "BUNDLES": "Packs", + "CHAT_COMMUNICATION": "Chat y comunicación", + "CLASS": "Clase", + "COMBAT": "Combate", + "COMPANIONS": "Compañeros", + "DATA_EXPORT": "Exportación de datos", + "DEVELOPMENT_TOOLS": "Herramientas de desarrollo", + "GUILD": "Hermandad", + "LIBRARIES": "Librerías", + "MAIL": "Correo", + "MAP_MINIMAP": "Mapa y minimapa", + "MISCELLANEOUS": "Miscelánea", + "MISSIONS": "Mesa de misiones", + "PLUGINS": "Plugins", + "PROFESSIONS": "Profesiones", + "PVP": "JcJ", + "QUESTS_LEVELING": "Misiones y leveo", + "ROLEPLAY": "Roleplay", + "TOOLTIPS": "Información adicional", + "UNIT_FRAMES": "Marcos de unidad" + }, + "ADDON_STATE": { + "IGNORED": "Ignorado", + "INSTALL": "Instalar", + "PENDING": "Pendiente", + "UNAVAILABLE": "No disponible", + "UNAVAILABLE_TOOLTIP": "El autor o proveedor ha marcado este addon como no disponible", + "UNINSTALL": "Desinstalar", + "UNKNOWN": "", + "UPDATE": "Actualizar", + "UPTODATE": "Actualizado", + "WARNING": "Advertencia" + }, + "ADDON_STATUS": { + "BACKINGUP": "Copiando", + "COMPLETE": "Instalado", + "DOWNLOADING": "Descargando", + "ERROR": "Error", + "INSTALLING": "Instalando", + "PENDING": "Pendiente", + "RETRY": "Retrying...", + "UNINSTALLING": "Desinstalando", + "UPDATING": "Actualizando" + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "Se ha detectado un problema con este addon. No es posible actualizarlo ni proveer detalles.", + "GENERIC_TOOLTIP": "Se ha detectado un problema con este addon", + "MISSING_ON_PROVIDER_DESCRIPTION": "Este addon parece haber sido eliminado por el autor o el proveedor.
No es posible actualizarlo ni proveer detalles.", + "MISSING_ON_PROVIDER_TOOLTIP": "Este addon parece haber sido eliminado por el proveedor", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} informa disponer de este addon, sin embargo, no posee ningún archivo que corresponda a esta versión del juego.
Una vez esté disponible una actualización que corresponda a esta versión del juego esta advertencia debería desaparecer.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} no dispone de ningún archivo que se corresponda con la versión del juego", + "TOC_NAME_MISMATCH_DESCRIPTION": "El nombre de carpeta de este addon no se corresponde con el archivo toc esperado, puede que experimente problemas con este addon dentro del juego.", + "TOC_NAME_MISMATCH_TOOLTIP": "La carpeta de este addon no se corresponde con el archivo toc" + }, + "CLIENT_TYPES": { + "BETA": "Beta de Dragonflight", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Reino público de pruebas (Wrath Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "Hace {count} {count, plural, one{día} other{días}}", + "HOURS_AGO": "Hace {count} {count, plural, one{hora} other{horas}}", + "JUST_NOW": "Ahora", + "MONTHS_AGO": "Hace {count} {count, plural, one{mes} other{meses}}", + "YEARS_AGO": "Hace {count} {count, plural, one{año} other{años}}", + "YESTERDAY": "Ayer" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} {dependencyCount, plural, one{dependencia requerida} other{dependencias requeridas}}" + }, + "DOWNLOAD_COUNT": { + "e+0": "{count}", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count} mil", + "e+4": "{count} mil", + "e+5": "{count} mil", + "e+6": "{count} {count, plural, =1{millón} other{millones}}", + "e+7": "{count} millones", + "e+8": "{count} millones", + "e+9": "{count} mil millones" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alfa", + "BETA": "Beta", + "STABLE": "Estable" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "No se pudieron activar las actualizaciones instantáneas en su cuenta. Por favor, inténtelo de nuevo más tarde o contacte con nosotros en Discord.", + "ADDON_INSTALL_ERROR": "Falló la instalación del addon {addonName}. Por favor, inténtelo de nuevo más tarde.", + "ADDON_SCAN_ERROR": "Ocurrió un error al comparar sus carpetas de addons con {providerName}. Por favor, inténtelo de nuevo más tarde.", + "ADDON_SYNC_ERROR": "Ocurrió un error al comprobar actualizaciones desde: {providerName}", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: Ocurrió un error al buscar actualizaciones de {addonName} desde {providerName}. Por favor, inténtelo de nuevo más tarde.", + "CHANGE_PROVIDER_ERROR": "Error al cambiar el proveedor del addon {addonName} a {providerName}", + "GITHUB_LIMIT_ERROR": "Ha alcanzado el límite de {max} peticiones a la API de GitHub.\nPor favor, espere hasta {reset} e inténtelo de nuevo.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Falló la comprobación de actualizaciones de {addonName}.\nPor favor, compruebe que el repositorio sea correcto o establezca este addon como ignorado." + }, + "PROGRESS_SPINNER": { + "LOADING": "Cargando..." + }, + "PROVIDER_ERROR": "Error al contactar con {providerName}", + "SEARCH": { + "NO_ADDONS": "No se encontraron addons" + }, + "WOW_EXE_SELECTION_NAME": "Ejecutable de WoW" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "ID del addon:", + "BY_AUTHOR": "Por {authorName}", + "CHANGELOG_TAB": "Listado de cambios", + "COPY_ADDON_ID_SNACKBAR": "El ID del addon se ha copiado al portapapeles", + "COPY_ADDON_ID_TOOLTIP": "Copia el ID del addon al portapapeles", + "DEPENDENCY_TEXT": "Este addon tiene {dependencyCount} {dependencyCount, plural, one{dependencia requerida} other{dependencias requeridas}}", + "DESCRIPTION_NOT_FOUND": "Descripción no encontrada", + "DESCRIPTION_TAB": "Descripción", + "FUNDING_LINK_TITLE": "Apoyar a este autor", + "IMAGES_TAB": "Previsualizaciones", + "MISSING_DEPENDENCIES": "Dependencias no encontradas", + "NO_CHANGELOG_TEXT": "No hay listado de cambios disponible", + "VIEW_IN_BROWSER_BUTTON": "Ver en el navegador", + "VIEW_ON_PROVIDER_PREFIX": "Ver en" + }, + "ALERT": { + "ERROR_TITLE": "Error", + "POSITIVE_BUTTON": "Aceptar" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "No", + "POSITIVE_BUTTON": "Sí" + }, + "CURSE_MIGRATION": { + "MESSAGE": "La API de CurseForge ha sido desactivada, por lo que WowUp no puede obtener información de ella.
Desafortunadamente, esto significa que todos aquellos addons cuyo proveedor es CurseForge no podrán seguir actualizándose.
Es recomendable que realice un reescaneo de addons para descubrir cuáles se encuentran disponibles en otros proveedores.
El proceso de reescaneo puede tardar un rato.
Leer más aquí (texto en inglés).", + "NEGATIVE_BUTTON": "Reescanear manualmente", + "POSITIVE_BUTTON": "Reescanear automáticamente", + "RE_SCAN_ERROR": "El reescaneo automático ha fallado, por favor inténtelo de nuevo manualmente.", + "RE_SCAN_SUCCESS": "El reescaneo automático ha finalizado, puede continuar.", + "TITLE": "Migración de CurseForge" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "¡Addon instalado!", + "ADDON_INSTALLING": "Instalando addon", + "CANCEL_BUTTON": "Cerrar", + "ERRORS": { + "ADDON_NOT_FOUND": "No se encontró ningún addon para el protocolo: {protocol}", + "GENERIC": "Error al obtener datos para el protocolo: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "No hay ningún cliente de WoW instalado para el protocolo: {protocol}" + }, + "INSTALL_BUTTON": "Instalar", + "TITLE": "Instalar addon desde {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "URL del addon", + "ADDON_URL_INPUT_PLACEHOLDER": "Ejemplo: URL de GitHub o WowInterface", + "CLOSE_BUTTON": "Cerrar", + "DESCRIPTION": "Si desea instalar un addon directamente desde una URL, péguelo a continuación para comenzar.", + "DOWNLOAD_COUNT": "{textCount} {count, plural, one{descarga} other{descargas}} en {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "No se encontró el recurso a descargar {message}.\n\nEs necesario que la publicación contenga un archivo zip válido para que WowUp pueda descargarlo.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "No se encontró el recurso a descargar desde {message}.\n\nEs necesario un archivo zip válido con el sufijo '-bc' para que WowUp pueda descargarlo.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "No se encontró el recurso a descargar desde {message}.\n\nEs necesario un archivo zip válido con el sufijo '-classic' para que WowUp pueda descargarlo.", + "FAILED_TO_CONNECT": "No se pudo conectar con la API. Por favor, espere un momento y vuelva a intentarlo.", + "INSTALL_FAILED": "Ocurrió un error al intentar instalar el addon. Por favor, inténtelo de nuevo.\n\nSi se sigue mostrando este mensaje, puede obtener ayuda en Discord en el canal #help-me.", + "INVALID_URL": "El valor introducido no es una URL válida. Ejemplos de URLs válidas:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "No se encontró el addon, asegúrese de que la URL apunta a la página correcta.\n\nSi está instalando desde GitHub, asegúrese de que el repositorio tiene una release tag con un archivo zip conteniendo el addon.", + "NO_RELEASE_FOUND": "No se encontraron publicaciones para {message}.\n\nSe requiere una versión válida con los recursos contenidos en un archivo zip para que WowUp pueda descargarla.", + "NO_SEARCH_RESULTS": "No se encontraron resultados.", + "TITLE": "Falló la instalación del addon", + "WRATH_ASSET_NOT_FOUND": "No se encontró el recurso a descargar desde {message}.\n\nEs necesario un archivo zip válido con el sufijo '-wrath' para que WowUp pueda descargarlo." + }, + "IMPORT_ASSET_WARNING": "No se pudo comprobar si la última versión de este addon es compatible con el cliente seleccionado. Sin embargo, se encontró un archivo zip \"{zipName}\".\n\n Puede instalarlo bajo su propia responsabilidad.", + "IMPORT_BUTTON": "Importar", + "IMPORT_WARNING_TITLE": "Advertencia de importación de addon", + "INSTALL_BUTTON": "Instalar", + "INSTALL_SUCCESS_LABEL": "¡Instalado!", + "SUPPORTED_SOURCES": "Soporta WowInterface y GitHub", + "TITLE": "Instalar addon desde URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Notas de la versión {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "distribuidores de anuncios", + "DESCRIPTION_BOTTOM": ". Haga clic en el botón Administrar para controlar sus consentimientos o para oponerse al procesado de sus datos. Puede cambiar sus preferencias en cualquier momento a través de la pantalla de configuración.", + "DESCRIPTION_TOP": " Para poder utilizar la integración con CurseForge de esta aplicación, es necesario su consentimiento para mostrarle publicidad de uno de sus ", + "MANAGE_BUTTON": "Administrar", + "TITLE": "CurseForge" + }, + "MESSAGE": "Antes de empezar, necesitamos configurar algunos permisos para la aplicación.", + "POSITIVE_BUTTON": "Confirmar", + "TELEMETRY": { + "DESCRIPTION": "¿Ayudar a mejorar WowUp enviando datos anónimos de instalación y/o error?", + "TOGGLE_LABEL": "Permitir telemetría" + }, + "TITLE": "Configuración de permisos de WowUp", + "WAGO": { + "DESCRIPTION": "¿Activar el proveedor de addons Wago.io? Esto hará que se muestre un anuncio requerido para usar el servicio.\n¡Dicha publicidad beneficia directamente a los autores tus addons favoritos!", + "TOGGLE_LABEL": "Activar Wago.io como proveedor" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "La ruta indicada no parece contener un cliente de World of Warcraft válido:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "¿Quiere ayudar a mejorar WowUp enviando datos de instalación y/o errores de forma anónima?", + "NEGATIVE_BUTTON": "No, gracias", + "POSITIVE_BUTTON": "¡Por supuesto!", + "TITLE": "Telemetría de WowUp" + }, + "TRUST_DOMAIN_CHECKBOX": "Confiar en este dominio y no volver a preguntar" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Atribuciones", + "CHANGE_LOG_SECTION_LABEL": "Registro de cambios", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "¡Visite la página web!" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "¡Iniciar sesión ahora!", + "LOGOUT_BUTTON": "Cerrar sesión", + "LOGOUT_CONFIRMATION_MESSAGE": "¿Quiere cerrar la sesión? Toda la información local sobre su cuenta será eliminada hasta que inicie sesión de nuevo.", + "LOGOUT_CONFIRMATION_TITLE": "¿Cerrar sesión?", + "MANAGE_ACCOUNT_BUTTON": "Administrar cuenta", + "TITLE": "Cuenta" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Categorías", + "ADDON_CATEGORIES_MENU_TITLE": "Categorías de addons", + "ADDON_CATEGORIES_SELECTED_TITLE": "Categoría: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Permite ver las diferentes categorías", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Instalar desde URL", + "INSTALL_FROM_URL_TOOLTIP": "Instala un addon desde una dirección URL", + "REFRESH_BUTTON": "Refrescar", + "REFRESH_TOOLTIP": "Refrescar resultados de addons", + "RESET_CATEGORY_TOOLTIP": "Restablecer categoría", + "SEARCH_LABEL": "Buscar", + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "AUTHOR_COLUMN_HEADER": "Autor/es", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Descargas", + "PROVIDER_COLUMN_HEADER": "Proveedor", + "RELEASED_AT_COLUMN_HEADER": "Publicado", + "STATUS_COLUMN_HEADER": "Estado" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "Acerca de", + "ACCOUNT_TAB_TITLE": "Cuenta", + "COLLAPSE_BUTTON_TITLE": "Contraer", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Expandir", + "GET_ADDONS_TAB_TITLE": "Obtener addons", + "GUIDE_TAB_TITLE": "Guía", + "MIGRATING_ADDONS": "Migrando lista de addons...", + "MY_ADDONS_TAB_TITLE": "Mis addons", + "NEWS_TAB_TITLE": "Noticias", + "OPTIONS_TAB_TITLE": "Opciones" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count} {count, plural, =1{addon seleccionado} other{addons seleccionados}}", + "ALPHA_ADDON_CHANNEL": "Alfa", + "AUTO_UPDATE_ADDON_BUTTON": "Actualización automática", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Mostrar notificaciones", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "Canal", + "IGNORE_ADDON_BUTTON": "Ignorar", + "PROVIDER_SUBMENU_TITLE": "Proveedor", + "REINSTALL_ADDON_BUTTON": "Reinstalar", + "REMOVE_ADDON_BUTTON": "Eliminar", + "SHOW_FOLDER": "Mostrar Carpeta", + "STABLE_ADDON_CHANNEL": "Estable" + }, + "ADDON_IS_CODE_REPOSITORY": "El addon parece ser un repositorio de código", + "ADDON_REMOVED_SNACKBAR": "Eliminación satisfactoria: {addonName} ", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "¿Quiere cambiar el proveedor del addon {addonName} a {providerName}? Esta operación desinstalará el addon existente y lo reemplazará con la versión disponible en el nuevo proveedor.", + "TITLE": "¿Cambiar proveedor del addon?" + }, + "CHECK_UPDATES_BUTTON": "Buscar actualizaciones", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Busca las últimas actualizaciones de los addons instalados", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{actualización} other{actualizaciones}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Mostrar Columna" + }, + "ERROR_SNACKBAR": "Ocurrió un error", + "FILTER_LABEL": "Filtro", + "FUNDING_TOOLTIP": { + "CUSTOM": "Apoyar a este autor", + "GENERIC": "Apoyar a este autor en {platform}", + "GITHUB": "Apoyar a este autor en GitHub", + "PATREON": "Apoyar a este autor en Patreon", + "PAYPAL": "Apoyar a este autor en PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Importar/Exportar addons", + "MULTIPLE_PROVIDERS_TOOLTIP": "Este addon está disponible en varios proveedores", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{addon} other{addons}}", + "JOIN_DISCORD": "Charle con nosotros en Discord", + "PATREON_SUPPORT": "Apoye WowUp en Patreon", + "SEARCH_RESULTS": "{count} {count, plural, =1{resultado} other{resultados}}", + "VIEW_GITHUB": "Eche un vistazo al código en GitHub", + "VIEW_GUIDE": "Descubra de lo que es capaz WowUp con nuestra guía" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Dependencia requerida no encontrada", + "RESCAN_FOLDERS_BUTTON": "Reescanear carpetas", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Vuelve a buscar addons instalados en la carpeta del juego", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "WowUp intentará averiguar qué addons están instalados actualmente. Al hacerlo, la información sobre los addons ya conocidos se podría reiniciar. Utilice esta función cuando ciertos addons no sean reconocidos o las versiones de los addons no se reconozcan correctamente. Este escaneo no elimina los addons instalados, sólo la información que WowUp tiene acerca de ellos.\n\nEl escaneo puede tardar un poco.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "¿Comenzar el reescaneo?", + "SPINNER": { + "GATHERING_ADDONS": "Recopilando información sobre los addons...", + "UPDATING": "Actualizando {updateCount}/{addonCount}", + "UPDATING_WITH_ADDON_NAME": "Actualizando {updateCount}/{addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "ADDON_INSTALL_BUTTON": "Instalar", + "ADDON_UPDATE_BUTTON": "Actualizar", + "AUTHOR_COLUMN_HEADER": "Autor/es", + "AUTO_UPDATE_ICON_TOOLTIP": "Actualización automática habilitada", + "GAME_VERSION_COLUMN_HEADER": "Versión de WoW", + "LATEST_VERSION_COLUMN_HEADER": "Última versión", + "PROVIDER_COLUMN_HEADER": "Proveedor", + "PROVIDER_RELEASE_CHANNEL": "Canal", + "RELEASED_AT_COLUMN_HEADER": "Publicado", + "STATUS_COLUMN_HEADER": "Estado", + "UPDATED_AT_COLUMN_HEADER": "Actualizado" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Esto eliminará todas las carpetas realicionadas con el addon dentro de la carpeta de World of Warcraft.", + "CONFIRMATION_LESS_THAN_THREE": "¿Quiere eliminar los siguientes {count} addons?", + "CONFIRMATION_MORE_THAN_THREE": "¿Quiere eliminar los {count} addons seleccionados?", + "CONFIRMATION_ONE": "¿Quiere eliminar {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} tiene {dependencyCount} {dependencyCount, plural, one{dependencia} other{dependencias}}. ¿Quiere eliminarlas también?", + "DEPENDENCY_TITLE": "¿Eliminar dependencias del addon?", + "TITLE": "¿Desinstalar {count, plural, =1{addon} other{addons}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "El addon instalado no se pudo emparejar con ninguno de los proveedores configurados", + "UPDATE_ALL_BUTTON": "Actualizar todo", + "UPDATE_ALL_BUTTON_TOOLTIP": "Actualiza todos los addons para este cliente", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Actualizar todos los clientes", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Actualizar Retail/Classic" + }, + "WTF_BACKUP_BUTTON": "Copia de seguridad" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "Enlace de la noticia copiado al portapapeles", + "NEWS_LINK_COPY_TOOLTIP": "Copiar enlace de la noticia", + "PAGE_CONTEXT_FOOTER": "{count} titulares", + "REFRESH_TOOLTIP": "Actualizar titulares" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Publicidad o clave de acceso requeridos", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "Si ha solicitado una clave para la API de CurseForge, puede introducirla aquí para conectar con ella.", + "API_KEY_TITLE": "Clave de API CurseForge", + "PROVIDER_NOTE": "Clave de API requerida" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Determina qué proveedores serán utilizados para la búsqueda e instalación de nuevos addons.", + "FIELD_LABEL": "Selección de proveedores de addons", + "INPUT_LABEL": "Proveedores" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "Un token de acceso personal incrementa la cantidad de llamadas a la API de GitHub que puede realizarse, está disponible de manera libre. Saber más", + "PLACEHOLDER": "Token de acceso personal", + "TITLE": "Token de acceso personal de GitHub" + }, + "TITLE": "Addons", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Utilice el proveedor Wago sin ver anuncios. Saber más", + "PLACEHOLDER": "Clave de acceso", + "TITLE": "Clave de acceso de Wago" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Cambiar al canal Beta permitirá que reciba versiones experimentales que contienen correcciones de fallos, así como nuevas funciones en desarrollo. Solo podrá volver a la versión estable desinstalando la aplicación y volviéndola a instalar desde wowup.io\n\nAunque el canal Beta es funcional, su uso será bajo su propia responsabilidad.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Cambiar al canal Estable evitará que reciba más versiones Beta. La próxima actualización de la aplicación se realizará cuando se lance la siguiente versión Estable.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Configuración del canal de publicación de WowUp", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Sí, lo entiendo", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Cambia entre las versiones Beta y Estable de la aplicación", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Canal", + "APP_RELEASE_CHANNEL_LABEL": "Canal de publicación de la aplicación", + "CURRENT_LANGUAGE_LABEL": "Idioma actual", + "CURSE_PROTOCOL_DESCRIPTION": "Al descargar addons desde la web de CurseForge, WowUp se hará cargo de la instalación", + "CURSE_PROTOCOL_LABEL": "Tramitar enlaces de descarga de CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Muestra un contador en el icono de la aplicación en la barra de tareas con el número de addons con actualizaciones disponibles.", + "ENABLE_APP_BADGE_LABEL": "Activar contador en el icono", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Activa varios mensajes de notificación del sistema, tales como cuando los addons son actualizados automáticamente.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Activar notificaciones del sistema", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "Al abrir los detalles de un addon, selecciona automáticamente la última pestaña abierta.", + "KEEP_LAST_OPENED_TAB_LABEL": "Recordar la última pestaña abierta", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Al cerrar la ventana de WowUp, minimizarla a la barra de menú", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Al cerrar la ventana de WowUp, minimizarla a la bandeja del sistema.", + "MINIMIZE_ON_CLOSE_LABEL": "Minimizar al cerrar", + "PROTOCOL_DESCRIPTION": "WowUp registrará un protocolo URI personalizado en el sistema y se hará cargo de las peticiones entrantes", + "PROTOCOL_LABEL": "Permitir a WowUp tramitar el protocolo URI wowup://", + "SCALE_DESCRIPTION": "Cambia el nivel de zoom de la interfaz de WowUp.", + "SCALE_LABEL": "Escala", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Cambiar el idioma predeterminado requiere reiniciar la aplicación.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Estableciendo un nuevo idioma predeterminado", + "SET_LANGUAGE_DESCRIPTION": "Establece el idioma en el que se mostrará WowUp.", + "SET_LANGUAGE_LABEL": "Establecer idioma", + "START_MINIMIZED_DESCRIPTION": "WowUp se iniciará minimizado y no se mostrará en pantalla.", + "START_MINIMIZED_LABEL": "Iniciar WowUp minimizado", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp se iniciará automáticamente durante el arranque del sistema.", + "START_WITH_SYSTEM_LABEL": "Iniciar WowUp con el sistema", + "TELEMETRY_DESCRIPTION": "Ayuda a mejorar WowUp enviando datos de instalación y/o errores de forma anónima.", + "TELEMETRY_LABEL": "Telemetría", + "THEME_DESCRIPTION": "Cambia el color del tema de la aplicación.", + "THEME_LABEL": "Apariencia", + "TITLE": "Aplicación", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp puede asignarse a sí mismo como controlador predeterminado de enlaces de descarga de CurseForge. Esto podría ocasionar problemas si también usa la aplicación CurseForge. ¿Quiere continuar?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "¿Tramitar descargas de CurseForge?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "¿Quiere reiniciar la aplicación?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "Desactivar la aceleración por hardware puede resolver problemas de FPS y corregir otros problemas de renderizado en esta aplicación. Es necesario reiniciar tras cambiar esta opción.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Desactivar la aceleración por hardware requiere reinicar la aplicación.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Activar la aceleración por hardware requiere reinicar la aplicación.", + "USE_HARDWARE_ACCELERATION_LABEL": "Activar aceleración por hardware", + "USE_SYMLINK_SUPPORT": "Activar soporte de enlaces simbólicos (symlink)", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Activar el soporte de enlaces simbólicos permite a WowUp reconocerlos durante el reescaneo.\n\nAdvertencia: Si no sabe lo que es un enlace simbólico, no necesita activar esto. Al actualizar addons, los enlaces simbólicos serán reemplazados por copias de las carpetas y el enlace se perderá.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "¿Activar soporte de enlaces simbólicos?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Permite a WowUp escanear enlaces simbólicos en la carpeta de addons.\nAdvertecia: Los enlaces serán reemplazados al actualizar/instalar los addons." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "Ver y administrar la manera en que los anunciantes de CurseForge pueden usar sus datos para la personalización de la publicidad", + "ADS_OPTION_LABEL": "Personalización de publicidad y datos", + "ADS_OPTION_MANAGE": "Administrar", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Mostrar archivos de configuración", + "CONFIG_FILES_DESCRIPTION": "Abre la carpeta donde se encuentra almacenado addons.json y preferences.json por ejemplo.", + "CONFIG_FILES_LABEL": "Archivos de configuración", + "DEBUG_DATA_BUTTON": "Eliminar datos de depuración", + "DEBUG_DATA_DESCRIPTION": "Registra datos de depuración y ayuda a diagnosticar problemas potenciales. Puede curiosearlo abriendo el último archivo de registro.", + "DEBUG_DATA_LABEL": "Datos de depuración", + "LOG_FILES_BUTTON": "Mostrar archivos de registro", + "LOG_FILES_DESCRIPTION": "Abre la carpeta que contiene los últimos archivos de registro.", + "LOG_FILES_LABEL": "Archivos de registro", + "TITLE": "Depuración" + }, + "TABS": { + "ABOUT": "Acerca de", + "ADDONS": "Addons", + "APPLICATION": "Aplicación", + "CLIENTS": "Clientes", + "CURSEFORGE": "CurseForge", + "DEBUG": "Depuración", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Añadir nuevo", + "AUTO_UPDATE_DESCRIPTION": "Los addons instalados después de activar esta opción se configurarán para actualizarse automáticamente de forma predeterminada. No afecta a los addons instalados con anterioridad.", + "AUTO_UPDATE_LABEL": "Actualización automática", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Cancelar", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "¿Quiere borrar la ruta de instalación de la versión {clientName}? Esto eliminará toda la información sobre los addons almacenada para este cliente de WoW.\n\nLas carpetas de los addons no serán eliminadas.", + "TITLE": "¿Borrar la ruta de instalación?" + }, + "CLIENT_TYPE_INPUT_HINT": "La carpeta que contiene la versión {clientTypeName} del cliente de World of Warcraft \"{clientFolderName}\"", + "CLIENT_TYPE_PATH_LABEL": "Ruta de {clientTypeName}", + "DEFAULT_ADDON_CHANNEL_LABEL": "Canal de addons predeterminado", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canal de addons", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Editar", + "MOVE_DOWN_BUTTON": "Mover abajo", + "MOVE_UP_BUTTON": "Mover arriba", + "NO_CLIENTS_FOUND_TEXT": "No se encontraron instalaciones de World of Warcraft. Por favor, asegúrese de que el cliente Battle.net esté actualizado o añada un cliante manualmente", + "OPEN_FOLDER_BUTTON": "Abrir carpeta", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Seleccionar", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Eliminar", + "RESCAN_CLIENTS_BUTTON": "Reescanear", + "RESCAN_CLIENTS_LABEL": "Volver a escanear los productos de World of Warcraft instalados", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Guardar", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "¿Quiere restaurar esta copia de seguridad a su configuración de interfaz?\n\nAsegúrese que World of Warcraft no está en funcionamiento antes de iniciar la restauración.\n\nEsta operación no se puede deshacer.", + "TITLE": "¿Restaurar copia de seguridad de la configuración WTF?" + }, + "BACKUP_APPLY_SUCCESS": "Copia de seguridad restaurada con éxito: {name}", + "BACKUP_COUNT_TEXT": "Se {count, plural, =1{encontró} other{encontraron}} {count} {count, plural, =1{copia de seguridad} other{copias de seguridad}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Restaurando copia de seguridad...", + "CREATING_BACKUP": "Creando copia de seguridad de {count} archivos...", + "LOADING_BACKUPS": "Cargando copias de seguridad...", + "REMOVING_BACKUP": "Eliminando copia de seguridad..." + }, + "CREATE_BACKUP_BUTTON": "Crear copia de seguridad", + "DELETE_CONFIRMATION": { + "MESSAGE": "Quiere eliminar la copia de seguridad {name}?\nEsta acción no se podrá deshacer.", + "TITLE": "¿Eliminar copia de seguridad de la carpeta WTF?" + }, + "DIALOG_TITLE": "Copia de seguridad de la carpeta de configuración WTF: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Error al restaurar la copia de seguridad: {name}", + "FAILED_TO_DELETE": "Error al eliminar la copia de seguridad: {name}", + "GENERIC_ERROR": "Ocurrió un error procesando esta copia de seguridad", + "INVALID_CONTENTS": "Ocurrió un error procesando esta copia de seguridad", + "INVALID_CREATED_AT": "Ocurrió un error procesando esta copia de seguridad", + "INVALID_CREATED_BY": "Ocurrió un error procesando esta copia de seguridad" + }, + "SHOW_FOLDER_BUTTON": "Mostrar carpeta", + "TOOL_TIP": { + "APPLY_BUTTON": "Restaurar esta copia de seguridad", + "DELETE_BUTTON": "Eliminar esta copia de seguridad" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/fr.json b/WowUp/wowup-electron/src/assets/i18n/fr.json new file mode 100644 index 0000000..97953bb --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/fr.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Active addons: {count}", + "ADDED_BADGE_TOOLTIP": "We will attempt to install this addon", + "CONFLICT_BADGE_TOOLTIP": "Addons in conflict will not be modified", + "COPY_BUTTON": "Copy", + "DIALOG_TITLE": "Import/Export Addons: {clientType}", + "EXPORT_STRING_COPIED": "Export string copied to clipboard", + "EXPORT_STRING_PASTED": "Inserted clipboard contents", + "EXPORT_TAB_LABEL": "Export", + "EXPORT_TEXT_LABEL": "Addon Export Data", + "GENERIC_IMPORT_ERROR": "An error occurred during import", + "IGNORED_ADDON_COUNT": "Ignored addons: {count}", + "IMPORT_ADDED_COUNT": "{count} added", + "IMPORT_BADGE_ADDED": "New", + "IMPORT_BADGE_CONFLICT": "Conflict", + "IMPORT_BADGE_NO_CHANGE": "No Change", + "IMPORT_BUTTON": "Import", + "IMPORT_CONFLICT_COUNT": "{count} in conflict", + "IMPORT_NO_CHANGE_COUNT": "{count} unchanged", + "IMPORT_STRING_INVALID": "Import string was invalid", + "IMPORT_TAB_LABEL": "Import", + "IMPORT_TEXT_INSTRUCTIONS": "Paste WowUp addon export data into the field below to get started", + "IMPORT_TEXT_LABEL": "Import data", + "IMPORT_TOTAL_COUNT": "Importing {count} {count, plural, =1{addon} other{addons}}", + "INSTALL_BUTTON": "Install", + "INVALID_CLIENT_TYPE": "Import string did not match your selected client type", + "NO_CHANGE_BADGE_TOOLTIP": "You already have this addon installed", + "PASTE_BUTTON": "Paste", + "PROVIDER_MISMATCH": "Addon provider does not match", + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Copier", + "CUT": "Couper", + "LABEL": "Éditer", + "PASTE": "Coller", + "REDO": "Refaire", + "SELECT_ALL": "Tout sélectionner", + "UNDO": "Annuler" + }, + "QUIT": "Quitter", + "VIEW": { + "FORCE_RELOAD": "Forcer le rechargement", + "LABEL": "Voir", + "RELOAD": "Recharger", + "TOGGLE_DEV_TOOLS": "Afficher les outils de développement", + "TOGGLE_FULL_SCREEN": "Afficher en plein écran", + "ZOOM_IN": "Zoomer", + "ZOOM_OUT": "Dézoomer", + "ZOOM_RESET": "Réinitialiser le zoom" + }, + "WINDOW": { + "CLOSE": "Fermer", + "LABEL": "Fenêtre" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Mise à jour automatique de \r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "Mise à jour automatique de {count} {count, plural, =1{addon} other{addons}}.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Mises à jour Auto", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Quitter le mode plein écran", + "FULLSCREEN_SNACKBAR": { + "MAC": "Appuyez sur ^⌘F pour quitter le mode plein écran", + "WINDOWS": "Appuyez sur F11 pour quitter le mode plein écran" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nAre you sure you want to open this 3rd party page in your default browser?", + "TITLE": "You are about to leave WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Inconnu" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Analyse des addons terminée...", + "ADDON_SCAN_STARTED": "Début de l'analyse des addons...", + "ADDON_SCAN_UPDATE": "Analyse de {count} dossiers..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Vérification des mises à jour...", + "QUIT_ACTION": "Quitter", + "SHOW_ACTION": "Afficher" + }, + "THEME": { + "ALLIANCE": "Alliance", + "DEFAULT": "Défaut", + "GROUP_DARK": "Sombre", + "GROUP_LIGHT": "Clair", + "HORDE": "Horde" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Plein écran", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Checking for update", + "DOWNLOADED_TOOLTIP": "Installer la mise à jour WowUp", + "DOWNLOADING_UPDATE": "Downloading update", + "INSTALL_MESSAGE": "Voulez-vous redémarrer WowUp pour installer la mise à jour ?", + "INSTALL_TITLE": "Mise à jour WowUp prête", + "NOT_AVAILABLE": "Vous disposez déjà de la dernière version de WowUp", + "PORTABLE_DOWNLOAD_MESSAGE": "Voulez-vous télécharger manuellement la dernière version portable ?\n\nVous devrez fermer l'application manuellement et copier la nouvelle version.", + "PORTABLE_DOWNLOAD_TITLE": "Téléchargement manuel requis", + "SNACKBAR_ACTION": "Mise à jour & Restart", + "SNACKBAR_TEXT": "Une nouvelle version de WowUp est disponible", + "TOOLTIP": "Mise à jour de WowUp disponible", + "UPDATE_AVAILABLE": "Starting download", + "UPDATE_ERROR": "Impossible d'obtenir la mise à jour de WowUp" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Achievements", + "ACTION_BARS": "Action Bars", + "ALL_ADDONS": "All Addons", + "AUCTION_ECONOMY": "Auction & Economy", + "BAGS_INVENTORY": "Bags & Inventory", + "BOSS_ENCOUNTERS": "Boss Encounters", + "BUFFS_DEBUFFS": "Buffs & Debuffs", + "BUNDLES": "Bundles", + "CHAT_COMMUNICATION": "Chat & Communication", + "CLASS": "Class", + "COMBAT": "Combat", + "COMPANIONS": "Companions", + "DATA_EXPORT": "Data Export", + "DEVELOPMENT_TOOLS": "Development Tools", + "GUILD": "Guild", + "LIBRARIES": "Libraries", + "MAIL": "Mail", + "MAP_MINIMAP": "Map & Minimap", + "MISCELLANEOUS": "Miscellaneous", + "MISSIONS": "Missions", + "PLUGINS": "Plugins", + "PROFESSIONS": "Professions", + "PVP": "PVP", + "QUESTS_LEVELING": "Quests & Leveling", + "ROLEPLAY": "Roleplay", + "TOOLTIPS": "Tooltips", + "UNIT_FRAMES": "Unit Frames" + }, + "ADDON_STATE": { + "IGNORED": "Ignoré", + "INSTALL": "Installer", + "PENDING": "Pending", + "UNAVAILABLE": "Unavailable", + "UNAVAILABLE_TOOLTIP": "This author or provider has made this addon unavailable", + "UNINSTALL": "Désinstaller", + "UNKNOWN": "", + "UPDATE": "Mise à jour", + "UPTODATE": "À jour", + "WARNING": "Warning" + }, + "ADDON_STATUS": { + "BACKINGUP": "Sauvegarde", + "COMPLETE": "Installé", + "DOWNLOADING": "Téléchargement", + "ERROR": "Erreur", + "INSTALLING": "Installation", + "PENDING": "En attente", + "RETRY": "Retrying...", + "UNINSTALLING": "Désinstallation", + "UPDATING": "En cours de mise à jour" + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "We have detected an issue with this addon. We are no longer able to update this addon or provide details.", + "GENERIC_TOOLTIP": "We have detected an issue with this addon", + "MISSING_ON_PROVIDER_DESCRIPTION": "This addon appears to have been removed by either the author or the addon provider.
We are no longer able to update this addon or provide details.", + "MISSING_ON_PROVIDER_TOOLTIP": "This addon appears to have been removed by the provider", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} properly returned this addon, however, it did not have any files matching this game version.
Once there is an update matching this game version this warning should go away.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} did not have any matching game version files", + "TOC_NAME_MISMATCH_DESCRIPTION": "This addon's folder name did not match with the expected toc file, you may experience issues with this addon in-game.", + "TOC_NAME_MISMATCH_TOOLTIP": "This addon's folder does not match the toc" + }, + "CLIENT_TYPES": { + "BETA": "Bêta de Dragonflight", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Royaume public de test (Wrath of the Lich King Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "il y a {count} {count, plural, one{jour} other{jours}}", + "HOURS_AGO": "il y a {count} {count, plural, one{heure} other{heures}}", + "JUST_NOW": "A l'instant", + "MONTHS_AGO": "il y a {count} {count, plural, one{mois} other{mois}}", + "YEARS_AGO": "il y a {count} {count, plural, one{an} other{ans}}", + "YESTERDAY": "Hier" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} {dependencyCount, plural, one{dépendance requise} other{dépendances requises}}" + }, + "DOWNLOAD_COUNT": { + "e+0": "{rawCount}", + "e+1": "{rawCount}", + "e+2": "{rawCount}", + "e+3": "{rawCount}", + "e+4": "{rawCount}", + "e+5": "{rawCount}", + "e+6": "{count} {count, plural, one{million} other{millions}}", + "e+7": "{count} millions", + "e+8": "{count} millions", + "e+9": "{count} {count, plural, one{milliard} other{milliards}}" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alpha", + "BETA": "Beta", + "STABLE": "Stable" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Failed to toggle instant updates for your account. Please try again later, or reach out on Discord.", + "ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.", + "ADDON_SCAN_ERROR": "Une erreur s'est produite lors de la correspondance de vos dossiers d'addons avec {providerName}, merci de réessayer plus tard.", + "ADDON_SYNC_ERROR": "Une erreur s'est produite lors de la vérification des mises à jour depuis : {providerName}", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: An error occurred checking for updates to {addonName} from {providerName}, please try again later.", + "CHANGE_PROVIDER_ERROR": "Impossible de changer le fournisseur {providerName} pour l´addon {addonName}", + "GITHUB_LIMIT_ERROR": "Vous avez atteint la limite de {max} requêtes API Github.\nMerci d'attendre {reset} et de réessayer.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Failed to check updates for {addonName}.\nPlease verify that the repository is correct or set this addon to be ignored." + }, + "PROGRESS_SPINNER": { + "LOADING": "Chargement..." + }, + "PROVIDER_ERROR": "Error contacting {providerName}", + "SEARCH": { + "NO_ADDONS": "Aucun addon trouvé" + }, + "WOW_EXE_SELECTION_NAME": "WoW Executable" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "ID de l'addon :", + "BY_AUTHOR": "Par {authorName}", + "CHANGELOG_TAB": "Notes de mise à jour", + "COPY_ADDON_ID_SNACKBAR": "ID de l'addon copié dans le presse-papier", + "COPY_ADDON_ID_TOOLTIP": "Copier l'ID de l'addon dans le presse-papier", + "DEPENDENCY_TEXT": "Cet addon a {dependencyCount} {dependencyCount, plural, one{dépendance requise} other{dépendances requises}}", + "DESCRIPTION_NOT_FOUND": "No description found", + "DESCRIPTION_TAB": "Description", + "FUNDING_LINK_TITLE": "Supporter cet auteur", + "IMAGES_TAB": "Previews", + "MISSING_DEPENDENCIES": "Dépendances manquantes", + "NO_CHANGELOG_TEXT": "No changelog available", + "VIEW_IN_BROWSER_BUTTON": "Voir dans le navigateur", + "VIEW_ON_PROVIDER_PREFIX": "Voir sur" + }, + "ALERT": { + "ERROR_TITLE": "Erreur", + "POSITIVE_BUTTON": "Ok" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "Non", + "POSITIVE_BUTTON": "Oui" + }, + "CURSE_MIGRATION": { + "MESSAGE": "The CurseForge API has been shutdown so WowUp can no longer fetch addon information from it.
Unfortunately, this means that the CurseForge provider has been removed and your addons from CurseForge will no longer be updated.
It is recommended that you perform a re-scan in order to see what addons can be found on other providers.
The Re-Scan process may take a while.
Read more here.", + "NEGATIVE_BUTTON": "Manually Re-Scan", + "POSITIVE_BUTTON": "Automatically Re-Scan", + "RE_SCAN_ERROR": "The automatic Re-Scan failed, please try again manually.", + "RE_SCAN_SUCCESS": "The automatic Re-Scan has finished you're ready to go.", + "TITLE": "CurseForge Migration" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Addon is installed!", + "ADDON_INSTALLING": "Installing addon", + "CANCEL_BUTTON": "Close", + "ERRORS": { + "ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}", + "GENERIC": "Error fetching data for protocol: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}" + }, + "INSTALL_BUTTON": "Install", + "TITLE": "Install Addon From {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "URL de l'Addon", + "ADDON_URL_INPUT_PLACEHOLDER": "Ex. URL de GitHub ou WowInterface", + "CLOSE_BUTTON": "Fermer", + "DESCRIPTION": "Si vous voulez installer un addon directement à partir d'une URL, collez-là ci-dessous.", + "DOWNLOAD_COUNT": "{textCount} {count, plural, one{téléchargement} other{téléchargements}} sur {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "No asset was found to download {message}.\n\nA valid zip file is required to be in the release in order for WowUp to download it.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-bc' is required in order for WowUp to download it.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-classic' is required in order for WowUp to download it.", + "FAILED_TO_CONNECT": "Impossible de se connecter à l'API, veuillez patienter quelques instants et réessayer.", + "INSTALL_FAILED": "Une erreur s'est produite lors de la tentative d'installation de l'addon, veuillez réessayer.\n\nSi ce message persiste, vous pouvez obtenir de l'aide sur Discord dans le canal #help-me.", + "INVALID_URL": "La valeure rentrée n'est pas une URL valide. Exemples d'URL valides:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "Aucun addon n'a été trouvé, assurez-vous que votre URL pointe vers la bonne page.\n\nLors de l'installation à partir de github, veuillez vous assurer que le référentiel a une balise de version avec une archive zip contenant l'addon.", + "NO_RELEASE_FOUND": "No releases were found for {message}.\n\nValid release with zip file assets are required for WowUp to download it.", + "NO_SEARCH_RESULTS": "La recherche n'a retourné aucun résultat.", + "TITLE": "Echec de l'installation de l'addon", + "WRATH_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-wrath' is required in order for WowUp to download it." + }, + "IMPORT_ASSET_WARNING": "We were unable to verify if the latest release of this addon is compatible with your selected client.\n\nBut we did find a zip file \"{zipName}\".\n\nInstall at your own risk.", + "IMPORT_BUTTON": "Importer", + "IMPORT_WARNING_TITLE": "Addon Import Warning", + "INSTALL_BUTTON": "Installer", + "INSTALL_SUCCESS_LABEL": "Installé !", + "SUPPORTED_SOURCES": "Supporte WowInterface et GitHub", + "TITLE": "URL de l'addon à installer" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Patch Notes {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Confirm", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, + "TITLE": "WowUp Permissions Setup", + "WAGO": { + "DESCRIPTION": "Enabled the Wago.io addon provider, this will display an ad required to use their service.\nThe ads directly benefit the authors of your favorite addons!", + "TOGGLE_LABEL": "Enable Wago.io Provider" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "This does not appear to be a valid World of Warcraft application:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Aidez-moi à améliorer WowUp en envoyant anonymement les données d'installation et / ou erreurs ?", + "NEGATIVE_BUTTON": "Non Merci", + "POSITIVE_BUTTON": "Bien sûr!", + "TITLE": "Télémétrie WowUp" + }, + "TRUST_DOMAIN_CHECKBOX": "Trust this domain and do not ask me in the future" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Attributions", + "CHANGE_LOG_SECTION_LABEL": "Journal des modifications", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Découvrez le site web !" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Login Now!", + "LOGOUT_BUTTON": "Logout", + "LOGOUT_CONFIRMATION_MESSAGE": "Are you sure you want to log out? All of your local account data will be removed, until you login again.", + "LOGOUT_CONFIRMATION_TITLE": "Logout?", + "MANAGE_ACCOUNT_BUTTON": "Manage Account", + "TITLE": "Account" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Categories", + "ADDON_CATEGORIES_MENU_TITLE": "Addon Categories", + "ADDON_CATEGORIES_SELECTED_TITLE": "Category: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Browse various categories", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Installer depuis l'URL", + "INSTALL_FROM_URL_TOOLTIP": "Install and addon from a URL", + "REFRESH_BUTTON": "Rafraîchir", + "REFRESH_TOOLTIP": "Refresh addon results", + "RESET_CATEGORY_TOOLTIP": "Reset Category", + "SEARCH_LABEL": "Chercher", + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "AUTHOR_COLUMN_HEADER": "Auteur", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Téléchargements", + "PROVIDER_COLUMN_HEADER": "Fournisseur", + "RELEASED_AT_COLUMN_HEADER": "Publié", + "STATUS_COLUMN_HEADER": "Statut" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "À propos de", + "ACCOUNT_TAB_TITLE": "Account", + "COLLAPSE_BUTTON_TITLE": "Collapse", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Expand", + "GET_ADDONS_TAB_TITLE": "Obtenir des Addons", + "GUIDE_TAB_TITLE": "Guide", + "MIGRATING_ADDONS": "Migration des addons...", + "MY_ADDONS_TAB_TITLE": "Mes Addons", + "NEWS_TAB_TITLE": "News", + "OPTIONS_TAB_TITLE": "Options" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count} {count, plural, =1{addon sélectionné} other{addons selectionnés}}", + "ALPHA_ADDON_CHANNEL": "Alpha", + "AUTO_UPDATE_ADDON_BUTTON": "Mise à jour automatique", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Notifications Enabled", + "BETA_ADDON_CHANNEL": "Bêta", + "CHANNEL_SUBMENU_TITLE": "Canal", + "IGNORE_ADDON_BUTTON": "Ignorer", + "PROVIDER_SUBMENU_TITLE": "Fournisseurs", + "REINSTALL_ADDON_BUTTON": "Réinstaller", + "REMOVE_ADDON_BUTTON": "Désinstaller", + "SHOW_FOLDER": "Afficher le dossier", + "STABLE_ADDON_CHANNEL": "Stable" + }, + "ADDON_IS_CODE_REPOSITORY": "Addon appears to be a code repository", + "ADDON_REMOVED_SNACKBAR": "Successfully removed: {addonName} ", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Souhaitez-vous changer le fournisseur d´addons pour {addonName} par {providerName}? Cette opération désinstallera l´addon existant et le remplacera par une copie du nouveau fournisseur.", + "TITLE": "Changer de fournisseur d´addon" + }, + "CHECK_UPDATES_BUTTON": "Vérifier les mises à jour", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Vérifier les dernières mises à jour des addons", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{update} other{updates}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Afficher les colonnes" + }, + "ERROR_SNACKBAR": "An error occurred", + "FILTER_LABEL": "Filter", + "FUNDING_TOOLTIP": { + "CUSTOM": "Supporter ce créateur", + "GENERIC": "Supporter ce créateur sur {platform}", + "GITHUB": "Supporter ce créateur sur GitHub", + "PATREON": "Supporter ce créateur sur Patreon", + "PAYPAL": "Supporter ce créateur sur PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Import/Export Addons", + "MULTIPLE_PROVIDERS_TOOLTIP": "Cet addon a de multiples fournisseurs", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{addon} other{addons}}", + "JOIN_DISCORD": "Discutez avec nous sur Discord", + "PATREON_SUPPORT": "Soutenez WowUp sur Patreon", + "SEARCH_RESULTS": "{count} {count, plural, =1{resultat} other{resultats}}", + "VIEW_GITHUB": "Voir le code sur Github", + "VIEW_GUIDE": "Consultez notre guide pour voir toutes les fonctionnalités de WowUp" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Required dependency missing", + "RESCAN_FOLDERS_BUTTON": "Réanalyser les dossiers", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Analysez votre dossier client pour trouver les addons installés", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "Faire une nouvelle analyse tentera de deviner les addons que vous avez actuellement installés, cela pourrait réinitialiser les informations connues des addons. Utilisez cette fonction lorsque certains addons ne sont pas reconnus ou que les versions des addons ne s'affichent pas correctement. Cette analyse ne supprimera jamais vos addons installés, seulement ce que WowUp sait à leur sujet.\n\nL'analyse peut prendre un peu de temps.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Débuter une réanalyse ?", + "SPINNER": { + "GATHERING_ADDONS": "Récolte des addons...", + "UPDATING": "Mise à jour {updateCount}/{addonCount}", + "UPDATING_WITH_ADDON_NAME": "Mise à jour {updateCount}/{addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "ADDON_INSTALL_BUTTON": "Installer", + "ADDON_UPDATE_BUTTON": "Mise à jour", + "AUTHOR_COLUMN_HEADER": "Auteur", + "AUTO_UPDATE_ICON_TOOLTIP": "Mise à jour automatique activée", + "GAME_VERSION_COLUMN_HEADER": "Version du jeu", + "LATEST_VERSION_COLUMN_HEADER": "Dernière version", + "PROVIDER_COLUMN_HEADER": "Fournisseur", + "PROVIDER_RELEASE_CHANNEL": "Canal", + "RELEASED_AT_COLUMN_HEADER": "Publié", + "STATUS_COLUMN_HEADER": "Statut", + "UPDATED_AT_COLUMN_HEADER": "Mis à jour" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Cela supprimera tous les dossiers associés de l'addon dans votre répertoire World of Warcraft.", + "CONFIRMATION_LESS_THAN_THREE": "Êtes-vous sûr de vouloir désinstaller les {count} addons suivants ?", + "CONFIRMATION_MORE_THAN_THREE": "Êtes-vous sûr de vouloir désinstaller les {count} addons sélectionnés ?", + "CONFIRMATION_ONE": "Êtes-vous certain de vouloir désinstaller {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} a {dependencyCount} {dependencyCount, plural, one{dépendance} other{dépendances}}. Voulez-vous également les supprimer ?", + "DEPENDENCY_TITLE": "Supprimer les dépendances de l'addon ?", + "TITLE": "Désinstaller {count, plural, =1{addon} other{addons}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "The installed addon did not match with any of the configured providers", + "UPDATE_ALL_BUTTON": "Tout mettre à jour", + "UPDATE_ALL_BUTTON_TOOLTIP": "Mettre à jour tous les addons pour ce client", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Mettre à jour tous les clients", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Mise à jour Retail / Classic" + }, + "WTF_BACKUP_BUTTON": "Interface Settings Backup" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "News link copied to clipboard", + "NEWS_LINK_COPY_TOOLTIP": "Copy news link", + "PAGE_CONTEXT_FOOTER": "{count} news stories", + "REFRESH_TOOLTIP": "Refresh news feed" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Ad or access key required", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "If you have requested a CurseForge API key you can input it here to connect to their API.", + "API_KEY_TITLE": "CurseForge API Key", + "PROVIDER_NOTE": "API Key Required" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Sélectionnez quels fournisseurs vous voulez utiliser pour la recherche et l'installation de nouveaux addons.", + "FIELD_LABEL": "Activer les fournisseurs d'addons", + "INPUT_LABEL": "Fournisseurs" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "A personal access token will increase the amount of GitHub API calls you can make, it is freely available. Learn More", + "PLACEHOLDER": "Personal Access Token", + "TITLE": "GitHub Personal Access Token" + }, + "TITLE": "Addons", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Switching to the Beta channel will allow you to receive experimental builds that contain bug fixes, as well as new and upcoming features. you may only go back to the current stable version by uninstalling your existing app and re-installing from wowup.io.\n\nWhile the Beta channel is functional, use at your own risk.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Switching to the Stable channel for the app will prevent you from receiving more Beta builds, the next update will be the next Stable release.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "Langue actuelle", + "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", + "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Show a badge on the app icon with the number of addons with available updates.", + "ENABLE_APP_BADGE_LABEL": "Enable app badge notification", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Activer les fenêtres contextuelles système comme la mise à jour auto des addons.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Activer le système de notification", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "A l'ouverture de la fenêtre de détails d'un addon, selectionne automatiquement le dernier onglet utilisé", + "KEEP_LAST_OPENED_TAB_LABEL": "Garder le dernier onglet de détail ouvert", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Lorsque vous fermez la fenêtre WowUp, minimise dans la barre des menus", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Lorsque vous fermez la fenêtre WowUp, minimise dans la barre d'état système.", + "MINIMIZE_ON_CLOSE_LABEL": "Minimiser à la fermeture", + "PROTOCOL_DESCRIPTION": "WowUp will register a custom URI protocol in your system and handle its incoming inquries", + "PROTOCOL_LABEL": "Allow WowUp to handle wowup:// URI", + "SCALE_DESCRIPTION": "Change zoom factor for entire app.", + "SCALE_LABEL": "Echelle", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Changer la langue par défaut nécessite le redémarrage de l'application.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Définir une langue par défaut", + "SET_LANGUAGE_DESCRIPTION": "Sélectionner une langue pour l'application.", + "SET_LANGUAGE_LABEL": "Définir une langue", + "START_MINIMIZED_DESCRIPTION": "WowUP sera démarré minimisé et n'apparaîtra pas à l'écran", + "START_MINIMIZED_LABEL": "Lancer WowUp minimisé", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp sera démarré au lancement de votre système d'exploitation", + "START_WITH_SYSTEM_LABEL": "Lancer WowUp avec le système", + "TELEMETRY_DESCRIPTION": "Aidez à améliorer WowUp en envoyant des données d'installation et / ou des erreurs anonymes.", + "TELEMETRY_LABEL": "Télémétrie", + "THEME_DESCRIPTION": "Changez la couleur du thème par ce que vous aimez", + "THEME_LABEL": "Couleur du thème", + "TITLE": "Application", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp can set itself as the default handler for CurseForge download links. This may cause issues if you try to use the CursForge app, are you sure you want to continue?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Handle CurseForge Downloads?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Voulez-vous redémarrer ?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "La désactivation de l'accélération matérielle peut résoudre les problèmes de FPS et résoudre d'autres problèmes de rendu dans cette application. La modification de ce paramètre nécessite un redémarrage.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "La désactivation de l'accélération matérielle nécessite le redémarrage de l'application.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "L'activation de l'accélération matérielle nécessite le redémarrage de l'application.", + "USE_HARDWARE_ACCELERATION_LABEL": "Activer l'accélération matérielle", + "USE_SYMLINK_SUPPORT": "Enable Symlink Support", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Enabling symlink support will allow WowUp to recognize symlinks when performing a re-scan. Warning: If you do not know what a symlink is, you do not need this. When updating symlinks will currently be replaced with an actual folder and the link lost.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Enable symlink support?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Allow WowUp to scan symlink folders in your addon folder. Warning: they will be replaced when updating/installing." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Dump des données de débogage", + "DEBUG_DATA_DESCRIPTION": "Log les données de débogage pour aider à diagnostiquer les problèmes potentiels. Cela peut être trouvé dans votre dernier fichier journal pour les curieux.", + "DEBUG_DATA_LABEL": "Déboguer les données", + "LOG_FILES_BUTTON": "Afficher les fichiers de log", + "LOG_FILES_DESCRIPTION": "Ouvrez le dossier qui contient vos derniers fichiers journaux.", + "LOG_FILES_LABEL": "Fichiers de log", + "TITLE": "Déboguage" + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "Addons", + "APPLICATION": "Application", + "CLIENTS": "Clients", + "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Add New", + "AUTO_UPDATE_DESCRIPTION": "Les addons nouvellement installés seront mises à jour automatiquement par défaut", + "AUTO_UPDATE_LABEL": "Mise à jour automatique", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Cancel", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Voulez-vous vraiment effacer le chemin d'installation de {clientName} ? Cette action supprimera toutes les informations des addons stockées pour ce client.\n\nVos dossiers d'addons ne seront pas supprimées.", + "TITLE": "Effacer le chemin d'installation ?" + }, + "CLIENT_TYPE_INPUT_HINT": "Ce dossier contient le client {clientTypeName} identifié par le dossier \"{clientFolderName}\"", + "CLIENT_TYPE_PATH_LABEL": "Chemin de {clientTypeName}", + "DEFAULT_ADDON_CHANNEL_LABEL": "Canal d'extension par défaut", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canal d'Addon", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Edit", + "MOVE_DOWN_BUTTON": "Move Down", + "MOVE_UP_BUTTON": "Move Up", + "NO_CLIENTS_FOUND_TEXT": "No World of Warcraft installations found, please make sure your Battle.net client is up to date or add a client manually", + "OPEN_FOLDER_BUTTON": "Open Folder", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Sélectionner", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Remove", + "RESCAN_CLIENTS_BUTTON": "Réanalyser", + "RESCAN_CLIENTS_LABEL": "Réanalyser les clients de World of Warcraft installés", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Save", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Are you sure you want to apply this backup to your interface settings?\n\nMake sure that the World of Warcraft game is not running before you apply a backup.\n\nThis operation cannot be undone.", + "TITLE": "Apply WTF Backup?" + }, + "BACKUP_APPLY_SUCCESS": "Successfully applied backup: {name}", + "BACKUP_COUNT_TEXT": "Found {count} {count, plural, =1{backup} other{backups}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Applying backup...", + "CREATING_BACKUP": "Creating backup from {count} files...", + "LOADING_BACKUPS": "Loading backups...", + "REMOVING_BACKUP": "Removing backup..." + }, + "CREATE_BACKUP_BUTTON": "Create Backup", + "DELETE_CONFIRMATION": { + "MESSAGE": "Are you sure you want to delete backup {name}?\nThis cannot be undone.", + "TITLE": "Delete WTF Backup?" + }, + "DIALOG_TITLE": "WTF Settings Backup: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Failed to apply backup: {name}", + "FAILED_TO_DELETE": "Failed to delete backup: {name}", + "GENERIC_ERROR": "There was an issue processing this backup", + "INVALID_CONTENTS": "There was an issue processing this backup", + "INVALID_CREATED_AT": "There was an issue processing this backup", + "INVALID_CREATED_BY": "There was an issue processing this backup" + }, + "SHOW_FOLDER_BUTTON": "Show Folder", + "TOOL_TIP": { + "APPLY_BUTTON": "Apply this backup", + "DELETE_BUTTON": "Delete this backup" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/it.json b/WowUp/wowup-electron/src/assets/i18n/it.json new file mode 100644 index 0000000..6635a6a --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/it.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Active addons: {count}", + "ADDED_BADGE_TOOLTIP": "We will attempt to install this addon", + "CONFLICT_BADGE_TOOLTIP": "Addons in conflict will not be modified", + "COPY_BUTTON": "Copy", + "DIALOG_TITLE": "Import/Export Addons: {clientType}", + "EXPORT_STRING_COPIED": "Export string copied to clipboard", + "EXPORT_STRING_PASTED": "Inserted clipboard contents", + "EXPORT_TAB_LABEL": "Export", + "EXPORT_TEXT_LABEL": "Addon Export Data", + "GENERIC_IMPORT_ERROR": "An error occurred during import", + "IGNORED_ADDON_COUNT": "Ignored addons: {count}", + "IMPORT_ADDED_COUNT": "{count} added", + "IMPORT_BADGE_ADDED": "New", + "IMPORT_BADGE_CONFLICT": "Conflict", + "IMPORT_BADGE_NO_CHANGE": "No Change", + "IMPORT_BUTTON": "Import", + "IMPORT_CONFLICT_COUNT": "{count} in conflict", + "IMPORT_NO_CHANGE_COUNT": "{count} unchanged", + "IMPORT_STRING_INVALID": "Import string was invalid", + "IMPORT_TAB_LABEL": "Import", + "IMPORT_TEXT_INSTRUCTIONS": "Paste WowUp addon export data into the field below to get started", + "IMPORT_TEXT_LABEL": "Import data", + "IMPORT_TOTAL_COUNT": "Importing {count} {count, plural, =1{addon} other{addons}}", + "INSTALL_BUTTON": "Install", + "INVALID_CLIENT_TYPE": "Import string did not match your selected client type", + "NO_CHANGE_BADGE_TOOLTIP": "You already have this addon installed", + "PASTE_BUTTON": "Paste", + "PROVIDER_MISMATCH": "Addon provider does not match", + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Copia", + "CUT": "Taglia", + "LABEL": "Modifica", + "PASTE": "Incolla", + "REDO": "Ripeti", + "SELECT_ALL": "Seleziona Tutto", + "UNDO": "Annulla" + }, + "QUIT": "Esci", + "VIEW": { + "FORCE_RELOAD": "Forza Ricarica", + "LABEL": "Visualizza", + "RELOAD": "Ricarica", + "TOGGLE_DEV_TOOLS": "Attiva Strumenti di Sviluppo", + "TOGGLE_FULL_SCREEN": "Passa a Schermo Sntero", + "ZOOM_IN": "Zoom Più", + "ZOOM_OUT": "Zoom Meno", + "ZOOM_RESET": "Reset Zoom" + }, + "WINDOW": { + "CLOSE": "Chiudi", + "LABEL": "Finestra" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Automaticamente aggiornato\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "Automaticamente {count, plural, =1{aggiornato} other{aggiornati}} {count, plural, one{un addon} other{{count} addons}}.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Aggiornamenti Automatici", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Esci dalla modalità schermo intero", + "FULLSCREEN_SNACKBAR": { + "MAC": "Premere ^⌘F per uscire dalla modalità schermo intero", + "WINDOWS": "Premere F11 per uscire dalla modalità schermo intero" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nSei sicuro di voler aprire questa pagina di terze parti sul tuo browser predefinito?", + "TITLE": "Stai per lasciare WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Sconosciuto" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Scansione degli addons completata.", + "ADDON_SCAN_STARTED": "Scansione degli addons iniziata...", + "ADDON_SCAN_UPDATE": "Scansionando {count, plural, one{una cartella} other{{count} cartelle}}..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Controlla Aggiornamenti", + "QUIT_ACTION": "Chiudi", + "SHOW_ACTION": "Mostra" + }, + "THEME": { + "ALLIANCE": "Alleanza", + "DEFAULT": "WowUp", + "GROUP_DARK": "Scuro", + "GROUP_LIGHT": "Chiaro", + "HORDE": "Orda" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Schermo Intero", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Controllo aggiornamenti", + "DOWNLOADED_TOOLTIP": "Installa l'aggiornamento di WowUp", + "DOWNLOADING_UPDATE": "Scaricando l'aggiornamento", + "INSTALL_MESSAGE": "Vuoi riavviare WowUp per installare l'aggiornamento?", + "INSTALL_TITLE": "Aggiornamento di WowUp pronto", + "NOT_AVAILABLE": "L'ultima versione di WowUp è già installata", + "PORTABLE_DOWNLOAD_MESSAGE": "Vuoi scaricare manualmente l'ultima versione portatile?\n\nDovrai chiudere l'applicazione manualmente e sovrascrivere la nuova versione.", + "PORTABLE_DOWNLOAD_TITLE": "Richiesto Download Manuale", + "SNACKBAR_ACTION": "Aggiorna & Riavvia", + "SNACKBAR_TEXT": "È disponibile una nuova versione di WowUp", + "TOOLTIP": "Aggiornamento di WowUp disponibile", + "UPDATE_AVAILABLE": "Avviando il downlaod", + "UPDATE_ERROR": "Errore nel download dell'ultima versione di WowUp" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Trofei", + "ACTION_BARS": "Barre delle Azioni", + "ALL_ADDONS": "Tutti gli Addons", + "AUCTION_ECONOMY": "Asta & Economia", + "BAGS_INVENTORY": "Borse & Inventario", + "BOSS_ENCOUNTERS": "Incontri coi Boss", + "BUFFS_DEBUFFS": "Buffs & Debuffs", + "BUNDLES": "Collezioni", + "CHAT_COMMUNICATION": "Chat & Comunicazione", + "CLASS": "Classi", + "COMBAT": "Combattimento", + "COMPANIONS": "Compagni", + "DATA_EXPORT": "Esportazione Dati", + "DEVELOPMENT_TOOLS": "Strumenti di Sviluppo", + "GUILD": "Gilda", + "LIBRARIES": "Librerie", + "MAIL": "Mail", + "MAP_MINIMAP": "Mappa & Minimappa", + "MISCELLANEOUS": "Varie", + "MISSIONS": "Missioni", + "PLUGINS": "Plugins", + "PROFESSIONS": "Professioni", + "PVP": "PVP", + "QUESTS_LEVELING": "Quests & Leveling", + "ROLEPLAY": "Gioco di Ruolo", + "TOOLTIPS": "Tooltips", + "UNIT_FRAMES": "Frames delle Unità" + }, + "ADDON_STATE": { + "IGNORED": "Ignorato", + "INSTALL": "Installa", + "PENDING": "In attesa", + "UNAVAILABLE": "Unavailable", + "UNAVAILABLE_TOOLTIP": "This author or provider has made this addon unavailable", + "UNINSTALL": "Disinstalla", + "UNKNOWN": "Sconosciuto", + "UPDATE": "Aggiorna", + "UPTODATE": "Aggiornato", + "WARNING": "Avviso" + }, + "ADDON_STATUS": { + "BACKINGUP": "Backup in corso...", + "COMPLETE": "Installato", + "DOWNLOADING": "Download in corso...", + "ERROR": "Errore", + "INSTALLING": "Installazione in corso...", + "PENDING": "In attesa", + "RETRY": "Retrying...", + "UNINSTALLING": "Disinstallazione in corso...", + "UPDATING": "Aggiornamento in corso..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "Abbiamo trovato un problema con questo addon. Non siamo più in grado di aggiornare questo addon o di fornire dettagli.", + "GENERIC_TOOLTIP": "Abbiamo trovato un problema con questo addon", + "MISSING_ON_PROVIDER_DESCRIPTION": "Questo addon sembra essere stato rimosso dal suo autore o dal provider.
Non siamo più in grado di aggiornare questo addon o di fornire dettagli.", + "MISSING_ON_PROVIDER_TOOLTIP": "Questo addon sembra essere stato rimosso dal provider", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} ha restituito questo addon correttamente, ma non c'erano file corrispondenti a questa versione del gioco.
Non appena ci sarà un aggiornamento che si adatta a questa versione del gioco, questo avviso dovrebbe scomparire.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} non aveva files corrispondenti alla versione di gioco ", + "TOC_NAME_MISMATCH_DESCRIPTION": "This addon's folder name did not match with the expected toc file, you may experience issues with this addon in-game.", + "TOC_NAME_MISMATCH_TOOLTIP": "This addon's folder does not match the toc" + }, + "CLIENT_TYPES": { + "BETA": "Beta di Dragonflight", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Reame Pubblico di Prova - PTR (Wrath Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count, plural, one{Un giorno} other{{count} giorni}} fa", + "HOURS_AGO": "{count, plural, one{Un'ora} other{{count} ore}} fa", + "JUST_NOW": "Ora", + "MONTHS_AGO": "{count, plural, one{Un mese} other{{count} mesi}} fa", + "YEARS_AGO": "{count, plural, one{Un anno} other{{count} anni}} fa", + "YESTERDAY": "Ieri" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount, plural, one{Una dipendenza} other{{dependencyCount} dipendenze}} {dependencyCount, plural, one{richiesta} other{richieste}}" + }, + "DOWNLOAD_COUNT": { + "e+0": "{count}", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count, plural, one{Mille} other{{count} mila}}", + "e+4": "{count, plural, one{Mille} other{{count} mila}}", + "e+5": "{count, plural, one{Mille} other{{count} mila}}", + "e+6": "{count, plural, one{Un milione} other{{count} milioni}}", + "e+7": "{count, plural, one{Un milione} other{{count} milioni}}", + "e+8": "{count, plural, one{Un milione} other{{count} milioni}}", + "e+9": "{count, plural, one{Un miliardo} other{{count} miliardi}}" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alpha", + "BETA": "Beta", + "STABLE": "Stabile" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Failed to toggle instant updates for your account. Please try again later, or reach out on Discord.", + "ADDON_INSTALL_ERROR": "Fallito nell'installare l'addon {addonName}. Per favore riprovare più tardi.", + "ADDON_SCAN_ERROR": "Errore nel sincronizzare le cartelle dell'addon con {providerName}, per favore riprovare più tardi.", + "ADDON_SYNC_ERROR": "Errore nel controllare gli aggioranementi da: {providerName}", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: Errore nel controllare gli aggiornamenti per {addonName} da {providerName}, per favore riprovare più tardi.", + "CHANGE_PROVIDER_ERROR": "Errore nel cambio di provider per {addonName} con {providerName}", + "GITHUB_LIMIT_ERROR": "Hai raggiunto il limite di {max} richieste per la tua API di GitHub.\nPer favore attendere fino a {reset} e riprovare.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Errore nel controllare gli aggiornamenti per {addonName}.\nPer favore controllare che la repository sia corretta o impostare questo addon per essere ignorato." + }, + "PROGRESS_SPINNER": { + "LOADING": "Caricamento in corso..." + }, + "PROVIDER_ERROR": "C'è stato un errore nel contattare {providerName}", + "SEARCH": { + "NO_ADDONS": "Nessun addon trovato" + }, + "WOW_EXE_SELECTION_NAME": "WoW (eseguibile)" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "Addon ID:", + "BY_AUTHOR": "Di {authorName}", + "CHANGELOG_TAB": "Changelog", + "COPY_ADDON_ID_SNACKBAR": "ID dell'addon copiato negli appunti", + "COPY_ADDON_ID_TOOLTIP": "Copia l'ID dell'addon negli appunti", + "DEPENDENCY_TEXT": "Questo addon ha {dependencyCount, plural, one{una dipendenza} other{{dependencyCount} dipendenze}} {dependencyCount, plural, one{richiesta} other{richieste}}", + "DESCRIPTION_NOT_FOUND": "Nessuna descrizione trovata", + "DESCRIPTION_TAB": "Descrizione", + "FUNDING_LINK_TITLE": "Supporta questo autore", + "IMAGES_TAB": "Previews", + "MISSING_DEPENDENCIES": "Dipendenze mancanti", + "NO_CHANGELOG_TEXT": "Nessun changelog disponibile", + "VIEW_IN_BROWSER_BUTTON": "Visualizza nel browser", + "VIEW_ON_PROVIDER_PREFIX": "Visualizza su" + }, + "ALERT": { + "ERROR_TITLE": "Errore", + "POSITIVE_BUTTON": "Okay" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "No", + "POSITIVE_BUTTON": "Sì" + }, + "CURSE_MIGRATION": { + "MESSAGE": "The CurseForge API has been shutdown so WowUp can no longer fetch addon information from it.
Unfortunately, this means that the CurseForge provider has been removed and your addons from CurseForge will no longer be updated.
It is recommended that you perform a re-scan in order to see what addons can be found on other providers.
The Re-Scan process may take a while.
Read more here.", + "NEGATIVE_BUTTON": "Manually Re-Scan", + "POSITIVE_BUTTON": "Automatically Re-Scan", + "RE_SCAN_ERROR": "The automatic Re-Scan failed, please try again manually.", + "RE_SCAN_SUCCESS": "The automatic Re-Scan has finished you're ready to go.", + "TITLE": "CurseForge Migration" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Addon installato!", + "ADDON_INSTALLING": "Installazione addon in corso...", + "CANCEL_BUTTON": "Chiudi", + "ERRORS": { + "ADDON_NOT_FOUND": "Nessun addon trovato per il protocollo: {protocol}", + "GENERIC": "Errore nel recuper dei dati per il protocollo: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "Nessun client di WoW è installato per il protocollo: {protocol}" + }, + "INSTALL_BUTTON": "Installa", + "TITLE": "Installa Addon Da {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "Addon URL", + "ADDON_URL_INPUT_PLACEHOLDER": "Esempio URL di GitHub o WowInterface", + "CLOSE_BUTTON": "Chiudi", + "DESCRIPTION": "Se si desidera installare un addon direttamente da un URL, incollare l'URL qui sotto per iniziare.", + "DOWNLOAD_COUNT": "{count, plural, one{Un download} other{{textCount} downloads}} su {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "Non è stato trovato alcun asset per scaricare {message}.\n\nÈ necessario ci sia un file zip valido nella release affinché che WowUp possa scaricarlo.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "Non è stato trovato alcun asset da scaricare da {message}.\n\nA valid zip file ending with '-bc' is required in order for WowUp to download it.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "Non è stato trovato alcun asset da scaricare da {message}.\n\nÈ necessario ci sia un file zip valido che termini in '-classic' nella release affinché WowUp possa scaricarlo.", + "FAILED_TO_CONNECT": "Impossibile connettersi all'API, per favore riprovare più tardi.", + "INSTALL_FAILED": "Qualcosa è andato storto nell'installazione dell'addon, per favore riprovare.\n\nSe questo messaggio continua ad apparire puoi chiederci aiuto su Discord, nel canale #help-me.", + "INVALID_URL": "Il valore fornito non è un URL valido. Esempi di URLs di addon validi:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "Non è stato trovato alcun addon, assicurarsi che l'URL sia corretto.\n\nSe si sta installando da GitHub, assicurarsi che la repository abbia un release tag con un file zip contenente l'addon.", + "NO_RELEASE_FOUND": "Non è stata trovata alcuna release per {message}.\n\nÈ necessaro ci sia un file zip negli assets affinché WowUp possa scaricarlo.", + "NO_SEARCH_RESULTS": "Nessun risultato dalla ricerca.", + "TITLE": "Installazione dell'Addon Fallita", + "WRATH_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-wrath' is required in order for WowUp to download it." + }, + "IMPORT_ASSET_WARNING": "We were unable to verify if the latest release of this addon is compatible with your selected client.\n\nBut we did find a zip file \"{zipName}\".\n\nInstall at your own risk.", + "IMPORT_BUTTON": "Importa", + "IMPORT_WARNING_TITLE": "Addon Import Warning", + "INSTALL_BUTTON": "Installa", + "INSTALL_SUCCESS_LABEL": "Installato!", + "SUPPORTED_SOURCES": "(Supporta WowInterface e GitHub)", + "TITLE": "Installa un Addon tramite URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Note della Patch {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Confirm", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, + "TITLE": "WowUp Permissions Setup", + "WAGO": { + "DESCRIPTION": "Enabled the Wago.io addon provider, this will display an ad required to use their service.\nThe ads directly benefit the authors of your favorite addons!", + "TOGGLE_LABEL": "Enable Wago.io Provider" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "Questa non sembra essere una applicazione valida di World of Warcraft :\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Vuoi contribuire a migliorare WowUp inviando dati di installazione anonimi e/o errori??", + "NEGATIVE_BUTTON": "No grazie", + "POSITIVE_BUTTON": "Certo!", + "TITLE": "Telemetria WowUp" + }, + "TRUST_DOMAIN_CHECKBOX": "Considera affidabile questo dominio e non chiedermelo più in futuro" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Riconoscimenti", + "CHANGE_LOG_SECTION_LABEL": "Changelog", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Dai un'occhiata al sito!" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Login Now!", + "LOGOUT_BUTTON": "Logout", + "LOGOUT_CONFIRMATION_MESSAGE": "Are you sure you want to log out? All of your local account data will be removed, until you login again.", + "LOGOUT_CONFIRMATION_TITLE": "Logout?", + "MANAGE_ACCOUNT_BUTTON": "Manage Account", + "TITLE": "Account" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Categorie", + "ADDON_CATEGORIES_MENU_TITLE": "Categorie Addons", + "ADDON_CATEGORIES_SELECTED_TITLE": "Categoria: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Esplora varie categorie", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Installa da URL", + "INSTALL_FROM_URL_TOOLTIP": "Installa un addon da un URL", + "REFRESH_BUTTON": "Ricarica", + "REFRESH_TOOLTIP": "Ricarica risultati addons", + "RESET_CATEGORY_TOOLTIP": "Ripristina categoria", + "SEARCH_LABEL": "Cerca", + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "AUTHOR_COLUMN_HEADER": "Autore/i", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Numero di download", + "PROVIDER_COLUMN_HEADER": "Provider", + "RELEASED_AT_COLUMN_HEADER": "Rilasciato", + "STATUS_COLUMN_HEADER": "Stato" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "Informazioni", + "ACCOUNT_TAB_TITLE": "Account", + "COLLAPSE_BUTTON_TITLE": "Collapse", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Expand", + "GET_ADDONS_TAB_TITLE": "Ottieni Addons", + "GUIDE_TAB_TITLE": "Guide", + "MIGRATING_ADDONS": "Trasferimento degli addons in corso...", + "MY_ADDONS_TAB_TITLE": "I Miei Addons", + "NEWS_TAB_TITLE": "Notizie", + "OPTIONS_TAB_TITLE": "Opzioni" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count, plural, =1{Un addon} other{{count} addons}} {count, plural, =1{selezionato} other{selezionati}}", + "ALPHA_ADDON_CHANNEL": "Alpha", + "AUTO_UPDATE_ADDON_BUTTON": "Aggiornamenti Automatici", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Notifications Enabled", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "Canale", + "IGNORE_ADDON_BUTTON": "Ignora", + "PROVIDER_SUBMENU_TITLE": "Providers", + "REINSTALL_ADDON_BUTTON": "Reinstalla", + "REMOVE_ADDON_BUTTON": "Rimuovi", + "SHOW_FOLDER": "Mostra Cartella", + "STABLE_ADDON_CHANNEL": "Stabile" + }, + "ADDON_IS_CODE_REPOSITORY": "L'addon sembra essere un code repository", + "ADDON_REMOVED_SNACKBAR": "Rimosso con successo: {addonName} ", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Vuoi cambiare il provider dell'addon {addonName} con {providerName}? Questa operazione disinstallerà il tuo addon esistente e lo sostituirà con una copia dal nuovo provider.", + "TITLE": "Cambiare Provider dell'Addon?" + }, + "CHECK_UPDATES_BUTTON": "Controlla Aggiornamenti", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Controlla gli ultimi aggiornamenti degli addons", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{update} other{updates}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Mostra Colonne" + }, + "ERROR_SNACKBAR": "C'è stato un errore", + "FILTER_LABEL": "Filtra", + "FUNDING_TOOLTIP": { + "CUSTOM": "Supporta questo autore", + "GENERIC": "Supporta questo autore su {platform}", + "GITHUB": "Supporta questo autore su GitHub", + "PATREON": "Supporta questo autore su Patreon", + "PAYPAL": "Supporta questo autore su PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Import/Export Addons", + "MULTIPLE_PROVIDERS_TOOLTIP": "Questo addon ha più di un provider", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{addon} other{addons}}", + "JOIN_DISCORD": "Contattaci su Discord", + "PATREON_SUPPORT": "Supporta WowUp su Patreon", + "SEARCH_RESULTS": "{count, plural, =1{Un risultato} other{{count} risultati}}", + "VIEW_GITHUB": "Controlla il codice su GitHub", + "VIEW_GUIDE": "Controlla la nostra guida per vedere cosa può fare Wowup" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Dipendenze richieste mancanti", + "RESCAN_FOLDERS_BUTTON": "Riscansiona Cartelle", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Scansiona la cartella client per gli addons installati", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "Eseguire una nuova scansione verificherà quali addons sono correntemente installati; ciò resetterà le informazioni conosciute degli addons. Utilizzare questa funzione quando alcuni addons non sono riconosciuti o le versioni degli addons non sono mostrate correttamente. Questa operazione non eliminerà i tuoi addons installati, solo cosa WowUp sa riguardo a loro. Questa operazione può richiedere un po' di tempo.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Iniziare una nuova scansione?", + "SPINNER": { + "GATHERING_ADDONS": "Raccogliendo gli addons...", + "UPDATING": "Aggiornando {updateCount}/{addonCount}", + "UPDATING_WITH_ADDON_NAME": "Aggiorando {updateCount}/{addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "ADDON_INSTALL_BUTTON": "Installa", + "ADDON_UPDATE_BUTTON": "Aggiorna", + "AUTHOR_COLUMN_HEADER": "Autore/i", + "AUTO_UPDATE_ICON_TOOLTIP": "Aggiornamenti automatici abilitati", + "GAME_VERSION_COLUMN_HEADER": "Versione Gioco", + "LATEST_VERSION_COLUMN_HEADER": "Ultima Versione", + "PROVIDER_COLUMN_HEADER": "Provider", + "PROVIDER_RELEASE_CHANNEL": "Canale del Provider", + "RELEASED_AT_COLUMN_HEADER": "Rilasciato", + "STATUS_COLUMN_HEADER": "Stato", + "UPDATED_AT_COLUMN_HEADER": "Aggiornato" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "WowUp rimuoverà l'addon selezionato dal percorso 'Interface>AddOns'. Le impostazioni del personaggio per questo addon non verranno rimosse.", + "CONFIRMATION_LESS_THAN_THREE": "Sei sicuro di voler rimuovere {count, plural, =1{il seguente} other{i seguenti}} {count, plural, one{addon} other{{count} addons}}?", + "CONFIRMATION_MORE_THAN_THREE": "Sei sicuro di voler rimuovere i {count} addons selezionati?", + "CONFIRMATION_ONE": "Sei sicuro di voler rimuovere {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} ha {dependencyCount, plural, one{una dipendenza} other{{dependencyCount} dipendenze}}. Vuoi rimuover{dependencyCount, plural, one{la} other{le}}?", + "DEPENDENCY_TITLE": "Rimuovere Dipendenze dell'Addon?", + "TITLE": "Disinstallare {count, plural, =1{l'addon} other{gli addons}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "L'addon installato non corrispondeva a nessun provider configurato", + "UPDATE_ALL_BUTTON": "Aggiorna Tutto", + "UPDATE_ALL_BUTTON_TOOLTIP": "Aggiorna tutti gli addons per questo client", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Aggiorna Tutti i Clients", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Aggiorna Retail/Classic" + }, + "WTF_BACKUP_BUTTON": "Interface Settings Backup" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "News link copied to clipboard", + "NEWS_LINK_COPY_TOOLTIP": "Copy news link", + "PAGE_CONTEXT_FOOTER": "{count} nuove storie", + "REFRESH_TOOLTIP": "Aggiorna feed notizie" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Ad or access key required", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "If you have requested a CurseForge API key you can input it here to connect to their API.", + "API_KEY_TITLE": "CurseForge API Key", + "PROVIDER_NOTE": "API Key Required" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Selezionare quali providers utilizzare per cercare ed installare nuovi addons", + "FIELD_LABEL": "Abilita Providers degli Addons", + "INPUT_LABEL": "Providers" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "A personal access token will increase the amount of GitHub API calls you can make, it is freely available. Learn More", + "PLACEHOLDER": "Personal Access Token", + "TITLE": "GitHub Personal Access Token" + }, + "TITLE": "Addons", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Switching to the Beta channel will allow you to receive experimental builds that contain bug fixes, as well as new and upcoming features. you may only go back to the current stable version by uninstalling your existing app and re-installing from wowup.io.\n\nWhile the Beta channel is functional, use at your own risk.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Switching to the Stable channel for the app will prevent you from receiving more Beta builds, the next update will be the next Stable release.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "Lingua Corrente", + "CURSE_PROTOCOL_DESCRIPTION": "Quando si scaricano addons dal sito Web CurseForge, WowUp ne gestirà l'installazione", + "CURSE_PROTOCOL_LABEL": "Gestisci i link dei downloads da CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Mostra un indicatore di notifica sull'icona dell'applicazione con il numero di addons con un aggiornamento disponibile.", + "ENABLE_APP_BADGE_LABEL": "Abilita indicatore di notifica", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Abilita i vari popup di notifica del sistema, come gli addons aggiornati automaticamente.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Notifiche di Sistema", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Alla chiusura, WowUp verrà ridotto a icona nella barra dei menu.", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Alla chiusura, WowUp verrà ridotto a icona nella barra delle applicazioni.", + "MINIMIZE_ON_CLOSE_LABEL": "Minimizza alla Chiusura", + "PROTOCOL_DESCRIPTION": "WowUp registrerà un protocollo URI personalizzato nel tuo sistema e gestirà le sue richieste in arrivo", + "PROTOCOL_LABEL": "Abilita il protocollo wowup:// URI", + "SCALE_DESCRIPTION": "Seleziona lo zoom per l'intera app.", + "SCALE_LABEL": "Zoom", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Cambiare lingua richiede il riavvio dell'applicazione.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Impostare una nuova lingua", + "SET_LANGUAGE_DESCRIPTION": "Scegli una lingua per l'applicazione", + "SET_LANGUAGE_LABEL": "Lingua", + "START_MINIMIZED_DESCRIPTION": "WowUp verrà lanciato minimizzato e non apparirà sullo schermo.", + "START_MINIMIZED_LABEL": "Lancia WowUp ridotto ad icona", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp verrà lanciato all'avvio del sistema.", + "START_WITH_SYSTEM_LABEL": "Lancia WowUp all'avvio del sistema", + "TELEMETRY_DESCRIPTION": "Aiuta a migliorare WowUp inviando dati di installazione e/o errori anonimi.", + "TELEMETRY_LABEL": "Telemetria", + "THEME_DESCRIPTION": "Seleziona il tema che preferisci", + "THEME_LABEL": "Tema", + "TITLE": "Applicazione", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp può impostarsi come gestore predefinito per i link di download da CurseForge. Ciò potrebbe causare problemi se provi a utilizzare l'app CurseForge, sei sicuro di voler continuare?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Gestire i download di CurseForge?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Vuoi riavviare?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "La disabilitazione dell'accelerazione hardware potrebbe risolvere i problemi di FPS e altri problemi di rendering in questa app. La modifica di questa impostazione richiede il riavvio di WowUp.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "La disabilitazione dell'accelerazione hardware richiede il riavvio dell'applicazione.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "L'abilitazione dell'accelerazione hardware richiede il riavvio dell'applicazione.", + "USE_HARDWARE_ACCELERATION_LABEL": "Accelerazione Hardware", + "USE_SYMLINK_SUPPORT": "Abilita il Supporto Symlink", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "L'abilitazione del supporto symlink consentirà a WowUp di riconoscere i symlinks durante l'esecuzione di una nuova scansione. Attenzione: se non sai cos'è un symlink, puoi anche non procedere. Durante l'aggiornamento, i symlinks verranno sostituiti con una cartella reale e il collegamento viene perso.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Abilitare il supporto symlink?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Consenti a WowUp di scansionare le cartelle dei symlinks nella cartella dell'addon. Attenzione: verranno sostituiti durante l'aggiornamento/installazione." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Dump Dati Di Debug", + "DEBUG_DATA_DESCRIPTION": "Registra i dati di debug per aiutare a diagnosticare potenziali problemi. Questi dati possono essere trovati nei tuoi ultimi file di log.", + "DEBUG_DATA_LABEL": "Dati di Debug", + "LOG_FILES_BUTTON": "Mostra File di Log", + "LOG_FILES_DESCRIPTION": "Aprire la cartella che contiene i tuoi ultimi file di log.", + "LOG_FILES_LABEL": "File di Log", + "TITLE": "Debug" + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "Addons", + "APPLICATION": "Applicazione", + "CLIENTS": "Clients", + "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Aggiungi Nuovo", + "AUTO_UPDATE_DESCRIPTION": "I nuovi addons installati saranno impostati per l'aggiornamento automatico ", + "AUTO_UPDATE_LABEL": "Aggiornamento Automatico", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Cancella", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Sei sicuro di voler ripulire il percorso d'installazione per {clientName}? Questo rimuverà tutte le informazioni salvate dell'addon per questo client.\n\nLe tue cartelle degli addons non verranno rimosse.", + "TITLE": "Ripulire Percorso d'Installazione?" + }, + "CLIENT_TYPE_INPUT_HINT": "La cartella che contiene il client {clientTypeName}: \"{clientFolderName}\"", + "CLIENT_TYPE_PATH_LABEL": "Percorso {clientTypeName}", + "DEFAULT_ADDON_CHANNEL_LABEL": "Canale Addon Predefinito", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canale Addon", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Modifica", + "MOVE_DOWN_BUTTON": "Muovi in Giù", + "MOVE_UP_BUTTON": "Muovi in Su", + "NO_CLIENTS_FOUND_TEXT": "No World of Warcraft installations found, please make sure your Battle.net client is up to date or add a client manually", + "OPEN_FOLDER_BUTTON": "Open Folder", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Seleziona", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Rimuovi", + "RESCAN_CLIENTS_BUTTON": "Riscansiona", + "RESCAN_CLIENTS_LABEL": "Riscansiona i prodotti World of Warcraft installati", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Salva", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Are you sure you want to apply this backup to your interface settings?\n\nMake sure that the World of Warcraft game is not running before you apply a backup.\n\nThis operation cannot be undone.", + "TITLE": "Apply WTF Backup?" + }, + "BACKUP_APPLY_SUCCESS": "Successfully applied backup: {name}", + "BACKUP_COUNT_TEXT": "Found {count} {count, plural, =1{backup} other{backups}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Applying backup...", + "CREATING_BACKUP": "Creating backup from {count} files...", + "LOADING_BACKUPS": "Loading backups...", + "REMOVING_BACKUP": "Removing backup..." + }, + "CREATE_BACKUP_BUTTON": "Create Backup", + "DELETE_CONFIRMATION": { + "MESSAGE": "Are you sure you want to delete backup {name}?\nThis cannot be undone.", + "TITLE": "Delete WTF Backup?" + }, + "DIALOG_TITLE": "WTF Settings Backup: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Failed to apply backup: {name}", + "FAILED_TO_DELETE": "Failed to delete backup: {name}", + "GENERIC_ERROR": "There was an issue processing this backup", + "INVALID_CONTENTS": "There was an issue processing this backup", + "INVALID_CREATED_AT": "There was an issue processing this backup", + "INVALID_CREATED_BY": "There was an issue processing this backup" + }, + "SHOW_FOLDER_BUTTON": "Show Folder", + "TOOL_TIP": { + "APPLY_BUTTON": "Apply this backup", + "DELETE_BUTTON": "Delete this backup" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/ko.json b/WowUp/wowup-electron/src/assets/i18n/ko.json new file mode 100644 index 0000000..2e0756a --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/ko.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Active addons: {count}", + "ADDED_BADGE_TOOLTIP": "We will attempt to install this addon", + "CONFLICT_BADGE_TOOLTIP": "Addons in conflict will not be modified", + "COPY_BUTTON": "Copy", + "DIALOG_TITLE": "Import/Export Addons: {clientType}", + "EXPORT_STRING_COPIED": "Export string copied to clipboard", + "EXPORT_STRING_PASTED": "Inserted clipboard contents", + "EXPORT_TAB_LABEL": "Export", + "EXPORT_TEXT_LABEL": "Addon Export Data", + "GENERIC_IMPORT_ERROR": "An error occurred during import", + "IGNORED_ADDON_COUNT": "Ignored addons: {count}", + "IMPORT_ADDED_COUNT": "{count} added", + "IMPORT_BADGE_ADDED": "New", + "IMPORT_BADGE_CONFLICT": "Conflict", + "IMPORT_BADGE_NO_CHANGE": "No Change", + "IMPORT_BUTTON": "Import", + "IMPORT_CONFLICT_COUNT": "{count} in conflict", + "IMPORT_NO_CHANGE_COUNT": "{count} unchanged", + "IMPORT_STRING_INVALID": "Import string was invalid", + "IMPORT_TAB_LABEL": "Import", + "IMPORT_TEXT_INSTRUCTIONS": "Paste WowUp addon export data into the field below to get started", + "IMPORT_TEXT_LABEL": "Import data", + "IMPORT_TOTAL_COUNT": "Importing {count} {count, plural, =1{addon} other{addons}}", + "INSTALL_BUTTON": "Install", + "INVALID_CLIENT_TYPE": "Import string did not match your selected client type", + "NO_CHANGE_BADGE_TOOLTIP": "You already have this addon installed", + "PASTE_BUTTON": "Paste", + "PROVIDER_MISMATCH": "Addon provider does not match", + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Versions do not match" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Why am I seeing this ad?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "In order to use wago.io / CurseForge as an addon provider and support their authors for their hard work on your favorite addons we are required to show this advertisement.\n\nIf you do not want to see this ad, you can always disable wago.io / CurseForge as a provider in the options tab.", + "TITLE": "Why am I seeing this ad?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Copy", + "CUT": "Cut", + "LABEL": "Edit", + "PASTE": "Paste", + "REDO": "Redo", + "SELECT_ALL": "SelectAll", + "UNDO": "Undo" + }, + "QUIT": "Quit", + "VIEW": { + "FORCE_RELOAD": "Force Reload", + "LABEL": "View", + "RELOAD": "Reload", + "TOGGLE_DEV_TOOLS": "Toggle Dev Tools", + "TOGGLE_FULL_SCREEN": "Toggle Full Screen", + "ZOOM_IN": "Zoom In", + "ZOOM_OUT": "Zoom Out", + "ZOOM_RESET": "Reset Zoom" + }, + "WINDOW": { + "CLOSE": "Close", + "LABEL": "Window" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "다음 애드온들이 자동 업데이트되었습니다.\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "{count}개의 애드온이 자동업데이트됨", + "AUTO_UPDATE_NOTIFICATION_TITLE": "자동 업데이트", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "전체 화면 종료하기", + "FULLSCREEN_SNACKBAR": { + "MAC": "전체 화면을 종료하려면 ^⌘F를 누르세요.", + "WINDOWS": "전체 화면을 종료하려면 F11을 누르세요." + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\n기본 브라우저로 이 서드 파티 페이지를 여시겠습니까?", + "TITLE": "WoWUp 바깥으로 나가려고 하고 있습니다" + }, + "PROVIDERS": { + "UNKNOWN": "Unknown" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "애드온 탐색 완료", + "ADDON_SCAN_STARTED": "애드온 탐색 시작", + "ADDON_SCAN_UPDATE": "{count}개의 디렉토리 탐색중..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "업데이트 확인", + "QUIT_ACTION": "종료", + "SHOW_ACTION": "창 열기" + }, + "THEME": { + "ALLIANCE": "얼라이언스", + "DEFAULT": "기본", + "GROUP_DARK": "다크모드", + "GROUP_LIGHT": "라이트모드", + "HORDE": "호드" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - 전체 화면", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Checking for update", + "DOWNLOADED_TOOLTIP": "WowUp 업데이트 설치", + "DOWNLOADING_UPDATE": "Downloading update", + "INSTALL_MESSAGE": "업데이트를 설치하기 위해 프로그램을 재시작할까요?", + "INSTALL_TITLE": "WowUp 업데이트 준비됨", + "NOT_AVAILABLE": "최신 버전입니다", + "PORTABLE_DOWNLOAD_MESSAGE": "최신 포터블 버전을 수동 다운로드하시겠습니까?\n\n앱을 수동으로 닫고 새 버전 파일로 엎어써야 합니다.", + "PORTABLE_DOWNLOAD_TITLE": "수동 다운로드 필요", + "SNACKBAR_ACTION": "업데이트 & Restart", + "SNACKBAR_TEXT": "새 버전 설치 가능", + "TOOLTIP": "프로그램 업데이트가 있습니다.", + "UPDATE_AVAILABLE": "Starting download", + "UPDATE_ERROR": "업데이트 실패" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Achievements", + "ACTION_BARS": "Action Bars", + "ALL_ADDONS": "All Addons", + "AUCTION_ECONOMY": "Auction & Economy", + "BAGS_INVENTORY": "Bags & Inventory", + "BOSS_ENCOUNTERS": "Boss Encounters", + "BUFFS_DEBUFFS": "Buffs & Debuffs", + "BUNDLES": "Bundles", + "CHAT_COMMUNICATION": "Chat & Communication", + "CLASS": "Class", + "COMBAT": "Combat", + "COMPANIONS": "Companions", + "DATA_EXPORT": "Data Export", + "DEVELOPMENT_TOOLS": "Development Tools", + "GUILD": "Guild", + "LIBRARIES": "Libraries", + "MAIL": "Mail", + "MAP_MINIMAP": "Map & Minimap", + "MISCELLANEOUS": "Miscellaneous", + "MISSIONS": "Missions", + "PLUGINS": "Plugins", + "PROFESSIONS": "Professions", + "PVP": "PVP", + "QUESTS_LEVELING": "Quests & Leveling", + "ROLEPLAY": "Roleplay", + "TOOLTIPS": "Tooltips", + "UNIT_FRAMES": "Unit Frames" + }, + "ADDON_STATE": { + "IGNORED": "제외됨", + "INSTALL": "설치", + "PENDING": "Pending", + "UNAVAILABLE": "Unavailable", + "UNAVAILABLE_TOOLTIP": "This author or provider has made this addon unavailable", + "UNINSTALL": "삭제", + "UNKNOWN": "", + "UPDATE": "업데이트", + "UPTODATE": "최신", + "WARNING": "Warning" + }, + "ADDON_STATUS": { + "BACKINGUP": "백업 중", + "COMPLETE": "설치됨", + "DOWNLOADING": "다운로드 중", + "ERROR": "에러", + "INSTALLING": "설치 중", + "PENDING": "일시정지됨", + "RETRY": "Retrying...", + "UNINSTALLING": "삭제 중", + "UPDATING": "업데이트 중..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "We have detected an issue with this addon. We are no longer able to update this addon or provide details.", + "GENERIC_TOOLTIP": "We have detected an issue with this addon", + "MISSING_ON_PROVIDER_DESCRIPTION": "This addon appears to have been removed by either the author or the addon provider.
We are no longer able to update this addon or provide details.", + "MISSING_ON_PROVIDER_TOOLTIP": "This addon appears to have been removed by the provider", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} properly returned this addon, however, it did not have any files matching this game version.
Once there is an update matching this game version this warning should go away.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} did not have any matching game version files", + "TOC_NAME_MISMATCH_DESCRIPTION": "This addon's folder name did not match with the expected toc file, you may experience issues with this addon in-game.", + "TOC_NAME_MISMATCH_TOOLTIP": "This addon's folder does not match the toc" + }, + "CLIENT_TYPES": { + "BETA": "용군단 베타", + "CLASSIC": "리치 왕의 분노 클래식", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "월드 오브 워크래프트 클래식", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "공개 테스트 서버 (리치 왕의 분노 클래식)", + "RETAIL": "용군단", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count}일 전", + "HOURS_AGO": "{count}시간 전", + "JUST_NOW": "방금", + "MONTHS_AGO": "{count}개월 전", + "YEARS_AGO": "{count}년 전", + "YESTERDAY": "어제" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount}개의 다른 애드온 참조" + }, + "DOWNLOAD_COUNT": { + "e+0": "{myriadCount}", + "e+1": "{myriadCount}", + "e+2": "{myriadCount}", + "e+3": "{myriadCount}", + "e+4": "{myriadCount}만", + "e+5": "{myriadCount}만", + "e+6": "{myriadCount}만", + "e+7": "{myriadCount}만", + "e+8": "{myriadCount}억", + "e+9": "{myriadCount}억" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "알파", + "BETA": "베타", + "STABLE": "안정" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Failed to toggle instant updates for your account. Please try again later, or reach out on Discord.", + "ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.", + "ADDON_SCAN_ERROR": "An error occurred matching your addon folders with {providerName}, please try again later.", + "ADDON_SYNC_ERROR": "An error occurred checking for updates from: {providerName}", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: An error occurred checking for updates to {addonName} from {providerName}, please try again later.", + "CHANGE_PROVIDER_ERROR": "{addonName} 애드온 제공자를 {providerName}로 변경하는 과정에서 오류가 발생했습니다", + "GITHUB_LIMIT_ERROR": "You have reached your GitHub API limit of {max} requests.\nPlease wait until {reset} and try again.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Failed to check updates for {addonName}.\nPlease verify that the repository is correct or set this addon to be ignored." + }, + "PROGRESS_SPINNER": { + "LOADING": "불러오는 중..." + }, + "PROVIDER_ERROR": "Error contacting {providerName}", + "SEARCH": { + "NO_ADDONS": "애드온을 찾을 수 없습니다" + }, + "WOW_EXE_SELECTION_NAME": "WoW Executable" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "애드온 ID:", + "BY_AUTHOR": "제작자: {authorName}", + "CHANGELOG_TAB": "변경 내역", + "COPY_ADDON_ID_SNACKBAR": "애드온 ID가 클립보드에 복사되었습니다.", + "COPY_ADDON_ID_TOOLTIP": "애드온 ID 클립보드에 복사하기", + "DEPENDENCY_TEXT": "이 애드온은 {dependencyCount}개의 다른 애드온을 참조합니다.", + "DESCRIPTION_NOT_FOUND": "No description found", + "DESCRIPTION_TAB": "설명", + "FUNDING_LINK_TITLE": "제작자 지원하기", + "IMAGES_TAB": "Previews", + "MISSING_DEPENDENCIES": "Missing dependencies", + "NO_CHANGELOG_TEXT": "불러올 변경 내역이 없습니다.", + "VIEW_IN_BROWSER_BUTTON": "브라우저에서 열기", + "VIEW_ON_PROVIDER_PREFIX": "다음에서 보기: " + }, + "ALERT": { + "ERROR_TITLE": "오류", + "POSITIVE_BUTTON": "확인" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "취소", + "POSITIVE_BUTTON": "확인" + }, + "CURSE_MIGRATION": { + "MESSAGE": "The CurseForge API has been shutdown so WowUp can no longer fetch addon information from it.
Unfortunately, this means that the CurseForge provider has been removed and your addons from CurseForge will no longer be updated.
It is recommended that you perform a re-scan in order to see what addons can be found on other providers.
The Re-Scan process may take a while.
Read more here.", + "NEGATIVE_BUTTON": "Manually Re-Scan", + "POSITIVE_BUTTON": "Automatically Re-Scan", + "RE_SCAN_ERROR": "The automatic Re-Scan failed, please try again manually.", + "RE_SCAN_SUCCESS": "The automatic Re-Scan has finished you're ready to go.", + "TITLE": "CurseForge Migration" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Addon is installed!", + "ADDON_INSTALLING": "Installing addon", + "CANCEL_BUTTON": "Close", + "ERRORS": { + "ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}", + "GENERIC": "Error fetching data for protocol: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}" + }, + "INSTALL_BUTTON": "Install", + "TITLE": "Install Addon From {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "애드온 URL 주소", + "ADDON_URL_INPUT_PLACEHOLDER": "예) GitHub 또는 WowInterface URL", + "CLOSE_BUTTON": "닫기", + "DESCRIPTION": "URL 주소로 직접 설치를 하려면 아래 입력란에 애드온 URL을 붙여넣으세요.", + "DOWNLOAD_COUNT": "{textCount} {count, plural, =1{download} other{downloads}} on {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "No asset was found to download {message}.\n\nA valid zip file is required to be in the release in order for WowUp to download it.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-bc' is required in order for WowUp to download it.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-classic' is required in order for WowUp to download it.", + "FAILED_TO_CONNECT": "API 연결 실패. 잠시 후 다시 시도해주세요.", + "INSTALL_FAILED": "설치 실패! 뭔가 잘못됐습니다.다시 시도해주세요.\n\n이 메세지가 반복된다면, 디스코드의 #help-me 채널에서 도움을 요청해보세요.", + "INVALID_URL": "URL 주소 형식이 잘못되었습니다. 올바른 애드온 주소 예제: \n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "애드온을 찾을 수 없습니다. 입력하신 주소가 올바른 애드온 페이지를 가리키고 있는지 확인해주세요.\n\nGithub에서 설치할 때는, 애드온을 zip 파일로 묶은 릴리즈 태그가 존재하는 저장소인지 확인하세요.", + "NO_RELEASE_FOUND": "No releases were found for {message}.\n\nValid release with zip file assets are required for WowUp to download it.", + "NO_SEARCH_RESULTS": "검색 결과가 없습니다.", + "TITLE": "애드온 설치 실패", + "WRATH_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-wrath' is required in order for WowUp to download it." + }, + "IMPORT_ASSET_WARNING": "We were unable to verify if the latest release of this addon is compatible with your selected client.\n\nBut we did find a zip file \"{zipName}\".\n\nInstall at your own risk.", + "IMPORT_BUTTON": "가져오기", + "IMPORT_WARNING_TITLE": "Addon Import Warning", + "INSTALL_BUTTON": "설치", + "INSTALL_SUCCESS_LABEL": "설치됨!", + "SUPPORTED_SOURCES": "WowInterface 와 GitHub 주소가 지원됩니다", + "TITLE": "설치할 애드온 URL 주소" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Patch Notes {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "Before we get started we need to setup a few permissions for the app.", + "POSITIVE_BUTTON": "Confirm", + "TELEMETRY": { + "DESCRIPTION": "Help improve WowUp by sending anonymous app install data and/or errors?", + "TOGGLE_LABEL": "Allow Telemetry" + }, + "TITLE": "WowUp Permissions Setup", + "WAGO": { + "DESCRIPTION": "Enabled the Wago.io addon provider, this will display an ad required to use their service.\nThe ads directly benefit the authors of your favorite addons!", + "TOGGLE_LABEL": "Enable Wago.io Provider" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "This does not appear to be a valid World of Warcraft application:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "앱 설치 데이터 또는 오류를 익명으로 전송하여 WowUp 서비스 개선에 도움을 주시겠습니까?", + "NEGATIVE_BUTTON": "괜찮습니다.", + "POSITIVE_BUTTON": "물론!", + "TITLE": "원격 분석용 데이터 전송" + }, + "TRUST_DOMAIN_CHECKBOX": "Trust this domain and do not ask me in the future" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Attributions", + "CHANGE_LOG_SECTION_LABEL": "변경 이력", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "웹사이트로 이동" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Login Now!", + "LOGOUT_BUTTON": "Logout", + "LOGOUT_CONFIRMATION_MESSAGE": "Are you sure you want to log out? All of your local account data will be removed, until you login again.", + "LOGOUT_CONFIRMATION_TITLE": "Logout?", + "MANAGE_ACCOUNT_BUTTON": "Manage Account", + "TITLE": "Account" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Categories", + "ADDON_CATEGORIES_MENU_TITLE": "Addon Categories", + "ADDON_CATEGORIES_SELECTED_TITLE": "Category: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Browse various categories", + "CLIENT_TYPE_SELECT_LABEL": "월드 오브 워크래프트", + "INSTALL_FROM_URL_BUTTON": "URL 주소로 설치", + "INSTALL_FROM_URL_TOOLTIP": "Install and addon from a URL", + "REFRESH_BUTTON": "다시 불러오기", + "REFRESH_TOOLTIP": "Refresh addon results", + "RESET_CATEGORY_TOOLTIP": "Reset Category", + "SEARCH_LABEL": "찾기", + "TABLE": { + "ADDON_COLUMN_HEADER": "애드온", + "AUTHOR_COLUMN_HEADER": "제작자", + "DOWNLOAD_COUNT_COLUMN_HEADER": "다운로드수", + "PROVIDER_COLUMN_HEADER": "제공자", + "RELEASED_AT_COLUMN_HEADER": "출시일", + "STATUS_COLUMN_HEADER": "상태" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "프로그램 소개", + "ACCOUNT_TAB_TITLE": "Account", + "COLLAPSE_BUTTON_TITLE": "Collapse", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Expand", + "GET_ADDONS_TAB_TITLE": "애드온 찾기", + "GUIDE_TAB_TITLE": "Guide", + "MIGRATING_ADDONS": "Migrating addons...", + "MY_ADDONS_TAB_TITLE": "내 애드온", + "NEWS_TAB_TITLE": "News", + "OPTIONS_TAB_TITLE": "설정" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count}개의 애드온이 선택됨", + "ALPHA_ADDON_CHANNEL": "알파", + "AUTO_UPDATE_ADDON_BUTTON": "자동 업데이트", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Notifications Enabled", + "BETA_ADDON_CHANNEL": "베타", + "CHANNEL_SUBMENU_TITLE": "채널", + "IGNORE_ADDON_BUTTON": "관리제외", + "PROVIDER_SUBMENU_TITLE": "제공자", + "REINSTALL_ADDON_BUTTON": "재설치", + "REMOVE_ADDON_BUTTON": "삭제", + "SHOW_FOLDER": "디렉토리 보기", + "STABLE_ADDON_CHANNEL": "안정" + }, + "ADDON_IS_CODE_REPOSITORY": "Addon appears to be a code repository", + "ADDON_REMOVED_SNACKBAR": "Successfully removed: {addonName} ", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "{addonName} 애드온의 제공자를 {providerName}로 변경하시겠습니까? 애드온이 먼저 삭제된 후, 새 제공자의 버전으로 다시 설치됩니다", + "TITLE": "애드온 제공자를 변경할까요?" + }, + "CHECK_UPDATES_BUTTON": "업데이트 체크", + "CHECK_UPDATES_BUTTON_TOOLTIP": "최신 애드온 업데이트를 체크", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{update} other{updates}} ", + "CLIENT_TYPE_SELECT_LABEL": "월드 오브 워크래프트", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "컬럼 보기" + }, + "ERROR_SNACKBAR": "에러가 발생했습니다.", + "FILTER_LABEL": "필터", + "FUNDING_TOOLTIP": { + "CUSTOM": "제작자 지원하기", + "GENERIC": "{platform}에서 제작자 지원하기", + "GITHUB": "GitHub에서 제작자 지원하기", + "PATREON": "Patreon에서 제작자 지원하기", + "PAYPAL": "PayPal에서 제작자 지원하기" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Import/Export Addons", + "MULTIPLE_PROVIDERS_TOOLTIP": "복수의 제공자가 존재합니다", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count}개의 애드온", + "JOIN_DISCORD": "디스코드 입장", + "PATREON_SUPPORT": "Patreon에서 WowUp을 후원해주세요.", + "SEARCH_RESULTS": "{count}개의 결과", + "VIEW_GITHUB": "GitHub에서 코드 확인", + "VIEW_GUIDE": "WowUp 가이드 문서 보기" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Required dependency missing", + "RESCAN_FOLDERS_BUTTON": "디렉토리 재탐색", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "설치된 애드온 목록 확인을 위해 클라이언트 디렉토리를 탐색합니다", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "설치된 애드온을 찾기 위해 재탐색을 할 경우, 애드온 정보가 초기화될 수 있습니다. 특정 애드온이 인식되지 않았거나 버전 정보가 올바르게 표시되지 않을 경우에만 사용하세요. 탐색 작업 중 애드온이 삭제되지는 않으며, WowUp의 정보와 매칭작업만 이루어집니다.\n\n탐색 작업에 시간이 소요될 수 있습니다.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "재탐색을 하시겠습니까?", + "SPINNER": { + "GATHERING_ADDONS": "애드온 수집 중...", + "UPDATING": "{updateCount}/{addonCount} 업데이트 중", + "UPDATING_WITH_ADDON_NAME": "{updateCount}/{addonCount} 업데이트 중\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "애드온", + "ADDON_INSTALL_BUTTON": "설치", + "ADDON_UPDATE_BUTTON": "업데이트", + "AUTHOR_COLUMN_HEADER": "제작자", + "AUTO_UPDATE_ICON_TOOLTIP": "자동 업데이트 활성화됨", + "GAME_VERSION_COLUMN_HEADER": "게임 버전", + "LATEST_VERSION_COLUMN_HEADER": "최신 버전", + "PROVIDER_COLUMN_HEADER": "제공자", + "PROVIDER_RELEASE_CHANNEL": "제공자 채널", + "RELEASED_AT_COLUMN_HEADER": "출시일", + "STATUS_COLUMN_HEADER": "상태", + "UPDATED_AT_COLUMN_HEADER": "업데이트 날짜" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "월드 오브 워크래프트 디렉토리에서 관련 파일 및 폴더가 모두 삭제됩니다.", + "CONFIRMATION_LESS_THAN_THREE": "정말 다음 {count}개의 애드온을 삭제하시겠습니까?", + "CONFIRMATION_MORE_THAN_THREE": "정말 선택한 {count}개의 애드온을 삭제하시겠습니까?", + "CONFIRMATION_ONE": "{addonName} 애드온을 삭제하시겠습니까?", + "DEPENDENCY_MESSAGE": "{addonName} 애드온이 참조하는 {dependencyCount} 개의 애드온이 추가설치되었습니다. 해당 애드온도 삭제할까요?", + "DEPENDENCY_TITLE": "참조하는 다른 애드온 삭제 확인", + "TITLE": "애드온 삭제" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "The installed addon did not match with any of the configured providers", + "UPDATE_ALL_BUTTON": "모두 업데이트", + "UPDATE_ALL_BUTTON_TOOLTIP": "이 클라이언트의 모든 애드온을 업데이트합니다", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "모든 클라이언트 업데이트", + "UPDATE_RETAIL_CLASSIC_BUTTON": "리테일/클래식 업데이트" + }, + "WTF_BACKUP_BUTTON": "Interface Settings Backup" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "News link copied to clipboard", + "NEWS_LINK_COPY_TOOLTIP": "Copy news link", + "PAGE_CONTEXT_FOOTER": "{count} news stories", + "REFRESH_TOOLTIP": "Refresh news feed" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Ad or access key required", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "If you have requested a CurseForge API key you can input it here to connect to their API.", + "API_KEY_TITLE": "CurseForge API Key", + "PROVIDER_NOTE": "API Key Required" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "애드온을 검색하고 설치할 때 사용할 애드온 제공자를 선택하세요", + "FIELD_LABEL": "활성화된 애드온 제공자들", + "INPUT_LABEL": "Providers" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "A personal access token will increase the amount of GitHub API calls you can make, it is freely available. Learn More", + "PLACEHOLDER": "Personal Access Token", + "TITLE": "GitHub Personal Access Token" + }, + "TITLE": "애드온", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Switching to the Beta channel will allow you to receive experimental builds that contain bug fixes, as well as new and upcoming features. you may only go back to the current stable version by uninstalling your existing app and re-installing from wowup.io.\n\nWhile the Beta channel is functional, use at your own risk.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Switching to the Stable channel for the app will prevent you from receiving more Beta builds, the next update will be the next Stable release.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Setting the app release channel", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Yes, I understand", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Toggle between the Beta and Stable releases of the application", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Channel", + "APP_RELEASE_CHANNEL_LABEL": "Application Release Channel", + "CURRENT_LANGUAGE_LABEL": "현재 언어", + "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", + "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Show a badge on the app icon with the number of addons with available updates.", + "ENABLE_APP_BADGE_LABEL": "Enable app badge notification", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "애드온 자동업데이트 등과 같은 다양한 시스템 알림 팝업 활성화 여부", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "시스템 알림 활성화", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "창을 닫으면 메뉴바로 최소화", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "창을 닫으면 시스템 트레이로 최소화", + "MINIMIZE_ON_CLOSE_LABEL": "종료 시 최소화", + "PROTOCOL_DESCRIPTION": "WowUp will register a custom URI protocol in your system and handle its incoming inquiries", + "PROTOCOL_LABEL": "Allow WowUp to handle wowup:// URI", + "SCALE_DESCRIPTION": "앱 전체의 확대/축소 비율을 변경합니다.", + "SCALE_LABEL": "확대/축소", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "기본 언어를 바꾸려면 애플리케이션을 다시 시작해야 합니다.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "새 기본 언어 설정", + "SET_LANGUAGE_DESCRIPTION": "언어를 선택하세요.", + "SET_LANGUAGE_LABEL": "언어 설정", + "START_MINIMIZED_DESCRIPTION": "최소화된 채로 시작합니다.", + "START_MINIMIZED_LABEL": "시작 시 최소화", + "START_WITH_SYSTEM_DESCRIPTION": "OS 시동 시에 WowUp이 자동으로 실행됩니다.", + "START_WITH_SYSTEM_LABEL": "시동 시 자동 실행", + "TELEMETRY_DESCRIPTION": "앱 설치 데이터 또는 오류를 익명으로 전송하여 WowUp 서비스 개선에 도움을 줍니다.", + "TELEMETRY_LABEL": "데이터 전송", + "THEME_DESCRIPTION": "원하시는 테마 색상을 선택하세요.", + "THEME_LABEL": "테마", + "TITLE": "애플리케이션", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp can set itself as the default handler for CurseForge download links. This may cause issues if you try to use the CursForge app, are you sure you want to continue?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Handle CurseForge Downloads?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "재시작하시겠습니까?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "FPS 문제 등 렌더링 이슈를 겪고 있다면 하드웨어 가속을 비활성화해보세요. 변경 시 재시작이 필요합니다", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "하드웨어 가속을 비활성화 하려면 재시작이 필요합니다.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "하드웨어 가속을 활성화 하려면 재시작이 필요합니다.", + "USE_HARDWARE_ACCELERATION_LABEL": "하드웨어 가속 활성화", + "USE_SYMLINK_SUPPORT": "Enable Symlink Support", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Enabling symlink support will allow WowUp to recognize symlinks when performing a re-scan. Warning: If you do not know what a symlink is, you do not need this. When updating symlinks will currently be replaced with an actual folder and the link lost.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Enable symlink support?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Allow WowUp to scan symlink folders in your addon folder. Warning: they will be replaced when updating/installing." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "디버그 데이터 덤프", + "DEBUG_DATA_DESCRIPTION": "잠재적인 문제를 진단하는데 도움을 주기 위해 디버그 데이터를 기록합니다. 궁금하시다면 최신 로그 파일에서 확인하실 수 있습니다.", + "DEBUG_DATA_LABEL": "디버그 데이터", + "LOG_FILES_BUTTON": "로그 파일 보기", + "LOG_FILES_DESCRIPTION": "최근 로그파일이 포함된 폴더 열기", + "LOG_FILES_LABEL": "로그 파일", + "TITLE": "디버그" + }, + "TABS": { + "ABOUT": "About", + "ADDONS": "애드온", + "APPLICATION": "게임 애플리케이션", + "CLIENTS": "클라이언트", + "CURSEFORGE": "CurseForge", + "DEBUG": "디버그", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Add New", + "AUTO_UPDATE_DESCRIPTION": "새로 설치되는 애드온의 자동업데이트 옵션을 활성화합니다.", + "AUTO_UPDATE_LABEL": "자동 업데이트", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Cancel", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "{clientName}의 설치 위치 정보를 삭제할까요?\n이 클라이언트의 저장된 애드온 정보는 모두 사라지지만 애드온 디렉토리는 삭제되지 않습니다.", + "TITLE": "설치 위치 정보 초기화" + }, + "CLIENT_TYPE_INPUT_HINT": "\"{clientFolderName}\" 디렉토리를 포함하는 위치", + "CLIENT_TYPE_PATH_LABEL": "{clientTypeName} 설치 위치", + "DEFAULT_ADDON_CHANNEL_LABEL": "기본 애드온 채널", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "애드온 채널", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Edit", + "MOVE_DOWN_BUTTON": "Move Down", + "MOVE_UP_BUTTON": "Move Up", + "NO_CLIENTS_FOUND_TEXT": "No World of Warcraft installations found, please make sure your Battle.net client is up to date or add a client manually", + "OPEN_FOLDER_BUTTON": "Open Folder", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "선택", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Remove", + "RESCAN_CLIENTS_BUTTON": "재탐색", + "RESCAN_CLIENTS_LABEL": "설치된 월드 오브 워크래프트 제품 재탐색", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Save", + "TITLE": "월드 오브 워크래프트" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Are you sure you want to apply this backup to your interface settings?\n\nMake sure that the World of Warcraft game is not running before you apply a backup.\n\nThis operation cannot be undone.", + "TITLE": "Apply WTF Backup?" + }, + "BACKUP_APPLY_SUCCESS": "Successfully applied backup: {name}", + "BACKUP_COUNT_TEXT": "Found {count} {count, plural, =1{backup} other{backups}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Applying backup...", + "CREATING_BACKUP": "Creating backup from {count} files...", + "LOADING_BACKUPS": "Loading backups...", + "REMOVING_BACKUP": "Removing backup..." + }, + "CREATE_BACKUP_BUTTON": "Create Backup", + "DELETE_CONFIRMATION": { + "MESSAGE": "Are you sure you want to delete backup {name}?\nThis cannot be undone.", + "TITLE": "Delete WTF Backup?" + }, + "DIALOG_TITLE": "WTF Settings Backup: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Failed to apply backup: {name}", + "FAILED_TO_DELETE": "Failed to delete backup: {name}", + "GENERIC_ERROR": "There was an issue processing this backup", + "INVALID_CONTENTS": "There was an issue processing this backup", + "INVALID_CREATED_AT": "There was an issue processing this backup", + "INVALID_CREATED_BY": "There was an issue processing this backup" + }, + "SHOW_FOLDER_BUTTON": "Show Folder", + "TOOL_TIP": { + "APPLY_BUTTON": "Apply this backup", + "DELETE_BUTTON": "Delete this backup" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/nb.json b/WowUp/wowup-electron/src/assets/i18n/nb.json new file mode 100644 index 0000000..e052b38 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/nb.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Aktive utvidelser: {count}", + "ADDED_BADGE_TOOLTIP": "Vi vil forsøke å installere denne utvidelsen", + "CONFLICT_BADGE_TOOLTIP": "Utvidelser som er i konflikt vil ikke bli modifisert", + "COPY_BUTTON": "Kopier", + "DIALOG_TITLE": "Importer/Eksporter utvidelser: {clientType}", + "EXPORT_STRING_COPIED": "Eksportstreng ble kopiert til utklippstavlen", + "EXPORT_STRING_PASTED": "Limte inn innhold fra utklippstavlen", + "EXPORT_TAB_LABEL": "Eksporter", + "EXPORT_TEXT_LABEL": "Eksportdata for utvidelser", + "GENERIC_IMPORT_ERROR": "Det oppstod en feil under importen", + "IGNORED_ADDON_COUNT": "Ignorerte utvidelser: {count}", + "IMPORT_ADDED_COUNT": "{count} lagt til", + "IMPORT_BADGE_ADDED": "Ny", + "IMPORT_BADGE_CONFLICT": "Konflikt", + "IMPORT_BADGE_NO_CHANGE": "Ingen endring", + "IMPORT_BUTTON": "Importer", + "IMPORT_CONFLICT_COUNT": "{count} i konflikt", + "IMPORT_NO_CHANGE_COUNT": "{count} uendret", + "IMPORT_STRING_INVALID": "Importstreng var ugyldig", + "IMPORT_TAB_LABEL": "Importer", + "IMPORT_TEXT_INSTRUCTIONS": "Lim inn eksportdata for utvidelser fra WowUp i feltet nedenfor for å begynne", + "IMPORT_TEXT_LABEL": "Importdata", + "IMPORT_TOTAL_COUNT": "Importerer {count} {count, plural, =1{utvidelse} other{utvidelser}}", + "INSTALL_BUTTON": "Installer", + "INVALID_CLIENT_TYPE": "Importstrengen samsvarer ikke med den valgte klienttypen", + "NO_CHANGE_BADGE_TOOLTIP": "Du har allerede denne utvidelsen installert", + "PASTE_BUTTON": "Lim inn", + "PROVIDER_MISMATCH": "Utvidelsesleverandør samsvarer ikke", + "RESET_BUTTON": "Tilbakestill", + "VERSION_MISMATCH": "Versjonene samsvarer ikke" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Hvorfor ser jeg denne annonsen?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "For å kunne bruke wago.io / CurseForge som leverandør av utvidelser, og støtte forfatterene deres for deres harde arbeid med favorittutvidelsene dine, er vi nødt til å vise denne annonsen.\n\nHvis du ikke vil se denne annonsen kan du alltids deaktivere Wago / CurseForge som leverandør i fanen for alternativer.", + "TITLE": "Hvorfor ser jeg denne annonsen?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Kopier", + "CUT": "Klipp ut", + "LABEL": "Rediger", + "PASTE": "Lim inn", + "REDO": "Gjenta", + "SELECT_ALL": "Velg alt", + "UNDO": "Angre" + }, + "QUIT": "Avslutt", + "VIEW": { + "FORCE_RELOAD": "Tving omlasting", + "LABEL": "Vis", + "RELOAD": "Last på nytt", + "TOGGLE_DEV_TOOLS": "Slå på/av utviklerverktøy", + "TOGGLE_FULL_SCREEN": "Slå på/av fullskjerm", + "ZOOM_IN": "Zoom inn", + "ZOOM_OUT": "Zoom ut", + "ZOOM_RESET": "Tilbakestill zoom" + }, + "WINDOW": { + "CLOSE": "Lukk", + "LABEL": "Vindu" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Automatisk oppdatert\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "{count} {count, plural, =1{utvidelse} other{utvidelser}} ble automatisk oppdatert.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Automatiske oppdateringer", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Forlat fullskjerm", + "FULLSCREEN_SNACKBAR": { + "MAC": "Trykk ^⌘F for å forlate fullskjerm", + "WINDOWS": "Trykk F11 for å forlate fullskjerm" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nEr du sikker på at du vil åpne denne tredjepartssiden i standardnettleseren din?", + "TITLE": "Du er i ferd med å forlate WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Ukjent" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Skanning av utvidelser er fullført...", + "ADDON_SCAN_STARTED": "Skanning av utvidelser er startet...", + "ADDON_SCAN_UPDATE": "Skanner {count} mapper..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Se etter oppdateringer...", + "QUIT_ACTION": "Avslutt", + "SHOW_ACTION": "Åpne" + }, + "THEME": { + "ALLIANCE": "Alliansen", + "DEFAULT": "Standard", + "GROUP_DARK": "Mørk", + "GROUP_LIGHT": "Lys", + "HORDE": "Horde" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Fullskjerm", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Ser etter oppdateringer", + "DOWNLOADED_TOOLTIP": "Installer oppdatering til WowUp", + "DOWNLOADING_UPDATE": "Laster ned oppdatering", + "INSTALL_MESSAGE": "Vil du restarte WowUp for å installere oppdateringen?", + "INSTALL_TITLE": "WowUp-oppdatering er klar", + "NOT_AVAILABLE": "Den nyeste versjonen av WowUp er allerede installert", + "PORTABLE_DOWNLOAD_MESSAGE": "Vil du laste ned den nyeste portable versjonen manuelt?\n\nDu må lukke programmet manuelt og kopiere den nye versjonen over den gamle.", + "PORTABLE_DOWNLOAD_TITLE": "Manuell nedlasting kreves", + "SNACKBAR_ACTION": "Oppdater og start på nytt", + "SNACKBAR_TEXT": "En ny versjon av WowUp er tilgjengelig", + "TOOLTIP": "WowUp-oppdatering tilgjengelig", + "UPDATE_AVAILABLE": "Nedlastingen starter", + "UPDATE_ERROR": "Kunne ikke hente WowUp-oppdatering" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Achievements", + "ACTION_BARS": "Action Bars", + "ALL_ADDONS": "All Addons", + "AUCTION_ECONOMY": "Auction & Economy", + "BAGS_INVENTORY": "Bags & Inventory", + "BOSS_ENCOUNTERS": "Boss Encounters", + "BUFFS_DEBUFFS": "Buffs & Debuffs", + "BUNDLES": "Bundles", + "CHAT_COMMUNICATION": "Chat & Communication", + "CLASS": "Class", + "COMBAT": "Combat", + "COMPANIONS": "Companions", + "DATA_EXPORT": "Data Export", + "DEVELOPMENT_TOOLS": "Development Tools", + "GUILD": "Guild", + "LIBRARIES": "Libraries", + "MAIL": "Mail", + "MAP_MINIMAP": "Map & Minimap", + "MISCELLANEOUS": "Miscellaneous", + "MISSIONS": "Missions", + "PLUGINS": "Plugins", + "PROFESSIONS": "Professions", + "PVP": "PVP", + "QUESTS_LEVELING": "Quests & Leveling", + "ROLEPLAY": "Roleplay", + "TOOLTIPS": "Tooltips", + "UNIT_FRAMES": "Unit Frames" + }, + "ADDON_STATE": { + "IGNORED": "Ignorert", + "INSTALL": "Installer", + "PENDING": "Venter", + "UNAVAILABLE": "Utilgjengelig", + "UNAVAILABLE_TOOLTIP": "Denne forfatteren eller leverandøren har gjort denne utvidelsen utilgjengelig", + "UNINSTALL": "Avinstaller", + "UNKNOWN": "Ukjent", + "UPDATE": "Oppdater", + "UPTODATE": "Oppdatert", + "WARNING": "Advarsel" + }, + "ADDON_STATUS": { + "BACKINGUP": "Sikkerhetskopierer", + "COMPLETE": "Installert", + "DOWNLOADING": "Laster ned", + "ERROR": "Feil", + "INSTALLING": "Installerer", + "PENDING": "Venter", + "RETRY": "Retrying...", + "UNINSTALLING": "Avinstallerer", + "UPDATING": "Oppdaterer..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "Vi har støtt på et problem med denne utvidelsen. Vi kan ikke lenger oppdatere denne utvidelsen eller gi detaljer om den.", + "GENERIC_TOOLTIP": "Vi har støtt på et problem med denne utvidelsen", + "MISSING_ON_PROVIDER_DESCRIPTION": "{providerName} returnerte ikke utvidelsen da vi ba om den.
Vi kan ikke lenger oppdatere denne utvidelsen eller gi detaljer før {providerName} fikser det.", + "MISSING_ON_PROVIDER_TOOLTIP": "{providerName} returnerer ikke denne utvidelsen som forventet", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} returnerte denne utvidelsen på korrekt måte, men hadde ingen filer som samsvarte med den gjeldende spillversjonen.
Når det er en oppdatering som samsvarer med denne spillversjonen vil denne advarselen forsvinne.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} hadde ingen filer som samsvarer med denne spillversjonen", + "TOC_NAME_MISMATCH_DESCRIPTION": "Mappenavnet til denne utvidelsen samsvarte ikke med navnet som er registrert i TOC-filen. Du kan oppleve problemer med denne utvidelsen i spillet.", + "TOC_NAME_MISMATCH_TOOLTIP": "Mappen til denne utvidelsen samsvarer ikke med TOC-filen" + }, + "CLIENT_TYPES": { + "BETA": "Dragonflight Beta", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Public Test Realm (Wrath Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count} {count, plural, =1{dag} other{dager}} siden", + "HOURS_AGO": "{count} {count, plural, =1{time} other{timer}} siden", + "JUST_NOW": "Akkurat nå", + "MONTHS_AGO": "{count} {count, plural, =1{måned} other{måneder}} siden", + "YEARS_AGO": "{count} år siden", + "YESTERDAY": "I går" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} {dependencyCount, plural, =1{avhengighet} other{avhengigheter}} påkreves" + }, + "DOWNLOAD_COUNT": { + "e+0": "Få", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count} tusen", + "e+4": "{count} tusen", + "e+5": "{count} tusen", + "e+6": "{count} {count, plural, =1{million} other{millioner}}", + "e+7": "{count} millioner", + "e+8": "{count} millioner", + "e+9": "{count} {count, plural, =1{milliard} other{milliarder}}" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alfa", + "BETA": "Beta", + "STABLE": "Stabil" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Live-oppdateringer kunne ikke slås på for kontoen din. Vennligst prøv igjen senere eller ta kontakt med oss på Discord.", + "ADDON_INSTALL_ERROR": "Utvidelsen '{addonName}' kunne ikke installeres. Vennligst prøv igjen senere.", + "ADDON_SCAN_ERROR": "Det oppstod en feil når vi skulle matche mappene til utvidelsen med {providerName}. Vennligst prøv igjen senere.", + "ADDON_SYNC_ERROR": "Det oppstod en feil under søk etter oppdateringer fra {providerName}. Vennligst prøv igjen senere.", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: Det oppstod en feil under søk etter oppdateringer til '{addonName}' fra {providerName}. Vennligst prøv igjen senere.", + "CHANGE_PROVIDER_ERROR": "Kunne ikke endre installasjonsleverandør for {addonName} til {providerName}", + "GITHUB_LIMIT_ERROR": "Du har nådd API-grensen til GitHub på {max} forespørsler.\nVent til {reset} og prøv igjen.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Kunne ikke sjekke etter oppdateringer for {addonName}. Vennligst verifiser at repoet er korrekt, eller sett denne utvidelsen til å være 'Ignorert'" + }, + "PROGRESS_SPINNER": { + "LOADING": "Laster..." + }, + "PROVIDER_ERROR": "Kunne ikke koble til {providerName}", + "SEARCH": { + "NO_ADDONS": "Ingen utvidelser funnet" + }, + "WOW_EXE_SELECTION_NAME": "WoW App (kjørbar)" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "Utvidelsens ID:", + "BY_AUTHOR": "Av {authorName}", + "CHANGELOG_TAB": "Endringslogg", + "COPY_ADDON_ID_SNACKBAR": "Utvidelsens ID kopiert til utklippstavlen", + "COPY_ADDON_ID_TOOLTIP": "Kopier utvidelsens ID til utklippstavlen", + "DEPENDENCY_TEXT": "Denne utvidelsen har {dependencyCount} {dependencyCount, plural, =1{nødvendig} other{nødvendige}} {dependencyCount, plural, =1{avhengighet} other{avhengigheter}}", + "DESCRIPTION_NOT_FOUND": "Ingen beskrivelse funnet", + "DESCRIPTION_TAB": "Beskrivelse", + "FUNDING_LINK_TITLE": "Støtt denne forfatteren", + "IMAGES_TAB": "Forhåndsvisning", + "MISSING_DEPENDENCIES": "Manglende avhengigheter", + "NO_CHANGELOG_TEXT": "Endringslogg utilgjengelig", + "VIEW_IN_BROWSER_BUTTON": "Se i nettleser", + "VIEW_ON_PROVIDER_PREFIX": "Se på" + }, + "ALERT": { + "ERROR_TITLE": "Feil", + "POSITIVE_BUTTON": "OK" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "Nei", + "POSITIVE_BUTTON": "Ja" + }, + "CURSE_MIGRATION": { + "MESSAGE": "CurseForge sitt API har blitt stengt. Derfor kan ikke WowUp lenger hente informasjon om utvidelser derfra.
Dessverre betyr dette at CurseForge-leverandøren er fjernet, og utvidelsene dine fra CurseForge vil ikke lenger oppdateres.
Det anbefales at du utfører en ny skann for å se hvilke utvidelser som finnes hos andre leverandører.
Å skanne på nytt kan ta en stund.
Les mer her.", + "NEGATIVE_BUTTON": "Skann på nytt manuelt senere", + "POSITIVE_BUTTON": "Automatisk skanning på nytt", + "RE_SCAN_ERROR": "Den automatiske skannen feilet, vennligst prøv på nytt med en manuell skann.", + "RE_SCAN_SUCCESS": "Den automatiske skannen er ferdig, og du kan fortsette.", + "TITLE": "CurseForge-migrasjon" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Utvidelsen er installert!", + "ADDON_INSTALLING": "Installerer utvidelse", + "CANCEL_BUTTON": "Lukk", + "ERRORS": { + "ADDON_NOT_FOUND": "Ingen utvidelse ble funnet via protokollen '{protocol}'", + "GENERIC": "Feil ved henting av data via {protocol}-protokollen", + "NO_VALID_WOW_INSTALLATIONS": "Ingen WoW-klient installert for protokollen '{protocol}'" + }, + "INSTALL_BUTTON": "Installer", + "TITLE": "Installer utvidelse fra {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "Utvidelsens URL", + "ADDON_URL_INPUT_PLACEHOLDER": "F.eks. GitHub- eller WowInterface-URL", + "CLOSE_BUTTON": "Lukk", + "DESCRIPTION": "Hvis du vil installere en utvidelse direkte fra en URL kan du lime den inn i feltet nedenfor.", + "DOWNLOAD_COUNT": "{textCount} {count, plural, =1{nedlasting} other{nedlastinger}} på {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "Ingen ressurs ble funnet for å laste ned {message}.\n\nEn gyldig Zip-fil må inkluderes med utgivelsen for at WowUp skal kunne laste den ned.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "Ingen ressurs ble funnet for å laste ned fra {message}.\n\nEn gyldig Zip-fil som slutter på '-bc' må inkluderes i utgivelsen for at WowUp skal kunne laste den ned.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "Ingen ressurs ble funnet for å laste ned fra {message}.\n\nEn gyldig Zip-fil som slutter på '-classic' må inkluderes i utgivelsen for at WowUp skal kunne laste den ned.", + "FAILED_TO_CONNECT": "Kunne ikke koble til API-et. Vent litt og prøv igjen.", + "INSTALL_FAILED": "Noe gikk galt under forsøket på å installere utvidelsen, vennligst prøv igjen.\n\nHvis du får denne meldingen gjentatte ganger kan du be om hjelp på vår Discord i kanalen #help-me", + "INVALID_URL": "Den angitte nettadressen er ugyldig. Eksempler på gyldige nettadresser er:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "Ingen utvidelser ble funnet. Sørg for at URL-en går til riktig nettsted.\n\nHvis du prøver å installere en utvidelse fra GitHub, sørg for at repoet har Release-Tags med et Zip-arkiv som inneholder utvidelsen.", + "NO_RELEASE_FOUND": "Ingen utgivelse ble funnet for {message}.\n\nEn gyldig utgivelse med Zip-fil-ressurser kreves for at WowUp skal kunne laste den ned.", + "NO_SEARCH_RESULTS": "Ingen søkeresultater ble funnet", + "TITLE": "Installasjon av utvidelse feilet", + "WRATH_ASSET_NOT_FOUND": "Ingen ressurs ble funnet å laste ned fra {message}.\n\nEn gyldig Zip-fil som slutter med '-wrath' kreves for at WowUp skal kunne laste den ned." + }, + "IMPORT_ASSET_WARNING": "Vi kunne ikke verifisere om den nyeste versjonen av denne utvidelsen er kompatibel med den valgte klienten.\n\nMen vi fant en Zip-fil \"{zipName}\".\n\nInstaller på egen risiko.", + "IMPORT_BUTTON": "Importer", + "IMPORT_WARNING_TITLE": "Advarsel om utvidelsesimport", + "INSTALL_BUTTON": "Installer", + "INSTALL_SUCCESS_LABEL": "Installert!", + "SUPPORTED_SOURCES": "Støtter WowInterface og GitHub", + "TITLE": "Installer utvidelse fra URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Nytt i versjon {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "Før vi kan komme igang må vi sette opp noen tillatelser for appen.", + "POSITIVE_BUTTON": "Bekreft", + "TELEMETRY": { + "DESCRIPTION": "Vil du bidra til å forbedre WowUp ved å sende anonyme installasjonsdata og/eller feil?", + "TOGGLE_LABEL": "Tillat telemetri" + }, + "TITLE": "Sett opp tillatelser for WowUp", + "WAGO": { + "DESCRIPTION": "Aktiver Wago.io som utvidelsesleverandør for å hjelpe til med å støtte forfatterene av favorittutvidelsene dine! Dette vil vise et kampanjepanel som kreves for å bruke tjenesten deres.\nVed å registrere deg godtar du vilkårene for bruk og Datasamtykke.", + "TOGGLE_LABEL": "Aktiver leverandøren Wago.io" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "Dette ser ikke ut til å være en gyldig World of Warcraft-applikasjon:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Vil du bidra til å forbedre WowUp ved å sende anonyme installasjonsdata og/eller feil?", + "NEGATIVE_BUTTON": "Nei takk", + "POSITIVE_BUTTON": "Selvfølgelig!", + "TITLE": "WowUp-telemetri" + }, + "TRUST_DOMAIN_CHECKBOX": "Stol på dette domenet og ikke spør meg igjen i fremtiden" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Attribusjoner", + "CHANGE_LOG_SECTION_LABEL": "Endringslogg", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Besøk nettsiden!" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Logg inn nå!", + "LOGOUT_BUTTON": "Logg ut", + "LOGOUT_CONFIRMATION_MESSAGE": "Er du sikker på at du vil logge ut? Alle dine lokale kontodetaljer vil bli fjernet til du logger på igjen.", + "LOGOUT_CONFIRMATION_TITLE": "Logg ut?", + "MANAGE_ACCOUNT_BUTTON": "Administrer konto", + "TITLE": "Konto" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Kategorier", + "ADDON_CATEGORIES_MENU_TITLE": "Utvidelseskategorier", + "ADDON_CATEGORIES_SELECTED_TITLE": "Kategori: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Bla gjennom ulike kategorier", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Installer fra URL", + "INSTALL_FROM_URL_TOOLTIP": "Installer en utvidelse fra en URL", + "REFRESH_BUTTON": "Oppdater", + "REFRESH_TOOLTIP": "Oppdater utvidelsesresultater", + "RESET_CATEGORY_TOOLTIP": "Tilbakestill kategori", + "SEARCH_LABEL": "Søk", + "TABLE": { + "ADDON_COLUMN_HEADER": "Utvidelse", + "AUTHOR_COLUMN_HEADER": "Forfatter(e)", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Nedlastinger", + "PROVIDER_COLUMN_HEADER": "Leverandør", + "RELEASED_AT_COLUMN_HEADER": "Utgivelsesdato", + "STATUS_COLUMN_HEADER": "Status" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "Om", + "ACCOUNT_TAB_TITLE": "Konto", + "COLLAPSE_BUTTON_TITLE": "Kollaps", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Ekspander", + "GET_ADDONS_TAB_TITLE": "Finn utvidelser", + "GUIDE_TAB_TITLE": "Guide", + "MIGRATING_ADDONS": "Migrerer utvidelser...", + "MY_ADDONS_TAB_TITLE": "Mine utvidelser", + "NEWS_TAB_TITLE": "Nyheter", + "OPTIONS_TAB_TITLE": "Alternativer" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count} {count, plural, =1{utvidelse} other{utvidelser}} valgt", + "ALPHA_ADDON_CHANNEL": "Alfa", + "AUTO_UPDATE_ADDON_BUTTON": "Oppdater automatisk", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Notifikasjoner aktivert", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "Kanal", + "IGNORE_ADDON_BUTTON": "Ignorer", + "PROVIDER_SUBMENU_TITLE": "Leverandører", + "REINSTALL_ADDON_BUTTON": "Reinstaller", + "REMOVE_ADDON_BUTTON": "Fjern", + "SHOW_FOLDER": "Vis mappe", + "STABLE_ADDON_CHANNEL": "Stabil" + }, + "ADDON_IS_CODE_REPOSITORY": "Utvidelsen ser ut til å være et koderepository", + "ADDON_REMOVED_SNACKBAR": "{addonName} ble fjernet", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Vil du endre utvidelsesleverandøren for {addonName} til {providerName}? Denne operasjonen vil avinstallere den eksisterende utvidelsen og erstatte den med en ny kopi fra den nye leverandøren.", + "TITLE": "Endre utvidelsesleverandør?" + }, + "CHECK_UPDATES_BUTTON": "Se etter oppdateringer", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Finn de siste oppdateringene til utvidelsene dine", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{oppdatering} other{oppdateringer}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Vis kolonner" + }, + "ERROR_SNACKBAR": "Det har oppstått en feil", + "FILTER_LABEL": "Filter", + "FUNDING_TOOLTIP": { + "CUSTOM": "Støtt denne forfatteren", + "GENERIC": "Støtt denne forfatteren på {platform}", + "GITHUB": "Støtt denne forfatteren på GitHub", + "PATREON": "Støtt denne forfatteren på Patreon", + "PAYPAL": "Støtt denne forfatteren på PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Importer/eksporter utvidelser", + "MULTIPLE_PROVIDERS_TOOLTIP": "Denne utvidelsen har flere leverandører", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{utvidelse} other{utvidelser}}", + "JOIN_DISCORD": "Chat med oss på Discord", + "PATREON_SUPPORT": "Støtt WowUp på Patreon", + "SEARCH_RESULTS": "{count} {count, plural, =1{resultat} other{resultater}}", + "VIEW_GITHUB": "Sjekk ut koden på GitHub", + "VIEW_GUIDE": "Sjekk ut vår guide for å se hva WowUp kan gjøre" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Påkrevd avhengighet mangler", + "RESCAN_FOLDERS_BUTTON": "Skann mapper på nytt", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Skann klientmappen din for å finne installerte utvidelser", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "En ny skann vil forsøke å gjette hvilke utvidelser du har installert. Dette kan tilbakestille kjent utvidelsesinformasjon. Bruk denne funksjonen når visse utvidelser ikke gjenkjennes eller utvidelsesversjoner ikke vises riktig. Denne skannen vil aldri slette de installerte utvidelsene dine, bare det WowUp vet om dem.\n\nSkanningen kan ta noen øyeblikk.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Start ny skann?", + "SPINNER": { + "GATHERING_ADDONS": "Samler utvidelser...", + "UPDATING": "Oppdaterer {updateCount}/{addonCount}", + "UPDATING_WITH_ADDON_NAME": "Oppdaterer {updateCount}/{addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Utvidelse", + "ADDON_INSTALL_BUTTON": "Installer", + "ADDON_UPDATE_BUTTON": "Oppdater", + "AUTHOR_COLUMN_HEADER": "Forfatter", + "AUTO_UPDATE_ICON_TOOLTIP": "Automatisk oppdatering aktivert", + "GAME_VERSION_COLUMN_HEADER": "Spillversjon", + "LATEST_VERSION_COLUMN_HEADER": "Siste versjon", + "PROVIDER_COLUMN_HEADER": "Leverandør", + "PROVIDER_RELEASE_CHANNEL": "Leverandørkanal", + "RELEASED_AT_COLUMN_HEADER": "Utgivelsesdato", + "STATUS_COLUMN_HEADER": "Status", + "UPDATED_AT_COLUMN_HEADER": "Oppdatert" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Når du fjerner en utvidelse via WowUp vil den valgte utvidelsen også fjernes fra 'Interface/AddOns'-mappen din. Instillingene for utvidelsen vil ikke bli fjernet.", + "CONFIRMATION_LESS_THAN_THREE": "Er du sikker på at du vil fjerne disse {count} utvidelsene?", + "CONFIRMATION_MORE_THAN_THREE": "Er du sikker på at du vil fjerne de {count} valgte utvidelsene?", + "CONFIRMATION_ONE": "Er du sikker på at du vil fjerne {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} har {dependencyCount} {dependencyCount, plural, =1{avhengighet} other{avhengigheter}}. Vil du fjerne disse også?", + "DEPENDENCY_TITLE": "Fjern utvidelsesavhengigheter?", + "TITLE": "Avinstaller {count, plural, =1{utvidelse} other{utvidelser}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "Den installerte utvidelsen samsvarer ikke med noen av de konfigurerte leverandørene", + "UPDATE_ALL_BUTTON": "Oppdater alle", + "UPDATE_ALL_BUTTON_TOOLTIP": "Oppdater alle utvidelser for denne klienten", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Oppdater alle klienter", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Oppdater Retail/Classic" + }, + "WTF_BACKUP_BUTTON": "Opprett sikkerhetskopi av WTF-mappen" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "Nyhetslenken er kopiert til utklippstavlen", + "NEWS_LINK_COPY_TOOLTIP": "Kopier nyhetslenke", + "PAGE_CONTEXT_FOOTER": "{count} nyheter", + "REFRESH_TOOLTIP": "Oppdater nyhetsstrømmen" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Aktivering av dette krever at du oppgir en tilgangsnøkkel eller viser en annonse", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "Hvis du har bedt om en CurseForge API-nøkkel, kan du legge den inn her for å koble til deres API.", + "API_KEY_TITLE": "CurseForge API-nøkkel", + "PROVIDER_NOTE": "API-nøkkel kreves" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Velg hvilke leverandører som kan brukes til å søke etter, og installer nye utvidelser", + "FIELD_LABEL": "Aktiverte utvidelsesleverandører", + "INPUT_LABEL": "Leverandører" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "En personlig tilgangstoken vil øke mulige antall forespørsler til GitHub sitt API, det er gratis tilgjengelig. Lær mer", + "PLACEHOLDER": "Personlig tilgangstoken", + "TITLE": "GitHub personlig tilgangstoken" + }, + "TITLE": "Utvidelser", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Bruk Wago-leverandøren uten å se annonser. Lær mer", + "PLACEHOLDER": "Tilgangsnøkkel", + "TITLE": "Wago tilgangsnøkkel" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Ved å bytte til betakanalen kan du motta eksperimentelle bygg som inneholder feilrettinger, samt nye og kommende funksjoner. Du kan bare gå tilbake til den nåværende stabile versjonen ved å avinstallere den eksisterende appen din og installere på nytt fra wowup.io.\n\nSå lenge betakanalen er funksjonell, bruk den på egen risiko.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Hvis du bytter til applikasjonens stabile kanal, vil du ikke lenger motta beta-bygg. Den neste oppdateringen vil være den neste stabile versjonen.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Konfigurer apputgivelseskanalen", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Ja, jeg forstår", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Veksle mellom beta- og stabilutgivelsene av applikasjonen", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Kanal", + "APP_RELEASE_CHANNEL_LABEL": "Apputgivelseskanal", + "CURRENT_LANGUAGE_LABEL": "Nåværende språk", + "CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install", + "CURSE_PROTOCOL_LABEL": "Handle CurseForge download links", + "ENABLE_APP_BADGE_DESCRIPTION": "Viser en teller på appikonet med antall tilgjengelige oppdateringer.", + "ENABLE_APP_BADGE_LABEL": "Aktiver notifikasjonsbadge for appen", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Aktiver ulike systemvarsler, som for eksempel for automatisk oppdaterte utvidelser.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Aktiver systemvarsler", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "Når du åpner en detaljside for en utvidelse, åpnes automatisk den sist åpnede fanen", + "KEEP_LAST_OPENED_TAB_LABEL": "Behold siste åpnede fane", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Når WowUp-vinduet lukkes, minimer det til menylinjen.", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Når WowUp-vinduet lukkes, minimer det til oppgavelinjens varselområde.", + "MINIMIZE_ON_CLOSE_LABEL": "Minimer ved lukking", + "PROTOCOL_DESCRIPTION": "WowUp registrerer en tilpasset URI-protokoll på systemet ditt og håndterer dens innkommende forespørsler", + "PROTOCOL_LABEL": "Tillat WowUp å håndtere wowup:// URI", + "SCALE_DESCRIPTION": "Endre zoomfaktor for hele appen.", + "SCALE_LABEL": "Skala", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "For å endre standardspråket må applikasjonen startes på nytt.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Angi et nytt standardspråk", + "SET_LANGUAGE_DESCRIPTION": "Velg språket du vil bytte til", + "SET_LANGUAGE_LABEL": "Angi språk", + "START_MINIMIZED_DESCRIPTION": "WowUp starter minimert og dukker ikke opp på skjermen din.", + "START_MINIMIZED_LABEL": "Start WowUp minimert", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp vil startes automatisk når du starter opp systemet ditt.", + "START_WITH_SYSTEM_LABEL": "Start WowUp sammen med maskinen", + "TELEMETRY_DESCRIPTION": "Bidra til å forbedre WowUp ved å sende anonyme installasjonsdata og/eller feil.", + "TELEMETRY_LABEL": "Telemetri", + "THEME_DESCRIPTION": "Endre fargetemaet til din smak", + "THEME_LABEL": "Fargetema", + "TITLE": "Applikasjon", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp kan angi seg selv som standardbehandler for CurseForge-nedlastingslenker. Dette kan forårsake problemer hvis du prøver å bruke CursForge-appen. Er du sikker på at du vil fortsette?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Håndter CurseForge-nedlastinger?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Vil du start på nytt?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "Deaktivering av maskinvareakselerasjon kan fikse FPS- og andre renderingsproblemer i denne appen.
Endring av denne innstillingen krever omstart.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Deaktivering av maskinvareakselerasjon krever at du starter programmet på nytt.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Aktivering av maskinvareakselerasjon krever at du starter programmet på nytt.", + "USE_HARDWARE_ACCELERATION_LABEL": "Aktiver maskinvareakselerasjon", + "USE_SYMLINK_SUPPORT": "Aktiver symlink-støtte", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Aktivering av symlink-støtte lar WowUp oppdage symlinker ved reskanning. Advarsel: Hvis du ikke vet hva en symlink er, trenger du ikke denne. For øyeblikket, når du oppdaterer, erstattes symlinker med en faktisk mappe, og koblingen går tapt.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Aktiver symlink-støtte?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Tillat WowUp å skanne symlink-mapper i utvidelsesmappen din. Advarsel: de vil bli erstattet ved oppdatering/installering." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Vis konfigurasjonsfiler", + "CONFIG_FILES_DESCRIPTION": "Åpne mappen der for eksempel addons.json og preferences.json er lagret.", + "CONFIG_FILES_LABEL": "Konfigurasjonsfiler", + "DEBUG_DATA_BUTTON": "Lagre debugdata", + "DEBUG_DATA_DESCRIPTION": "Logg debugdata for å hjelpe deg med å diagnostisere potensielle problemer. Hvis du er nyskjerrig kan du finne dette i den siste loggfilen.", + "DEBUG_DATA_LABEL": "Debugdata", + "LOG_FILES_BUTTON": "Vis loggfiler", + "LOG_FILES_DESCRIPTION": "Åpne mappen som inneholder de siste loggfilene dine.", + "LOG_FILES_LABEL": "Loggfiler", + "TITLE": "Debug" + }, + "TABS": { + "ABOUT": "Om", + "ADDONS": "Utvidelser", + "APPLICATION": "Applikasjon", + "CLIENTS": "Klienter", + "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Legg til ny", + "AUTO_UPDATE_DESCRIPTION": "Alle eksisterende og nyinstallerte utvidelser vil bli satt til å oppdateres automatisk som standard", + "AUTO_UPDATE_LABEL": "Oppdater automatisk", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Avbryt", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Er du sikker på at du vil fjerne installasjonen på \"{location}\"? Dette vil fjerne all lagret utvidelsesinformasjon for denne klienten.\n\nDine utvidelsesmapper vil ikke bli fjernet.", + "TITLE": "Fjern World of Warcraft installasjonslokasjon" + }, + "CLIENT_TYPE_INPUT_HINT": "Mappen som inneholder {clientTypeName} klientmappen \"{clientFolderName}\"", + "CLIENT_TYPE_PATH_LABEL": "{clientTypeName} filbane", + "DEFAULT_ADDON_CHANNEL_LABEL": "Primærkanal for utvidelser", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Kanal for utvidelser", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Rediger", + "MOVE_DOWN_BUTTON": "Flytt ned", + "MOVE_UP_BUTTON": "Flytt opp", + "NO_CLIENTS_FOUND_TEXT": "Ingen World of Warcraft-installasjon funnet. Sørg for at Battle.net-klienten din er oppdatert eller legg til en klient manuelt", + "OPEN_FOLDER_BUTTON": "Åpne mappe", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Velg", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Fjern", + "RESCAN_CLIENTS_BUTTON": "Skann på nytt", + "RESCAN_CLIENTS_LABEL": "Skann installerte World of Warcraft-produkter på nytt", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Lagre", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Er du sikker på at du vil gjenopprette denne sikkerhetskopien av WTF-mappen din (spill- og karakterinnstillinger)?\n\nSørg for at World of Warcraft ikke kjører før du gjenoppretter en sikkerhetskopi.\n\nDenne operasjonen kan ikke angres.", + "TITLE": "Vil du gjenopprette sikkerhetskopi av WTF-mappen din?" + }, + "BACKUP_APPLY_SUCCESS": "Gjenoppretting av sikkerhetskopien '{name}' var vellykket", + "BACKUP_COUNT_TEXT": "Fant {count} {count, plural, =1{sikkerhetskopi} other{sikkerhetskopier}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Gjenoppretter sikkerhetskopien...", + "CREATING_BACKUP": "Oppretter sikkerhetskopi av {count} filer...", + "LOADING_BACKUPS": "Laster sikkerhetskopier...", + "REMOVING_BACKUP": "Sletter sikkerhetskopien..." + }, + "CREATE_BACKUP_BUTTON": "Opprett sikkerhetskopi", + "DELETE_CONFIRMATION": { + "MESSAGE": "Er du sikker på at du vil slette sikkerhetskopi '{name}'? \nDenne handlingen kan ikke angres.", + "TITLE": "Vil du slette sikkerhetskopi av WTF-mappen din?" + }, + "DIALOG_TITLE": "Sikkerhetskopiering av WTF-innstillinger: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Kunne ikke gjenopprette fra sikkerhetskopi: {name}", + "FAILED_TO_DELETE": "Kunne ikke slette sikkerhetskopi: {name}", + "GENERIC_ERROR": "Det oppsto et problem under behandlingen av denne sikkerhetskopien", + "INVALID_CONTENTS": "Det oppsto et problem under behandlingen av denne sikkerhetskopien", + "INVALID_CREATED_AT": "Det oppsto et problem under behandlingen av denne sikkerhetskopien", + "INVALID_CREATED_BY": "Det oppsto et problem under behandlingen av denne sikkerhetskopien" + }, + "SHOW_FOLDER_BUTTON": "Vis mappe", + "TOOL_TIP": { + "APPLY_BUTTON": "Gjennopprett denne sikkerhetskopien", + "DELETE_BUTTON": "Slett denne sikkerhetskopien" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/pl.json b/WowUp/wowup-electron/src/assets/i18n/pl.json new file mode 100644 index 0000000..84c051b --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/pl.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Aktywne addony: {count}", + "ADDED_BADGE_TOOLTIP": "Podejmiemy próbę zainstalowania addonu", + "CONFLICT_BADGE_TOOLTIP": "Niezgodne addony nie będą modyfikowane", + "COPY_BUTTON": "Kopiuj", + "DIALOG_TITLE": "Import/Eksport Addonów: {clientType}", + "EXPORT_STRING_COPIED": "Tekst z eksportu skopiowany do schowka", + "EXPORT_STRING_PASTED": "Zawartość schowka wstawiona", + "EXPORT_TAB_LABEL": "Eksport", + "EXPORT_TEXT_LABEL": "Dane Addonu z Eksportu", + "GENERIC_IMPORT_ERROR": "Wystąpił błąd podczas importu", + "IGNORED_ADDON_COUNT": "Zignorowane addony: {count}", + "IMPORT_ADDED_COUNT": "{count} dodane", + "IMPORT_BADGE_ADDED": "Nowy", + "IMPORT_BADGE_CONFLICT": "Niezgodność", + "IMPORT_BADGE_NO_CHANGE": "Brak zmian", + "IMPORT_BUTTON": "Import", + "IMPORT_CONFLICT_COUNT": "{count} niezgodnych", + "IMPORT_NO_CHANGE_COUNT": "{count} niezmienionych", + "IMPORT_STRING_INVALID": "Zawartość importu była niepoprawna", + "IMPORT_TAB_LABEL": "Import", + "IMPORT_TEXT_INSTRUCTIONS": "Wklej zawartość danych addonu WowUp do okienka poniżej aby zacząć", + "IMPORT_TEXT_LABEL": "Import danych", + "IMPORT_TOTAL_COUNT": "Importowanie {count} {count, plural, =1{addonu} other{addonów}}", + "INSTALL_BUTTON": "Zainstaluj", + "INVALID_CLIENT_TYPE": "Zawartość importu nie jest zgodna z klientem", + "NO_CHANGE_BADGE_TOOLTIP": "Masz już zainstalowany ten addon", + "PASTE_BUTTON": "Wklej", + "PROVIDER_MISMATCH": "Ten addon jest już zainstalowany, ale dostawca nie zgadza się", + "RESET_BUTTON": "Reset", + "VERSION_MISMATCH": "Ten addon jest już zainstalowany, ale wersja nie zgadza się" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Czemu widzę tę reklamę?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "Aby używać wago.io / CurseForge jako dostawcę addonu i pomagać autorom za ich ciężką pracę nad twoimi ulubionimi addonami, jesteśmy zmuszeni pokazywać tę reklamę.\n\nJeśli nie chcesz widzieć tej reklamy, zawsze możesz zablokować Wago / CurseForge jako dostawcę w zakładce opcje.", + "TITLE": "Czemu widzę tę reklamę?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Kopiuj", + "CUT": "Wytnij", + "LABEL": "Edytuj", + "PASTE": "Wklej", + "REDO": "Powtórz", + "SELECT_ALL": "Zaznacz wszystko", + "UNDO": "Cofnij" + }, + "QUIT": "Wyjdź", + "VIEW": { + "FORCE_RELOAD": "Wymuś odświeżanie", + "LABEL": "Widok", + "RELOAD": "Odśwież", + "TOGGLE_DEV_TOOLS": "Przełącz narzędzia Devów", + "TOGGLE_FULL_SCREEN": "Przełącz pełny ekran", + "ZOOM_IN": "Przybliż", + "ZOOM_OUT": "Oddal", + "ZOOM_RESET": "Reset lupy" + }, + "WINDOW": { + "CLOSE": "Zamknij", + "LABEL": "Okno" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Automatycznie zaktaulizowane\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "Automatycznie zaktualizowano {count} {count, plural, =1{addon} other{addonów}}.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Automatyczna aktualizacja", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Opuść pełny ekran", + "FULLSCREEN_SNACKBAR": { + "MAC": "Wciśnij ^⌘F aby wyjść z pełnego ekranu", + "WINDOWS": "Wciśnij F11 aby wyjść z pełnego ekranu" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nJesteś pewnien że chcesz otworzyć zewnętrzną stronę w twojej domyślnej przeglądarce?", + "TITLE": "Zamierzasz wyjść z WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Nieznany" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Skanowanie addonów zakończone...", + "ADDON_SCAN_STARTED": "Skanowanie addonów zaczęte...", + "ADDON_SCAN_UPDATE": "Skanowanie {count} folderów..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Sprawdź aktualizacje...", + "QUIT_ACTION": "Wyjdź", + "SHOW_ACTION": "Pokaż" + }, + "THEME": { + "ALLIANCE": "Przymierze", + "DEFAULT": "WowUp", + "GROUP_DARK": "Ciemny", + "GROUP_LIGHT": "Jasny", + "HORDE": "Horda" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Pełny ekran", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Sprawdzanie aktualizacji", + "DOWNLOADED_TOOLTIP": "Zainstaluj aktualizacje WowUp", + "DOWNLOADING_UPDATE": "Pobieranie aktualizacji", + "INSTALL_MESSAGE": "Chcesz zrestartować WowUp i zainstalować aktualizacje?", + "INSTALL_TITLE": "Aktualizacja WowUp gotowa", + "NOT_AVAILABLE": "Najnowsza wersja WowUp jest już zainstalowana", + "PORTABLE_DOWNLOAD_MESSAGE": "Chcesz ręcznie zainstalować najnowszą przenośną wersje?\n\nPotrzebujesz ręcznie zamknąć aplikacje i skopiować na nową wersje.", + "PORTABLE_DOWNLOAD_TITLE": "Wymagane ręczne pobieranie", + "SNACKBAR_ACTION": "Aktualizacja & Resetowanie", + "SNACKBAR_TEXT": "Najnowsza wersja WowUp jest dostępna", + "TOOLTIP": "Aktualizacja WowUp jest dostępna", + "UPDATE_AVAILABLE": "Rozpoczynanie pobierania", + "UPDATE_ERROR": "Nie udało się zaktualizować WowUp" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Osiągnięcia", + "ACTION_BARS": "Paski umiejętności", + "ALL_ADDONS": "Wszystkie addony", + "AUCTION_ECONOMY": "Aukcja & Ekonomia", + "BAGS_INVENTORY": "Plecak & Ekwipunek", + "BOSS_ENCOUNTERS": "Starcia z bossami", + "BUFFS_DEBUFFS": "Buffy & Debuffy", + "BUNDLES": "Pakiety", + "CHAT_COMMUNICATION": "Czat & Komunikacja", + "CLASS": "Klasa", + "COMBAT": "Walka", + "COMPANIONS": "Towarzysze", + "DATA_EXPORT": "Eksport danych", + "DEVELOPMENT_TOOLS": "Narzędzia programistyczne", + "GUILD": "Gildia", + "LIBRARIES": "Bibliotetki", + "MAIL": "Poczta", + "MAP_MINIMAP": "Mapa & Minimapa", + "MISCELLANEOUS": "Pozostałe", + "MISSIONS": "Misje", + "PLUGINS": "Wtyczki", + "PROFESSIONS": "Profesje", + "PVP": "PVP", + "QUESTS_LEVELING": "Zadania & Leveling", + "ROLEPLAY": "Roleplay", + "TOOLTIPS": "Podpowiedzi", + "UNIT_FRAMES": "Unit Framesy" + }, + "ADDON_STATE": { + "IGNORED": "Zignorowany", + "INSTALL": "Zainstaluj", + "PENDING": "W toku", + "UNAVAILABLE": "Niedostępny", + "UNAVAILABLE_TOOLTIP": "Ten autor lub dostawca uczynił ten addon niedostępnym.", + "UNINSTALL": "Odinstaluj", + "UNKNOWN": "", + "UPDATE": "Aktualizacja", + "UPTODATE": "Aktualny", + "WARNING": "Ostrzeżenie" + }, + "ADDON_STATUS": { + "BACKINGUP": "Tworzenie kopii zapasowej", + "COMPLETE": "Zainstalowany", + "DOWNLOADING": "Pobieranie", + "ERROR": "Błąd", + "INSTALLING": "Instalowanie", + "PENDING": "W toku", + "RETRY": "Retrying...", + "UNINSTALLING": "Odinstalowanie", + "UPDATING": "Aktualizowanie..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "Wykryto problem z tym addonem. Nie możemy zaktualizować tego addonu czy zapewnić szczegółów.", + "GENERIC_TOOLTIP": "Wykryto problem z tym addonem", + "MISSING_ON_PROVIDER_DESCRIPTION": "{providerName} nie zwróciliśmy addonu gdy o to prosiliśmy.
Nie możemy zaktualizować tego addonu czy zapewnić szczegółów dopóki {providerName} nie naprawi go.", + "MISSING_ON_PROVIDER_TOOLTIP": "{providerName} nie zwraca tego dodatku zgodnie z oczekiwaniami", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} prawidłowo zwrócił ten addon, lecz nie ma żadnych pasujących plików do tej wersji.
Kiedy będzie aktualizacja wspierająca tę wersje, komunikat powienien zniknąć.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} nie ma żadnych pasujących plików dla tej wersji gry", + "TOC_NAME_MISMATCH_DESCRIPTION": "Nazwa folderu tego addonu nie jest zgodna z oczekiwanym plikiem toc, w związku z czym mogą wystąpić problemy z tym addonem w grze.", + "TOC_NAME_MISMATCH_TOOLTIP": "Folder tego addonu nie jest zgodny z toc" + }, + "CLIENT_TYPES": { + "BETA": "Beta Dragonflight", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Publiczny Serwer Testowy (Wrath Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count} {count, plural, =1{dzień} other{dni}} temu", + "HOURS_AGO": "{count} {count, plural, =1{godzinę} other{godziny}} temu", + "JUST_NOW": "Przed chwilą", + "MONTHS_AGO": "{count} {count, plural, =1{miesiąc} other{miesięcy}} temu", + "YEARS_AGO": "{count} {count, plural, =1{rok} other{lata}} temu", + "YESTERDAY": "Wczoraj" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} potrzebuje {dependencyCount, plural, =1{zależność} other{zależności}}" + }, + "DOWNLOAD_COUNT": { + "e+0": "kilka", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count} tys.", + "e+4": "{count} tys.", + "e+5": "{count} tys.", + "e+6": "{count} mln", + "e+7": "{count} mln", + "e+8": "{count} mln", + "e+9": "{count} mld" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alfa", + "BETA": "Beta", + "STABLE": "Stabilna" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Nie można włączyć natychmiastowych aktualizacji dla twojego konta. Proszę spróbować póżniej lub dołąćzyć na naszego Discorda.", + "ADDON_INSTALL_ERROR": "Nie można zainstalować addona, {addonName}. Proszę spróbować póżniej.", + "ADDON_SCAN_ERROR": "Wystąpił błąd pasującego folderu addonu z {providerName}, proszę spróbować póżniej.", + "ADDON_SYNC_ERROR": "Wystąpił bląd w sprawdzaniu aktualizacji z {providerName}, proszę spróbować póżniej.", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: Wystąpił bląd w sprawdzaniu aktualizacji dla {addonName} z {providerName}, proszę spróbować póżniej.", + "CHANGE_PROVIDER_ERROR": "Nie udało się zmienić dostawcy addonu {addonName} na {providerName}", + "GITHUB_LIMIT_ERROR": "Osiągnąłeś swój GitHub API limit z {max} prób.\nPoczekaj dopóki {reset} i spróbuj ponownie.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Nie można sprawdzić aktualizacji dla {addonName}.\nProszę sprawdzić czy repozytorium jest poprawne lub zingnoruj ten addon." + }, + "PROGRESS_SPINNER": { + "LOADING": "Ładowanie..." + }, + "PROVIDER_ERROR": "Błąd, kontakt z {providerName}", + "SEARCH": { + "NO_ADDONS": "Nie znaleziono addonów" + }, + "WOW_EXE_SELECTION_NAME": "Ścieżka do pliku WoW" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "Addon ID:", + "BY_AUTHOR": "Przez {authorName}", + "CHANGELOG_TAB": "Lista zmian", + "COPY_ADDON_ID_SNACKBAR": "Addon ID skopiowany do schowka", + "COPY_ADDON_ID_TOOLTIP": "Skopiuj ID addonu do schowka", + "DEPENDENCY_TEXT": "Ten addon potrzebuje {dependencyCount} {dependencyCount, plural, =1{zależność} other{zależności}}", + "DESCRIPTION_NOT_FOUND": "Nie znaleziono opisu", + "DESCRIPTION_TAB": "Opis", + "FUNDING_LINK_TITLE": "Wspieraj tego autora", + "IMAGES_TAB": "Podglądy", + "MISSING_DEPENDENCIES": "Brakujące zależności", + "NO_CHANGELOG_TEXT": "Brak listy zmian", + "VIEW_IN_BROWSER_BUTTON": "Otwórz w przeglądarce", + "VIEW_ON_PROVIDER_PREFIX": "Otwórz w" + }, + "ALERT": { + "ERROR_TITLE": "Błąd", + "POSITIVE_BUTTON": "Okej" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "Nie", + "POSITIVE_BUTTON": "Tak" + }, + "CURSE_MIGRATION": { + "MESSAGE": "API CurseForge zostało wyłączone, więc WowUp nie może już pobierać z niego informacji o addonach.
Niestety, oznacza to, że dostawca CurseForge został usunięty, a Twoje addony z CurseForge nie będą już aktualizowane.
Zaleca się przeprowadzenie ponownego skanowania w celu sprawdzenia, jakie addony można znaleźć u innych dostawców.
Proces ponownego skanowania może chwilę potrwać.
Przeczytaj więcej tutaj.", + "NEGATIVE_BUTTON": "Ponowne skanowanie ręczne", + "POSITIVE_BUTTON": "Ponowne skanowanie automatyczne", + "RE_SCAN_ERROR": "Ponowne skanowanie automatyczne nie powiodło się, spróbuj ponownie ręcznie.", + "RE_SCAN_SUCCESS": "Skończone ponowne skanowanie automatyczne, można przystąpić do pracy.", + "TITLE": "Migracja CurseForge" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Addon zainstalowany!", + "ADDON_INSTALLING": "Instalowanie addonu", + "CANCEL_BUTTON": "Zamknij", + "ERRORS": { + "ADDON_NOT_FOUND": "Żaden addon nie został znaleziony dla protokołu: {protocol}", + "GENERIC": "Błąd pobierania danych dla protokołu: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "Brak zainstalowanych klientów WoW dla protokołu: {protocol}" + }, + "INSTALL_BUTTON": "Zainstaluj", + "TITLE": "Zainstaluj addon z {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "Addon URL", + "ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub lub WowInterface URL", + "CLOSE_BUTTON": "Zamknij", + "DESCRIPTION": "Jeżeli chcesz zainstalować addon bezpośrednio z linka, wklej go poniżej aby zacząć.", + "DOWNLOAD_COUNT": "{textCount} {count, plural, =1{pobranie} other{pobrań}} u {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "Nie znaleziono zasobów aby pobrać {message}.\n\nA Potrzebny jest prawidłowy plik zip w wydaniu aby pobrać go dla WowUp.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "Nie znaleziono zasobów aby pobrać z {message}.\n\nA prawidłowy plik zip z konćówką '-bc' jest potrzebny aby pobrać go dla WowUp.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "Nie znaleziono zasobów aby pobrać z {message}.\n\nA prawidłowy plik zip z konćówką '-classic' jest potrzebny aby pobrać go dla WowUp.", + "FAILED_TO_CONNECT": "Nie można połączyć się z API, proszę poczekać i spróbować ponownie.", + "INSTALL_FAILED": "Coś poszło nie tak z instalacją addona, proszę spróbować ponownie.\n\nJeżeli wiadomość nadal pojawia się, możesz otrzymać pomoc na Discordzie na kanale #help-me channel.", + "INVALID_URL": "Podana wartość nie jest poprawna dla adresu URL. Przykłady poprawnych adresów URL dla addonów są tutaj:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "Nie znaleziono addonu, upewnij się że twój adres URL wskazuje na właściwą stronę.\n\nKiedy instalujesz z githuba, upewnij się że repozytorium ma znacznik wydania z archiwum zip zawierające addon.", + "NO_RELEASE_FOUND": "Nie znaleziono żadnych wydań dla {message}.\n\nPoprawnie wydanie z plikiem zasobów zip są potrzebne aby pobrać je dla WoWUP.", + "NO_SEARCH_RESULTS": "Nie znaleziono żadnych wyników wyszukiwania.", + "TITLE": "Instalacja addonu nie powiodła się", + "WRATH_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-wrath' is required in order for WowUp to download it." + }, + "IMPORT_ASSET_WARNING": "Nie mogliśmy zwerifikować czy najnowsza wersja tego addonu jest kompatybilna z twoim zaznaczonym klientem.\n\nAle znaleźliśmy plik zip \"{zipName}\".\n\nInstalacja na własną odpowiedzialność.", + "IMPORT_BUTTON": "Importuj", + "IMPORT_WARNING_TITLE": "Ostrzeżenie importu addonu", + "INSTALL_BUTTON": "Instaluj", + "INSTALL_SUCCESS_LABEL": "Zainstalowany!", + "SUPPORTED_SOURCES": "Wspieraj WowInterface oraz GitHub", + "TITLE": "Zainstaluj addon z URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Opis łatki {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "Przed rozpoczęciem potrzebujemy ustawić klika uprawień dla aplikacji.", + "POSITIVE_BUTTON": "Potwierdź", + "TELEMETRY": { + "DESCRIPTION": "Pomóż ulepszyć WowUp poprzez wysyłanie anonimowych danych o instalacji aplikacji lub o ich błędach.", + "TOGGLE_LABEL": "Zezwalaj na telemetrię" + }, + "TITLE": "WowUp Konfiguracja uprawnień", + "WAGO": { + "DESCRIPTION": "Włącz dostawcę addonu Wago.io aby pomóc twórcom twojego ulubionego addonu! Spowoduje to wyświetlenie panelu promocyjnego wymaganego do korzystania z ich usług.\nDecydując się na udział w programie wyrażasz zgodę na ich Warunki świadczenia usług/a> oraz Zgodę na przetwarzanie danych.", + "TOGGLE_LABEL": "Włącz dostawcę Wago.io" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "Nie wygląda to na poprawną aplikację World of Warcraft:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Pomóż ulepszać WowUp poprzez wysyłanie anonimowych danych o instalacji aplikacji lub o ich błędach.", + "NEGATIVE_BUTTON": "Nie dzięki", + "POSITIVE_BUTTON": "Pewnie!", + "TITLE": "WowUp Telemetria" + }, + "TRUST_DOMAIN_CHECKBOX": "Zaufaj tej domenie i nie pytaj mnie w przyszłości" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Podziękowania", + "CHANGE_LOG_SECTION_LABEL": "Lista zmian", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Sprawdź stronę!" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Zaloguj się teraz!", + "LOGOUT_BUTTON": "Wyloguj się", + "LOGOUT_CONFIRMATION_MESSAGE": "Jesteś pewny że chcesz się wylogować? Wszystkie twoje lokalne dane konta zostaną usunięte, dopóki nie zalogujesz się ponownie.", + "LOGOUT_CONFIRMATION_TITLE": "Wyloguj?", + "MANAGE_ACCOUNT_BUTTON": "Zarządzaj kontem", + "TITLE": "Konto" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Kategorie", + "ADDON_CATEGORIES_MENU_TITLE": "Kategorie addonu", + "ADDON_CATEGORIES_SELECTED_TITLE": "Kategoria: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Przeglądaj różne kategorie", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Zainstaluj z adresu URL", + "INSTALL_FROM_URL_TOOLTIP": "Zainstaluj addon z adresu URL", + "REFRESH_BUTTON": "Odśwież", + "REFRESH_TOOLTIP": "Odśwież wyniki addonów", + "RESET_CATEGORY_TOOLTIP": "Reset kategorii", + "SEARCH_LABEL": "Szukaj", + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "AUTHOR_COLUMN_HEADER": "Autor(-rzy)", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Pobrania", + "PROVIDER_COLUMN_HEADER": "Dostawca", + "RELEASED_AT_COLUMN_HEADER": "Wydany", + "STATUS_COLUMN_HEADER": "Status" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "Informacje", + "ACCOUNT_TAB_TITLE": "Konto", + "COLLAPSE_BUTTON_TITLE": "Zwiń", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Rozwiń", + "GET_ADDONS_TAB_TITLE": "Pobierz addony", + "GUIDE_TAB_TITLE": "Przewodnik", + "MIGRATING_ADDONS": "Migrowanie addonów...", + "MY_ADDONS_TAB_TITLE": "Moje addony", + "NEWS_TAB_TITLE": "Aktualności", + "OPTIONS_TAB_TITLE": "Opcje" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count} {count, plural, =1{addon} other{addonów}} zaznaczono", + "ALPHA_ADDON_CHANNEL": "Alfa", + "AUTO_UPDATE_ADDON_BUTTON": "Automatyczna aktualizacja", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Powiadomienia włączone", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "Kanał", + "IGNORE_ADDON_BUTTON": "Ignoruj", + "PROVIDER_SUBMENU_TITLE": "Dostawcy", + "REINSTALL_ADDON_BUTTON": "Zainstaluj ponownie", + "REMOVE_ADDON_BUTTON": "Usuń", + "SHOW_FOLDER": "Pokaż folder", + "STABLE_ADDON_CHANNEL": "Stabilna" + }, + "ADDON_IS_CODE_REPOSITORY": "Addon wydaje się być repozytorium kodu", + "ADDON_REMOVED_SNACKBAR": "Pomyślnie usunięto: {addonName} ", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Chcesz zmienić dostawcę addonu dla {addonName} na {providerName}? Ta operacja spowoduje odinstalowanie istniejącego addonu i zastąpi go kopią od nowego dostawcy.", + "TITLE": "Zmienić dostawcę addonu?" + }, + "CHECK_UPDATES_BUTTON": "Sprawdź aktualizacje", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Sprawdź najnowsze aktualizacje addonów", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{aktualizacja} other{aktualizacje}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Pokaż kolumny" + }, + "ERROR_SNACKBAR": "Wystąpił błąd", + "FILTER_LABEL": "Filtruj", + "FUNDING_TOOLTIP": { + "CUSTOM": "Wesprzyj tego autora", + "GENERIC": "Wesprzyj tego autora na {platform}", + "GITHUB": "Wesprzyj tego autora na GitHub", + "PATREON": "Wesprzyj tego autora na Patreon", + "PAYPAL": "Wesprzyj tego autora na PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Import/Eksport addonów", + "MULTIPLE_PROVIDERS_TOOLTIP": "Ten addon ma wielu dostawców", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{addon} other{addonów}}", + "JOIN_DISCORD": "Porozmawiaj z nami na Discordzie", + "PATREON_SUPPORT": "Wspieraj WowUp na Patreonie", + "SEARCH_RESULTS": "{count} {count, plural, =1{wynik} other{wyników}}", + "VIEW_GITHUB": "Sprawdź kod na GitHubie", + "VIEW_GUIDE": "Sprawdź nasz przewodnik, aby zobaczyć, co potrafi WowUp." + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Brak wymaganej zależności", + "RESCAN_FOLDERS_BUTTON": "Ponowne skanowanie folderów", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Przeskanuj folder klienta pod kątem zainstalowanych dodatków", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "Ponowne skanowanie będzie skutkowało próbą odgadnięcia jakie addony są aktualnie zainstalowane, skutkując wymazanie informacji o addonie. Użyj tej funkcji, jeśli niektóre addony nie są rozpoznawane lub wersje addonów nie są wyświetlane poprawnie. To skanowanie nigdy nie usunie zainstalowanych addonów, które zna WowUp.\n\nSkanowanie może potrwać kilka chwil.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Rozpocząć ponowne skanowanie?", + "SPINNER": { + "GATHERING_ADDONS": "Zbieranie addonów...", + "UPDATING": "Aktualizowanie {updateCount}/{addonCount}", + "UPDATING_WITH_ADDON_NAME": "Aktualizowanie {updateCount}/{addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "ADDON_INSTALL_BUTTON": "Instaluj", + "ADDON_UPDATE_BUTTON": "Aktualizuj", + "AUTHOR_COLUMN_HEADER": "Autor(-rzy)", + "AUTO_UPDATE_ICON_TOOLTIP": "Automatyczna aktualizacja włączona", + "GAME_VERSION_COLUMN_HEADER": "Wersja gry", + "LATEST_VERSION_COLUMN_HEADER": "Ostatnia wersja", + "PROVIDER_COLUMN_HEADER": "Dostawca", + "PROVIDER_RELEASE_CHANNEL": "Kanał dostawcy", + "RELEASED_AT_COLUMN_HEADER": "Wydany", + "STATUS_COLUMN_HEADER": "Status", + "UPDATED_AT_COLUMN_HEADER": "Zaktualizowany" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Usunięcie addonu przez WowUp usunie zaznaczony addon z twojego folderu interfejs/addony. Ustawienia postaci dla tego addonu nie zostaną usunięte.", + "CONFIRMATION_LESS_THAN_THREE": "Czy na pewno chcesz usunąć {count} addonów?", + "CONFIRMATION_MORE_THAN_THREE": "Czy na pewno chcesz usunąć wybrane {count} addony?", + "CONFIRMATION_ONE": "Czy na pewno chcesz usunąć {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} ma {dependencyCount} {dependencyCount, plural, =1{zależność} other{zależności}}. Czy chcesz je również usunąć?", + "DEPENDENCY_TITLE": "Usunąć zależności addonów?", + "TITLE": "Odinstaluj {count, plural, =1{addon} other{addonów}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "Zainstalowany addon nie pasuje do żadnego z skonfigurowanych dostawców", + "UPDATE_ALL_BUTTON": "Aktualizuj wszystko", + "UPDATE_ALL_BUTTON_TOOLTIP": "Zaktualizuj wszystkie addony dla tego klienta", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Aktualizuj wszystkie klienty", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Aktualizuj Retail/Classic" + }, + "WTF_BACKUP_BUTTON": "Ustawienia kopii zapasowej interfejsu" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "Link do wiadomości skopiowany do schowka", + "NEWS_LINK_COPY_TOOLTIP": "Skopiuj link wiadomości", + "PAGE_CONTEXT_FOOTER": "{count} nowości", + "REFRESH_TOOLTIP": "Odśwież kanał wiadomości" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Ad or access key required", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "Jeśli zażądałeś klucza API CurseForge, możesz go wprowadzić tutaj, aby połączyć się z API.", + "API_KEY_TITLE": "Klucz API CurseForge", + "PROVIDER_NOTE": "Wymagany klucz API" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Wybierz, którzy dostawcy mogą być używani do wyszukiwania i instalowania nowych addonów", + "FIELD_LABEL": "Włączone addony dostawców", + "INPUT_LABEL": "Dostawcy" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "Osobisty token dostępu zwiększy liczbę wywołań API GitHuba, które możesz swobodnie wykonywać. Dowiedz się więcej", + "PLACEHOLDER": "Osobisty token dostępu", + "TITLE": "Osobisty token dostępu do GitHuba" + }, + "TITLE": "Addony", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Przełączenie na kanał Beta pozwoli Ci otrzymywać eksperymentalne wersje, które zawierają poprawki błędów, jak również nowe i nadchodzące funkcje. Możesz powrócić do aktualnej stabilnej wersji tylko poprzez odinstalowanie istniejącej aplikacji i ponowną instalację z wowup.io.\n\nChociaż kanał Beta jest funkcjonalny, korzystasz z niego na własne ryzyko.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Przełączenie się na kanał Stabilny uniemożliwi Ci otrzymywanie kolejnych wersji Beta, następna aktualizacja będzie następną wersją Stabilną.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Ustawianie kanału wydania aplikacji", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Tak, rozumiem.", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Przełączanie pomiędzy wersjami Beta i Stabilną aplikacji", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Kanał", + "APP_RELEASE_CHANNEL_LABEL": "Kanał udostępniania aplikacji", + "CURRENT_LANGUAGE_LABEL": "Aktualny język", + "CURSE_PROTOCOL_DESCRIPTION": "Podczas pobierania dodatków ze strony CurseForge, WowUp zajmie się instalacją", + "CURSE_PROTOCOL_LABEL": "Obsługa pobranych linków CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Pokaż plakietkę na ikonie aplikacji z liczbą dodatków z dostępnymi aktualizacjami.", + "ENABLE_APP_BADGE_LABEL": "Włącz powiadamianie o odznakach aplikacji", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Włącz różne wyskakujące okienka powiadomień, np. o automatycznie aktualizowanych addonach.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Włącz powiadomienia systemowe", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "Podczas otwierania strony szczegółów addonu, automatycznie wybieraj ostatnio otwartą kartę", + "KEEP_LAST_OPENED_TAB_LABEL": "Zachowaj ostatnio otwartą kartę", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Zamykając okno WowUp, zminimalizuj je do paska menu.", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Podczas zamykania okna WowUp zminimalizuj je do obszaru powiadomień paska zadań.", + "MINIMIZE_ON_CLOSE_LABEL": "Zminimalizuj przy zamykaniu", + "PROTOCOL_DESCRIPTION": "WowUp zarejestruje niestandardowy protokół URI w Twoim systemie i będzie obsługiwał przychodzące zapytania.", + "PROTOCOL_LABEL": "Włącz protokół wowup:// URI", + "SCALE_DESCRIPTION": "Zmiana współczynnika powiększenia dla całej aplikacji.", + "SCALE_LABEL": "Skalowanie", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Zmiana domyślnego języka wymaga ponownego uruchomienia aplikacji.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Ustawianie nowego języka domyślnego", + "SET_LANGUAGE_DESCRIPTION": "Wybierz język, na który chcesz zmienić", + "SET_LANGUAGE_LABEL": "Ustaw język", + "START_MINIMIZED_DESCRIPTION": "WowUp zostanie zminimalizowany i nie pojawi się.", + "START_MINIMIZED_LABEL": "Uruchom WowUp zminimalizowany", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp zostanie uruchomiony automatycznie przy starcie systemu.", + "START_WITH_SYSTEM_LABEL": "Uruchom WowUp z systemem", + "TELEMETRY_DESCRIPTION": "Pomóż ulepszyć WowUp, wysyłając anonimowe dane dotyczące instalacji lub błędów.", + "TELEMETRY_LABEL": "Telemetria", + "THEME_DESCRIPTION": "Zmień motyw kolorystyczny na taki, jaki Ci się podoba", + "THEME_LABEL": "Kolor motywu", + "TITLE": "Aplikacja", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp może ustawić się jako domyślna obsługa pobierania linków do CurseForge. Może to spowodować problemy, jeśli spróbujesz użyć aplikacji CurseForge, czy na pewno chcesz kontynuować?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Obsługiwać pobieranie z CurseForge?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Czy chcesz ponownie uruchomić?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "Disabling hardware acceleration might solve FPS issues and fix other rendering issues in this app.
Changing this setting requires a restart.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Wyłączenie akceleracji sprzętowej wymaga ponownego uruchomienia aplikacji.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Włączenie akceleracji sprzętowej wymaga ponownego uruchomienia aplikacji.", + "USE_HARDWARE_ACCELERATION_LABEL": "Włącz akcelerację sprzętową", + "USE_SYMLINK_SUPPORT": "Włącz obsługę Symlink", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Włączenie obsługi symlink pozwoli WowUp na rozpoznawanie symlinków podczas ponownego skanowania. Ostrzeżenie: Jeśli nie wiesz, co to jest symlink, nie potrzebujesz tego. Podczas aktualizacji symlink będzie obecnie zastępowany rzeczywistym folderem, a link zostanie utracony.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Włączyć obsługę symlink?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Pozwól WowUp na skanowanie folderów z symlinkami w folderze addonów. Ostrzeżenie: zostaną one zastąpione podczas aktualizacji/instalacji." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Pokaż pliki konfiguracyjne", + "CONFIG_FILES_DESCRIPTION": "Otwórz folder, w którym przechowywane są na przykład pliki addons.json oraz preferences.json.", + "CONFIG_FILES_LABEL": "Pliki konfiguracyjne", + "DEBUG_DATA_BUTTON": "Zrzut danych debugowania", + "DEBUG_DATA_DESCRIPTION": "Rejestruj dane debugowania, aby pomóc w diagnozowaniu potencjalnych problemów. Można je znaleźć w najnowszym pliku dziennika dla ciekawskich.", + "DEBUG_DATA_LABEL": "Dane debugowania", + "LOG_FILES_BUTTON": "Pokaż pliki dziennika", + "LOG_FILES_DESCRIPTION": "Otwórz folder, w którym znajduje się kilka ostatnich plików dziennika.", + "LOG_FILES_LABEL": "Pliki dziennika", + "TITLE": "Debugowanie" + }, + "TABS": { + "ABOUT": "Informacje", + "ADDONS": "Addony", + "APPLICATION": "Aplikacja", + "CLIENTS": "Klienty", + "CURSEFORGE": "CurseForge", + "DEBUG": "Debugowanie", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Dodaj nowy", + "AUTO_UPDATE_DESCRIPTION": "Wszystkie istniejące i nowo zainstalowane addony będą domyślnie ustawione na automatyczną aktualizację.", + "AUTO_UPDATE_LABEL": "Automatyczna aktualizacja", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Anuluj", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Czy na pewno chcesz usunąć instalację w \"{location}\"? Spowoduje to usunięcie wszystkich zapisanych informacji o addonach dla tego klienta.\n\nTwoje foldery addonów nie zostaną usunięte.", + "TITLE": "Usunąć instalację World of Warcraft?" + }, + "CLIENT_TYPE_INPUT_HINT": "Zaznacz aplikacje {clientTypeName} \"{clientFolderName}\"", + "CLIENT_TYPE_PATH_LABEL": "{clientTypeName} ścieżka", + "DEFAULT_ADDON_CHANNEL_LABEL": "Domyślny kanał addonu", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Kanał addonu", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Edytuj", + "MOVE_DOWN_BUTTON": "Przesuń w dół", + "MOVE_UP_BUTTON": "Przesuń w górę", + "NO_CLIENTS_FOUND_TEXT": "Nie znaleziono instalacji World of Warcraft, proszę upewnić się, że klient Battle.net jest aktualny lub dodać klienta ręcznie", + "OPEN_FOLDER_BUTTON": "Otwórz folder", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Wybierz", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Usuń", + "RESCAN_CLIENTS_BUTTON": "Ponowne skanowanie", + "RESCAN_CLIENTS_LABEL": "Ponowne skanowanie zainstalowanych klientów World of Warcraft", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Zapisz", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Czy na pewno chcesz zastosować tę kopię zapasową do ustawień interfejsu?\n\nPrzed utworzeniem kopii zapasowej upewnij się, że gra World of Warcraft nie jest uruchomiona.\n\nTej operacji nie można cofnąć.", + "TITLE": "Zastosować kopię zapasową WTF?" + }, + "BACKUP_APPLY_SUCCESS": "Pomyślne zastosowanie kopii zapasowej: {name}", + "BACKUP_COUNT_TEXT": "Znaleziono {count} {count, plural, =1{kopię zapasową} other{kopie zapasowe}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Stosowanie kopii zapasowej...", + "CREATING_BACKUP": "Tworzenie kopii zapasowej z {count} plików...", + "LOADING_BACKUPS": "Ładowanie kopii zapasowych...", + "REMOVING_BACKUP": "Usuwanie kopii zapasowej..." + }, + "CREATE_BACKUP_BUTTON": "Utwórz kopię zapasową", + "DELETE_CONFIRMATION": { + "MESSAGE": "Czy na pewno chcesz usunąć kopię zapasową {name}?\nNie można tego cofnąć.", + "TITLE": "Usunąć kopię zapasową WTF?" + }, + "DIALOG_TITLE": "Kopia zapasowa ustawień WTF: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Nie udało się zastosować kopii zapasowej: {name}", + "FAILED_TO_DELETE": "Nie udało się usunąć kopii zapasowej: {name}", + "GENERIC_ERROR": "Wystąpił problem z przetwarzaniem tej kopii zapasowej", + "INVALID_CONTENTS": "Wystąpił problem z przetwarzaniem tej kopii zapasowej", + "INVALID_CREATED_AT": "Wystąpił problem z przetwarzaniem tej kopii zapasowej", + "INVALID_CREATED_BY": "Wystąpił problem z przetwarzaniem tej kopii zapasowej" + }, + "SHOW_FOLDER_BUTTON": "Pokaż folder", + "TOOL_TIP": { + "APPLY_BUTTON": "Zastosuj tę kopię zapasową", + "DELETE_BUTTON": "Usuń tę kopię zapasową" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/pt.json b/WowUp/wowup-electron/src/assets/i18n/pt.json new file mode 100644 index 0000000..12e4f82 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/pt.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Addons ativos: {count}", + "ADDED_BADGE_TOOLTIP": "Iremos tentar instalar este addon", + "CONFLICT_BADGE_TOOLTIP": "Addons conflitantes não serão modificados", + "COPY_BUTTON": "Copiar", + "DIALOG_TITLE": "Importar/Exportar Addons: {clientType}", + "EXPORT_STRING_COPIED": "String de exportação copiado para a área de transferência.", + "EXPORT_STRING_PASTED": "Conteúdos da área de transferência inseridos", + "EXPORT_TAB_LABEL": "Exportar", + "EXPORT_TEXT_LABEL": "Dados de Exportação de Addon", + "GENERIC_IMPORT_ERROR": "Ocorreu um erro durante a importação", + "IGNORED_ADDON_COUNT": "Addons ignorados: {count}", + "IMPORT_ADDED_COUNT": "{count} adicionado(s)", + "IMPORT_BADGE_ADDED": "Novo", + "IMPORT_BADGE_CONFLICT": "Conflito", + "IMPORT_BADGE_NO_CHANGE": "Sem mudança", + "IMPORT_BUTTON": "Importar", + "IMPORT_CONFLICT_COUNT": "{count} em conflito", + "IMPORT_NO_CHANGE_COUNT": "{count} não modificado(s)", + "IMPORT_STRING_INVALID": "String de importação era inválido", + "IMPORT_TAB_LABEL": "Importar", + "IMPORT_TEXT_INSTRUCTIONS": "Cole os dados de exportação de Addon no campo abaixo para começar", + "IMPORT_TEXT_LABEL": "Importar dados", + "IMPORT_TOTAL_COUNT": "Importando {count} {count, plural, =1{addon} other{addons}}", + "INSTALL_BUTTON": "Instalar", + "INVALID_CLIENT_TYPE": "String de importação não corresponde ao seu tipo de cliente selecionado", + "NO_CHANGE_BADGE_TOOLTIP": "Você já possui este addon instalado", + "PASTE_BUTTON": "Colar", + "PROVIDER_MISMATCH": "Provedor de addon não corresponde", + "RESET_BUTTON": "Redefinir", + "VERSION_MISMATCH": "Versões não correspondem" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Por que estou vendo este anúncio?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "Para poder usar wago.io / CurseForge como provedor de addon e apoiar seus autores pelo trabalho duro em seus addons favoritos nós precisamos mostrar este anúncio.\n\nSe você não quer mais ver esse anúncio, você pode sempre desabilitar wago.io / CurseForge como provedor na guia de opções.", + "TITLE": "Por que estou vendo este anúncio?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Copiar", + "CUT": "Recortar", + "LABEL": "Editar", + "PASTE": "Colar", + "REDO": "Refazer", + "SELECT_ALL": "Selecionar tudo", + "UNDO": "Desfazer" + }, + "QUIT": "Sair", + "VIEW": { + "FORCE_RELOAD": "Forçar recarregamento", + "LABEL": "Visualizar", + "RELOAD": "Recarregar", + "TOGGLE_DEV_TOOLS": "Alternar ferramentas Dev", + "TOGGLE_FULL_SCREEN": "Alternar Tela Cheia", + "ZOOM_IN": "Ampliar Zoom", + "ZOOM_OUT": "Reduzir Zoom", + "ZOOM_RESET": "Redefinir Zoom" + }, + "WINDOW": { + "CLOSE": "Fechar", + "LABEL": "Janela" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Automaticamente atualizou\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "Automaticamente atualizou {count} {count, plural, =1{Addon} other{Addons}}.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Atualizações Automáticas", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Sair do modo Tela Cheia", + "FULLSCREEN_SNACKBAR": { + "MAC": "Pressione ^⌘F para sair da tela cheia", + "WINDOWS": "Pressione F11 para sair da tela cheia" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nVocê tem certeza de que deseja abrir essa página de terceiros no seu navegador padrão?", + "TITLE": "Você está prestes a deixar o WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Desconhecido" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Escaneamento de addons completo...", + "ADDON_SCAN_STARTED": "Escaneamento de addons iniciado...", + "ADDON_SCAN_UPDATE": "Escaneando {count} pastas..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Buscar Atualizações...", + "QUIT_ACTION": "Sair", + "SHOW_ACTION": "Mostrar" + }, + "THEME": { + "ALLIANCE": "Aliança", + "DEFAULT": "Padrão", + "GROUP_DARK": "Escuro", + "GROUP_LIGHT": "Claro", + "HORDE": "Horda" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - Tela Cheia", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Verificando por atualizações", + "DOWNLOADED_TOOLTIP": "Instalar atualização do WowUp", + "DOWNLOADING_UPDATE": "Fazendo download da atualização", + "INSTALL_MESSAGE": "Você gostaria de reiniciar o WowUp para instalar a atualização?", + "INSTALL_TITLE": "Atualização do WowUp Pronta", + "NOT_AVAILABLE": "A última atualização do WowUp já está instalada", + "PORTABLE_DOWNLOAD_MESSAGE": "Deseja baixar manualmente a versão portátil mais recente?\n\nVocê precisará fechar o aplicativo manualmente e copiar a nova versão.", + "PORTABLE_DOWNLOAD_TITLE": "Download manual é necessário", + "SNACKBAR_ACTION": "Atualizar & Reiniciar", + "SNACKBAR_TEXT": "Uma nova versão do WowUp está disponível", + "TOOLTIP": "Atualização do WowUp disponível", + "UPDATE_AVAILABLE": "Começando o download", + "UPDATE_ERROR": "Não foi possível atualizar o WowUp" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Conquistas", + "ACTION_BARS": "Barra de ações", + "ALL_ADDONS": "Todos os Addons", + "AUCTION_ECONOMY": "Leilão & Economia", + "BAGS_INVENTORY": "Bolsas & Inventário", + "BOSS_ENCOUNTERS": "Confrontos de Boss", + "BUFFS_DEBUFFS": "Buffs & Debuffs", + "BUNDLES": "Pacotes", + "CHAT_COMMUNICATION": "Chat & Comunicação", + "CLASS": "Classe", + "COMBAT": "Combate", + "COMPANIONS": "Companions", + "DATA_EXPORT": "Exportação de Dados", + "DEVELOPMENT_TOOLS": "Ferramentas de Desenvolvimento", + "GUILD": "Guilda", + "LIBRARIES": "Bibliotecas", + "MAIL": "Correio", + "MAP_MINIMAP": "Mapa & Minimapa", + "MISCELLANEOUS": "Miscelânea", + "MISSIONS": "Missões", + "PLUGINS": "Plugins", + "PROFESSIONS": "Profissões", + "PVP": "PVP", + "QUESTS_LEVELING": "Missões & Nivelamento", + "ROLEPLAY": "Roleplay", + "TOOLTIPS": "Dica de ferramenta", + "UNIT_FRAMES": "Unit Frames" + }, + "ADDON_STATE": { + "IGNORED": "Ignorado", + "INSTALL": "Instalar", + "PENDING": "Pendente", + "UNAVAILABLE": "Unavailable", + "UNAVAILABLE_TOOLTIP": "This author or provider has made this addon unavailable", + "UNINSTALL": "Desinstalar", + "UNKNOWN": "", + "UPDATE": "Atualizar", + "UPTODATE": "Atualizado", + "WARNING": "Aviso" + }, + "ADDON_STATUS": { + "BACKINGUP": "Fazendo Backup", + "COMPLETE": "Instalado", + "DOWNLOADING": "Baixando", + "ERROR": "Erro", + "INSTALLING": "Instalando", + "PENDING": "Pendente", + "RETRY": "Retrying...", + "UNINSTALLING": "Desinstalando", + "UPDATING": "Atualizando..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "Nós detectamos um problema com este addon. Não poderemos mais atualizar este addon ou providenciar detalhes.", + "GENERIC_TOOLTIP": "Nós detectamos um problema com este addon", + "MISSING_ON_PROVIDER_DESCRIPTION": "Este addon parece ter sido removido pelo autor ou pelo provedor de addon.
Nós não poderemos mais atualizar este addon ou providenciar detalhes.", + "MISSING_ON_PROVIDER_TOOLTIP": "Este addon parece ter sido removido pelo provedor", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} retornou este addon apropriadamente, porém, não teve nenhum arquivo que corresponde à versão do jogo.
Assim que houver uma atualização que corresponda à versão do jogo este aviso deverá ir embora.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} não teve nenhum arquivo de versão do jogo correspondente", + "TOC_NAME_MISMATCH_DESCRIPTION": "This addon's folder name did not match with the expected toc file, you may experience issues with this addon in-game.", + "TOC_NAME_MISMATCH_TOOLTIP": "This addon's folder does not match the toc" + }, + "CLIENT_TYPES": { + "BETA": "Beta de Dragonflight", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "Reino de Teste Público (Wrath Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count} {count, plural, one{dia} other{dias}} atrás", + "HOURS_AGO": "{count} {count, plural, one{hora} other{horas}} atrás", + "JUST_NOW": "Recentemente", + "MONTHS_AGO": "{count} {count, plural, one{mês} other{meses}} atrás", + "YEARS_AGO": "{count} {count, plural, one{ano} other{anos}} atrás", + "YESTERDAY": "Ontem" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} {dependencyCount, plural, one{dependência necessária} other{dependências necessárias}}" + }, + "DOWNLOAD_COUNT": { + "e+0": "{count}", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count} mil", + "e+4": "{count} mil", + "e+5": "{count} mil", + "e+6": "{count} {count, plural, =1{milhão} other{milhões}}", + "e+7": "{count} {count, plural, =1{milhão} other{milhões}}", + "e+8": "{count} {count, plural, =1{milhão} other{milhões}}", + "e+9": "{count} {count, plural, =1{bilhão} other{bilhões}}" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alfa", + "BETA": "Beta", + "STABLE": "Estável" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Falha ao alternar atualizações instantâneas para sua conta. Por favor tente mais tarde ou entre em contato no Discord.", + "ADDON_INSTALL_ERROR": "Falha ao instalar o addon, {addonName}. Por favor tente mais tarde.", + "ADDON_SCAN_ERROR": "Ocorreu um erro ao corresponder suas pastas de addon com {providerName}, por favor tente mais tarde.", + "ADDON_SYNC_ERROR": "Ocorreu um erro ao procurar por atualizações em: {providerName}", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: Ocorreu um erro ao procurar por atualizações para {addonName} de {providerName}, por favor tente mais tarde.", + "CHANGE_PROVIDER_ERROR": "Falha ao alterar o provedor de {addonName} para {providerName}", + "GITHUB_LIMIT_ERROR": "Você chegou ao seu limite de {max} solicitações do GitHub API.\nPor favor espere {reset} e tente novamente.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Falha ao procurar atualizações de {addonName}.\nPor favor verifique se o repositório está correto ou defina esse addon para ser ignorado." + }, + "PROGRESS_SPINNER": { + "LOADING": "Carregando..." + }, + "PROVIDER_ERROR": "Erro ao contatar {providerName}", + "SEARCH": { + "NO_ADDONS": "Nenhum Addon encontrado" + }, + "WOW_EXE_SELECTION_NAME": "Executável do WoW" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "ID do Addon:", + "BY_AUTHOR": "Por {authorName}", + "CHANGELOG_TAB": "Changelog", + "COPY_ADDON_ID_SNACKBAR": "ID do Addon copiado para a área de transferência", + "COPY_ADDON_ID_TOOLTIP": "Copiar ID do addon para área de transferência", + "DEPENDENCY_TEXT": "Este Addon tem {dependencyCount} {dependencyCount, plural, one{dependência necessária} other{dependências necessárias}}", + "DESCRIPTION_NOT_FOUND": "Nenhuma descrição encontrada", + "DESCRIPTION_TAB": "Descrição", + "FUNDING_LINK_TITLE": "Apoiar esse autor", + "IMAGES_TAB": "Visualizações", + "MISSING_DEPENDENCIES": "Dependencias ausentes", + "NO_CHANGELOG_TEXT": "Nenhum changelog disponível", + "VIEW_IN_BROWSER_BUTTON": "Visualizar no navegador", + "VIEW_ON_PROVIDER_PREFIX": "Ver em" + }, + "ALERT": { + "ERROR_TITLE": "Erro", + "POSITIVE_BUTTON": "OK" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "Não", + "POSITIVE_BUTTON": "Sim" + }, + "CURSE_MIGRATION": { + "MESSAGE": "The CurseForge API has been shutdown so WowUp can no longer fetch addon information from it.
Unfortunately, this means that the CurseForge provider has been removed and your addons from CurseForge will no longer be updated.
It is recommended that you perform a re-scan in order to see what addons can be found on other providers.
The Re-Scan process may take a while.
Read more here.", + "NEGATIVE_BUTTON": "Manually Re-Scan", + "POSITIVE_BUTTON": "Automatically Re-Scan", + "RE_SCAN_ERROR": "The automatic Re-Scan failed, please try again manually.", + "RE_SCAN_SUCCESS": "The automatic Re-Scan has finished you're ready to go.", + "TITLE": "CurseForge Migration" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Addon instalado!", + "ADDON_INSTALLING": "Instalando addon", + "CANCEL_BUTTON": "Fechar", + "ERRORS": { + "ADDON_NOT_FOUND": "Nenhum addon foi encontrado para o protocolo: {protocol}", + "GENERIC": "Erro ao obter dados para o protocolo: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "Nenhum cliente do WoW instalado para o protocolo: {protocol}" + }, + "INSTALL_BUTTON": "Instalar", + "TITLE": "Instalar addon de {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "URL do Addon ", + "ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub ou WowInterface URL", + "CLOSE_BUTTON": "Fechar", + "DESCRIPTION": "Se você deseja instalar um Addon diretamente de uma URL, cole-a abaixo para iniciar.", + "DOWNLOAD_COUNT": "{textCount} {count, plural, one{download} other{downloads}} de {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "Nenhum recurso encontrado para download de {message}.\n\nUm arquivo zip valido é necessário conter no release para que o WowUp possa fazer o download.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "Nenhum recurso encontrado para download de {message}.\n\nUm arquivo zip válido terminando com '-bc' é necessário para que o WowUp possa fazer o download.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "Nenhum recurso encontrado para download de {message}.\n\nUm arquivo zip válido terminando com '-classic' é necessário para que o WowUp possa fazer o download.", + "FAILED_TO_CONNECT": "Não foi possível conectar à API, por favor espere um pouco e tente novamente.", + "INSTALL_FAILED": "Algo deu errado ao tentar instalar o Addon, por favor tente novamente.\n\nSe esta mensagem continuar aparecendo, você pode conseguir ajuda no Discord no canal #help-me.", + "INVALID_URL": "O valor fornecido não é uma URL válida. Exemplos de URLs de Addon válidos são:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "O Addon não foi encontrado, tenha certeza de que a URL está apontando para a página correta.\n\nQuanto estiver instalando do github, certifique-se de que o repositório possui uma tag release com um arquivo zip contendo o Addon.", + "NO_RELEASE_FOUND": "Nenhum release foi encontrado para {message}.\n\nRelease válido com recursos em arquivo zip são necessários para que o WowUp possa fazer o download.", + "NO_SEARCH_RESULTS": "Nenhum resultado de busca encontrado.", + "TITLE": "Instalação do Addon Falhou", + "WRATH_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-wrath' is required in order for WowUp to download it." + }, + "IMPORT_ASSET_WARNING": "Não foi possivel verificar se o ultimo release desse addon é compatível com o seu cliente selecionado.\n\nMas nós encontramos um arquivo zip \"{zipName}\".\n\nInstale por sua conta e risco.", + "IMPORT_BUTTON": "Importar", + "IMPORT_WARNING_TITLE": "Aviso de importação de Addon", + "INSTALL_BUTTON": "Instalar", + "INSTALL_SUCCESS_LABEL": "Instalado!", + "SUPPORTED_SOURCES": "Suporta WowInterface e GitHub", + "TITLE": "Instalar Addon pela URL" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Notas do Patch {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "Antes de começarmos nós precisamos configurar algumas permissões para o app.", + "POSITIVE_BUTTON": "Confirmar", + "TELEMETRY": { + "DESCRIPTION": "Ajudar a melhorar o WowUp enviando dados e/ou erros anônimos de instalação do app?", + "TOGGLE_LABEL": "Permitir Telemetria" + }, + "TITLE": "Configurar permissões do WowUp", + "WAGO": { + "DESCRIPTION": "Provedor de addons Wago.io ativado, isso irá mostrar um anúncio necessário para usar o serviço deles.\nOs anúncios beneficiam diretamente os autores dos seus addons favoritos!", + "TOGGLE_LABEL": "Habilitar o provedor Wago.io" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "Isso não parece uma aplicação válida do World of Warcraft:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Ajude-nos a melhorar o WowUp enviando dados e/ou erros de instalação do aplicativo anônimamente?", + "NEGATIVE_BUTTON": "Não obrigado", + "POSITIVE_BUTTON": "Claro!", + "TITLE": "Telemetria do WowUp" + }, + "TRUST_DOMAIN_CHECKBOX": "Confiar nesse domínio e não me perguntar no futuro" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Atribuições", + "CHANGE_LOG_SECTION_LABEL": "Registro de Alterações", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Conheça o nosso site!" + }, + "ACCOUNT": { + "BETA": "Beta", + "LOGIN_BUTTON": "Entrar agora!", + "LOGOUT_BUTTON": "Sair", + "LOGOUT_CONFIRMATION_MESSAGE": "Tem certeza de que deseja sair? Todos os dados da sua conta local serão removidos, até que você entre de novo.", + "LOGOUT_CONFIRMATION_TITLE": "Sair?", + "MANAGE_ACCOUNT_BUTTON": "Administrar conta", + "TITLE": "Conta" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Categorias", + "ADDON_CATEGORIES_MENU_TITLE": "Categorias de Addon", + "ADDON_CATEGORIES_SELECTED_TITLE": "Categoria: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Navegue as várias categorias", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Instalar pela URL", + "INSTALL_FROM_URL_TOOLTIP": "Instalar um addon pela URL", + "REFRESH_BUTTON": "Atualizar", + "REFRESH_TOOLTIP": "Atualizar resultados de addon", + "RESET_CATEGORY_TOOLTIP": "Redefinir Categoria", + "SEARCH_LABEL": "Procurar", + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "AUTHOR_COLUMN_HEADER": "Autor", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Downloads", + "PROVIDER_COLUMN_HEADER": "Provedor", + "RELEASED_AT_COLUMN_HEADER": "Lançado em", + "STATUS_COLUMN_HEADER": "Estado" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "Sobre", + "ACCOUNT_TAB_TITLE": "Conta", + "COLLAPSE_BUTTON_TITLE": "Recolher", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Expandir", + "GET_ADDONS_TAB_TITLE": "Obtenha Addons", + "GUIDE_TAB_TITLE": "Guia", + "MIGRATING_ADDONS": "Migrando addons...", + "MY_ADDONS_TAB_TITLE": "Meus Addons", + "NEWS_TAB_TITLE": "Notícias", + "OPTIONS_TAB_TITLE": "Opções" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count} {count, plural, =1{Addon selecionado} other{Addons selecionados}}", + "ALPHA_ADDON_CHANNEL": "Alfa", + "AUTO_UPDATE_ADDON_BUTTON": "Atualização Automática", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Notificações Ativadas", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "Canal", + "IGNORE_ADDON_BUTTON": "Ignorar", + "PROVIDER_SUBMENU_TITLE": "Provedores", + "REINSTALL_ADDON_BUTTON": "Reinstalar", + "REMOVE_ADDON_BUTTON": "Remover", + "SHOW_FOLDER": "Mostrar Pasta", + "STABLE_ADDON_CHANNEL": "Estável" + }, + "ADDON_IS_CODE_REPOSITORY": "Addon parece ser repositório de código", + "ADDON_REMOVED_SNACKBAR": "Removido com sucesso: {addonName} ", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Deseja alterar o provedor de Addons de {addonName} para {providerName}? Esta operação irá desinstalar seu Addon existente e substituí-lo por uma cópia do novo provedor.", + "TITLE": "Alterar provedor de Addons?" + }, + "CHECK_UPDATES_BUTTON": "Verificar Atualizações", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Verificar atualizações recentes", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{update} other{updates}} ", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Exibir Colunas" + }, + "ERROR_SNACKBAR": "Ocorreu um erro", + "FILTER_LABEL": "Filtrar", + "FUNDING_TOOLTIP": { + "CUSTOM": "Apoiar esse autor", + "GENERIC": "Apoiar esse autor em {platform}", + "GITHUB": "Apoiar esse autor em GitHub", + "PATREON": "Apoiar esse autor em Patreon", + "PAYPAL": "Apoiar esse autor em PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Importar/Exportar Addons", + "MULTIPLE_PROVIDERS_TOOLTIP": "Este Addon tem múltiplos provedores", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, =1{Addon} other{Addons}}", + "JOIN_DISCORD": "Converse conosco no Discord", + "PATREON_SUPPORT": "Ajude o WowUp no Patreon", + "SEARCH_RESULTS": "{count} {count, plural, =1{resultado} other{resultados}}", + "VIEW_GITHUB": "Olhe o código no GitHub", + "VIEW_GUIDE": "Olhe o nosso guia para ver o que o WowUp pode fazer" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Dependência necessária faltando", + "RESCAN_FOLDERS_BUTTON": "Re-escanear pastas", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Procura por Addons instalados", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "Fazer um novo escaneamento irá tentar conseguir quais Addons você tem instalado, mas fazer isso pode resetar a informação dos Addons conhecidos.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Começar novo escaneamento?", + "SPINNER": { + "GATHERING_ADDONS": "Coletando Addons...", + "UPDATING": "Atualizando {updateCount}/{addonCount}", + "UPDATING_WITH_ADDON_NAME": "Atualizando {updateCount}/{addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Addon", + "ADDON_INSTALL_BUTTON": "Instalar", + "ADDON_UPDATE_BUTTON": "Atualizar", + "AUTHOR_COLUMN_HEADER": "Autor", + "AUTO_UPDATE_ICON_TOOLTIP": "Atualização automática habilitada", + "GAME_VERSION_COLUMN_HEADER": "Versão do Jogo", + "LATEST_VERSION_COLUMN_HEADER": "Última versão", + "PROVIDER_COLUMN_HEADER": "Provedor", + "PROVIDER_RELEASE_CHANNEL": "Provider Channel", + "RELEASED_AT_COLUMN_HEADER": "Lançado em", + "STATUS_COLUMN_HEADER": "Estado", + "UPDATED_AT_COLUMN_HEADER": "Atualizado em" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Isto irá remover todas as pastas relacionadas de sua pasta do World of Warcraft.", + "CONFIRMATION_LESS_THAN_THREE": "Você tem certeza que quer remover os {count} Addons abaixo?", + "CONFIRMATION_MORE_THAN_THREE": "Você tem certeza que quer remover os {count} Addons selecionados?", + "CONFIRMATION_ONE": "Você tem certeza que quer remover {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} tem {dependencyCount} {dependencyCount, plural, one{dependência} other{dependências}}. Você deseja removê-los também?", + "DEPENDENCY_TITLE": "Remover dependências do Addon?", + "TITLE": "Desinstalar {count, plural, =1{Addon} other{Addons}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "The installed addon did not match with any of the configured providers", + "UPDATE_ALL_BUTTON": "Atualizar todos", + "UPDATE_ALL_BUTTON_TOOLTIP": "Atualizar todos os Addons", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Atualizar todos os clientes", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Atualizar Retail/Clássico" + }, + "WTF_BACKUP_BUTTON": "Backup de configurações de Interface" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "News link copied to clipboard", + "NEWS_LINK_COPY_TOOLTIP": "Copy news link", + "PAGE_CONTEXT_FOOTER": "{count} notícias", + "REFRESH_TOOLTIP": "Atualizar feed de notícias" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Ad or access key required", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "If you have requested a CurseForge API key you can input it here to connect to their API.", + "API_KEY_TITLE": "CurseForge API Key", + "PROVIDER_NOTE": "API Key Required" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Selecionar quais provedores serão usados para buscar, e instalar novos addons", + "FIELD_LABEL": "Provedores de Addon ativados", + "INPUT_LABEL": "Provedores" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "A personal access token will increase the amount of GitHub API calls you can make, it is freely available. Learn More", + "PLACEHOLDER": "Personal Access Token", + "TITLE": "GitHub Personal Access Token" + }, + "TITLE": "Addons", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Mudar para o canal Beta irá permitir que você receba builds experimentais que podem conter correções de bug, assim como novos e os próximos recursos. Você só pode voltar para a atual versão estável desinstalando o seu app existence e reinstalando de wowup.io.\n\nEnquanto o canal beta estiver funcional, use por sua conta e risco.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Mudar para o canal Estável do app irá prevenir que você receba builds Beta, a próxima atualização será de um release Estável.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Configurar canal de release do app", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Sim, eu entendo", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Alternar entre releases Beta e Estável para a aplicação", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Canal", + "APP_RELEASE_CHANNEL_LABEL": "Canal de release da aplicação", + "CURRENT_LANGUAGE_LABEL": "Idioma Atual", + "CURSE_PROTOCOL_DESCRIPTION": "Quando baixando addons do site CurseForge, WowUp ira cuidar da instalação", + "CURSE_PROTOCOL_LABEL": "Gerenciar links de download CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Mostrar um selo no icone da aplicação com o número de atualizações de addon disponíveis.", + "ENABLE_APP_BADGE_LABEL": "Habilitar selo de notificação do app", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Habilitar várias notificações e popups do sistema, como os de aviso de Addons atualizados automaticamente.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Habilitar notificações do sistema", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "When opening an addon detail page, automatically select the last opened tab", + "KEEP_LAST_OPENED_TAB_LABEL": "Keep last opened tab", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Ao fechar a janela do WowUp, minimize para a barra de menu", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Ao fechar a janela do WowUp, minimize para a bandeja do sistema.", + "MINIMIZE_ON_CLOSE_LABEL": "Minimizar ao Fechar", + "PROTOCOL_DESCRIPTION": "WowUp irá registrar um protocolo URI personalizado no seu sistema e gerenciar suas consultas recebidas", + "PROTOCOL_LABEL": "Permitir o WowUp gerenciar o URI wowup://", + "SCALE_DESCRIPTION": "Alterar o fator de zoom para todo o app.", + "SCALE_LABEL": "Escala", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Para mudar o idioma padrão é necessário reiniciar a aplicação.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Definir um novo idioma padrão", + "SET_LANGUAGE_DESCRIPTION": "Selecione um novo idioma", + "SET_LANGUAGE_LABEL": "Selecione um idioma", + "START_MINIMIZED_DESCRIPTION": "...e não será exibido na tela", + "START_MINIMIZED_LABEL": "Iniciar WowUp minimizado", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp será iniciado imediatamente assim que o seu sistema operacional carregar...", + "START_WITH_SYSTEM_LABEL": "Iniciar WowUp com o sistema", + "TELEMETRY_DESCRIPTION": "Ajude a melhorar o WowUp enviando dados e/ou erros de instalação anonimamente.", + "TELEMETRY_LABEL": "Telemetria", + "THEME_DESCRIPTION": "Mude o tema do WowUp", + "THEME_LABEL": "Cor do tema", + "TITLE": "Aplicativo", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp pode configurar a si mesmo como gerenciador padrão para links de download do CurseForge. Isso pode causar problemas se você tentar usar o app do CurseForge, tem certeza de que deseja continuar?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Gerenciar Downloads do CurseForge", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Você quer reiniciar?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "Desabilitar aceleração de hardware pode solucionar problemas de FPS de corrigir outros problemas de renderização no app. Mudar esta opção exige reiniciar o aplicativo.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Desabilitar a aceleração de hardware exige reiniciar o aplicativo.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Habilitar a aceleração de hardware exige reiniciar o aplicativo.", + "USE_HARDWARE_ACCELERATION_LABEL": "Habilitar Aceleração de Hardware", + "USE_SYMLINK_SUPPORT": "Habilitar suporte a Symlink", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Habilitando o suporte à symlink irá permitir ao WowUp reconhecer symlinks quando fazer um re-escaneamento. Aviso: Se você não sabe o que é um symlink, você não precisa disso. Quando atualizar os symlinks serão atualmente substituídos com uma pasta e o link será perdido.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Habilitar suporte a symlink?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Permitir ao WowUp escanear pastas symlink na sua pasta de Addons. Aviso: elas serão substituidas quando atualizar/instalar." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Show Config Files", + "CONFIG_FILES_DESCRIPTION": "Open the folder where for example your addons.json and preferences.json are stored.", + "CONFIG_FILES_LABEL": "Config Files", + "DEBUG_DATA_BUTTON": "Esvaziar log de depuração de dados", + "DEBUG_DATA_DESCRIPTION": "Registra os dados de depuração e ajuda a diagnosticar problemas potenciais. Apenas por o curiosidade, isso pode ser encontrado em seu último arquivo de registro.", + "DEBUG_DATA_LABEL": "Depurar Dados", + "LOG_FILES_BUTTON": "Mostrar Arquivos de Registro", + "LOG_FILES_DESCRIPTION": "Abre a pasta que contém seus últimos arquivos de registro.", + "LOG_FILES_LABEL": "Arquivos de Registro", + "TITLE": "Depurar" + }, + "TABS": { + "ABOUT": "Sobre", + "ADDONS": "Addons", + "APPLICATION": "Aplicação", + "CLIENTS": "Clientes", + "CURSEFORGE": "CurseForge", + "DEBUG": "Debug", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Adicionar Novo", + "AUTO_UPDATE_DESCRIPTION": "Addons recém-instalados serão definidos para atualizar automáticamente por padrão", + "AUTO_UPDATE_LABEL": "Atualização Automática", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Cancelar", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Tem certeza de que deseja apagar o local de instalação para {clientName}? Isso irá remover todas as informações de addon guardadas para este cliente.\n\nSuas pastas de addons não serão removidas.", + "TITLE": "Apagar Local de Instalação?" + }, + "CLIENT_TYPE_INPUT_HINT": "A pasta \"{clientFolderName}\" que contém o {clientTypeName}", + "CLIENT_TYPE_PATH_LABEL": "Pasta {clientTypeName}", + "DEFAULT_ADDON_CHANNEL_LABEL": "Canal de Addon Padrão", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canal de Addon", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Editar", + "MOVE_DOWN_BUTTON": "Mover para Baixo", + "MOVE_UP_BUTTON": "Mover para Cima", + "NO_CLIENTS_FOUND_TEXT": "Nenhuma instalação do World of Warcraft encontrada, por favor verifique que o seu cliente Battle.net está atualizado ou adicione o cliente manualmente", + "OPEN_FOLDER_BUTTON": "Abrir pasta", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Selecionar", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Remover", + "RESCAN_CLIENTS_BUTTON": "Reescanear", + "RESCAN_CLIENTS_LABEL": "Reescanear clientes World of Warcraft instalados", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Salvar", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Tem certeza de que deseja aplicar esse backup em suas configurações de interface?\n\nTenha certeza de que o jogo World of Warcraft não esteja aberto antes de aplicar o backup.\n\nEssa operação não pode ser desfeita.", + "TITLE": "Aplicar backup WTF?" + }, + "BACKUP_APPLY_SUCCESS": "Backup aplicado com sucesso: {name}", + "BACKUP_COUNT_TEXT": "Encontrado(s) {count} {count, plural, =1{backup} other{backups}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Aplicando backup...", + "CREATING_BACKUP": "Criando backup de {count} arquivo(s)...", + "LOADING_BACKUPS": "Carregando backups...", + "REMOVING_BACKUP": "Removendo backup..." + }, + "CREATE_BACKUP_BUTTON": "Criar Backup", + "DELETE_CONFIRMATION": { + "MESSAGE": "Tem certeza de que deseja apagar o backup {name}?\nIsso não pode ser desfeito.", + "TITLE": "Apagar Backup WTF?" + }, + "DIALOG_TITLE": "Backup de Configurações WTF: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Falha ao aplicar backup: {name}", + "FAILED_TO_DELETE": "Falha ao apagar backup: {name}", + "GENERIC_ERROR": "Houve um problema ao processar este backup", + "INVALID_CONTENTS": "Houve um problema ao processar este backup", + "INVALID_CREATED_AT": "Houve um problema ao processar este backup", + "INVALID_CREATED_BY": "Houve um problema ao processar este backup" + }, + "SHOW_FOLDER_BUTTON": "Mostrar Pasta", + "TOOL_TIP": { + "APPLY_BUTTON": "Aplicar este backup", + "DELETE_BUTTON": "Apagar este backup" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/ru.json b/WowUp/wowup-electron/src/assets/i18n/ru.json new file mode 100644 index 0000000..08cbfd5 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/ru.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "Активных модификаций: {count}", + "ADDED_BADGE_TOOLTIP": "Мы попытаемся установить эту модификацию", + "CONFLICT_BADGE_TOOLTIP": "Конфликтующие модификации не будут изменены", + "COPY_BUTTON": "Копировать", + "DIALOG_TITLE": "Импорт/Экспорт модификаций: {clientType}", + "EXPORT_STRING_COPIED": "Строка экспорта скопирована в буфер обмена", + "EXPORT_STRING_PASTED": "Введено содержание буфера обмена", + "EXPORT_TAB_LABEL": "Экспорт", + "EXPORT_TEXT_LABEL": "Данные экспорта модификаций", + "GENERIC_IMPORT_ERROR": "При импорте произошла ошибка", + "IGNORED_ADDON_COUNT": "Игнорируемых модификаций: {count}", + "IMPORT_ADDED_COUNT": "{count} {count, plural, =1{добавлена} other{добавлено}}", + "IMPORT_BADGE_ADDED": "Новая", + "IMPORT_BADGE_CONFLICT": "Конфликтующая", + "IMPORT_BADGE_NO_CHANGE": "Без изменений", + "IMPORT_BUTTON": "Импорт", + "IMPORT_CONFLICT_COUNT": "{count} {count, plural, =1{конфликтует} other{конфликтуют}}", + "IMPORT_NO_CHANGE_COUNT": "{count} без изменений", + "IMPORT_STRING_INVALID": "Строка импорта неверна", + "IMPORT_TAB_LABEL": "Импорт", + "IMPORT_TEXT_INSTRUCTIONS": "Вставьте данные экспорта модификаций в поле ниже, чтобы начать", + "IMPORT_TEXT_LABEL": "Данные импорта", + "IMPORT_TOTAL_COUNT": "Импорт {count} {count, plural, =1{модификации} other{модификаций}}", + "INSTALL_BUTTON": "Установить", + "INVALID_CLIENT_TYPE": "Строка импорта не совпадает с выбранным типом клиента", + "NO_CHANGE_BADGE_TOOLTIP": "У Вас уже установлена эта модификация", + "PASTE_BUTTON": "Вставить", + "PROVIDER_MISMATCH": "Не совпадает источник модификации", + "RESET_BUTTON": "Очистить", + "VERSION_MISMATCH": "Не совпадают версии" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "Почему я вижу эту рекламу?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "Чтобы использовать wago.io / CurseForge в виде источника модификаций и для поддержки их авторов за тяжёлый труд над Вашими любимыми модификациями, мы должны показывать эту рекламу.\n\nЕсли Вы не хотите видеть эту рекламу, то всегда можете отключить wago.io / CurseForge как источник модификаций во вкладке настроек.", + "TITLE": "Почему я вижу эту рекламу?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "Копировать", + "CUT": "Вырезать", + "LABEL": "Править", + "PASTE": "Вставить", + "REDO": "Повторить", + "SELECT_ALL": "Выбрать всё", + "UNDO": "Отменить" + }, + "QUIT": "Выход", + "VIEW": { + "FORCE_RELOAD": "Принудительная перезагрузка", + "LABEL": "Просмотр", + "RELOAD": "Перезагрузка", + "TOGGLE_DEV_TOOLS": "Переключить инструменты разработчика", + "TOGGLE_FULL_SCREEN": "Переключить полноэкранный режим", + "ZOOM_IN": "Приблизить", + "ZOOM_OUT": "Отдалить", + "ZOOM_RESET": "Сброс масштабирования" + }, + "WINDOW": { + "CLOSE": "Закрыть", + "LABEL": "Окно" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "Автоматически обновлены\r\n{addonNames}", + "AUTO_UPDATE_NOTIFICATION_BODY": "Автоматически {count, plural, one{обновлена} other{обновлено}} {count} {count, plural, one{модификация} few{модификации} other{модификаций}}.", + "AUTO_UPDATE_NOTIFICATION_TITLE": "Автоматические обновления", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "Выйти из полноэкранного режима", + "FULLSCREEN_SNACKBAR": { + "MAC": "Нажмите ^⌘F, чтобы выйти из полноэкранного режима", + "WINDOWS": "Нажмите F11, чтобы выйти из полноэкранного режима" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\nВы уверены, что хотите открыть эту стороннюю ссылку в браузере по умолчанию?", + "TITLE": "Вы собираетесь покинуть WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "Неизвестно" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "Сканирование модификаций завершено...", + "ADDON_SCAN_STARTED": "Началось сканирование модификаций...", + "ADDON_SCAN_UPDATE": "Сканируется {count} {count, plural, one{папка} few{папки} other{папок}}..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "Проверить наличие обновлений", + "QUIT_ACTION": "Выход", + "SHOW_ACTION": "Показать" + }, + "THEME": { + "ALLIANCE": "Альянс", + "DEFAULT": "По умолчанию", + "GROUP_DARK": "Тёмные", + "GROUP_LIGHT": "Светлые", + "HORDE": "Орда" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io — Полный экран", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "Проверка на наличие обновления", + "DOWNLOADED_TOOLTIP": "Установить обновление WowUp", + "DOWNLOADING_UPDATE": "Загрузка обновления", + "INSTALL_MESSAGE": "Вы хотите перезапустить WowUp, чтобы установить обновление?", + "INSTALL_TITLE": "Для WowUp готово обновление", + "NOT_AVAILABLE": "Последняя версия WowUp уже установлена", + "PORTABLE_DOWNLOAD_MESSAGE": "Хотите вручную установить последнюю портативную версию?\n\nНужно будет закрыть приложение вручную и заменить его новой версией.", + "PORTABLE_DOWNLOAD_TITLE": "Необходима загрузка вручную", + "SNACKBAR_ACTION": "Обновить и перезапустить", + "SNACKBAR_TEXT": "Доступна новая версия WowUp", + "TOOLTIP": "Доступно обновление для WowUp", + "UPDATE_AVAILABLE": "Начинается загрузка", + "UPDATE_ERROR": "Неудачная попытка получить обновление для WowUp" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "Достижения", + "ACTION_BARS": "Панели действий", + "ALL_ADDONS": "Все модификации", + "AUCTION_ECONOMY": "Аукцион и экономика", + "BAGS_INVENTORY": "Сумки и инвентарь", + "BOSS_ENCOUNTERS": "Сражение с боссами", + "BUFFS_DEBUFFS": "Баффы и дебаффы", + "BUNDLES": "Комплекты", + "CHAT_COMMUNICATION": "Чат и общение", + "CLASS": "Класс", + "COMBAT": "Бой", + "COMPANIONS": "Коллекционирование", + "DATA_EXPORT": "Экспорт данных", + "DEVELOPMENT_TOOLS": "Инструменты для разработки", + "GUILD": "Гильдия", + "LIBRARIES": "Библиотеки", + "MAIL": "Почта", + "MAP_MINIMAP": "Карта и мини-карта", + "MISCELLANEOUS": "Разное", + "MISSIONS": "Миссии", + "PLUGINS": "Плагины", + "PROFESSIONS": "Профессии", + "PVP": "PVP", + "QUESTS_LEVELING": "Задания и прокачка", + "ROLEPLAY": "Ролевая игра", + "TOOLTIPS": "Описания", + "UNIT_FRAMES": "Рамки юнитов" + }, + "ADDON_STATE": { + "IGNORED": "Игнорируется", + "INSTALL": "Установить", + "PENDING": "В ожидании", + "UNAVAILABLE": "Недоступна", + "UNAVAILABLE_TOOLTIP": "Этот автор или источник сделал эту модификацию недоступной", + "UNINSTALL": "Удалить", + "UNKNOWN": "", + "UPDATE": "Обновить", + "UPTODATE": "Актуальная", + "WARNING": "Внимание" + }, + "ADDON_STATUS": { + "BACKINGUP": "Резервирование", + "COMPLETE": "Установлена", + "DOWNLOADING": "Загрузка", + "ERROR": "Ошибка", + "INSTALLING": "Установка", + "PENDING": "В ожидании", + "RETRY": "Retrying...", + "UNINSTALLING": "Удаление", + "UPDATING": "Обновление..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "Мы обнаружили неполадку с этой модификацией. Мы больше неспособны обновлять эту модификацию или показывать её детали.", + "GENERIC_TOOLTIP": "Мы обнаружили неполадку с этой модификацией", + "MISSING_ON_PROVIDER_DESCRIPTION": "Эта модификация, вероятно, была удалена автором или источником модификаций.
Мы больше неспособны обновлять эту модификацию или показывать её детали.", + "MISSING_ON_PROVIDER_TOOLTIP": "Эта модификация, вероятно, была удалена источником", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} верно определил модификацию, однако он не имеет каких-либо файлов соответствующих этой версии игры.
Это предупреждение исчезнет, как только будет доступно обновление для данной версии игры.", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} не имеет файлов, соответствующих версии игры", + "TOC_NAME_MISMATCH_DESCRIPTION": "Название папки этой модификации не совпадает с ожидаемым toc-файлом, у Вас могут возникнуть проблемы с этой модификацией в игре.", + "TOC_NAME_MISMATCH_TOOLTIP": "Папка этой модификации не соответствует названию" + }, + "CLIENT_TYPES": { + "BETA": "Бета Dragonflight", + "CLASSIC": "Cataclysm Classic", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "World of Warcraft Classic", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "PTR (Wrath Classic)", + "RETAIL": "Dragonflight", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count} {count, plural, one{день} few{дня} other{дней}} назад", + "HOURS_AGO": "{count} {count, plural, one{час} few{часа} other{часов}} назад", + "JUST_NOW": "Только что", + "MONTHS_AGO": "{count} {count, plural, one{месяц} few{месяца} other{месяцев}} назад", + "YEARS_AGO": "{count} {count, plural, one{год} few{года} other{лет}} назад", + "YESTERDAY": "Вчера" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount, plural, one{Необходима} other{Необходимо}} {dependencyCount} {dependencyCount, plural, one{зависимость} few{зависимости} other{зависимостей}}" + }, + "DOWNLOAD_COUNT": { + "e+0": "{count}", + "e+1": "{count}", + "e+2": "{count}", + "e+3": "{count} {count, plural, one{тысяча} few{тысячи} other{тысяч}}", + "e+4": "{count} {count, plural, one{тысяча} few{тысячи} other{тысяч}}", + "e+5": "{count} {count, plural, one{тысяча} few{тысячи} other{тысяч}}", + "e+6": "{count} {count, plural, one{миллион} few{миллиона} other{миллионов}}", + "e+7": "{count} {count, plural, one{миллион} few{миллиона} other{миллионов}}", + "e+8": "{count} {count, plural, one{миллион} few{миллиона} other{миллионов}}", + "e+9": "{count} {count, plural, one{миллиард} few{миллиарда} other{миллиардов}}" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Альфа", + "BETA": "Бета", + "STABLE": "Стабильная" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Не удалось включить мгновенные обновления для Вашего аккаунта. Попробуйте позже или сообщите об этом в Discord.", + "ADDON_INSTALL_ERROR": "Не удалось установить модификацию {addonName}. Попробуйте позже.", + "ADDON_SCAN_ERROR": "Возникла ошибка при сравнении ваших папок с {providerName}, попробуйте позже.", + "ADDON_SYNC_ERROR": "Возникла ошибка при проверке обновлений с: {providerName}", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: Произошла ошибка при проверке наличия обновлений для {addonName} из {providerName}, попробуйте позже.", + "CHANGE_PROVIDER_ERROR": "Источник для {addonName} не удалось изменить на {providerName}", + "GITHUB_LIMIT_ERROR": "Вы достигли лимит в {max} запросов к GitHub API.\nПодождите до {reset} и попробуйте снова.", + "GITHUB_REPOSITORY_FETCH_ERROR": "Не удалось проверить обновления для {addonName}.\nУбедитесь, что хранилище указано верно или установите игнорирование на эту модификацию." + }, + "PROGRESS_SPINNER": { + "LOADING": "Загрузка..." + }, + "PROVIDER_ERROR": "Ошибка при связи с {providerName}", + "SEARCH": { + "NO_ADDONS": "Модификаций не найдено" + }, + "WOW_EXE_SELECTION_NAME": "Исполняемый файл WoW" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "ID модификации:", + "BY_AUTHOR": "От {authorName}", + "CHANGELOG_TAB": "Список изменений", + "COPY_ADDON_ID_SNACKBAR": "ID модификации скопирован в буфер обмена", + "COPY_ADDON_ID_TOOLTIP": "Скопировать ID модификации в буфер обмена", + "DEPENDENCY_TEXT": "Эта модификация имеет {dependencyCount} {dependencyCount, plural, one{необходимую} other{необходимых}} {dependencyCount, plural, one{зависимость} few{зависимости} other{зависимостей}}", + "DESCRIPTION_NOT_FOUND": "Описание не найдено", + "DESCRIPTION_TAB": "Описание", + "FUNDING_LINK_TITLE": "Поддержать этого автора", + "IMAGES_TAB": "Просмотр", + "MISSING_DEPENDENCIES": "Отсутствующие зависимости", + "NO_CHANGELOG_TEXT": "Нет доступного списка изменений", + "VIEW_IN_BROWSER_BUTTON": "Посмотреть в браузере", + "VIEW_ON_PROVIDER_PREFIX": "Посмотреть на" + }, + "ALERT": { + "ERROR_TITLE": "Ошибка", + "POSITIVE_BUTTON": "Окей" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "Нет", + "POSITIVE_BUTTON": "Да" + }, + "CURSE_MIGRATION": { + "MESSAGE": "CurseForge API был отключён, так что WowUp больше не может получать с него информацию о модификации.
К сожалению, это значит, что источник CurseForge был убран и ваши модификации с CurseForge не будут обновляться.
Рекомендуется выполнить сканирование папок, чтобы увидеть какие модификации можно найти у других источников.
Процесс сканирования папок может занять некоторое время.
Подробнее здесь.", + "NEGATIVE_BUTTON": "Сканировать папки вручную", + "POSITIVE_BUTTON": "Сканировать папки автоматически", + "RE_SCAN_ERROR": "Автоматическое сканирование не удалось, попробуйте вручную.", + "RE_SCAN_SUCCESS": "Автоматическое сканирование завершено, всё готово.", + "TITLE": "Миграция с CurseForge" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "Модификация установлена!", + "ADDON_INSTALLING": "Установка модификации", + "CANCEL_BUTTON": "Закрыть", + "ERRORS": { + "ADDON_NOT_FOUND": "Для следующего протокола не была найдена модификация: {protocol}", + "GENERIC": "Ошибка при получении данных для протокола: {protocol}", + "NO_VALID_WOW_INSTALLATIONS": "Для следующего протокола не был установлен клиент WoW: {protocol}" + }, + "INSTALL_BUTTON": "Установить", + "TITLE": "Установить модификацию с {providerName}" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "Ссылка на модификацию", + "ADDON_URL_INPUT_PLACEHOLDER": "Например, ссылки GitHub или WowInterface", + "CLOSE_BUTTON": "Закрыть", + "DESCRIPTION": "Если Вы хотите установить модификацию непосредственно по ссылке, вставьте её ниже, чтобы начать.", + "DOWNLOAD_COUNT": "{textCount} {shortCount, plural, one{загрузка} few{загрузки} other{загрузок}} на {provider}", + "ERROR": { + "ASSET_NOT_FOUND": "Не найдено ресурсов для загрузки {message}.\n\nНеобходим верный .zip файл в выпусках, чтобы WowUp мог загрузить его.", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "Не найдено ресурсов для загрузки из {message}.\n\nНеобходим верный .zip файл с окончанием '-bc', чтобы WowUp мог загрузить его.", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "Не найдено ресурсов для загрузки из {message}.\n\nНеобходим верный .zip файл с окончанием '-classic', чтобы WowUp мог загрузить его.", + "FAILED_TO_CONNECT": "Не удается подключиться к API, подождите немного и повторите попытку.", + "INSTALL_FAILED": "Что-то пошло не так при попытке установить модификацию, попытайтесь снова.\n\nЕсли это сообщение продолжает появляться, Вы можете получить помощь в канале #help-me нашего Discord сообщества.", + "INVALID_URL": "Введённое значение не является правильной ссылкой. Примеры правильных ссылок для модификаций:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "Модификация не была найдена, убедитесь, что Ваша ссылка указывает на правильную страницу.\n\nПри установке с GitHub, убедитесь, что в репозитории есть тег выпуска с zip-архивом, содержащим модификацию.", + "NO_RELEASE_FOUND": "Не найдено выпусков для {message}.\n\nНеобходим верный выпуск с ресурсами в .zip файле, чтобы WowUp мог загрузить его.", + "NO_SEARCH_RESULTS": "Поиск не дал результатов.", + "TITLE": "Установка модификации не удалась", + "WRATH_ASSET_NOT_FOUND": "Не найден ресурс для загрузки с {message}.\n\nДля того, чтобы WowUp загрузил его, требуется действительный zip-файл, оканчивающийся на «-wrath»." + }, + "IMPORT_ASSET_WARNING": "Мы не смогли убедиться в совместимости последнего выпуска этой модификации с выбранным Вами клиентом.\n\nНо мы нашли zip файл \"{zipName}\".\n\nУстанавливайте его на свой страх и риск.", + "IMPORT_BUTTON": "Импорт", + "IMPORT_WARNING_TITLE": "Предупреждение импорта модификаций", + "INSTALL_BUTTON": "Установить", + "INSTALL_SUCCESS_LABEL": "Установлена!", + "SUPPORTED_SOURCES": "Поддерживаются WowInterface и GitHub", + "TITLE": "Ссылка на установку модификации" + }, + "NEW_VERSION_POPUP": { + "TITLE": "Список изменений в версии {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "поставщики рекламы", + "DESCRIPTION_BOTTOM": ". Нажмите на кнопку 'Управление', чтобы контролировать свои согласия или возражать против обработки Ваших данных. Вы можете изменить свои предпочтения в любое время через экран настроек.", + "DESCRIPTION_TOP": " Для того, чтобы использовать интеграцию этого приложения с CurseForge, они требуют, чтобы Вы разрешили им показывать Вам рекламу от одного из своих приложений ", + "MANAGE_BUTTON": "Управление", + "TITLE": "CurseForge" + }, + "MESSAGE": "Прежде чем мы начнем, нам нужно настроить несколько разрешений для приложения.", + "POSITIVE_BUTTON": "Подтвердить", + "TELEMETRY": { + "DESCRIPTION": "Хотите помочь улучшить WowUp, анонимно отправляя данные об установке и ошибках?", + "TOGGLE_LABEL": "Разрешить телеметрию" + }, + "TITLE": "Установка разрешений для WowUp", + "WAGO": { + "DESCRIPTION": "Включение источника модификаций Wago.io будет отображать рекламу, необходимую для использования их сервиса.\nРеклама приносит прямую прибыль авторам Ваших любимых модификаций!", + "TOGGLE_LABEL": "Включить источник Wago.io" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "Данный путь является неверным для приложения World of Warcraft:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "Хотите помочь улучшить WowUp, анонимно отправляя данные об установке и ошибках?", + "NEGATIVE_BUTTON": "Нет, спасибо", + "POSITIVE_BUTTON": "Конечно!", + "TITLE": "Телеметрия WowUp" + }, + "TRUST_DOMAIN_CHECKBOX": "Доверять этому домену и не спрашивать меня в будущем" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "Атрибутика", + "CHANGE_LOG_SECTION_LABEL": "Журнал изменений", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "Посетите наш сайт!" + }, + "ACCOUNT": { + "BETA": "Бета", + "LOGIN_BUTTON": "Войдите!", + "LOGOUT_BUTTON": "Выход", + "LOGOUT_CONFIRMATION_MESSAGE": "Вы уверены, что хотите выйти? Все локальные данные аккаунта будут удалены до следующего входа.", + "LOGOUT_CONFIRMATION_TITLE": "Выйти?", + "MANAGE_ACCOUNT_BUTTON": "Управление аккаунтом", + "TITLE": "Аккаунт" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "Категории", + "ADDON_CATEGORIES_MENU_TITLE": "Категории модификаций", + "ADDON_CATEGORIES_SELECTED_TITLE": "Категория: {category}", + "ADDON_CATEGORIES_TOOLTIP": "Просматривайте различные категории", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "INSTALL_FROM_URL_BUTTON": "Установить по ссылке", + "INSTALL_FROM_URL_TOOLTIP": "Установить модификацию по ссылке", + "REFRESH_BUTTON": "Обновить", + "REFRESH_TOOLTIP": "Обновить результаты модификаций", + "RESET_CATEGORY_TOOLTIP": "Сбросить категорию", + "SEARCH_LABEL": "Искать", + "TABLE": { + "ADDON_COLUMN_HEADER": "Модификация", + "AUTHOR_COLUMN_HEADER": "Автор(ы)", + "DOWNLOAD_COUNT_COLUMN_HEADER": "Загрузок", + "PROVIDER_COLUMN_HEADER": "Источник", + "RELEASED_AT_COLUMN_HEADER": "Выпущена", + "STATUS_COLUMN_HEADER": "Статус" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "О программе", + "ACCOUNT_TAB_TITLE": "Аккаунт", + "COLLAPSE_BUTTON_TITLE": "Свернуть", + "DISCORD_TAB_TITLE": "Discord", + "EXPAND_BUTTON_TITLE": "Развернуть", + "GET_ADDONS_TAB_TITLE": "Получить модификации", + "GUIDE_TAB_TITLE": "Руководство (англ.)", + "MIGRATING_ADDONS": "Перенос модификаций...", + "MY_ADDONS_TAB_TITLE": "Мои модификации", + "NEWS_TAB_TITLE": "Новости", + "OPTIONS_TAB_TITLE": "Настройки" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "{count, plural, one{Выбрана} other{Выбрано}} {count} {count, plural, one{модификация} few{модификации} other{модификаций}}", + "ALPHA_ADDON_CHANNEL": "Альфа", + "AUTO_UPDATE_ADDON_BUTTON": "Автообновление", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "Уведомления включены", + "BETA_ADDON_CHANNEL": "Бета", + "CHANNEL_SUBMENU_TITLE": "Тип выпуска", + "IGNORE_ADDON_BUTTON": "Пропускать", + "PROVIDER_SUBMENU_TITLE": "Источники", + "REINSTALL_ADDON_BUTTON": "Переустановить", + "REMOVE_ADDON_BUTTON": "Удалить", + "SHOW_FOLDER": "Показать папку", + "STABLE_ADDON_CHANNEL": "Стабильная" + }, + "ADDON_IS_CODE_REPOSITORY": "Модификация является репозиторием кода", + "ADDON_REMOVED_SNACKBAR": "Успешно удалена: {addonName}", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "Хотите изменить источник модификаций для {addonName} на {providerName}? Это действие удалит существующую модификацию и заменит её копией из нового источника.", + "TITLE": "Изменить источник модификации?" + }, + "CHECK_UPDATES_BUTTON": "Проверить обновления", + "CHECK_UPDATES_BUTTON_TOOLTIP": "Проверить наличие последних обновлений модификации", + "CLIENT_TYPE_SELECT_BADGE": "{count} {count, plural, =1{обновление} few{обновления} other{обновлений}}", + "CLIENT_TYPE_SELECT_LABEL": "World of Warcraft", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "Показать столбцы" + }, + "ERROR_SNACKBAR": "Произошла ошибка", + "FILTER_LABEL": "Фильтр", + "FUNDING_TOOLTIP": { + "CUSTOM": "Поддержать этого автора", + "GENERIC": "Поддержать этого автора на {platform}", + "GITHUB": "Поддержать этого автора на GitHub", + "PATREON": "Поддержать этого автора на Patreon", + "PAYPAL": "Поддержать этого автора на PayPal" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "Импорт/Экспорт модификаций", + "MULTIPLE_PROVIDERS_TOOLTIP": "Эта модификация имеет несколько источников", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "{count} {count, plural, one{модификация} few{модификации} other{модификаций}}", + "JOIN_DISCORD": "Общайтесь с нами в Discord", + "PATREON_SUPPORT": "Поддержите WowUp на Patreon", + "SEARCH_RESULTS": "{count} {count, plural, one{результат} few{результата} other{результатов}}", + "VIEW_GITHUB": "Просмотреть код на GitHub", + "VIEW_GUIDE": "Просмотрите наше руководство, чтобы узнать, что может WowUp" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "Нет необходимой зависимости", + "RESCAN_FOLDERS_BUTTON": "Сканировать папки", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "Сканирование папки клиента на наличие установленных модификаций", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "Повторное сканирование может попытаться угадать какие модификации уже установлены, это действие может обнулить известную о модификации информацию. Используйте эту функцию, если не определяются некоторые модификации или их версии отображаются некорректно. Это действие никогда не удалит Ваши установленные модификации, лишь то, что WowUp знает о них.\n\nЭта процедура займёт немного времени.", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "Начать повторное сканирование?", + "SPINNER": { + "GATHERING_ADDONS": "Сбор модификаций...", + "UPDATING": "{updateCount, plural, one{Обновлена} other{Обновлено}} {updateCount} из {addonCount}", + "UPDATING_WITH_ADDON_NAME": "{updateCount, plural, one{Обновлена} other{Обновлено}} {updateCount} из {addonCount}\n{clientType}: {addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "Модификация", + "ADDON_INSTALL_BUTTON": "Установить", + "ADDON_UPDATE_BUTTON": "Обновить", + "AUTHOR_COLUMN_HEADER": "Автор(ы)", + "AUTO_UPDATE_ICON_TOOLTIP": "Автообновление включено", + "GAME_VERSION_COLUMN_HEADER": "Версия игры", + "LATEST_VERSION_COLUMN_HEADER": "Последняя версия", + "PROVIDER_COLUMN_HEADER": "Источник", + "PROVIDER_RELEASE_CHANNEL": "Канал выпуска", + "RELEASED_AT_COLUMN_HEADER": "Выпущена", + "STATUS_COLUMN_HEADER": "Статус", + "UPDATED_AT_COLUMN_HEADER": "Обновлена" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "Удаление модификации через WowUp удалит выбранную модификацию из директории Interface/AddOns. Настройки персонажа для этой модификации удалены не будут.", + "CONFIRMATION_LESS_THAN_THREE": "Вы действительно хотите удалить следующие {count} {count, plural, few{модификации} other{модификаций}}?", + "CONFIRMATION_MORE_THAN_THREE": "Вы действительно хотите удалить выбранные {count} {count, plural, few{модификации} other{модификаций}}?", + "CONFIRMATION_ONE": "Вы действительно хотите удалить {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} имеет {dependencyCount} {dependencyCount, plural, one{зависимость} few{зависимости} other{зависимостей}}. Хотите удалить и их тоже?", + "DEPENDENCY_TITLE": "Удалить зависимости модификации?", + "TITLE": "Удалить {count, plural, one{модификацию} other{модификации}}?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "Установленная модификация не соответствует ни одному из известных источников", + "UPDATE_ALL_BUTTON": "Обновить всё", + "UPDATE_ALL_BUTTON_TOOLTIP": "Обновить все модификации для этого клиента", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "Обновить все клиенты", + "UPDATE_RETAIL_CLASSIC_BUTTON": "Обновить Актуальный/Классический" + }, + "WTF_BACKUP_BUTTON": "Резервное копир. настр. интерфейса" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "Ссылка на новость скопирована в буфер обмена", + "NEWS_LINK_COPY_TOOLTIP": "Скопировать ссылку на новость", + "PAGE_CONTEXT_FOOTER": "{count} {count, plural, one{новость} few{новости} other{новостей}}", + "REFRESH_TOOLTIP": "Обновить ленту новостей" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "Ad or access key required", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "Если Вы запросили ключ CurseForge API, Вы можете ввести его здесь, чтобы подключиться к их API.", + "API_KEY_TITLE": "Ключ CurseForge API", + "PROVIDER_NOTE": "Необходим ключ API" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "Выберите источники, используемые для поиска и установки новых модификаций", + "FIELD_LABEL": "Включённые источники модификаций", + "INPUT_LABEL": "Источники" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "Наличие токена личного доступа увеличит возможное количество запросов к GitHub API, он доступен бесплатно. Узнать больше", + "PLACEHOLDER": "Токен личного доступа", + "TITLE": "Токен личного доступа к GitHub" + }, + "TITLE": "Модификации", + "WAGO_ACCESS_KEY": { + "MESSAGE": "Use the Wago provider without seeing advertisements. Learn More", + "PLACEHOLDER": "Access Key", + "TITLE": "Wago Access Key" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "Переключение на канал Бета позволит Вам получать экспериментальные сборки, которые содержат исправления неполадок, а также новые и грядущие возможности. Вы сможете вернуться к текущей стабильной версии лишь удалив Ваше существующее приложение и переустановив его с wowup.io.\n\nПоскольку канал Бета содержит версии, находящиеся в разработке, используйте его на свой страх и риск.", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "Переключение на канал Стабильная запретит приложению получать бета-сборки, следующим обновлением будет следующая стабильная версия.", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "Установка канала выпуска приложения", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "Да, я понимаю", + "APP_RELEASE_CHANNEL_DESCRIPTION": "Переключение между бета-версией и стабильной версией приложения", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "Канал", + "APP_RELEASE_CHANNEL_LABEL": "Канал выпуска приложения", + "CURRENT_LANGUAGE_LABEL": "Текущий язык", + "CURSE_PROTOCOL_DESCRIPTION": "При загрузке модификаций с сайта CurseForge, WowUp будет брать установку на себя.", + "CURSE_PROTOCOL_LABEL": "Обрабатывать ссылки на установку с CurseForge", + "ENABLE_APP_BADGE_DESCRIPTION": "Показывать количество доступных обновлений на значке приложения.", + "ENABLE_APP_BADGE_LABEL": "Включить уведомления на значке приложения", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Включить различные окна системных уведомлений, такие как автоматически обновлённые модификации.", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Включить системные уведомления", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "При открытии страницы сведений о модификации автоматически выбирает последнюю открытую вкладку.", + "KEEP_LAST_OPENED_TAB_LABEL": "Сохранять последнюю открытую вкладку", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "При закрытии окна WowUp сворачивается в меню статуса.", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "При закрытии окна WowUp сворачивается в область уведомлений панели задач.", + "MINIMIZE_ON_CLOSE_LABEL": "Свернуть в трей при закрытии", + "PROTOCOL_DESCRIPTION": "WowUp может зарегистрировать протокол в Вашей системе и обрабатывать его запросы", + "PROTOCOL_LABEL": "Включить обработку протокола wowup://", + "SCALE_DESCRIPTION": "Изменить масштаб всего приложения.", + "SCALE_LABEL": "Масштабирование", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Изменение языка по умолчанию требует перезапуска приложения.", + "SET_LANGUAGE_CONFIRMATION_LABEL": "Установить новый язык по умолчанию", + "SET_LANGUAGE_DESCRIPTION": "Выберите язык для изменения", + "SET_LANGUAGE_LABEL": "Установить язык приложения", + "START_MINIMIZED_DESCRIPTION": "...и не будет показан на экране", + "START_MINIMIZED_LABEL": "Запускать WowUp свёрнутым", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp будет запущен сразу после загрузки операционной системы...", + "START_WITH_SYSTEM_LABEL": "Запускать WowUp вместе со стартом системы", + "TELEMETRY_DESCRIPTION": "Помогите улучшить WowUp, отправив анонимные данные об установке и/или ошибках.", + "TELEMETRY_LABEL": "Телеметрия", + "THEME_DESCRIPTION": "Изменить цветовую тему по вкусу", + "THEME_LABEL": "Цветовая тема", + "TITLE": "Приложение", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp может назначить себя обработчиком установочных ссылок с CurseForge по умолчанию. Это может вызвать неполадки, если Вы используете приложение CurseForge. Вы уверены, что хотите продолжить?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "Обрабатывать установки с CurseForge?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Хотите перезапустить приложение?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "Отключение аппаратного ускорения может решить проблемы с количеством кадров в секунду и исправить другие неполадки с приложением. Изменение этой настройки требует перезапуск приложения.", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Для отключения аппаратного ускорения нужен перезапуск приложения.", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Для включения аппаратного ускорения нужен перезапуск приложения.", + "USE_HARDWARE_ACCELERATION_LABEL": "Включить аппаратное ускорение", + "USE_SYMLINK_SUPPORT": "Включить поддержку символических ссылок", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "Включение поддержки символических ссылок позволит WowUp распознавать их при повторном сканировании. Предупреждение: Если Вы не знаете, что такое символическая ссылка, то, скорее всего, Вам это не нужно. При обновлении символические ссылки в настоящее время будут заменены фактической папкой, и ссылка будет потеряна.", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "Включить поддержку символических ссылок?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "Разрешает WowUp сканировать папки символических ссылок в папке Вашей модификации. Предупреждение: они будут заменены при обновлении/установке." + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "Просмотр и управление тем, как рекламодатели CurseForge могут использовать Ваши данные для персонализации рекламы", + "ADS_OPTION_LABEL": "Персонализация рекламы и данные", + "ADS_OPTION_MANAGE": "Управление", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "Показать файлы настроек", + "CONFIG_FILES_DESCRIPTION": "Открыть папку, в которой хранятся Ваши файлы addons.json и preferences.json.", + "CONFIG_FILES_LABEL": "Файлы настроек", + "DEBUG_DATA_BUTTON": "Дамп отладочных данных", + "DEBUG_DATA_DESCRIPTION": "Записывать отладочные данные, чтобы помочь в диагностике потенциальных проблем. Его можно найти в последнем лог-файле, если необходимо.", + "DEBUG_DATA_LABEL": "Отладка данных", + "LOG_FILES_BUTTON": "Показать лог-файлы", + "LOG_FILES_DESCRIPTION": "Открыть папку, содержащую несколько последних лог-файлов.", + "LOG_FILES_LABEL": "Файлы логов", + "TITLE": "Отладка" + }, + "TABS": { + "ABOUT": "О приложении", + "ADDONS": "Модификации", + "APPLICATION": "Приложение", + "CLIENTS": "Клиенты", + "CURSEFORGE": "CurseForge", + "DEBUG": "Отладка", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "Добавить новый", + "AUTO_UPDATE_DESCRIPTION": "Новые установленные модификации будут автоматически обновляться по умолчанию", + "AUTO_UPDATE_LABEL": "Автообновление", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Отмена", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "Вы точно хотите очистить путь установки для {clientName}? Это удалит всю хранимую информацию модификации для этого клиента.\n\nПапки Ваших модификаций не будут удалены.", + "TITLE": "Очистить место установки?" + }, + "CLIENT_TYPE_INPUT_HINT": "Директория, которая содержит папку клиента {clientTypeName} – \"{clientFolderName}\"", + "CLIENT_TYPE_PATH_LABEL": "Путь для {clientTypeName}", + "DEFAULT_ADDON_CHANNEL_LABEL": "Тип выпуска модификации по умолчанию", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Тип выпуска модификации", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Изменить", + "MOVE_DOWN_BUTTON": "Переместить вниз", + "MOVE_UP_BUTTON": "Переместить вверх", + "NO_CLIENTS_FOUND_TEXT": "Не обнаружено установленных клиентов World of Warcraft, убедитесь что Ваш клиент Battle.net последней версии или добавьте клиент вручную", + "OPEN_FOLDER_BUTTON": "Открыть папку", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Выбрать", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Удалить", + "RESCAN_CLIENTS_BUTTON": "Повторное сканирование", + "RESCAN_CLIENTS_LABEL": "Повторно найти установленные продукты World of Warcraft", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Сохранить", + "TITLE": "World of Warcraft" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "Вы уверены, что хотите применить эту резервную копию к Вашим настройкам интерфейса?\n\nУбедитесь, что клиент World of Warcraft не запущен во время применения резервной копии.\n\nЭто действие нельзя отменить.", + "TITLE": "Применить резервную копию WTF?" + }, + "BACKUP_APPLY_SUCCESS": "Успешно применена резервная копия: {name}", + "BACKUP_COUNT_TEXT": "{count, plural, =1{Найдена} other{Найдено}} {count} {count, plural, =1{резервная копия} few{резервные копии} other{резервных копий}}", + "BUSY_TEXT": { + "APPLYING_BACKUP": "Применение резервной копии...", + "CREATING_BACKUP": "Создание резервной копии из {count} {count, plural, =1{файла} other{файлов}}...", + "LOADING_BACKUPS": "Загрузка резервных копий...", + "REMOVING_BACKUP": "Удаление резервной копии..." + }, + "CREATE_BACKUP_BUTTON": "Создать резервную копию", + "DELETE_CONFIRMATION": { + "MESSAGE": "Вы уверены, что хотите удалить резервную копию {name}?\nЭто действие нельзя отменить.", + "TITLE": "Удалить резервную копию WTF?" + }, + "DIALOG_TITLE": "Резервная копия настроек WTF: {clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "Не удалось применить резервную копию: {name}", + "FAILED_TO_DELETE": "Не удалось удалить резервную копию: {name}", + "GENERIC_ERROR": "При обработке этой резервной копии возникла проблема", + "INVALID_CONTENTS": "При обработке этой резервной копии возникла проблема", + "INVALID_CREATED_AT": "При обработке этой резервной копии возникла проблема", + "INVALID_CREATED_BY": "При обработке этой резервной копии возникла проблема" + }, + "SHOW_FOLDER_BUTTON": "Показать папку", + "TOOL_TIP": { + "APPLY_BUTTON": "Применить эту резервную копию", + "DELETE_BUTTON": "Удалить эту резервную копию" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/zh-TW.json b/WowUp/wowup-electron/src/assets/i18n/zh-TW.json new file mode 100644 index 0000000..ef9601e --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/zh-TW.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "當前插件數:{count}", + "ADDED_BADGE_TOOLTIP": "嘗試安裝此插件", + "CONFLICT_BADGE_TOOLTIP": "不會修改衝突的插件", + "COPY_BUTTON": "複製", + "DIALOG_TITLE": "匯入/匯出插件:{clientType}", + "EXPORT_STRING_COPIED": "匯出字串已複製到剪貼簿", + "EXPORT_STRING_PASTED": "剪貼簿內容已插入", + "EXPORT_TAB_LABEL": "匯出", + "EXPORT_TEXT_LABEL": "插件匯出資料", + "GENERIC_IMPORT_ERROR": "匯入時發生錯誤", + "IGNORED_ADDON_COUNT": "忽略的插件數:{count}", + "IMPORT_ADDED_COUNT": "增加了 {count} 個插件", + "IMPORT_BADGE_ADDED": "新增", + "IMPORT_BADGE_CONFLICT": "衝突", + "IMPORT_BADGE_NO_CHANGE": "無變化", + "IMPORT_BUTTON": "匯入", + "IMPORT_CONFLICT_COUNT": "有 {count} 個插件存在衝突", + "IMPORT_NO_CHANGE_COUNT": "{count} 個無變化", + "IMPORT_STRING_INVALID": "匯入字串無效", + "IMPORT_TAB_LABEL": "匯入", + "IMPORT_TEXT_INSTRUCTIONS": "把 WowUp 匯出字串貼上到下方以開始匯入", + "IMPORT_TEXT_LABEL": "匯入資料", + "IMPORT_TOTAL_COUNT": "正在匯入 {count} 個插件", + "INSTALL_BUTTON": "安裝", + "INVALID_CLIENT_TYPE": "匯入字串和客戶端型別不匹配", + "NO_CHANGE_BADGE_TOOLTIP": "此插件已安裝", + "PASTE_BUTTON": "貼上", + "PROVIDER_MISMATCH": "插件安裝源不匹配", + "RESET_BUTTON": "重置", + "VERSION_MISMATCH": "版本號不匹配" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "為什麼會看見廣告?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "顯示此廣告是為了支援 wago.io / CurseForge 上辛勤工作、開發出優秀插件的作者們。如果不想看到此廣告,可以在「選項」中禁用 Wago.io / CurseForge 安裝源。", + "TITLE": "為什麼會看見廣告?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "複製", + "CUT": "剪下", + "LABEL": "編輯", + "PASTE": "貼上", + "REDO": "重做", + "SELECT_ALL": "全選", + "UNDO": "撤銷" + }, + "QUIT": "退出", + "VIEW": { + "FORCE_RELOAD": "強制重新整理", + "LABEL": "檢視", + "RELOAD": "重新整理", + "TOGGLE_DEV_TOOLS": "顯示/隱藏開發者工具", + "TOGGLE_FULL_SCREEN": "切換全螢幕模式", + "ZOOM_IN": "放大", + "ZOOM_OUT": "縮小", + "ZOOM_RESET": "原始大小" + }, + "WINDOW": { + "CLOSE": "關閉", + "LABEL": "視窗" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "{addonNames}\r\n已自動更新", + "AUTO_UPDATE_NOTIFICATION_BODY": "自動更新了 {count} 個插件。", + "AUTO_UPDATE_NOTIFICATION_TITLE": "自動更新", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "退出全螢幕模式", + "FULLSCREEN_SNACKBAR": { + "MAC": "按下 ^⌘F 以退出全螢幕模式", + "WINDOWS": "按下 F11 以退出全螢幕模式" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\n是否要在預設瀏覽器中開啟此第三方網頁?", + "TITLE": "即將離開 WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "未知" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "插件掃描完成...", + "ADDON_SCAN_STARTED": "開始插件掃描...", + "ADDON_SCAN_UPDATE": "正在掃描 {count} 個資料夾..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "檢查更新...", + "QUIT_ACTION": "退出", + "SHOW_ACTION": "顯示" + }, + "THEME": { + "ALLIANCE": "聯盟", + "DEFAULT": "WowUp", + "GROUP_DARK": "暗色", + "GROUP_LIGHT": "亮色", + "HORDE": "部落" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - 全螢幕模式", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "檢查更新", + "DOWNLOADED_TOOLTIP": "安裝 WowUp 更新", + "DOWNLOADING_UPDATE": "正在下載更新", + "INSTALL_MESSAGE": "是否重新啟動 WowUp 以完成更新?", + "INSTALL_TITLE": "WowUp 更新已準備就緒", + "NOT_AVAILABLE": "已安裝最新版 WowUp", + "PORTABLE_DOWNLOAD_MESSAGE": "是否手動下載最新綠色版?\n\n下載完成後請關閉本應用程式,然後用新版本覆蓋。", + "PORTABLE_DOWNLOAD_TITLE": "需要手動下載", + "SNACKBAR_ACTION": "更新並重啟", + "SNACKBAR_TEXT": "新版本 WowUp 可用", + "TOOLTIP": "WowUp 更新可用", + "UPDATE_AVAILABLE": "正在開始下載", + "UPDATE_ERROR": "WowUp 更新下載失敗" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "成就", + "ACTION_BARS": "快捷列", + "ALL_ADDONS": "全部插件", + "AUCTION_ECONOMY": "拍賣和經濟", + "BAGS_INVENTORY": "背包和物品欄", + "BOSS_ENCOUNTERS": "首領戰", + "BUFFS_DEBUFFS": "增益和減益", + "BUNDLES": "整合插件", + "CHAT_COMMUNICATION": "聊天和社交", + "CLASS": "職業", + "COMBAT": "戰鬥", + "COMPANIONS": "戰寵", + "DATA_EXPORT": "資料匯出", + "DEVELOPMENT_TOOLS": "開發工具", + "GUILD": "公會", + "LIBRARIES": "庫", + "MAIL": "郵件", + "MAP_MINIMAP": "地圖和小地圖", + "MISCELLANEOUS": "雜項", + "MISSIONS": "追隨者任務", + "PLUGINS": "可選模組", + "PROFESSIONS": "專業", + "PVP": "PvP", + "QUESTS_LEVELING": "任務和升級", + "ROLEPLAY": "角色扮演", + "TOOLTIPS": "工具提示", + "UNIT_FRAMES": "單位框體" + }, + "ADDON_STATE": { + "IGNORED": "忽略", + "INSTALL": "安裝", + "PENDING": "等待中", + "UNAVAILABLE": "不可用", + "UNAVAILABLE_TOOLTIP": "插件作者或安裝源將該插件的狀態設為不可用", + "UNINSTALL": "解除安裝", + "UNKNOWN": "未知", + "UPDATE": "更新", + "UPTODATE": "已安裝最新版", + "WARNING": "警告" + }, + "ADDON_STATUS": { + "BACKINGUP": "正在備份", + "COMPLETE": "已安裝", + "DOWNLOADING": "正在下載", + "ERROR": "错误", + "INSTALLING": "正在安裝", + "PENDING": "等待中", + "RETRY": "Retrying...", + "UNINSTALLING": "正在解除安裝", + "UPDATING": "正在更新..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "WowUp 檢測到此插件存在問題,無法更新此插件或提供詳細資訊。", + "GENERIC_TOOLTIP": "檢測到此插件存在問題", + "MISSING_ON_PROVIDER_DESCRIPTION": "此插件可能已被作者或安裝源移除。
WowUp 無法更新此插件或提供詳細資訊。", + "MISSING_ON_PROVIDER_TOOLTIP": "此插件可能已被安裝源移除", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} 返回了該插件的相關資訊,但不包含匹配當前遊戲版本的檔案。
一旦有匹配該遊戲版本的更新,此提示資訊將會消失。", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} 沒有匹配該遊戲版本的檔案", + "TOC_NAME_MISMATCH_DESCRIPTION": "此插件的資料夾名字和 TOC 檔案中記錄的名字不匹配,遊戲中可能會出現一些問題。", + "TOC_NAME_MISMATCH_TOOLTIP": "插件資料夾和 TOC 不匹配" + }, + "CLIENT_TYPES": { + "BETA": "巨龍崛起 Beta", + "CLASSIC": "巫妖王之怒", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "經典舊世", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "巫妖王之怒 PTR", + "RETAIL": "巨龍崛起", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count} 天前", + "HOURS_AGO": "{count} 小時前", + "JUST_NOW": "剛剛", + "MONTHS_AGO": "{count} 個月前", + "YEARS_AGO": "{count} 年前", + "YESTERDAY": "昨天" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} 個依賴項" + }, + "DOWNLOAD_COUNT": { + "e+0": "{myriadCount}", + "e+1": "{myriadCount}", + "e+2": "{myriadCount}", + "e+3": "{myriadCount}", + "e+4": "{myriadCount} 萬", + "e+5": "{myriadCount} 萬", + "e+6": "{myriadCount} 萬", + "e+7": "{myriadCount} 萬", + "e+8": "{myriadCount} 億", + "e+9": "{myriadCount} 億" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alpha", + "BETA": "Beta", + "STABLE": "穩定版" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "無法為戰網賬戶啟用實時更新。請稍後再試,或者到 Discord 上尋求幫助。", + "ADDON_INSTALL_ERROR": "安裝插件 {addonName} 失敗。請稍後再試。", + "ADDON_SCAN_ERROR": "在與 {providerName} 匹配插件資料夾時出現錯誤,請稍後再試。", + "ADDON_SYNC_ERROR": "從 {providerName} 上檢查更新時出現錯誤。", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: 從 {providerName} 上檢查 {addonName} 的更新時出現錯誤,請稍後再試。", + "CHANGE_PROVIDER_ERROR": "無法將 {addonName} 的安裝源修改為 {providerName}", + "GITHUB_LIMIT_ERROR": "GitHub API 請求次數達到 {max} 次的上限。\n請等到 {reset} 再試。", + "GITHUB_REPOSITORY_FETCH_ERROR": "檢查 {addonName} 的更新失敗。\n請檢查更新源是否正確,或者忽略此插件。" + }, + "PROGRESS_SPINNER": { + "LOADING": "正在載入..." + }, + "PROVIDER_ERROR": "連線 {providerName} 時發生錯誤", + "SEARCH": { + "NO_ADDONS": "未找到插件" + }, + "WOW_EXE_SELECTION_NAME": "WoW 可執行檔案" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "插件 ID:", + "BY_AUTHOR": "By {authorName}", + "CHANGELOG_TAB": "更新記錄", + "COPY_ADDON_ID_SNACKBAR": "插件 ID 已複製到剪貼簿", + "COPY_ADDON_ID_TOOLTIP": "複製插件 ID", + "DEPENDENCY_TEXT": "此插件有 {dependencyCount} 個依賴項", + "DESCRIPTION_NOT_FOUND": "未找到描述", + "DESCRIPTION_TAB": "描述", + "FUNDING_LINK_TITLE": "支援插件作者", + "IMAGES_TAB": "預覽", + "MISSING_DEPENDENCIES": "缺失的依賴項", + "NO_CHANGELOG_TEXT": "更新記錄不可用", + "VIEW_IN_BROWSER_BUTTON": "在瀏覽器中檢視", + "VIEW_ON_PROVIDER_PREFIX": "在該網站上檢視:" + }, + "ALERT": { + "ERROR_TITLE": "錯誤", + "POSITIVE_BUTTON": "確定" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "否", + "POSITIVE_BUTTON": "是" + }, + "CURSE_MIGRATION": { + "MESSAGE": "CurseForge API 已經關閉,因此 WowUp 不能再從此處獲取插件。
不幸的是,這意味著 CurseForge 安裝源已經被移除,且從此處安裝的插件再也不會更新了。
推薦進行重新掃描以檢查插件是否能從其他安裝源獲取。
重新掃描將會消耗一些時間。
閱讀詳細資訊(英語)。", + "NEGATIVE_BUTTON": "稍後手動重新掃描", + "POSITIVE_BUTTON": "自動重新掃描", + "RE_SCAN_ERROR": "自動重新掃描失敗,請嘗試手動掃描。", + "RE_SCAN_SUCCESS": "自動重新掃描完成,可以開始使用了。", + "TITLE": "CurseForge 遷移" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "插件已安裝!", + "ADDON_INSTALLING": "正在安裝插件", + "CANCEL_BUTTON": "關閉", + "ERRORS": { + "ADDON_NOT_FOUND": "在 {protocol} 協議下未找到插件", + "GENERIC": "在 {protocol} 協議下獲取資料失敗", + "NO_VALID_WOW_INSTALLATIONS": "在 {protocol} 協議下沒有《魔獸世界》客戶端" + }, + "INSTALL_BUTTON": "安裝", + "TITLE": "從 {providerName} 處安裝插件" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "插件 URL", + "ADDON_URL_INPUT_PLACEHOLDER": "例如,GitHub 或 WowInterface URL", + "CLOSE_BUTTON": "關閉", + "DESCRIPTION": "如需直接從某 URL 安裝插件,請將其貼上到下面以開始安裝。", + "DOWNLOAD_COUNT": "在 {provider} 上有 {textCount} 次下載", + "ERROR": { + "ASSET_NOT_FOUND": "未找到可下載的附件 {message}。\n\n要通過 WowUp 下載插件,釋出頁面(Releases)上需要有效的 zip 檔案。", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "未找到可下載的附件 {message}。\n\n要通過 WowUp 下載插件,釋出頁面(Releases)上需要檔名以「-bc」結尾的有效的 zip 檔案。", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "未找到可下載的附件 {message}。\n\n要通過 WowUp 下載插件,釋出頁面(Releases)上需要檔名以「-classic」結尾的有效的 zip 檔案。", + "FAILED_TO_CONNECT": "無法連線 API,請稍後重試。", + "INSTALL_FAILED": "安裝插件時出現了一些錯誤,請重試。\n\n如果一直出現此訊息,可以加入 Discord #help-me 頻道尋求幫助(英語)。", + "INVALID_URL": "輸入的 URL 無效。有效 URL 示例:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "未找到插件,請檢查 URL 是否指向正確的頁面。\n\n從 GitHub 安裝時,請檢查是否存在帶有 zip 包的 release tag。", + "NO_RELEASE_FOUND": "未找到釋出版本 {message}.\n\n要通過 WowUp 下載插件,需要帶有 zip 附件的釋出版本。", + "NO_SEARCH_RESULTS": "无搜索结果。", + "TITLE": "插件安裝失敗", + "WRATH_ASSET_NOT_FOUND": "未找到可下載的附件 {message}。\n\n要通過 WowUp 下載插件,釋出頁面(Releases)上需要檔名以「-wrath」結尾的有效的 zip 檔案。" + }, + "IMPORT_ASSET_WARNING": "已找到此插件的最新版本壓縮包「{zipName}」,但無法驗證它是否相容此《魔獸世界》客戶端。\n\n如要安裝,請自擔風險。", + "IMPORT_BUTTON": "匯入", + "IMPORT_WARNING_TITLE": "匯入插件警告", + "INSTALL_BUTTON": "安裝", + "INSTALL_SUCCESS_LABEL": "安裝成功!", + "SUPPORTED_SOURCES": "支援 WowInterface 和 GitHub", + "TITLE": "從 URL 安裝" + }, + "NEW_VERSION_POPUP": { + "TITLE": "更新記錄 {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "在開始使用 WowUp 之前,請設定此應用程式的許可權。", + "POSITIVE_BUTTON": "確認", + "TELEMETRY": { + "DESCRIPTION": "是否傳送匿名的插件統計資料和錯誤報告以幫助改進 WowUp?", + "TOGGLE_LABEL": "允許遙測" + }, + "TITLE": "WowUp 許可權設定", + "WAGO": { + "DESCRIPTION": "啟用 Wago.io 插件安裝源。啟用此選項將會顯示一個廣告,以支援你喜歡的插件的作者。", + "TOGGLE_LABEL": "啟用 Wago.io 安裝源" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "此路徑可能不是有效的《魔獸世界》應用程式:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "是否傳送匿名應用安裝資料和錯誤資訊以幫助改進 WowUp?", + "NEGATIVE_BUTTON": "否", + "POSITIVE_BUTTON": "是", + "TITLE": "WowUp 遙測" + }, + "TRUST_DOMAIN_CHECKBOX": "信任此域名,今後不再詢問" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "圖源", + "CHANGE_LOG_SECTION_LABEL": "更新記錄", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "訪問網站!" + }, + "ACCOUNT": { + "BETA": "測試版", + "LOGIN_BUTTON": "立即登入!", + "LOGOUT_BUTTON": "登出", + "LOGOUT_CONFIRMATION_MESSAGE": "確定要登出嗎?本地賬戶資料將會被刪除,重新登入時將會同步回來。", + "LOGOUT_CONFIRMATION_TITLE": "是否登出?", + "MANAGE_ACCOUNT_BUTTON": "管理賬戶", + "TITLE": "賬戶" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "類別", + "ADDON_CATEGORIES_MENU_TITLE": "插件類別", + "ADDON_CATEGORIES_SELECTED_TITLE": "類別: {category}", + "ADDON_CATEGORIES_TOOLTIP": "瀏覽插件類別", + "CLIENT_TYPE_SELECT_LABEL": "魔獸世界", + "INSTALL_FROM_URL_BUTTON": "從 URL 安裝", + "INSTALL_FROM_URL_TOOLTIP": "從 URL 安裝插件", + "REFRESH_BUTTON": "重新整理", + "REFRESH_TOOLTIP": "重新整理插件搜尋結果", + "RESET_CATEGORY_TOOLTIP": "重置類別", + "SEARCH_LABEL": "搜尋", + "TABLE": { + "ADDON_COLUMN_HEADER": "插件", + "AUTHOR_COLUMN_HEADER": "作者", + "DOWNLOAD_COUNT_COLUMN_HEADER": "下載次數", + "PROVIDER_COLUMN_HEADER": "安裝源", + "RELEASED_AT_COLUMN_HEADER": "釋出時間", + "STATUS_COLUMN_HEADER": "狀態" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "關於", + "ACCOUNT_TAB_TITLE": "賬戶", + "COLLAPSE_BUTTON_TITLE": "摺疊", + "DISCORD_TAB_TITLE": "Discord(英語)", + "EXPAND_BUTTON_TITLE": "展開", + "GET_ADDONS_TAB_TITLE": "獲取插件", + "GUIDE_TAB_TITLE": "指南(英語)", + "MIGRATING_ADDONS": "正在遷移插件...", + "MY_ADDONS_TAB_TITLE": "我的插件", + "NEWS_TAB_TITLE": "新聞(英語)", + "OPTIONS_TAB_TITLE": "選項" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "已選中 {count} 項", + "ALPHA_ADDON_CHANNEL": "Alpha", + "AUTO_UPDATE_ADDON_BUTTON": "自動更新", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "通知已開啟", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "更新通道", + "IGNORE_ADDON_BUTTON": "忽略", + "PROVIDER_SUBMENU_TITLE": "安裝源", + "REINSTALL_ADDON_BUTTON": "重新安裝", + "REMOVE_ADDON_BUTTON": "刪除", + "SHOW_FOLDER": "開啟資料夾", + "STABLE_ADDON_CHANNEL": "穩定版" + }, + "ADDON_IS_CODE_REPOSITORY": "插件似乎是程式碼倉庫", + "ADDON_REMOVED_SNACKBAR": "{addonName} 已成功移除", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "是否將 {addonName} 的安裝源修改為 {providerName}?此操作將會用 {providerName} 的版本替換現有版本。", + "TITLE": "修改插件安裝源?" + }, + "CHECK_UPDATES_BUTTON": "檢查更新", + "CHECK_UPDATES_BUTTON_TOOLTIP": "檢查最新的插件更新", + "CLIENT_TYPE_SELECT_BADGE": "{count} 個更新", + "CLIENT_TYPE_SELECT_LABEL": "魔獸世界", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "顯示列表項" + }, + "ERROR_SNACKBAR": "出現錯誤", + "FILTER_LABEL": "篩選", + "FUNDING_TOOLTIP": { + "CUSTOM": "支援作者", + "GENERIC": "在 {platform} 上支援作者", + "GITHUB": "在 GitHub 上支援作者", + "PATREON": "通过 Patreon 支援作者", + "PAYPAL": "通过 PayPal 支援作者" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "匯入/匯出插件", + "MULTIPLE_PROVIDERS_TOOLTIP": "此插件有多個安裝源", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "共 {count} 個插件", + "JOIN_DISCORD": "在 Discord 上與我們交流(英語)", + "PATREON_SUPPORT": "透過 Patreon 向 WowUp 捐助(英語)", + "SEARCH_RESULTS": "共 {count} 條結果", + "VIEW_GITHUB": "在 GitHub 上檢視原始碼", + "VIEW_GUIDE": "訪問指南,探索 WowUp 的功能" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "缺失必要的依賴項", + "RESCAN_FOLDERS_BUTTON": "重新掃描", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "掃描客戶端資料夾中已安裝的插件", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "重新掃描過程將會猜測已經安裝了哪些插件,可能會重置已知插件資訊。如果特定插件無法識別或顯示不正確,請使用此功能。掃描不會刪除插件本身,只會刪除 WowUp 記錄的插件資訊。\n\n掃描需要花費一些時間。", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "是否開始掃描?", + "SPINNER": { + "GATHERING_ADDONS": "正在收集插件資訊...", + "UPDATING": "正在更新第 {updateCount} 個,共 {addonCount} 個", + "UPDATING_WITH_ADDON_NAME": "正在更新第 {updateCount} 個,共 {addonCount} 個\n{clientType}:{addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "插件", + "ADDON_INSTALL_BUTTON": "安裝", + "ADDON_UPDATE_BUTTON": "更新", + "AUTHOR_COLUMN_HEADER": "作者", + "AUTO_UPDATE_ICON_TOOLTIP": "自動更新已啟用", + "GAME_VERSION_COLUMN_HEADER": "遊戲版本", + "LATEST_VERSION_COLUMN_HEADER": "最新版本", + "PROVIDER_COLUMN_HEADER": "安裝源", + "PROVIDER_RELEASE_CHANNEL": "更新通道", + "RELEASED_AT_COLUMN_HEADER": "釋出時間", + "STATUS_COLUMN_HEADER": "狀態", + "UPDATED_AT_COLUMN_HEADER": "更新時間" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "此操作將會從《魔獸世界》所在路徑下刪除所有與插件相關的資料夾。", + "CONFIRMATION_LESS_THAN_THREE": "是否確定要刪除以下 {count} 個插件?", + "CONFIRMATION_MORE_THAN_THREE": "是否確定要刪除選中的 {count} 個插件?", + "CONFIRMATION_ONE": "是否確定要刪除 {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} 有 {dependencyCount} 個依賴項,是否一併刪除?", + "DEPENDENCY_TITLE": "是否刪除插件依賴項?", + "TITLE": "是否解除安裝插件?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "已安裝的插件與所有安裝源均無法匹配。", + "UPDATE_ALL_BUTTON": "全部更新", + "UPDATE_ALL_BUTTON_TOOLTIP": "更新此客戶端的所有插件", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "更新所有客戶端", + "UPDATE_RETAIL_CLASSIC_BUTTON": "更新正式服/經典懷舊服" + }, + "WTF_BACKUP_BUTTON": "備份介面設定" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "新聞連結已複製到剪貼簿", + "NEWS_LINK_COPY_TOOLTIP": "複製新聞連結", + "PAGE_CONTEXT_FOOTER": "共 {count} 條新聞", + "REFRESH_TOOLTIP": "重新整理資訊流" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "啟用此項需要提供訪問金鑰或顯示廣告", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "如果申請過 CurseForge API 金鑰,請填入此處以連線到 CurseForge API。", + "API_KEY_TITLE": "CurseForge API 金鑰", + "PROVIDER_NOTE": "需要 API 金鑰" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "選擇從哪些安裝源搜尋和安裝新插件", + "FIELD_LABEL": "啟用插件安裝源", + "INPUT_LABEL": "安裝源" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "免費的個人訪問令牌(PAT)可以提高 GitHub API 呼叫次數的限制。檢視簡體中文文件(部分翻譯)英文文件。", + "PLACEHOLDER": "個人訪問令牌(PAT)", + "TITLE": "GitHub 個人訪問令牌(PAT)" + }, + "TITLE": "插件", + "WAGO_ACCESS_KEY": { + "MESSAGE": "啟用 Wago 安裝源而無需顯示廣告。瞭解詳細資訊(英語)", + "PLACEHOLDER": "訪問金鑰", + "TITLE": "Wago 訪問金鑰" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "切換到 Beta 通道後,將會收到包含最新的錯誤修正和功能的版本。要切換回穩定版本,只能解除安裝 WowUp 並重新安裝。\n\n儘管 Beta 通道通常能正常工作,但仍需自擔風險。", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "切換到穩定版通道後,將會停止接受 Beta 版本。切換到穩定版通道不會回退到上一個穩定版,而是在下一個穩定版釋出後更新到穩定版。", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "設定應用程式更新通道", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "我明白了", + "APP_RELEASE_CHANNEL_DESCRIPTION": "切換此應用程式的穩定版和 Beta 通道", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "更新通道", + "APP_RELEASE_CHANNEL_LABEL": "應用程式更新通道", + "CURRENT_LANGUAGE_LABEL": "當前語言", + "CURSE_PROTOCOL_DESCRIPTION": "從 CurseForge 網站下載插件時,WowUp 將會接管安裝過程", + "CURSE_PROTOCOL_LABEL": "接管 CurseForge 下載連結", + "ENABLE_APP_BADGE_DESCRIPTION": "在應用程式圖示上用角標顯示可更新的插件數量。", + "ENABLE_APP_BADGE_LABEL": "啟用數字角標通知", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "啟用各種系統通知彈窗,如自動更新插件通知。", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "啟用系統通知", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "開啟插件詳情頁時,自動切換到上一次開啟的選項卡", + "KEEP_LAST_OPENED_TAB_LABEL": "記住上一次開啟的選項卡", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "關閉 WowUp 視窗時,最小化到選單欄。", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "關閉 WowUp 視窗時,最小化到系統托盤。", + "MINIMIZE_ON_CLOSE_LABEL": "關閉時最小化", + "PROTOCOL_DESCRIPTION": "WowUp 將會在系統中註冊自定義 URI 協議以處理傳入的請求", + "PROTOCOL_LABEL": "允許 WowUp 處理 wowup:// URI", + "SCALE_DESCRIPTION": "修改整個應用程式的縮放係數。", + "SCALE_LABEL": "介面縮放", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "更改預設語言需要重新啟動客戶端。", + "SET_LANGUAGE_CONFIRMATION_LABEL": "設定新的預設語言", + "SET_LANGUAGE_DESCRIPTION": "選擇要使用的語言", + "SET_LANGUAGE_LABEL": "設定語言", + "START_MINIMIZED_DESCRIPTION": "……並且不顯示視窗。", + "START_MINIMIZED_LABEL": "以最小化視窗啟動 WowUp", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp 將在登入後自動啟動……", + "START_WITH_SYSTEM_LABEL": "登入後自動啟動 WowUp", + "TELEMETRY_DESCRIPTION": "傳送匿名應用安裝資料和錯誤資訊以幫助改進 WowUp。", + "TELEMETRY_LABEL": "遙測", + "THEME_DESCRIPTION": "選擇喜歡的配色主題", + "THEME_LABEL": "配色主題", + "TITLE": "應用程式", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp 可以被註冊為 CurseForge 下載連結的處理程式。開啟此選項可能會導致使用 CurseForge 客戶端時出現問題,確定要繼續嗎?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "是否接管 CurseForge 下載?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "是否重新啟動?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "禁用硬體加速可能會提高 FPS 並修復其他渲染問題。更改此項需要重新啟動。", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "禁用硬體加速需要重新啟動 WowUp。", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "啟用硬體加速需要重新啟動 WowUp。", + "USE_HARDWARE_ACCELERATION_LABEL": "啟用硬體加速", + "USE_SYMLINK_SUPPORT": "啟用符號連結", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "啟用符號連結使得 WowUp 在重新掃描時識別符號連結。警告:如果您不知道什麼是符號連結,請禁用此項。在當前版本中,符號連結將會在更新插件時被刪除,並被替換為真正的資料夾。", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "啟用符號連結?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "允許 WowUp 掃描插件所在路徑下的符號連結。警告:符號連結在安裝或更新插件時會被替換。" + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "顯示配置檔案", + "CONFIG_FILES_DESCRIPTION": "開啟儲存 addons.json、preferences.json 等配置檔案的資料夾。", + "CONFIG_FILES_LABEL": "配置檔案", + "DEBUG_DATA_BUTTON": "轉儲除錯資料", + "DEBUG_DATA_DESCRIPTION": "記錄除錯資料以幫助診斷潛在的問題。除錯資料可以在最新的日誌檔案中找到。", + "DEBUG_DATA_LABEL": "除錯資料", + "LOG_FILES_BUTTON": "顯示日誌檔案", + "LOG_FILES_DESCRIPTION": "開啟包含您最後幾個日誌檔案的資料夾。", + "LOG_FILES_LABEL": "日誌檔案", + "TITLE": "除錯" + }, + "TABS": { + "ABOUT": "關於", + "ADDONS": "插件", + "APPLICATION": "應用程式", + "CLIENTS": "客戶端", + "CURSEFORGE": "CurseForge", + "DEBUG": "除錯", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "新增", + "AUTO_UPDATE_DESCRIPTION": "新安裝的插件將預設設定為自動更新", + "AUTO_UPDATE_LABEL": "自動更新", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "取消", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "是否忘記 {clientName} 的安裝路徑?此操作將會刪除 WowUp 記錄的此客戶端所安裝插件資訊。\n\n插件資料夾不會被刪除。", + "TITLE": "忘記此路徑?" + }, + "CLIENT_TYPE_INPUT_HINT": "{clientTypeName} 客戶端「{clientFolderName}」主程式路徑", + "CLIENT_TYPE_PATH_LABEL": "{clientTypeName} 路徑", + "DEFAULT_ADDON_CHANNEL_LABEL": "預設插件更新通道", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "插件更新通道", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "編輯", + "MOVE_DOWN_BUTTON": "下移", + "MOVE_UP_BUTTON": "上移", + "NO_CLIENTS_FOUND_TEXT": "未找到《魔獸世界》客戶端,請檢查 Battle.net 是否為最新版本,或者手動新增客戶端。", + "OPEN_FOLDER_BUTTON": "在檔案管理器中顯示", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "瀏覽", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "移除", + "RESCAN_CLIENTS_BUTTON": "重新掃描", + "RESCAN_CLIENTS_LABEL": "重新掃描已安裝的《魔獸世界》客戶端", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "儲存", + "TITLE": "魔獸世界" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "確認要把這個備份還原到介面設定中嗎?\n\n還原之前務必關閉《魔獸世界》客戶端。\n\n此操作無法撤銷。", + "TITLE": "是否還原 WTF 備份?" + }, + "BACKUP_APPLY_SUCCESS": "還原備份 {name} 成功", + "BACKUP_COUNT_TEXT": "找到 {count} 個備份", + "BUSY_TEXT": { + "APPLYING_BACKUP": "正在還原備份...", + "CREATING_BACKUP": "正在建立 {count} 個檔案的備份...", + "LOADING_BACKUPS": "正在讀取備份...", + "REMOVING_BACKUP": "正在刪除備份..." + }, + "CREATE_BACKUP_BUTTON": "建立備份", + "DELETE_CONFIRMATION": { + "MESSAGE": "確認要刪除備份 {name} 嗎?\n此操作無法撤銷。", + "TITLE": "是否刪除 WTF 備份?" + }, + "DIALOG_TITLE": "WTF 設定備份:{clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "還原備份 {name} 失敗", + "FAILED_TO_DELETE": "刪除備份 {name} 失敗", + "GENERIC_ERROR": "處理備份檔案時出現了一點問題", + "INVALID_CONTENTS": "處理備份檔案時出現了一點問題", + "INVALID_CREATED_AT": "處理備份檔案時出現了一點問題", + "INVALID_CREATED_BY": "處理備份檔案時出現了一點問題" + }, + "SHOW_FOLDER_BUTTON": "在檔案管理器中顯示", + "TOOL_TIP": { + "APPLY_BUTTON": "還原此備份", + "DELETE_BUTTON": "刪除此備份" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/i18n/zh.json b/WowUp/wowup-electron/src/assets/i18n/zh.json new file mode 100644 index 0000000..4e5d1ea --- /dev/null +++ b/WowUp/wowup-electron/src/assets/i18n/zh.json @@ -0,0 +1,661 @@ +{ + "ADDON_IMPORT": { + "ACTIVE_ADDON_COUNT": "当前插件数:{count}", + "ADDED_BADGE_TOOLTIP": "尝试安装此插件", + "CONFLICT_BADGE_TOOLTIP": "不会修改冲突的插件", + "COPY_BUTTON": "复制", + "DIALOG_TITLE": "导入/导出插件:{clientType}", + "EXPORT_STRING_COPIED": "导出字符串已复制到剪贴板", + "EXPORT_STRING_PASTED": "剪贴板内容已插入", + "EXPORT_TAB_LABEL": "导出", + "EXPORT_TEXT_LABEL": "插件导出数据", + "GENERIC_IMPORT_ERROR": "导入时发生错误", + "IGNORED_ADDON_COUNT": "忽略的插件数:{count}", + "IMPORT_ADDED_COUNT": "增加了 {count} 个插件", + "IMPORT_BADGE_ADDED": "新增", + "IMPORT_BADGE_CONFLICT": "冲突", + "IMPORT_BADGE_NO_CHANGE": "无变化", + "IMPORT_BUTTON": "导入", + "IMPORT_CONFLICT_COUNT": "有 {count} 个插件存在冲突", + "IMPORT_NO_CHANGE_COUNT": "{count} 个无变化", + "IMPORT_STRING_INVALID": "导入字符串无效", + "IMPORT_TAB_LABEL": "导入", + "IMPORT_TEXT_INSTRUCTIONS": "把 WowUp 导出字符串粘贴到下方以开始导入", + "IMPORT_TEXT_LABEL": "导入数据", + "IMPORT_TOTAL_COUNT": "正在导入 {count} 个插件", + "INSTALL_BUTTON": "安装", + "INVALID_CLIENT_TYPE": "导入字符串和客户端类型不匹配", + "NO_CHANGE_BADGE_TOOLTIP": "此插件已安装", + "PASTE_BUTTON": "粘贴", + "PROVIDER_MISMATCH": "插件安装源不匹配", + "RESET_BUTTON": "重置", + "VERSION_MISMATCH": "版本号不匹配" + }, + "ADS": { + "AD_EXPLAINER_BUTTON": "为什么会看见广告?", + "AD_EXPLAINER_DIALOG": { + "MESSAGE": "显示此广告是为了支持 wago.io / CurseForge 上辛勤工作、开发出优秀插件的作者们。如果不想看到此广告,可以在 “选项” 中禁用 Wago.io / CurseForge 安装源。", + "TITLE": "为什么会看见广告?" + } + }, + "APP": { + "APP_MENU": { + "EDIT": { + "COPY": "复制", + "CUT": "剪切", + "LABEL": "编辑", + "PASTE": "粘贴", + "REDO": "重做", + "SELECT_ALL": "全选", + "UNDO": "撤销" + }, + "QUIT": "退出", + "VIEW": { + "FORCE_RELOAD": "强制刷新", + "LABEL": "视图", + "RELOAD": "刷新", + "TOGGLE_DEV_TOOLS": "显示/隐藏开发者工具", + "TOGGLE_FULL_SCREEN": "切换全屏模式", + "ZOOM_IN": "放大", + "ZOOM_OUT": "缩小", + "ZOOM_RESET": "原始大小" + }, + "WINDOW": { + "CLOSE": "关闭", + "LABEL": "窗口" + } + }, + "AUTO_UPDATE_FEW_NOTIFICATION_BODY": "{addonNames}\r\n已自动更新", + "AUTO_UPDATE_NOTIFICATION_BODY": "自动更新了 {count} 个插件。", + "AUTO_UPDATE_NOTIFICATION_TITLE": "自动更新", + "CLOSE_FULLSCREEN_BUTTON_TOOLTIP": "退出全屏模式", + "FULLSCREEN_SNACKBAR": { + "MAC": "按下 ^⌘F 以退出全屏模式", + "WINDOWS": "按下 F11 以退出全屏模式" + }, + "LINK_NAVIGATION": { + "MESSAGE": "{url}\n\n是否要在默认浏览器中打开此第三方网页?", + "TITLE": "即将离开 WowUp" + }, + "PROVIDERS": { + "UNKNOWN": "未知" + }, + "STATUS_TEXT": { + "ADDON_SCAN_COMPLETED": "插件扫描完成...", + "ADDON_SCAN_STARTED": "开始插件扫描...", + "ADDON_SCAN_UPDATE": "正在扫描 {count} 个文件夹..." + }, + "SYSTEM_TRAY": { + "CHECK_UPDATE": "检查更新...", + "QUIT_ACTION": "退出", + "SHOW_ACTION": "显示" + }, + "THEME": { + "ALLIANCE": "联盟", + "DEFAULT": "WowUp", + "GROUP_DARK": "暗色", + "GROUP_LIGHT": "亮色", + "HORDE": "部落" + }, + "WINDOW_TITLE": "WowUp.io", + "WINDOW_TITLE_FULLSCREEN": "WowUp.io - 全屏模式", + "WOWUP_UPDATE": { + "CHECKING_FOR_UPDATE": "检查更新", + "DOWNLOADED_TOOLTIP": "安装 WowUp 更新", + "DOWNLOADING_UPDATE": "正在下载更新", + "INSTALL_MESSAGE": "是否重新启动 WowUp 以完成更新?", + "INSTALL_TITLE": "WowUp 更新已准备就绪", + "NOT_AVAILABLE": "已安装最新版 WowUp", + "PORTABLE_DOWNLOAD_MESSAGE": "是否手动下载最新绿色版?\n\n下载完成后请关闭本应用程序,然后用新版本覆盖。", + "PORTABLE_DOWNLOAD_TITLE": "需要手动下载", + "SNACKBAR_ACTION": "更新并重启", + "SNACKBAR_TEXT": "新版本 WowUp 可用", + "TOOLTIP": "WowUp 更新可用", + "UPDATE_AVAILABLE": "正在开始下载", + "UPDATE_ERROR": "WowUp 更新下载失败" + } + }, + "COMMON": { + "ADDON_CATEGORIES": { + "ACHIEVEMENTS": "成就", + "ACTION_BARS": "动作条", + "ALL_ADDONS": "全部插件", + "AUCTION_ECONOMY": "拍卖和经济", + "BAGS_INVENTORY": "背包和物品栏", + "BOSS_ENCOUNTERS": "首领战", + "BUFFS_DEBUFFS": "增益和减益", + "BUNDLES": "整合插件", + "CHAT_COMMUNICATION": "聊天和社交", + "CLASS": "职业", + "COMBAT": "战斗", + "COMPANIONS": "战斗宠物", + "DATA_EXPORT": "数据导出", + "DEVELOPMENT_TOOLS": "开发工具", + "GUILD": "公会", + "LIBRARIES": "库", + "MAIL": "邮件", + "MAP_MINIMAP": "地图和微缩地图", + "MISCELLANEOUS": "杂项", + "MISSIONS": "追随者任务", + "PLUGINS": "可选模块", + "PROFESSIONS": "专业", + "PVP": "PvP", + "QUESTS_LEVELING": "任务和升级", + "ROLEPLAY": "角色扮演", + "TOOLTIPS": "工具提示", + "UNIT_FRAMES": "单位框体" + }, + "ADDON_STATE": { + "IGNORED": "忽略", + "INSTALL": "安装", + "PENDING": "等待中", + "UNAVAILABLE": "不可用", + "UNAVAILABLE_TOOLTIP": "插件作者或安装源将该插件的状态设为不可用", + "UNINSTALL": "卸载", + "UNKNOWN": "未知", + "UPDATE": "更新", + "UPTODATE": "已安装最新版", + "WARNING": "警告" + }, + "ADDON_STATUS": { + "BACKINGUP": "正在备份", + "COMPLETE": "已安装", + "DOWNLOADING": "正在下载", + "ERROR": "错误", + "INSTALLING": "正在安装", + "PENDING": "等待中", + "RETRY": "Retrying...", + "UNINSTALLING": "正在卸载", + "UPDATING": "正在更新..." + }, + "ADDON_WARNING": { + "GAME_VERSION_TOC_MISSING_DESCRIPTION": "This addon or its children have one or more missing toc files for this game version", + "GAME_VERSION_TOC_MISSING_TOOLTIP": "One or more missing toc files for this game version", + "GENERIC_DESCRIPTION": "WowUp 检测到此插件存在问题,无法更新此插件或提供详细信息。", + "GENERIC_TOOLTIP": "检测到此插件存在问题", + "MISSING_ON_PROVIDER_DESCRIPTION": "此插件可能已被作者或安装源移除。
WowUp 无法更新此插件或提供详细信息。", + "MISSING_ON_PROVIDER_TOOLTIP": "此插件可能已被安装源移除", + "NO_PROVIDER_FILES_DESCRIPTION": "{providerName} 返回了该插件的相关信息,但不包含匹配当前游戏版本的文件。
一旦有匹配该游戏版本的更新,此提示信息将会消失。", + "NO_PROVIDER_FILES_TOOLTIP": "{providerName} 没有匹配该游戏版本的文件", + "TOC_NAME_MISMATCH_DESCRIPTION": "此插件的文件夹名字和 TOC 文件中记录的名字不匹配,游戏中可能会出现一些问题。", + "TOC_NAME_MISMATCH_TOOLTIP": "插件文件夹和 TOC 不匹配" + }, + "CLIENT_TYPES": { + "BETA": "巨龙时代 Beta", + "CLASSIC": "巫妖王之怒", + "CLASSICBETA": "Cataclysm Classic Beta", + "CLASSICERA": "经典旧世", + "CLASSICERAPTR": "WoW Classic Era PTR", + "CLASSICPTR": "巫妖王之怒 PTR", + "RETAIL": "巨龙时代", + "RETAILPTR": "Public Test Realm (DF 10.2.5)", + "RETAILXPTR": "Public Test Realm (DF 10.2.0)" + }, + "DATES": { + "DAYS_AGO": "{count} 天前", + "HOURS_AGO": "{count} 小时前", + "JUST_NOW": "刚刚", + "MONTHS_AGO": "{count} 个月前", + "YEARS_AGO": "{count} 年前", + "YESTERDAY": "昨天" + }, + "DEPENDENCY": { + "TOOLTIP": "{dependencyCount} 个依赖项" + }, + "DOWNLOAD_COUNT": { + "e+0": "{myriadCount}", + "e+1": "{myriadCount}", + "e+2": "{myriadCount}", + "e+3": "{myriadCount}", + "e+4": "{myriadCount} 万", + "e+5": "{myriadCount} 万", + "e+6": "{myriadCount} 万", + "e+7": "{myriadCount} 万", + "e+8": "{myriadCount} 亿", + "e+9": "{myriadCount} 亿" + }, + "ENUM": { + "ADDON_CHANNEL_TYPE": { + "ALPHA": "Alpha", + "BETA": "Beta", + "STABLE": "稳定版" + } + }, + "ERRORS": { + "ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "无法为战网账户启用实时更新。请稍后再试,或者到 Discord 上寻求帮助。", + "ADDON_INSTALL_ERROR": "安装插件 {addonName} 失败。请稍后再试。", + "ADDON_SCAN_ERROR": "在与 {providerName} 匹配插件文件夹时出现错误,请稍后再试。", + "ADDON_SYNC_ERROR": "从 {providerName} 上检查更新时出现错误。", + "ADDON_SYNC_FULL_ERROR": "[{installationName}]: 从 {providerName} 上检查 {addonName} 的更新时出现错误,请稍后再试。", + "CHANGE_PROVIDER_ERROR": "无法将 {addonName} 的安装源修改为 {providerName}", + "GITHUB_LIMIT_ERROR": "GitHub API 请求次数达到 {max} 次的上限。\n请等到 {reset} 再试。", + "GITHUB_REPOSITORY_FETCH_ERROR": "检查 {addonName} 的更新失败。\n请检查更新源是否正确,或者忽略此插件。" + }, + "PROGRESS_SPINNER": { + "LOADING": "正在加载..." + }, + "PROVIDER_ERROR": "连接 {providerName} 时发生错误", + "SEARCH": { + "NO_ADDONS": "未找到插件" + }, + "WOW_EXE_SELECTION_NAME": "WoW 可执行文件" + }, + "DIALOGS": { + "ADDON_DETAILS": { + "ADDON_ID_PREFIX": "插件 ID:", + "BY_AUTHOR": "By {authorName}", + "CHANGELOG_TAB": "更新记录", + "COPY_ADDON_ID_SNACKBAR": "插件 ID 已复制到剪贴板", + "COPY_ADDON_ID_TOOLTIP": "复制插件 ID", + "DEPENDENCY_TEXT": "此插件有 {dependencyCount} 个依赖项", + "DESCRIPTION_NOT_FOUND": "未找到描述", + "DESCRIPTION_TAB": "描述", + "FUNDING_LINK_TITLE": "支持插件作者", + "IMAGES_TAB": "预览", + "MISSING_DEPENDENCIES": "缺失的依赖项", + "NO_CHANGELOG_TEXT": "更新记录不可用", + "VIEW_IN_BROWSER_BUTTON": "在浏览器中查看", + "VIEW_ON_PROVIDER_PREFIX": "在该网站上查看:" + }, + "ALERT": { + "ERROR_TITLE": "错误", + "POSITIVE_BUTTON": "确定" + }, + "CONFIRM": { + "NEGATIVE_BUTTON": "否", + "POSITIVE_BUTTON": "是" + }, + "CURSE_MIGRATION": { + "MESSAGE": "CurseForge API 已经关闭,因此 WowUp 不能再从此处获取插件。
不幸的是,这意味着 CurseForge 安装源已经被移除,且从此处安装的插件再也不会更新了。
推荐进行重新扫描以检查插件是否能从其他安装源获取。
重新扫描将会消耗一些时间。
阅读详细信息(英语)。", + "NEGATIVE_BUTTON": "稍后手动重新扫描", + "POSITIVE_BUTTON": "自动重新扫描", + "RE_SCAN_ERROR": "自动重新扫描失败,请尝试手动扫描。", + "RE_SCAN_SUCCESS": "自动重新扫描完成,可以开始使用了。", + "TITLE": "CurseForge 迁移" + }, + "INSTALL_FROM_PROTOCOL": { + "ADDON_INSTALLED": "插件已安装!", + "ADDON_INSTALLING": "正在安装插件", + "CANCEL_BUTTON": "关闭", + "ERRORS": { + "ADDON_NOT_FOUND": "在 {protocol} 协议下未找到插件", + "GENERIC": "在 {protocol} 协议下获取数据失败", + "NO_VALID_WOW_INSTALLATIONS": "在 {protocol} 协议下没有《魔兽世界》客户端" + }, + "INSTALL_BUTTON": "安装", + "TITLE": "从 {providerName} 处安装插件" + }, + "INSTALL_FROM_URL": { + "ADDON_URL_INPUT_LABEL": "插件 URL", + "ADDON_URL_INPUT_PLACEHOLDER": "例如,GitHub 或 WowInterface URL", + "CLOSE_BUTTON": "关闭", + "DESCRIPTION": "如需直接从某 URL 安装插件,请将其粘贴到下面以开始安装。", + "DOWNLOAD_COUNT": "在 {provider} 上有 {textCount} 次下载", + "ERROR": { + "ASSET_NOT_FOUND": "未找到可下载的附件 {message}。\n\n要通过 WowUp 下载插件,发布页面(Releases)上需要有效的 zip 文件。", + "BURNING_CRUSADE_ASSET_NOT_FOUND": "未找到可下载的附件 {message}。\n\n要通过 WowUp 下载插件,发布页面(Releases)上需要文件名以 “-bc” 结尾的有效的 zip 文件。", + "CATACLYSM_ASSET_NOT_FOUND": "No asset was found to download from {message}.\n\nA valid zip file ending with '-cata' is required in order for WowUp to download it.", + "CLASSIC_ASSET_NOT_FOUND": "未找到可下载的附件 {message}。\n\n要通过 WowUp 下载插件,发布页面(Releases)上需要文件名以 “-classic” 结尾的有效的 zip 文件。", + "FAILED_TO_CONNECT": "无法连接 API,请稍后重试。", + "INSTALL_FAILED": "安装插件时出现了一些错误,请重试。\n\n如果一直出现此消息,可以加入 Discord #help-me 频道寻求帮助(英语)。", + "INVALID_URL": "输入的 URL 无效。有效 URL 示例:\n\t- https://github.com/WowUp/WowUp.Addon\n\t- https://www.wowinterface.com/downloads/info25610-8.3-014.html\n\t- https://www.curseforge.com/wow/addons/altoholic", + "NO_ADDON_FOUND": "未找到插件,请检查 URL 是否指向正确的页面。\n\n从 GitHub 安装时,请检查是否存在带有 zip 包的 release tag。", + "NO_RELEASE_FOUND": "未找到发布版本 {message}.\n\n要通过 WowUp 下载插件,需要带有 zip 附件的发布版本。", + "NO_SEARCH_RESULTS": "无搜索结果。", + "TITLE": "插件安装失败", + "WRATH_ASSET_NOT_FOUND": "未找到可下载的附件 {message}。\n\n要通过 WowUp 下载插件,发布页面(Releases)上需要文件名以 “-wrath” 结尾的有效 zip 文件。" + }, + "IMPORT_ASSET_WARNING": "已找到此插件的最新版本压缩包 “{zipName}”,但无法验证它是否兼容此《魔兽世界》客户端。\n\n如要安装,请自担风险。", + "IMPORT_BUTTON": "导入", + "IMPORT_WARNING_TITLE": "导入插件警告", + "INSTALL_BUTTON": "安装", + "INSTALL_SUCCESS_LABEL": "安装成功!", + "SUPPORTED_SOURCES": "支持 WowInterface 和 GitHub", + "TITLE": "从 URL 安装" + }, + "NEW_VERSION_POPUP": { + "TITLE": "更新记录 {versionNumber}" + }, + "PERMISSIONS": { + "CURSEFORGE": { + "DESCRIPTION_AD_LINK": "ad vendors", + "DESCRIPTION_BOTTOM": ". Click on the Manage button to control you consents, or to object to the processing of your data. You can change your preferences any time via the settings screen.", + "DESCRIPTION_TOP": " In order to use this application's integration with CurseForge they require that you allow them to show you an ad from one of their ", + "MANAGE_BUTTON": "Manage", + "TITLE": "CurseForge" + }, + "MESSAGE": "在开始使用 WowUp 之前,请设置此应用程序的权限。", + "POSITIVE_BUTTON": "确认", + "TELEMETRY": { + "DESCRIPTION": "是否发送匿名的插件统计数据和错误报告以帮助改进 WowUp?", + "TOGGLE_LABEL": "允许遥测" + }, + "TITLE": "WowUp 权限设置", + "WAGO": { + "DESCRIPTION": "启用 Wago.io 插件安装源。启用此选项将会显示一个广告,以支持你喜欢的插件的作者。", + "TOGGLE_LABEL": "启用 Wago.io 安装源" + } + }, + "SELECT_INSTALLATION": { + "INVALID_INSTALLATION_PATH": "此路径可能不是有效的《魔兽世界》应用程序:\n{selectedPath}" + }, + "TELEMETRY": { + "DESCRIPTION": "是否发送匿名应用安装数据和错误信息以帮助改进 WowUp?", + "NEGATIVE_BUTTON": "否", + "POSITIVE_BUTTON": "是", + "TITLE": "WowUp 遥测" + }, + "TRUST_DOMAIN_CHECKBOX": "信任此域名,今后不再询问" + }, + "PAGES": { + "ABOUT": { + "ATTRIBUTIONS_TITLE": "图源", + "CHANGE_LOG_SECTION_LABEL": "更新记录", + "TITLE": "WowUp.io", + "WEBSITE_LINK_LABEL": "访问网站!" + }, + "ACCOUNT": { + "BETA": "测试版", + "LOGIN_BUTTON": "立即登录!", + "LOGOUT_BUTTON": "注销", + "LOGOUT_CONFIRMATION_MESSAGE": "确定要注销吗?本地账户数据将会被删除,重新登录时将会同步回来。", + "LOGOUT_CONFIRMATION_TITLE": "是否注销?", + "MANAGE_ACCOUNT_BUTTON": "管理账户", + "TITLE": "连接战网全球账户" + }, + "GET_ADDONS": { + "ADDON_CATEGORIES_BUTTON": "类别", + "ADDON_CATEGORIES_MENU_TITLE": "插件类别", + "ADDON_CATEGORIES_SELECTED_TITLE": "类别: {category}", + "ADDON_CATEGORIES_TOOLTIP": "浏览插件类别", + "CLIENT_TYPE_SELECT_LABEL": "魔兽世界", + "INSTALL_FROM_URL_BUTTON": "从 URL 安装", + "INSTALL_FROM_URL_TOOLTIP": "从 URL 安装插件", + "REFRESH_BUTTON": "刷新", + "REFRESH_TOOLTIP": "刷新插件搜索结果", + "RESET_CATEGORY_TOOLTIP": "重置类别", + "SEARCH_LABEL": "搜索", + "TABLE": { + "ADDON_COLUMN_HEADER": "插件", + "AUTHOR_COLUMN_HEADER": "作者", + "DOWNLOAD_COUNT_COLUMN_HEADER": "下载次数", + "PROVIDER_COLUMN_HEADER": "安装源", + "RELEASED_AT_COLUMN_HEADER": "发布时间", + "STATUS_COLUMN_HEADER": "状态" + } + }, + "HOME": { + "ABOUT_TAB_TITLE": "关于", + "ACCOUNT_TAB_TITLE": "连接战网全球账户", + "COLLAPSE_BUTTON_TITLE": "折叠", + "DISCORD_TAB_TITLE": "Discord(英语)", + "EXPAND_BUTTON_TITLE": "展开", + "GET_ADDONS_TAB_TITLE": "获取插件", + "GUIDE_TAB_TITLE": "指南(英语)", + "MIGRATING_ADDONS": "正在迁移插件...", + "MY_ADDONS_TAB_TITLE": "我的插件", + "NEWS_TAB_TITLE": "新闻(英语)", + "OPTIONS_TAB_TITLE": "选项" + }, + "MY_ADDONS": { + "ADDON_CONTEXT_MENU": { + "ADDONS_SELECTED": "已选中 {count} 项", + "ALPHA_ADDON_CHANNEL": "Alpha", + "AUTO_UPDATE_ADDON_BUTTON": "自动更新", + "AUTO_UPDATE_ADDON_NOTIFICATIONS_ENABLED_BUTTON": "通知已开启", + "BETA_ADDON_CHANNEL": "Beta", + "CHANNEL_SUBMENU_TITLE": "更新通道", + "IGNORE_ADDON_BUTTON": "忽略", + "PROVIDER_SUBMENU_TITLE": "安装源", + "REINSTALL_ADDON_BUTTON": "重新安装", + "REMOVE_ADDON_BUTTON": "删除", + "SHOW_FOLDER": "打开文件夹", + "STABLE_ADDON_CHANNEL": "稳定版" + }, + "ADDON_IS_CODE_REPOSITORY": "插件似乎是代码仓库", + "ADDON_REMOVED_SNACKBAR": "{addonName} 已成功移除", + "CHANGE_ADDON_PROVIDER_CONFIRMATION": { + "MESSAGE": "是否将 {addonName} 的安装源修改为 {providerName}?此操作将会用 {providerName} 的版本替换现有版本。", + "TITLE": "修改插件安装源?" + }, + "CHECK_UPDATES_BUTTON": "检查更新", + "CHECK_UPDATES_BUTTON_TOOLTIP": "检查最新的插件更新", + "CLIENT_TYPE_SELECT_BADGE": "{count} 个更新", + "CLIENT_TYPE_SELECT_LABEL": "魔兽世界", + "COLUMNS_CONTEXT_MENU": { + "TITLE": "显示列表项" + }, + "ERROR_SNACKBAR": "出现错误", + "FILTER_LABEL": "筛选", + "FUNDING_TOOLTIP": { + "CUSTOM": "支持作者", + "GENERIC": "在 {platform} 上支持作者", + "GITHUB": "在 GitHub 上支持作者", + "PATREON": "通过 Patreon 支持作者", + "PAYPAL": "通过 PayPal 支持作者" + }, + "IMPORT_EXPORT_ADDONS_BUTTON": "导入/导出插件", + "MULTIPLE_PROVIDERS_TOOLTIP": "此插件有多个安装源", + "PAGE_CONTEXT_FOOTER": { + "ADDONS_INSTALLED": "共 {count} 个插件", + "JOIN_DISCORD": "在 Discord 上与我们交流(英语)", + "PATREON_SUPPORT": "通过 Patreon 向 WowUp 捐助(英语)", + "SEARCH_RESULTS": "共 {count} 条结果", + "VIEW_GITHUB": "在 GitHub 上查看源代码", + "VIEW_GUIDE": "访问指南,探索 WowUp 的功能" + }, + "REQUIRED_DEPENDENCY_MISSING_TOOLTIP": "缺失必要的依赖项", + "RESCAN_FOLDERS_BUTTON": "重新扫描", + "RESCAN_FOLDERS_BUTTON_TOOLTIP": "扫描客户端文件夹中已安装的附加组件", + "RESCAN_FOLDERS_CONFIRMATION_DESCRIPTION": "重新扫描过程将会猜测已经安装了哪些插件,可能会重置已知插件信息。如果特定插件无法识别或显示不正确,请使用此功能。扫描不会删除插件本身,只会删除 WowUp 记录的插件信息。\n\n扫描需要花费一些时间。", + "RESCAN_FOLDERS_CONFIRMATION_TITLE": "是否开始扫描?", + "SPINNER": { + "GATHERING_ADDONS": "正在收集插件信息...", + "UPDATING": "正在更新第 {updateCount} 个,共 {addonCount} 个", + "UPDATING_WITH_ADDON_NAME": "正在更新第 {updateCount} 个,共 {addonCount} 个\n{clientType}:{addonName}" + }, + "TABLE": { + "ADDON_COLUMN_HEADER": "插件", + "ADDON_INSTALL_BUTTON": "安装", + "ADDON_UPDATE_BUTTON": "更新", + "AUTHOR_COLUMN_HEADER": "作者", + "AUTO_UPDATE_ICON_TOOLTIP": "自动更新已启用", + "GAME_VERSION_COLUMN_HEADER": "游戏版本", + "LATEST_VERSION_COLUMN_HEADER": "最新版本", + "PROVIDER_COLUMN_HEADER": "安装源", + "PROVIDER_RELEASE_CHANNEL": "更新通道", + "RELEASED_AT_COLUMN_HEADER": "发布时间", + "STATUS_COLUMN_HEADER": "状态", + "UPDATED_AT_COLUMN_HEADER": "更新时间" + }, + "UNINSTALL_POPUP": { + "CONFIRMATION_ACTION_EXPLANATION": "此操作将会从《魔兽世界》所在路径下删除所有与插件相关的文件夹。", + "CONFIRMATION_LESS_THAN_THREE": "是否确定要删除以下 {count} 个插件?", + "CONFIRMATION_MORE_THAN_THREE": "是否确定要删除选中的 {count} 个插件?", + "CONFIRMATION_ONE": "是否确定要删除 {addonName}?", + "DEPENDENCY_MESSAGE": "{addonName} 有 {dependencyCount} 个依赖项,是否一并删除?", + "DEPENDENCY_TITLE": "是否删除插件依赖项?", + "TITLE": "是否卸载插件?" + }, + "UNKNOWN_ADDON_INFO_TOOLTIP": "已安装的插件与所有安装源均无法匹配。", + "UPDATE_ALL_BUTTON": "全部更新", + "UPDATE_ALL_BUTTON_TOOLTIP": "更新此客户端的所有插件", + "UPDATE_ALL_CONTEXT_MENU": { + "UPDATE_ALL_CLIENTS_BUTTON": "更新所有客户端", + "UPDATE_RETAIL_CLASSIC_BUTTON": "更新正式服/经典怀旧服" + }, + "WTF_BACKUP_BUTTON": "备份界面设置" + }, + "NEWS": { + "NEWS_LINK_COPY_TOAST": "新闻链接已复制到剪贴板", + "NEWS_LINK_COPY_TOOLTIP": "复制新闻链接", + "PAGE_CONTEXT_FOOTER": "共 {count} 条新闻", + "REFRESH_TOOLTIP": "刷新信息流" + }, + "OPTIONS": { + "ADDON": { + "AD_REQUIRED_HINT": "启用此项需要提供访问密钥或显示广告", + "CURSE_FORGE_V2": { + "API_KEY_DESCRIPTION": "如果申请过 CurseForge API 密钥,请填入此处以连接到 CurseForge API。", + "API_KEY_TITLE": "CurseForge API 密钥", + "PROVIDER_NOTE": "需要 API 密钥" + }, + "ENABLED_PROVIDERS": { + "DESCRIPTION": "选择从哪些安装源搜索和安装新插件", + "FIELD_LABEL": "启用插件安装源", + "INPUT_LABEL": "安装源" + }, + "GITHUB_PERSONAL_ACCESS_TOKEN": { + "MESSAGE": "免费的个人访问令牌(PAT)可以提高 GitHub API 调用次数的限制。查看中文文档(部分翻译)英文文档。", + "PLACEHOLDER": "个人访问令牌(PAT)", + "TITLE": "GitHub 个人访问令牌(PAT)" + }, + "TITLE": "插件", + "WAGO_ACCESS_KEY": { + "MESSAGE": "启用 Wago 安装源而无需显示广告。了解详细信息(英语)", + "PLACEHOLDER": "访问密钥", + "TITLE": "Wago 访问密钥" + } + }, + "APPLICATION": { + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_BETA": "切换到 Beta 通道后,将会收到包含最新的错误修正和功能的版本。要切换回稳定版本,只能卸载 WowUp 并重新安装。\n\n尽管 Beta 通道通常能正常工作,但仍需自担风险。", + "APP_RELEASE_CHANNEL_CONFIRMATION_DESCRIPTION_STABLE": "切换到稳定版通道后,将会停止接受 Beta 版本。切换到稳定版通道不会回退到上一个稳定版,而是在下一个稳定版发布后更新到稳定版。", + "APP_RELEASE_CHANNEL_CONFIRMATION_LABEL": "设置应用程序更新通道", + "APP_RELEASE_CHANNEL_CONFIRMATION_POSITIVE_BUTTON": "我明白了", + "APP_RELEASE_CHANNEL_DESCRIPTION": "切换此应用程序的稳定版和 Beta 通道", + "APP_RELEASE_CHANNEL_DROPDOWN_LABEL": "更新通道", + "APP_RELEASE_CHANNEL_LABEL": "应用程序更新通道", + "CURRENT_LANGUAGE_LABEL": "当前语言", + "CURSE_PROTOCOL_DESCRIPTION": "从 CurseForge 网站下载插件时,WowUp 将会接管安装过程", + "CURSE_PROTOCOL_LABEL": "接管 CurseForge 下载链接", + "ENABLE_APP_BADGE_DESCRIPTION": "在应用程序图标上用角标显示可更新的插件数量。", + "ENABLE_APP_BADGE_LABEL": "启用数字角标通知", + "ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "启用各种系统通知弹窗,如自动更新插件通知。", + "ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "启用系统通知", + "KEEP_LAST_OPENED_TAB_DESCRIPTION": "打开插件详情页时,自动切换到上一次打开的选项卡", + "KEEP_LAST_OPENED_TAB_LABEL": "记住上一次打开的选项卡", + "MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "关闭 WowUp 窗口时,最小化到菜单栏。", + "MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "关闭 WowUp 窗口时,最小化到系统托盘。", + "MINIMIZE_ON_CLOSE_LABEL": "关闭时最小化", + "PROTOCOL_DESCRIPTION": "WowUp 将会在系统中注册自定义 URI 协议以处理传入的请求", + "PROTOCOL_LABEL": "允许 WowUp 处理 wowup:// URI", + "SCALE_DESCRIPTION": "修改整个应用程序的缩放系数。", + "SCALE_LABEL": "界面缩放", + "SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "更改默认语言需要重新启动客户端。", + "SET_LANGUAGE_CONFIRMATION_LABEL": "设置新的默认语言", + "SET_LANGUAGE_DESCRIPTION": "选择要使用的语言", + "SET_LANGUAGE_LABEL": "设置语言", + "START_MINIMIZED_DESCRIPTION": "……并且不显示窗口。", + "START_MINIMIZED_LABEL": "以最小化窗口启动 WowUp", + "START_WITH_SYSTEM_DESCRIPTION": "WowUp 将在登录后自动启动……", + "START_WITH_SYSTEM_LABEL": "登录后自动启动 WowUp", + "TELEMETRY_DESCRIPTION": "发送匿名应用安装数据和错误信息以帮助改进 WowUp。", + "TELEMETRY_LABEL": "遥测", + "THEME_DESCRIPTION": "选择喜欢的配色主题", + "THEME_LABEL": "配色主题", + "TITLE": "应用程序", + "USE_CURSE_PROTOCOL_CONFIRMATION_DESCRIPTION": "WowUp 可以被注册为 CurseForge 下载链接的处理程序。开启此选项可能会导致使用 CurseForge 客户端时出现问题,确定要继续吗?", + "USE_CURSE_PROTOCOL_CONFIRMATION_LABEL": "是否接管 CurseForge 下载?", + "USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "是否重新启动?", + "USE_HARDWARE_ACCELERATION_DESCRIPTION": "禁用硬件加速可能会提高 FPS 并修复其他渲染问题。更改此项需要重新启动。", + "USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "禁用硬件加速需要重新启动 WowUp。", + "USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "启用硬件加速需要重新启动 WowUp。", + "USE_HARDWARE_ACCELERATION_LABEL": "启用硬件加速", + "USE_SYMLINK_SUPPORT": "启用符号链接", + "USE_SYMLINK_SUPPORT_CONFIRMATION_DESCRIPTION": "启用符号链接使得 WowUp 在重新扫描时识别符号链接。警告:如果您不知道什么是符号链接,请禁用此项。在当前版本中,符号链接将会在更新插件时被删除,并被替换为真正的文件夹。", + "USE_SYMLINK_SUPPORT_CONFIRMATION_LABEL": "启用符号链接?", + "USE_SYMLINK_SUPPORT_DESCRIPTION": "允许 WowUp 扫描插件所在路径下的符号链接。警告:符号链接在安装或更新插件时会被替换。" + }, + "CURSEFORGE": { + "ADS_OPTION_DESCRIPTION": "View and manage how CurseForge advertisers may use your data for ad personalization", + "ADS_OPTION_LABEL": "Ads Personalization & Data", + "ADS_OPTION_MANAGE": "Manage", + "TITLE": "CurseForge" + }, + "DEBUG": { + "CONFIG_FILES_BUTTON": "显示配置文件", + "CONFIG_FILES_DESCRIPTION": "打开存储 addons.json、preferences.json 等配置文件的文件夹。", + "CONFIG_FILES_LABEL": "配置文件", + "DEBUG_DATA_BUTTON": "转储调试数据", + "DEBUG_DATA_DESCRIPTION": "记录调试数据以帮助诊断潜在的问题。调试数据可以在最新的日志文件中找到。", + "DEBUG_DATA_LABEL": "调试数据", + "LOG_FILES_BUTTON": "显示日志文件", + "LOG_FILES_DESCRIPTION": "打开包含您最后几个日志文件的文件夹。", + "LOG_FILES_LABEL": "日志文件", + "TITLE": "调试" + }, + "TABS": { + "ABOUT": "关于", + "ADDONS": "插件", + "APPLICATION": "应用程序", + "CLIENTS": "客户端", + "CURSEFORGE": "CurseForge", + "DEBUG": "调试", + "WTF_EXPLORER": "WTF Explorer" + }, + "WOW": { + "ADD_CLIENT_BUTTON": "新增", + "AUTO_UPDATE_DESCRIPTION": "新安装的插件将默认设置为自动更新", + "AUTO_UPDATE_LABEL": "自动更新", + "CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "取消", + "CLEAR_INSTALL_LOCATION_DIALOG": { + "MESSAGE": "是否忘记 {clientName} 的安装路径?此操作将会删除 WowUp 记录的此客户端所安装插件信息。\n\n插件文件夹不会被删除。", + "TITLE": "忘记此路径?" + }, + "CLIENT_TYPE_INPUT_HINT": "{clientTypeName} 客户端 “{clientFolderName}” 主程序路径", + "CLIENT_TYPE_PATH_LABEL": "{clientTypeName} 路径", + "DEFAULT_ADDON_CHANNEL_LABEL": "默认插件更新通道", + "DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "插件更新通道", + "EDIT_WOW_DIRECTORY_SELECT_BUTTON": "编辑", + "MOVE_DOWN_BUTTON": "下移", + "MOVE_UP_BUTTON": "上移", + "NO_CLIENTS_FOUND_TEXT": "未找到《魔兽世界》客户端,请检查《暴雪战网》是否为最新版本,或者手动添加客户端。", + "OPEN_FOLDER_BUTTON": "在文件管理器中显示", + "OPEN_WOW_DIRECTORY_SELECT_BUTTON": "浏览", + "REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "移除", + "RESCAN_CLIENTS_BUTTON": "重新扫描", + "RESCAN_CLIENTS_LABEL": "重新扫描已安装的《魔兽世界》客户端", + "SAVE_WOW_DIRECTORY_SELECT_BUTTON": "保存", + "TITLE": "魔兽世界" + }, + "WTF_EXPLORER": { + "FOLDER_PATH_LABEL": "Folder: ", + "PAGE_EXPLANATION": "The WTF Explorer allows you to inspect all the data your addons have saved.\nFiles in grey are typically things you can ignore such as backup files.\nFiles in red should belong to addons that you no longer have installed.", + "TITLE": "WTF Explorer" + } + } + }, + "WTF_BACKUP": { + "APPLY_CONFIRMATION": { + "MESSAGE": "确认要把这个备份还原到界面设置中吗?\n\n还原之前务必关闭《魔兽世界》客户端。\n\n此操作无法撤销。", + "TITLE": "是否还原 WTF 备份?" + }, + "BACKUP_APPLY_SUCCESS": "还原备份 {name} 成功", + "BACKUP_COUNT_TEXT": "找到 {count} 个备份", + "BUSY_TEXT": { + "APPLYING_BACKUP": "正在还原备份...", + "CREATING_BACKUP": "正在创建 {count} 个文件的备份...", + "LOADING_BACKUPS": "正在读取备份...", + "REMOVING_BACKUP": "正在删除备份..." + }, + "CREATE_BACKUP_BUTTON": "创建备份", + "DELETE_CONFIRMATION": { + "MESSAGE": "确认要删除备份 {name} 吗?\n此操作无法撤销。", + "TITLE": "是否删除 WTF 备份?" + }, + "DIALOG_TITLE": "WTF 设置备份:{clientType}", + "ERROR": { + "BACKUP_APPLY_FAILED": "还原备份 {name} 失败", + "FAILED_TO_DELETE": "删除备份 {name} 失败", + "GENERIC_ERROR": "处理备份文件时出现了一点问题", + "INVALID_CONTENTS": "处理备份文件时出现了一点问题", + "INVALID_CREATED_AT": "处理备份文件时出现了一点问题", + "INVALID_CREATED_BY": "处理备份文件时出现了一点问题" + }, + "SHOW_FOLDER_BUTTON": "在文件管理器中显示", + "TOOL_TIP": { + "APPLY_BUTTON": "还原此备份", + "DELETE_BUTTON": "删除此备份" + } + } +} diff --git a/WowUp/wowup-electron/src/assets/icon.png b/WowUp/wowup-electron/src/assets/icon.png new file mode 100644 index 0000000..05521e1 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icon.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/android-chrome-192x192.png b/WowUp/wowup-electron/src/assets/icons/android-chrome-192x192.png new file mode 100644 index 0000000..b75bf3e Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/android-chrome-192x192.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/android-chrome-512x512.png b/WowUp/wowup-electron/src/assets/icons/android-chrome-512x512.png new file mode 100644 index 0000000..ae30e67 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/android-chrome-512x512.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/apple-touch-icon.png b/WowUp/wowup-electron/src/assets/icons/apple-touch-icon.png new file mode 100644 index 0000000..b415751 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/apple-touch-icon.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/favicon-16x16.png b/WowUp/wowup-electron/src/assets/icons/favicon-16x16.png new file mode 100644 index 0000000..75cb440 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/favicon-16x16.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/favicon-32x32.png b/WowUp/wowup-electron/src/assets/icons/favicon-32x32.png new file mode 100644 index 0000000..1377ffc Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/favicon-32x32.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/favicon.256x256.png b/WowUp/wowup-electron/src/assets/icons/favicon.256x256.png new file mode 100644 index 0000000..b454f5f Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/favicon.256x256.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/favicon.512x512.png b/WowUp/wowup-electron/src/assets/icons/favicon.512x512.png new file mode 100644 index 0000000..ae30e67 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/favicon.512x512.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/favicon.icns b/WowUp/wowup-electron/src/assets/icons/favicon.icns new file mode 100644 index 0000000..0b6fc3d Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/favicon.icns differ diff --git a/WowUp/wowup-electron/src/assets/icons/favicon.ico b/WowUp/wowup-electron/src/assets/icons/favicon.ico new file mode 100644 index 0000000..a427cbc Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/favicon.ico differ diff --git a/WowUp/wowup-electron/src/assets/icons/favicon.png b/WowUp/wowup-electron/src/assets/icons/favicon.png new file mode 100644 index 0000000..b454f5f Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/favicon.png differ diff --git a/WowUp/wowup-electron/src/assets/icons/site.webmanifest b/WowUp/wowup-electron/src/assets/icons/site.webmanifest new file mode 100644 index 0000000..fa99de7 --- /dev/null +++ b/WowUp/wowup-electron/src/assets/icons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/WowUp/wowup-electron/src/assets/icons/wowup-nopad.png b/WowUp/wowup-electron/src/assets/icons/wowup-nopad.png new file mode 100644 index 0000000..33dcdfb Binary files /dev/null and b/WowUp/wowup-electron/src/assets/icons/wowup-nopad.png differ diff --git a/WowUp/wowup-electron/src/assets/images/alliance-1.png b/WowUp/wowup-electron/src/assets/images/alliance-1.png new file mode 100644 index 0000000..cd8ed80 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/alliance-1.png differ diff --git a/WowUp/wowup-electron/src/assets/images/alliance-dark-1.png b/WowUp/wowup-electron/src/assets/images/alliance-dark-1.png new file mode 100644 index 0000000..eb155cc Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/alliance-dark-1.png differ diff --git a/WowUp/wowup-electron/src/assets/images/checkbox-marked-circle-green.svg b/WowUp/wowup-electron/src/assets/images/checkbox-marked-circle-green.svg new file mode 100644 index 0000000..de9636e --- /dev/null +++ b/WowUp/wowup-electron/src/assets/images/checkbox-marked-circle-green.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/WowUp/wowup-electron/src/assets/images/custom_funding_logo_small.png b/WowUp/wowup-electron/src/assets/images/custom_funding_logo_small.png new file mode 100644 index 0000000..30dd81a Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/custom_funding_logo_small.png differ diff --git a/WowUp/wowup-electron/src/assets/images/discord_logo_small.png b/WowUp/wowup-electron/src/assets/images/discord_logo_small.png new file mode 100644 index 0000000..d5346b7 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/discord_logo_small.png differ diff --git a/WowUp/wowup-electron/src/assets/images/github_logo.png b/WowUp/wowup-electron/src/assets/images/github_logo.png new file mode 100644 index 0000000..192846a Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/github_logo.png differ diff --git a/WowUp/wowup-electron/src/assets/images/github_logo_small.png b/WowUp/wowup-electron/src/assets/images/github_logo_small.png new file mode 100644 index 0000000..192846a Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/github_logo_small.png differ diff --git a/WowUp/wowup-electron/src/assets/images/horde-1.png b/WowUp/wowup-electron/src/assets/images/horde-1.png new file mode 100644 index 0000000..afe7212 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/horde-1.png differ diff --git a/WowUp/wowup-electron/src/assets/images/horde-dark-1.png b/WowUp/wowup-electron/src/assets/images/horde-dark-1.png new file mode 100644 index 0000000..a391718 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/horde-dark-1.png differ diff --git a/WowUp/wowup-electron/src/assets/images/librepay_logo_small.png b/WowUp/wowup-electron/src/assets/images/librepay_logo_small.png new file mode 100644 index 0000000..7aef043 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/librepay_logo_small.png differ diff --git a/WowUp/wowup-electron/src/assets/images/patch/account-beta.png b/WowUp/wowup-electron/src/assets/images/patch/account-beta.png new file mode 100644 index 0000000..47f8d08 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/patch/account-beta.png differ diff --git a/WowUp/wowup-electron/src/assets/images/patch/backup-preview.png b/WowUp/wowup-electron/src/assets/images/patch/backup-preview.png new file mode 100644 index 0000000..685810f Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/patch/backup-preview.png differ diff --git a/WowUp/wowup-electron/src/assets/images/patch/import-export-preview.png b/WowUp/wowup-electron/src/assets/images/patch/import-export-preview.png new file mode 100644 index 0000000..ae7b933 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/patch/import-export-preview.png differ diff --git a/WowUp/wowup-electron/src/assets/images/patreon_logo_small.png b/WowUp/wowup-electron/src/assets/images/patreon_logo_small.png new file mode 100644 index 0000000..83c3f8c Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/patreon_logo_small.png differ diff --git a/WowUp/wowup-electron/src/assets/images/patreon_logo_white.png b/WowUp/wowup-electron/src/assets/images/patreon_logo_white.png new file mode 100644 index 0000000..9a521e3 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/patreon_logo_white.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wow-cata-background.jpg b/WowUp/wowup-electron/src/assets/images/wow-cata-background.jpg new file mode 100644 index 0000000..014c0e6 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wow-cata-background.jpg differ diff --git a/WowUp/wowup-electron/src/assets/images/wow-classic-cataclysm-logo.png b/WowUp/wowup-electron/src/assets/images/wow-classic-cataclysm-logo.png new file mode 100644 index 0000000..84d3885 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wow-classic-cataclysm-logo.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wow-classic-logo.png b/WowUp/wowup-electron/src/assets/images/wow-classic-logo.png new file mode 100644 index 0000000..76d0343 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wow-classic-logo.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wow-classic-tbc-logo.png b/WowUp/wowup-electron/src/assets/images/wow-classic-tbc-logo.png new file mode 100644 index 0000000..8a18c95 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wow-classic-tbc-logo.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wow-classic-wotlk-logo.png b/WowUp/wowup-electron/src/assets/images/wow-classic-wotlk-logo.png new file mode 100644 index 0000000..7752ec2 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wow-classic-wotlk-logo.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wow-dragonflight-logo.png b/WowUp/wowup-electron/src/assets/images/wow-dragonflight-logo.png new file mode 100644 index 0000000..f6309eb Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wow-dragonflight-logo.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wow-retail-logo.png b/WowUp/wowup-electron/src/assets/images/wow-retail-logo.png new file mode 100644 index 0000000..226ae95 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wow-retail-logo.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wowup-dark-1.png b/WowUp/wowup-electron/src/assets/images/wowup-dark-1.png new file mode 100644 index 0000000..49e5d09 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wowup-dark-1.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wowup-placeholder.png b/WowUp/wowup-electron/src/assets/images/wowup-placeholder.png new file mode 100644 index 0000000..39eafdf Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wowup-placeholder.png differ diff --git a/WowUp/wowup-electron/src/assets/images/wowup-white-1.png b/WowUp/wowup-electron/src/assets/images/wowup-white-1.png new file mode 100644 index 0000000..4884fd0 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/images/wowup-white-1.png differ diff --git a/WowUp/wowup-electron/src/assets/patron.png b/WowUp/wowup-electron/src/assets/patron.png new file mode 100644 index 0000000..5443ec9 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/patron.png differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2 new file mode 100644 index 0000000..1db8422 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 new file mode 100644 index 0000000..6362d7f Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2 new file mode 100644 index 0000000..86d474c Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2 new file mode 100644 index 0000000..ee7331f Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2 new file mode 100644 index 0000000..b4b5df7 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 new file mode 100644 index 0000000..530e22c Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2 new file mode 100644 index 0000000..5a458af Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2 new file mode 100644 index 0000000..507809d Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 new file mode 100644 index 0000000..ef8c883 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2 new file mode 100644 index 0000000..80ba571 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2 new file mode 100644 index 0000000..2c3e74e Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2 new file mode 100644 index 0000000..8323edd Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2 new file mode 100644 index 0000000..7d3dc27 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2 new file mode 100644 index 0000000..327d141 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu4WxKOzY.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu4WxKOzY.woff2 new file mode 100644 index 0000000..76fc366 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu4WxKOzY.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu4mxK.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu4mxK.woff2 new file mode 100644 index 0000000..1a53701 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu4mxK.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu5mxKOzY.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu5mxKOzY.woff2 new file mode 100644 index 0000000..de226b3 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu5mxKOzY.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu72xKOzY.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu72xKOzY.woff2 new file mode 100644 index 0000000..e3eabe7 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu72xKOzY.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 new file mode 100644 index 0000000..de83f9c Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7WxKOzY.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7WxKOzY.woff2 new file mode 100644 index 0000000..165cbac Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7WxKOzY.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7mxKOzY.woff2 b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7mxKOzY.woff2 new file mode 100644 index 0000000..c49a204 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/typeface-roboto/KFOmCnqEu92Fr1Mu7mxKOzY.woff2 differ diff --git a/WowUp/wowup-electron/src/assets/wowup_logo_512np.png b/WowUp/wowup-electron/src/assets/wowup_logo_512np.png new file mode 100644 index 0000000..05521e1 Binary files /dev/null and b/WowUp/wowup-electron/src/assets/wowup_logo_512np.png differ diff --git a/WowUp/wowup-electron/src/assets/wowup_logo_purple.png b/WowUp/wowup-electron/src/assets/wowup_logo_purple.png new file mode 100644 index 0000000..473a9df Binary files /dev/null and b/WowUp/wowup-electron/src/assets/wowup_logo_purple.png differ diff --git a/WowUp/wowup-electron/src/common/constants.ts b/WowUp/wowup-electron/src/common/constants.ts new file mode 100644 index 0000000..1fea14a --- /dev/null +++ b/WowUp/wowup-electron/src/common/constants.ts @@ -0,0 +1,225 @@ +export const APP_USER_MODEL_ID = "io.wowup.jliddev"; // Bundle ID + +// FEATURES +export const FEATURE_ACCOUNTS_ENABLED = false; + +export const ADDON_PROVIDER_WOWINTERFACE = "WowInterface"; +export const ADDON_PROVIDER_CURSEFORGE = "Curse"; +export const ADDON_PROVIDER_CURSEFORGEV2 = "CurseV2"; +export const ADDON_PROVIDER_GITHUB = "GitHub"; +export const ADDON_PROVIDER_RAIDERIO = "RaiderIO"; +export const ADDON_PROVIDER_TUKUI = "TukUI"; +export const ADDON_PROVIDER_UNKNOWN = "Unknown"; +export const ADDON_PROVIDER_HUB_LEGACY = "Hub"; +export const ADDON_PROVIDER_HUB = "WowUpHub"; +export const ADDON_PROVIDER_WAGO = "Wago"; +export const ADDON_PROVIDER_WOWUP_COMPANION = "WowUpCompanion"; +export const ADDON_PROVIDER_ZIP = "Zip"; + +export const APP_PROTOCOL_NAME = "wowup"; +export const CURSE_PROTOCOL_NAME = "curseforge"; + +// WOWUP ADDON +export const WOWUP_ADDON_FOLDER_NAME = "WowUp"; +export const WOWUP_DATA_ADDON_FOLDER_NAME = "wowup_data_addon"; +export const WOWUP_ASSET_FOLDER_NAME = "WowUpAddon"; + +// IPC CHANNELS +export const IPC_CURSE_GET_SCAN_RESULTS = "curse-get-scan-results"; +export const IPC_DOWNLOAD_FILE_CHANNEL = "download-file"; +export const IPC_COPY_DIRECTORY_CHANNEL = "copy-directory"; +export const IPC_CREATE_DIRECTORY_CHANNEL = "create-directory"; +export const IPC_DELETE_DIRECTORY_CHANNEL = "delete-directory"; +export const IPC_STAT_DIRECTORY_CHANNEL = "stat-directory"; +export const IPC_LIST_DIRECTORIES_CHANNEL = "list-directories"; +export const IPC_STAT_FILES_CHANNEL = "stat-files"; +export const IPC_PATH_EXISTS_CHANNEL = "path-exists"; +export const IPC_LIST_FILES_CHANNEL = "list-files"; +export const IPC_READ_FILE_CHANNEL = "read-file"; +export const IPC_READ_FILE_BUFFER_CHANNEL = "read-file-buffer"; +export const IPC_WRITE_FILE_CHANNEL = "write-file"; +export const IPC_UNZIP_FILE_CHANNEL = "unzip-file"; +export const IPC_COPY_FILE_CHANNEL = "copy-file"; +export const IPC_SHOW_DIRECTORY = "show-directory"; +export const IPC_WOWUP_GET_SCAN_RESULTS = "wowup-get-scan-results"; +export const IPC_GET_HOME_DIR = "get-home-dir"; +export const IPC_GET_ASSET_FILE_PATH = "get-asset-file-path"; +export const IPC_CREATE_TRAY_MENU_CHANNEL = "create-tray-menu"; +export const IPC_LIST_DISKS_WIN32 = "list-disks-win32"; +export const IPC_CREATE_APP_MENU_CHANNEL = "create-app-menu"; +export const IPC_OW_IS_CMP_REQUIRED = "ow-is-cmp-required"; +export const IPC_OW_OPEN_CMP = "ow-open-cmp"; +export const IPC_MENU_ZOOM_OUT_CHANNEL = "menu-zoom-out"; +export const IPC_MENU_ZOOM_IN_CHANNEL = "menu-zoom-in"; +export const IPC_MENU_ZOOM_RESET_CHANNEL = "menu-zoom-reset"; +export const IPC_MAXIMIZE_WINDOW = "maximize-window"; +export const IPC_WINDOW_IS_MAXIMIZED = "window-is-maximized"; +export const IPC_MINIMIZE_WINDOW = "minimize-window"; +export const IPC_FOCUS_WINDOW = "focus-window"; +export const IPC_WINDOW_MAXIMIZED = "window-maximized"; +export const IPC_WINDOW_UNMAXIMIZED = "window-unmaximized"; +export const IPC_WINDOW_MINIMIZED = "window-minimized"; +export const IPC_WINDOW_ENTER_FULLSCREEN = "enter-full-screen"; +export const IPC_WINDOW_LEAVE_FULLSCREEN = "leave-full-screen"; +export const IPC_WINDOW_IS_FULLSCREEN = "window-is-full-screen"; +export const IPC_CLOSE_WINDOW = "close-window"; +export const IPC_RESTART_APP = "restart-app"; +export const IPC_QUIT_APP = "quit-app"; +export const IPC_POWER_MONITOR_RESUME = "power-monitor-resume"; +export const IPC_POWER_MONITOR_SUSPEND = "power-monitor-suspend"; +export const IPC_POWER_MONITOR_LOCK = "power-monitor-lock"; +export const IPC_POWER_MONITOR_UNLOCK = "power-monitor-unlock"; +export const IPC_GET_ZOOM_FACTOR = "get-zoom-factor"; +export const IPC_SET_ZOOM_LIMITS = "set-zoom-limits"; +export const IPC_SET_ZOOM_FACTOR = "set-zoom-factor"; +export const IPC_GET_APP_VERSION = "get-app-version"; +export const IPC_GET_LOCALE = "get-locale"; +export const IPC_GET_LAUNCH_ARGS = "get-launch-args"; +export const IPC_GET_LOGIN_ITEM_SETTINGS = "get-login-item-settings"; +export const IPC_SET_LOGIN_ITEM_SETTINGS = "set-login-item-settings"; +export const IPC_LIST_ENTRIES = "list-entries"; +export const IPC_READDIR = "readdir"; +export const IPC_IS_DEFAULT_PROTOCOL_CLIENT = "is-default-protocol-client"; +export const IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT = "set-as-default-protocol-client"; +export const IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT = "remove-as-default-protocol-client"; +export const IPC_REQUEST_INSTALL_FROM_URL = "request-install-from-url"; +export const IPC_CUSTOM_PROTOCOL_RECEIVED = "custom-protocol-received"; +export const IPC_ADDONS_SAVE_ALL = "addons-save-all"; +export const IPC_GET_PENDING_OPEN_URLS = "get-pending-open-urls"; +export const IPC_GET_LATEST_DIR_UPDATE_TIME = "get-latest-dir-update-time"; +export const IPC_LIST_DIR_RECURSIVE = "list-dir-recursive"; +export const IPC_GET_DIRECTORY_TREE = "get-directory-tree"; +export const IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT = "system-preferences-get-user-default"; +export const IPC_SHOW_OPEN_DIALOG = "show-open-dialog"; +export const IPC_APP_UPDATE_STATE = "app-update-state"; +export const IPC_APP_INSTALL_UPDATE = "app-install-update"; +export const IPC_APP_CHECK_UPDATE = "app-check-update"; +export const IPC_UPDATE_APP_BADGE = "update-app-badge"; +export const IPC_WINDOW_RESUME = "window-resume"; +export const IPC_PUSH_INIT = "push-init"; +export const IPC_PUSH_REGISTER = "push-register"; +export const IPC_PUSH_UNREGISTER = "push-unregister"; +export const IPC_PUSH_SUBSCRIBE = "push-subscribe"; +export const IPC_PUSH_NOTIFICATION = "push-notification"; + +// IPC STORAGE +export const IPC_STORE_GET_OBJECT = "store-get-object"; +export const IPC_STORE_GET_OBJECT_SYNC = "store-get-object-sync"; +export const IPC_STORE_GET_ALL = "store-get-all"; +export const IPC_STORE_SET_OBJECT = "store-set-object"; +export const IPC_STORE_REMOVE_OBJECT = "store-remove-object"; + +// STORES +export const ADDON_STORE_NAME = "addons"; +export const PREFERENCE_STORE_NAME = "preferences"; +export const SENSITIVE_STORE_NAME = "sensitive"; +export const STORAGE_WOWUP_AUTH_TOKEN = "wowup-auth-token"; + +// PREFERENCES +export const ENABLE_SYSTEM_NOTIFICATIONS_PREFERENCE_KEY = "enable_system_notifications"; +export const COLLAPSE_TO_TRAY_PREFERENCE_KEY = "collapse_to_tray"; +export const WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY = "wowup_release_channel_2_6"; +export const DEFAULT_CHANNEL_PREFERENCE_KEY_SUFFIX = "_default_addon_channel"; +export const DEFAULT_AUTO_UPDATE_PREFERENCE_KEY_SUFFIX = "_default_auto_update"; +export const LAST_SELECTED_WOW_CLIENT_TYPE_PREFERENCE_KEY = "last_selected_client_type"; +export const USE_HARDWARE_ACCELERATION_PREFERENCE_KEY = "use_hardware_acceleration"; +export const USE_SYMLINK_MODE_PREFERENCE_KEY = "use_symlink_mode"; +export const START_WITH_SYSTEM_PREFERENCE_KEY = "start_with_system"; +export const START_MINIMIZED_PREFERENCE_KEY = "start_minimized"; +export const SELECTED_LANGUAGE_PREFERENCE_KEY = "selected_language"; +export const KEEP_ADDON_DETAIL_TAB_PREFERENCE_KEY = "keep_addon_detail_tab"; +export const MY_ADDONS_HIDDEN_COLUMNS_KEY = "my_addons_hidden_columns"; +export const MY_ADDONS_SORT_ORDER = "my_addons_sort_order"; +export const GET_ADDONS_HIDDEN_COLUMNS_KEY = "get_addons_hidden_columns"; +export const GET_ADDONS_SORT_ORDER = "get_addons_sort_order"; +export const ADDON_PROVIDERS_KEY = "addon_providers"; +export const WOW_INSTALLATIONS_KEY = "wow_installations"; +export const CURRENT_THEME_KEY = "current_theme"; +export const TELEMETRY_ENABLED_KEY = "telemetry_enabled"; +export const BLIZZARD_AGENT_PATH_KEY = "blizzard_agent_path"; +export const ZOOM_FACTOR_KEY = "zoom_factor"; +export const SELECTED_DETAILS_TAB_KEY = "selected_details_tab"; +export const ADDON_MIGRATION_VERSION_KEY = "addon_migration_version"; +export const UPDATE_NOTES_POPUP_VERSION_KEY = "update_notes_popup_version"; +export const ENABLE_APP_BADGE_KEY = "enable_app_badge"; +export const TRUSTED_DOMAINS_KEY = "trusted_domains"; +export const RETAIL_LOCATION_KEY = "wow_retail_location"; // TODO remove, deprecated +export const RETAIL_PTR_LOCATION_KEY = "wow_retail_ptr_location"; // TODO remove, deprecated +export const CLASSIC_LOCATION_KEY = "wow_classic_location"; // TODO remove, deprecated +export const CLASSIC_PTR_LOCATION_KEY = "wow_classic_ptr_location"; // TODO remove, deprecated +export const BETA_LOCATION_KEY = "wow_beta_location"; // TODO remove, deprecated +export const ACCT_PUSH_ENABLED_KEY = "acct_push_enabled"; +export const WAGO_PROMPT_KEY = "wago_prompt"; +export const PREF_TABS_COLLAPSED = "tabs_collapsed"; +export const PREF_CF2_API_KEY = "cf2_api_key"; +export const PREF_GITHUB_PERSONAL_ACCESS_TOKEN = "github_personal_access_token"; +export const PREF_WAGO_ACCESS_KEY = "wago_access_key"; + +export const ACCT_FEATURE_KEYS = [ACCT_PUSH_ENABLED_KEY]; + +// THEMES +export const DEFAULT_THEME = "default-theme"; +export const DEFAULT_LIGHT_THEME = "default-theme-light-theme"; +export const HORDE_THEME = "horde-theme"; +export const HORDE_LIGHT_THEME = "horde-theme-light-theme"; +export const ALLIANCE_THEME = "alliance-theme"; +export const ALLIANCE_LIGHT_THEME = "alliance-theme-light-theme"; +export const DEFAULT_BG_COLOR = "#444444"; +export const DEFAULT_LIGHT_BG_COLOR = "#f3f3f3"; + +// ERRORS +export const ERROR_ADDON_ALREADY_INSTALLED = "error-addon-already-installed"; +export const NO_SEARCH_RESULTS_ERROR = "no-search-results"; +export const NO_LATEST_SEARCH_RESULT_FILES_ERROR = "no-latest-search-result-files"; + +// VALUES +export const WINDOW_DEFAULT_WIDTH = 1280; +export const WINDOW_DEFAULT_HEIGHT = 720; +export const WINDOW_MIN_WIDTH = 940; +export const WINDOW_MIN_HEIGHT = 700; +export const MIN_VISIBLE_ON_SCREEN = 32; +export const WOWUP_LOGO_FILENAME = "wowup_logo_purple.png"; +export const WOWUP_LOGO_MAC_SYSTEM_TRAY = "wowupBlackLgNopadTemplate.png"; +export const DEFAULT_FILE_MODE = 0o655; +export const DEFAULT_TRUSTED_DOMAINS = [ + "wowup.io", + "dev.wowup.io", + "discord.gg", + "www.patreon.com", + "github.com", + "wago.io", + "addons.wago.io", +]; +export const WOW_BETA_FOLDER = "_beta_"; +export const WOW_CLASSIC_FOLDER = "_classic_"; +export const WOW_CLASSIC_BETA_FOLDER = "_classic_beta_"; +export const WOW_CLASSIC_ERA_FOLDER = "_classic_era_"; +export const WOW_CLASSIC_ERA_PTR_FOLDER = "_classic_era_ptr_"; +export const WOW_CLASSIC_PTR_FOLDER = "_classic_ptr_"; +export const WOW_RETAIL_PTR_FOLDER = "_ptr_"; +export const WOW_RETAIL_XPTR_FOLDER = "_xptr_"; +export const WOW_RETAIL_FOLDER = "_retail_"; + +export const WOW_ADDON_FOLDER_NAME = "AddOns"; +export const WOW_INTERFACE_FOLDER_NAME = "Interface"; + + +export const YEAR_SECONDS = 31536000; +export const MONTH_SECONDS = 2592000; +export const DAY_SECONDS = 86400; +export const HOUR_SECONDS = 3600; +export const MINUTE_SECONDS = 60; + +export const TAB_INDEX_MY_ADDONS = 0; +export const TAB_INDEX_GET_ADDONS = 1; +export const TAB_INDEX_ABOUT = 2; +export const TAB_INDEX_NEWS = 3; +export const TAB_INDEX_SETTINGS = 4; + +export const USER_ACTION_BROWSE_CATEGORY = "browse-category"; +export const USER_ACTION_OPEN_LINK = "open-link"; +export const USER_ACTION_ADDON_SEARCH = "addon-search"; +export const USER_ACTION_ADDON_PROTOCOL_SEARCH = "addon-protocol-search"; +export const USER_ACTION_ADDON_INSTALL = "addon-install-action"; + +export const TRUE_STR = true.toString(); diff --git a/WowUp/wowup-electron/src/common/curse/curse-models.ts b/WowUp/wowup-electron/src/common/curse/curse-models.ts new file mode 100644 index 0000000..de51272 --- /dev/null +++ b/WowUp/wowup-electron/src/common/curse/curse-models.ts @@ -0,0 +1,74 @@ +export type CurseGameVersionFlavor = "wow_retail" | "wow_classic" | "wow_burning_crusade"; + +// see https://addons-ecs.forgesvc.net/api/v2/category/section/1 +export enum CurseAddonCategory { + ChatCommunication = 1001, + AuctionEconomy = 1002, + AudioVideo = 1003, + PvP = 1004, + BuffsDebuffs = 1005, + Artwork = 1006, + DataExport = 1007, + Guild = 1008, + BagsInventory = 1009, + Libraries = 1010, + MapMinimap = 1011, + Mail = 1012, + QuestsLeveling = 1013, + BossEncounters = 1014, + Professions = 1015, + UnitFrames = 1016, + Miscellaneous = 1017, + ActionBars = 1018, + Combat = 1019, + Class = 1020, + Mage = 1021, + Paladin = 1022, + Druid = 1023, + Hunter = 1024, + Shaman = 1025, + Priest = 1026, + Rogue = 1027, + Warrior = 1028, + Warlock = 1029, + DevelopmentTools = 1031, + Healer = 1032, + Tank = 1033, + Caster = 1034, + DamageDealer = 1035, + DeathKnight = 1036, + RaidFrames = 1037, + Minigames = 1038, + HUDs = 1039, + Arena = 1040, + Battleground = 1041, + Alchemy = 1042, + Blacksmithing = 1043, + Cooking = 1044, + Enchanting = 1045, + Engineering = 1046, + FirstAid = 1047, + Fishing = 1048, + Herbalism = 1049, + Jewelcrafting = 1050, + Leatherworking = 1051, + Mining = 1052, + Skinning = 1053, + Tailoring = 1054, + Tooltip = 1055, + Inscription = 1059, + Roleplay = 1060, + Plugins = 1063, + FuBar = 1064, + TitanPanel = 1065, + DataBroker = 1066, + Achievements = 1067, + Companions = 1085, + Archaeology = 1103, + Transmogrification = 1171, + Monk = 1242, + BattlePets = 1243, + Garrison = 1469, + DemonHunters = 1502, + TwitchIntegration = 4675, +} diff --git a/WowUp/wowup-electron/src/common/models/copy-file-request.ts b/WowUp/wowup-electron/src/common/models/copy-file-request.ts new file mode 100644 index 0000000..0e13c40 --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/copy-file-request.ts @@ -0,0 +1,7 @@ +import { IpcRequest } from "./ipc-request"; + +export interface CopyFileRequest extends IpcRequest { + sourceFilePath: string; + destinationFilePath: string; + destinationFileChmod: string | number; +} diff --git a/WowUp/wowup-electron/src/common/models/download-request.ts b/WowUp/wowup-electron/src/common/models/download-request.ts new file mode 100644 index 0000000..43c06f1 --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/download-request.ts @@ -0,0 +1,9 @@ +import { DownloadAuth } from "wowup-lib-core"; +import { IpcRequest } from "./ipc-request"; + +export interface DownloadRequest extends IpcRequest { + url: string; + fileName: string; + outputFolder: string; + auth?: DownloadAuth; +} diff --git a/WowUp/wowup-electron/src/common/models/download-status-type.ts b/WowUp/wowup-electron/src/common/models/download-status-type.ts new file mode 100644 index 0000000..93fdfd2 --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/download-status-type.ts @@ -0,0 +1,6 @@ +export enum DownloadStatusType { + Pending, + Progress, + Complete, + Error, +} diff --git a/WowUp/wowup-electron/src/common/models/download-status.ts b/WowUp/wowup-electron/src/common/models/download-status.ts new file mode 100644 index 0000000..19e4d0b --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/download-status.ts @@ -0,0 +1,8 @@ +import { DownloadStatusType } from "./download-status-type"; + +export interface DownloadStatus { + type: DownloadStatusType; + progress?: number; + savePath?: string; + error?: Error; +} diff --git a/WowUp/wowup-electron/src/common/models/ipc-events.ts b/WowUp/wowup-electron/src/common/models/ipc-events.ts new file mode 100644 index 0000000..296cacf --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/ipc-events.ts @@ -0,0 +1,19 @@ +export interface FsDirent { + isFile: boolean; + isDirectory: boolean; + isBlockDevice: boolean; + isCharacterDevice: boolean; + isSymbolicLink: boolean; + isFIFO: boolean; + isSocket: boolean; + name: string; +} + +export interface TreeNode { + name: string; + path: string; + isDirectory: boolean; + children: TreeNode[]; + size: number; + hash?: string; +} diff --git a/WowUp/wowup-electron/src/common/models/ipc-request.ts b/WowUp/wowup-electron/src/common/models/ipc-request.ts new file mode 100644 index 0000000..7fbdad7 --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/ipc-request.ts @@ -0,0 +1,12 @@ +export interface IpcRequest { + responseKey: string; +} + +export interface GetDirectoryTreeOptions { + includeHash?: boolean; +} + +export interface GetDirectoryTreeRequest { + dirPath: string; + opts?: GetDirectoryTreeOptions; +} diff --git a/WowUp/wowup-electron/src/common/models/ipc-response.ts b/WowUp/wowup-electron/src/common/models/ipc-response.ts new file mode 100644 index 0000000..01524b6 --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/ipc-response.ts @@ -0,0 +1,17 @@ +export interface IpcResponse { + error?: Error; +} + +export interface BackupGetExistingResponse { + exists: boolean; +} + +export interface BackupCreateResponse { + error?: string; +} + +export interface ZipEntry { + name: string; + path: string; + isDirectory: boolean; +} diff --git a/WowUp/wowup-electron/src/common/models/unzip-request.ts b/WowUp/wowup-electron/src/common/models/unzip-request.ts new file mode 100644 index 0000000..8bb375f --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/unzip-request.ts @@ -0,0 +1,6 @@ +import { IpcRequest } from "./ipc-request"; + +export interface UnzipRequest extends IpcRequest { + zipFilePath: string; + outputFolder: string; +} diff --git a/WowUp/wowup-electron/src/common/models/value-request.ts b/WowUp/wowup-electron/src/common/models/value-request.ts new file mode 100644 index 0000000..b6b4ddf --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/value-request.ts @@ -0,0 +1,5 @@ +import { IpcRequest } from "./ipc-request"; + +export interface ValueRequest extends IpcRequest { + value: T; +} diff --git a/WowUp/wowup-electron/src/common/models/value-response.ts b/WowUp/wowup-electron/src/common/models/value-response.ts new file mode 100644 index 0000000..bf02631 --- /dev/null +++ b/WowUp/wowup-electron/src/common/models/value-response.ts @@ -0,0 +1,5 @@ +import { IpcResponse } from "./ipc-response"; + +export interface ValueResponse extends IpcResponse { + value: T; +} diff --git a/WowUp/wowup-electron/src/common/warcraft/index.ts b/WowUp/wowup-electron/src/common/warcraft/index.ts new file mode 100644 index 0000000..e903c72 --- /dev/null +++ b/WowUp/wowup-electron/src/common/warcraft/index.ts @@ -0,0 +1,27 @@ +import { WowClientType } from "wowup-lib-core"; +import * as constants from "../constants"; + +export function getWowClientFolderName(clientType: WowClientType): string { + switch (clientType) { + case WowClientType.Retail: + return constants.WOW_RETAIL_FOLDER; + case WowClientType.ClassicEra: + return constants.WOW_CLASSIC_ERA_FOLDER; + case WowClientType.Classic: + return constants.WOW_CLASSIC_FOLDER; + case WowClientType.RetailPtr: + return constants.WOW_RETAIL_PTR_FOLDER; + case WowClientType.RetailXPtr: + return constants.WOW_RETAIL_XPTR_FOLDER; + case WowClientType.ClassicPtr: + return constants.WOW_CLASSIC_PTR_FOLDER; + case WowClientType.Beta: + return constants.WOW_BETA_FOLDER; + case WowClientType.ClassicBeta: + return constants.WOW_CLASSIC_BETA_FOLDER; + case WowClientType.ClassicEraPtr: + return constants.WOW_CLASSIC_ERA_PTR_FOLDER; + default: + return ""; + } +} diff --git a/WowUp/wowup-electron/src/common/wowup.d.ts b/WowUp/wowup-electron/src/common/wowup.d.ts new file mode 100644 index 0000000..ff893c9 --- /dev/null +++ b/WowUp/wowup-electron/src/common/wowup.d.ts @@ -0,0 +1,126 @@ +import { IpcRendererEvent, OpenExternalOptions } from "electron"; + + +// Events that can be sent from main to renderer +declare type MainChannels = + | "app-update-check-start" + | "app-update-check-end" + | "app-update-downloaded" + | "app-update-not-available" + | "app-update-start-download" + | "app-update-state" + | "blur" + | "custom-protocol-received" + | "focus" + | "power-monitor-lock" + | "power-monitor-resume" + | "power-monitor-suspend" + | "power-monitor-unlock" + | "push-notification" + | "request-install-from-url" + | "window-maximized" + | "window-minimized" + | "window-unmaximized" + | "window-resume" + | "zoom-changed"; + +// Events that can be sent from renderer to main +declare type RendererChannels = + | "addons-save-all" + | "app-install-update" + | "app-update-check-for-update" + | "app-update-install" + | "app-update-start-download" + | "base64-decode" + | "base64-encode" + | "clipboard-read-text" + | "close-window" + | "copy-file" + | "create-app-menu" + | "create-directory" + | "create-tray-menu" + | "curse-get-scan-results" + | "decode-product-db" + | "delete-directory" + | "focus-window" + | "get-app-version" + | "get-asset-file-path" + | "get-directory-tree" + | "get-focus" + | "get-home-dir" + | "get-latest-dir-update-time" + | "get-launch-args" + | "get-locale" + | "get-login-item-settings" + | "get-pending-open-urls" + | "get-zoom-factor" + | "is-default-protocol-client" + | "leave-full-screen" + | "list-dir-recursive" + | "list-directories" + | "list-disks-win32" + | "list-entries" + | "list-files" + | "maximize-window" + | "minimize-window" + | "ow-is-cmp-required" + | "ow-open-cmp" + | "path-exists" + | "push-init" + | "push-register" + | "push-subscribe" + | "push-unregister" + | "quit-app" + | "read-file" + | "read-file-buffer" + | "readdir" + | "remove-as-default-protocol-client" + | "rename-file" + | "restart-app" + | "set-as-default-protocol-client" + | "set-login-item-settings" + | "set-release-channel" + | "set-zoom-factor" + | "set-zoom-limits" + | "show-directory" + | "show-item-in-folder" + | "show-open-dialog" + | "stat-files" + | "store-get-all" + | "store-get-object" + | "store-remove-object" + | "store-set-object" + | "system-preferences-get-user-default" + | "unzip-file" + | "update-app-badge" + | "wago-token-received" + | "wowup-get-scan-results" + | "write-file" + | "zip-file" + | "zip-read-file" + | "zip-list-files"; + +declare global { + interface Window { + log; + libs: { + handlebars: any; + autoLaunch: any; + }; + baseBgColor: string; + platform: string; + userDataPath: string; + logPath: string; + wowup: { + onRendererEvent: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => void; + onceRendererEvent: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => void; + rendererSend: (channel: string, ...args: any[]) => void; + rendererSendSync: (channel: string, ...args: any[]) => any; + rendererInvoke: (channel: string, ...args: any[]) => Promise; + rendererOff: (channel: string, listener: (...args: any[]) => void) => void; + rendererOn: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => void; + openExternal: (url: string, options?: OpenExternalOptions) => Promise; + openPath: (path: string) => Promise; + }; + } +} diff --git a/WowUp/wowup-electron/src/common/wowup/models.ts b/WowUp/wowup-electron/src/common/wowup/models.ts new file mode 100644 index 0000000..d8fffa6 --- /dev/null +++ b/WowUp/wowup-electron/src/common/wowup/models.ts @@ -0,0 +1,71 @@ +export enum AppUpdateState { + CheckingForUpdate = 1, + UpdateAvailable, + UpdateNotAvailable, + Downloading, + Downloaded, + Error, +} + +export interface AppUpdateEvent { + state: AppUpdateState; + progress?: AppUpdateDownloadProgress; + error?: string; +} + +export interface AppUpdateDownloadProgress { + bytesPerSecond: number; + percent: number; + transferred: number; + total: number; +} + +export interface AppOptions { + serve?: boolean; + hidden?: boolean; + quit?: boolean; +} + +export interface MenuConfig { + editLabel: string; + viewLabel: string; + zoomOutLabel: string; + zoomInLabel: string; + zoomResetLabel: string; + reloadLabel: string; + forceReloadLabel: string; + toggleDevToolsLabel: string; + toggleFullScreenLabel: string; + quitLabel: string; + undoLabel: string; + redoLabel: string; + cutLabel: string; + copyLabel: string; + pasteLabel: string; + selectAllLabel: string; + windowLabel: string; + windowCloseLabel: string; +} + +export interface SystemTrayConfig { + showLabel: string; + quitLabel: string; + checkUpdateLabel: string; +} + +export type PushAction = "addon-update"; + +export interface PushNotification { + action: PushAction; + sender: string; + message: string | T; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PushNotificationData {} + +export interface AddonUpdatePushNotification extends PushNotificationData { + provider: string; + addonName: string; + addonId: string; +} diff --git a/WowUp/wowup-electron/src/common/wowup/product-db.ts b/WowUp/wowup-electron/src/common/wowup/product-db.ts new file mode 100644 index 0000000..6ec4318 --- /dev/null +++ b/WowUp/wowup-electron/src/common/wowup/product-db.ts @@ -0,0 +1,34 @@ +import { Field, Message, Type } from "protobufjs"; + +@Type.d("Client") +export class Client extends Message { + @Field.d(1, "string") + public location = ""; + + @Field.d(13, "string") + public name = ""; +} + +@Type.d("Product") +export class Product extends Message { + @Field.d(1, "string") + public name = ""; + + @Field.d(2, "string") + public alias = ""; + + @Field.d(3, Client) + public client!: Client; + + @Field.d(6, "string") + public family = ""; +} + +@Type.d("ProductDb") +export class ProductDb extends Message { + @Field.d(1, Product, "repeated") + public products: Product[] = []; + + @Field.d(7, "string", "repeated") + public productNames: string[] = []; +} diff --git a/WowUp/wowup-electron/src/common/wowup/wowup-release-channel-type.ts b/WowUp/wowup-electron/src/common/wowup/wowup-release-channel-type.ts new file mode 100644 index 0000000..094d321 --- /dev/null +++ b/WowUp/wowup-electron/src/common/wowup/wowup-release-channel-type.ts @@ -0,0 +1,4 @@ +export enum WowUpReleaseChannelType { + Beta, + Stable, +} diff --git a/WowUp/wowup-electron/src/controls.scss b/WowUp/wowup-electron/src/controls.scss new file mode 100644 index 0000000..9e6396b --- /dev/null +++ b/WowUp/wowup-electron/src/controls.scss @@ -0,0 +1,62 @@ +.wu-btn { + font-family: Roboto, sans-serif; + font-size: 0.85rem; + border-radius: 4px; + border: none; + padding: 0.5rem; + font-weight: 400; + text-decoration: none; + color: var(--text-1); + // display: flex; + // align-items: center; + // justify-content: center; + + &:hover { + cursor: pointer; + } + + > .mat-icon { + height: 16px; + } + + &.wu-btn-icon { + height: 32px; + padding: 4px 0.3rem 0 0.3rem; + } + + &.wu-btn-primary { + background-color: var(--background-primary); + color: var(--text-1); + + &:hover { + background-color: var(--background-primary-2); + } + + &[disabled] { + background-color: grey; + cursor: not-allowed; + } + + &.wu-btn-flat { + color: var(--background-primary); + } + } + + &.wu-btn-warning { + color: #f44336; + + &[disabled] { + background-color: grey; + cursor: not-allowed; + } + } + + &.wu-btn-flat { + background-color: transparent; + border: 1px solid grey; + + &:hover { + background-color: rgba(0, 0, 0, 0.2); + } + } +} diff --git a/WowUp/wowup-electron/src/custom-theme.scss b/WowUp/wowup-electron/src/custom-theme.scss new file mode 100644 index 0000000..36f14ff --- /dev/null +++ b/WowUp/wowup-electron/src/custom-theme.scss @@ -0,0 +1,847 @@ +@use "@angular/material" as mat; +@import "./variables.scss"; + +@import "ag-grid-community/styles/ag-grid.css"; +@import "ag-grid-community/styles/ag-theme-material.css"; + +// Plus imports for other components in your app. + +// Include the common styles for Angular Material. We include this here so that you only +// have to load a single css file for Angular Material in your app. +// Be sure that you only ever include this mixin once! +// TODO(v15): As of v15 mat.legacy-core no longer includes default typography styles. +// The following line adds: +// 1. Default typography styles for all components +// 2. Styles for typography hierarchy classes (e.g. .mat-headline-1) +// If you specify typography styles for the components you use elsewhere, you should delete this line. +// If you don't need the default component typographies but still want the hierarchy styles, +// you can delete this line and instead use: +// `@include mat.legacy-typography-hierarchy(mat.define-legacy-typography-config());` +@include mat.all-component-typographies(); +@include mat.core(); + +:root { + --success-color: rgb(25, 135, 84); + --warning-color: rgb(255, 193, 7); + --secondary-color: rgb(108, 117, 125); + --mdc-filled-text-field-label-text-size: 14px; + --mat-select-trigger-text-size: 14px; + --mat-option-label-text-size: 14px; + --mdc-typography-button-letter-spacing: normal; +} + +$md-wowup-palette: ( + 50: #e8eaf6, + 100: #6b69d6, + 200: #504fa1, + 300: #383773, + 400: #5c6bc0, + 500: #3f51b5, + 600: #3949ab, + 700: #303f9f, + 800: #283593, + 900: #1a237e, + A100: $wowup-purple-1, + A200: $wowup-control, + A400: $wowup-purple-3, + A700: #6874bb, + contrast: ( + 50: #000000, + 100: #ffffff, + 200: #ffffff, + 300: #ffffff, + 400: #ffffff, + 500: #ffffff, + 600: #ffffff, + 700: #ffffff, + 800: #ffffff, + 900: #ffffff, + A100: #000000, + A200: #ffffff, + A400: #ffffff, + A700: #ffffff, + ), +); + +// Define the palettes for your theme using the Material Design palettes available in palette.scss +// (imported above). For each palette, you can optionally specify a default, lighter, and darker +// hue. Available color palettes: https://material.io/design/color/ +$wowup-default-primary: mat.define-palette($md-wowup-palette, 100, 50, 300); +$wowup-default-accent: mat.define-palette($md-wowup-palette, A200, A100, A400); + +// The warn palette is optional (defaults to red). +$wowup-default-warn: mat.define-palette(mat.$red-palette); + +// Create the theme object. A theme consists of configurations for individual +// theming systems such as `color` or `typography`. +$wowup-default-theme: mat.define-dark-theme( + ( + color: ( + primary: $wowup-default-primary, + accent: $wowup-default-accent, + warn: $wowup-default-warn, + ), + ) +); + +$wowup-light-theme: mat.define-light-theme( + ( + color: ( + primary: $wowup-default-primary, + accent: $wowup-default-accent, + warn: $wowup-default-warn, + ), + ) +); + +// Include theme styles for core and each component used in your app. +// Alternatively, you can import and @include the theme mixins for each component +// that you are using. +@include mat.all-component-themes($wowup-default-theme); + +.default-theme-light-theme { + @include mat.all-component-colors($wowup-light-theme); +} + +// Horde theme +$horde-palette: ( + 50: #e8eaf6, + 100: $horde-red-1, + 200: #504fa1, + 300: #383773, + 400: #5c6bc0, + 500: #3f51b5, + 600: #3949ab, + 700: #303f9f, + 800: #283593, + 900: #1a237e, + A100: $horde-red-1, + A200: $horde-control, + A400: $horde-red-3, + A700: #6874bb, + contrast: ( + 50: #000000, + 100: #ffffff, + 200: #ffffff, + 300: #ffffff, + 400: #ffffff, + 500: #ffffff, + 600: #ffffff, + 700: #ffffff, + 800: #ffffff, + 900: #ffffff, + A100: #000000, + A200: #ffffff, + A400: #ffffff, + A700: #ffffff, + ), +); +$horde-primary: mat.define-palette($horde-palette, 100, 50, 300); +$horde-accent: mat.define-palette($horde-palette, A200, A100, A400); +$horde-warn: mat.define-palette(mat.$deep-orange-palette); +$horde-theme: mat.define-dark-theme( + ( + color: ( + primary: $horde-primary, + accent: $horde-accent, + warn: $horde-warn, + ), + ) +); + +$horde-theme-light: mat.define-light-theme( + ( + color: ( + primary: $horde-primary, + accent: $horde-accent, + warn: $horde-warn, + ), + ) +); + +.horde-theme-light-theme { + @include mat.all-component-colors($horde-theme-light); +} + +.horde-theme { + @include mat.all-component-colors($horde-theme); +} + +// Alliance theme +$alliance-palette: ( + 50: #e8eaf6, + 100: $alliance-blue-1, + 200: $alliance-blue-2, + 300: #383773, + 400: #5c6bc0, + 500: #3f51b5, + 600: #3949ab, + 700: #303f9f, + 800: #283593, + 900: #1a237e, + A100: $alliance-blue-1, + A200: $alliance-control, + A400: $alliance-blue-3, + A700: #6874bb, + contrast: ( + 50: #000000, + 100: #ffffff, + 200: #ffffff, + 300: #ffffff, + 400: #ffffff, + 500: #ffffff, + 600: #ffffff, + 700: #ffffff, + 800: #ffffff, + 900: #ffffff, + A100: #000000, + A200: #ffffff, + A400: #ffffff, + A700: #ffffff, + ), +); +$alliance-primary: mat.define-palette($alliance-palette, 100, 50, 300); +$alliance-accent: mat.define-palette($alliance-palette, A200, A100, A400); +$alliance-warn: mat.define-palette(mat.$deep-orange-palette); +$alliance-theme: mat.define-dark-theme( + ( + color: ( + primary: $alliance-primary, + accent: $alliance-accent, + warn: $alliance-warn, + ), + ) +); + +$alliance-theme-light: mat.define-light-theme( + ( + color: ( + primary: $alliance-primary, + accent: $alliance-accent, + warn: $alliance-warn, + ), + ) +); + +.alliance-theme-light-theme { + @include mat.all-component-colors($alliance-theme-light); +} + +.alliance-theme { + @include mat.all-component-colors($alliance-theme); +} + +// VARS +body { + --patreon-color: #e64049; + --github-color: #9032ad; + --wow-gold-color: #ffcc00; + --text-white: #ffffff; +} + +.app { + --mdc-filled-text-field-disabled-container-color: transparent; + --mdc-filled-text-field-container-color: var(--background-secondary-2); + --mdc-filled-text-field-focus-label-text-color: #fff; + --mdc-filled-text-field-caret-color: #fff; + --mat-tab-header-active-label-text-color: var(--text-1); +} + +.mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled) .mdc-list-item__primary-text { + color: var(--text-3) !important; +} + +.mat-mdc-tab.mdc-tab--active .mdc-tab__text-label { + color: var(--text-1) !important; +} + +.mat-mdc-card { + background-color: var(--background-secondary-3) !important; +} + +.mat-mdc-slide-toggle-checked .mdc-switch .mdc-switch__handle::before { + background-color: transparent !important; +} + +.default-theme { + --background-primary: #6b69d6; + --background-primary-2: #504fa1; + --background-primary-3: #383773; + --background-secondary-1: rgba(102, 102, 102, 0.85); + --background-secondary-2: rgba(85, 85, 85, 0.9); + --background-secondary-2-fill: rgba(85, 85, 85, 1); + --background-secondary-3: rgba(68, 68, 68, 0.8); + --background-secondary-4: rgba(51, 51, 51, 0.8); + --background-secondary-5: rgba(34, 34, 34, 0.8); + --control-color: #536dfe; + --epic-color: #a335ee; + --rare-color: #0070dd; + --scrollbar-track-color: #333333; + --text-1: #ffffff; + --text-2: #dcddde; + --text-3: #cccccc; + --text-4: #bbbbbb; + --warn-color: #f44336; + --tab-text: #ffffff; + --tab-disabled: #ffffff; + --tab-inactive: #bbbbbb; + --divider-color: #666666; + --grid-border: rgba(255, 255, 255, 0.12); + --footer-hover: #444444; + + --title-logo: url("assets/images/wowup-white-1.png"); + --theme-logo: url("assets/images/wowup-white-1.png"); + --ad-placeholder: url("assets/images/wowup-placeholder.png"); + + --mat-sidenav-content-background-color: rgba(0, 0, 0, 0); + --mat-sidenav-container-background-color: rgba(0, 0, 0, 0); +} + +.default-theme-light-theme { + --background-primary: #6b69d6; + --background-primary-2: #504fa1; + --background-primary-3: #383773; + --background-secondary-1: rgba(255, 255, 255, 0.75); + --background-secondary-2: rgba(243, 243, 243, 0.8); + --background-secondary-2-fill: rgba(243, 243, 243, 1); + --background-secondary-3: rgba(235, 237, 239, 0.75); + --background-secondary-4: rgba(227, 229, 232, 0.75); + --background-secondary-5: rgba(202, 204, 206, 0.75); + --control-color: #536dfe; + --epic-color: #a335ee; + --rare-color: #0070dd; + --scrollbar-track-color: #e3e5e8; + --text-1: #000000; + --text-2: #666666; + --text-3: #888888; + --text-4: #aaaaaa; + --warn-color: #f44336; + --tab-text: #ffffff; + --tab-disabled: #ffffff; + --tab-inactive: #777; + --divider-color: #aaaaaa; + --grid-border: #cccccc; + --footer-hover: #cccccc; + + --title-logo: url("assets/images/wowup-white-1.png"); + --theme-logo: url("assets/images/wowup-dark-1.png"); +} + +.horde-theme { + --background-primary: #8c1616; + --background-primary-2: #9d3939; + --background-primary-3: #5c0606; + --background-secondary-1: rgba(102, 102, 102, 0.85); + --background-secondary-2: rgba(85, 85, 85, 0.9); + --background-secondary-2-fill: rgba(85, 85, 85, 1); + --background-secondary-3: rgba(68, 68, 68, 0.8); + --background-secondary-4: rgba(51, 51, 51, 0.8); + --background-secondary-5: rgba(34, 34, 34, 0.8); + --control-color: #ff5b5b; + --epic-color: #a335ee; + --rare-color: #0070dd; + --scrollbar-track-color: #333333; + --text-1: #ffffff; + --text-2: #dcddde; + --text-3: #cccccc; + --text-4: #bbbbbb; + --warn-color: #f44336; + --tab-text: #ffffff; + --tab-disabled: #ffffff; + --tab-inactive: #bbbbbb; + --divider-color: #666666; + --grid-border: rgba(255, 255, 255, 0.12); + --footer-hover: #444444; + + --title-logo: url("assets/images/horde-1.png"); + --theme-logo: url("assets/images/horde-1.png"); + --mat-sidenav-content-background-color: rgba(0, 0, 0, 0); + --mat-sidenav-container-background-color: rgba(0, 0, 0, 0); +} + +.horde-theme-light-theme { + --background-primary: #8c1616; + --background-primary-2: #9d3939; + --background-primary-3: #5c0606; + --background-secondary-1: rgba(255, 255, 255, 0.75); + --background-secondary-2: rgba(243, 243, 243, 0.8); + --background-secondary-2-fill: rgba(243, 243, 243, 1); + --background-secondary-3: rgba(235, 237, 239, 0.75); + --background-secondary-4: rgba(227, 229, 232, 0.75); + --background-secondary-5: rgba(202, 204, 206, 0.75); + --control-color: #ff5b5b; + --epic-color: #a335ee; + --rare-color: #0070dd; + --scrollbar-track-color: #e3e5e8; + --text-1: #000000; + --text-2: #666666; + --text-3: #888888; + --text-4: #aaaaaa; + --warn-color: #f44336; + --tab-text: #ffffff; + --tab-disabled: #ffffff; + --tab-inactive: #777; + --divider-color: #aaaaaa; + --grid-border: #cccccc; + --footer-hover: #cccccc; + + --title-logo: url("assets/images/horde-1.png"); + --theme-logo: url("assets/images/horde-dark-1.png"); +} + +.alliance-theme { + --background-primary: #162c57; + --background-primary-2: #394c70; + --background-primary-3: #06102c; + --background-secondary-1: rgba(102, 102, 102, 0.85); + --background-secondary-2: rgba(85, 85, 85, 0.9); + --background-secondary-2-fill: rgba(85, 85, 85, 1); + --background-secondary-3: rgba(68, 68, 68, 0.8); + --background-secondary-4: rgba(51, 51, 51, 0.8); + --background-secondary-5: rgba(34, 34, 34, 0.8); + --control-color: #335eff; + --epic-color: #a335ee; + --rare-color: #0070dd; + --scrollbar-track-color: #333333; + --text-1: #ffffff; + --text-2: #dcddde; + --text-3: #cccccc; + --text-4: #bbbbbb; + --warn-color: #f44336; + --tab-text: #ffffff; + --tab-disabled: #ffffff; + --tab-inactive: #bbbbbb; + --divider-color: #666666; + --grid-border: rgba(255, 255, 255, 0.12); + + --title-logo: url("assets/images/alliance-1.png"); + --theme-logo: url("assets/images/alliance-1.png"); + --mat-sidenav-content-background-color: rgba(0, 0, 0, 0); + --mat-sidenav-container-background-color: rgba(0, 0, 0, 0); +} + +.alliance-theme-light-theme { + --background-primary: #162c57; + --background-primary-2: #394c70; + --background-primary-3: #06102c; + --background-secondary-1: rgba(255, 255, 255, 0.75); + --background-secondary-2: rgba(243, 243, 243, 0.8); + --background-secondary-2-fill: rgba(243, 243, 243, 1); + --background-secondary-3: rgba(235, 237, 239, 0.75); + --background-secondary-4: rgba(227, 229, 232, 0.75); + --background-secondary-5: rgba(202, 204, 206, 0.75); + --control-color: #335eff; + --epic-color: #a335ee; + --rare-color: #0070dd; + --scrollbar-track-color: #e3e5e8; + --text-1: #000000; + --text-2: #666666; + --text-3: #888888; + --text-4: #aaaaaa; + --warn-color: #f44336; + --tab-text: #ffffff; + --tab-disabled: #ffffff; + --tab-inactive: #777; + --divider-color: #aaaaaa; + --grid-border: #cccccc; + --footer-hover: #cccccc; + + --title-logo: url("assets/images/alliance-1.png"); + --theme-logo: url("assets/images/alliance-dark-1.png"); +} + +.bg-primary { + background-color: var(--background-primary); +} + +.bg-primary-2 { + background-color: var(--background-primary-2); +} + +.bg-primary-3 { + background-color: var(--background-primary-3); +} + +.bg-secondary { + background-color: var(--background-secondary-1); +} + +.bg-secondary-2 { + background-color: var(--background-secondary-2); +} + +.bg-secondary-3 { + background-color: var(--background-secondary-3); +} + +.bg-secondary-4 { + background-color: var(--background-secondary-4); +} + +.bg-secondary-5 { + background-color: var(--background-secondary-5); +} + +.hover-primary-2:hover { + background-color: var(--background-primary-2); +} + +.hover-bg-secondary:hover { + background-color: var(--background-secondary-1); +} + +.hover-bg-secondary-2:hover { + background-color: var(--background-secondary-2); +} + +.hover-bg-secondary-3:hover { + background-color: var(--background-secondary-3); +} + +.hover-bg-secondary-4:hover { + background-color: var(--background-secondary-4); +} + +.hover-bg-secondary-5:hover { + background-color: var(--background-secondary-5); +} + +.border-primary { + border-color: var(--background-primary); +} + +.text-white { + color: var(--text-white); +} + +.text-1 { + color: var(--text-1); +} + +.text-1-hover { + &:hover { + color: var(--text-1) !important; + } +} + +.text-2 { + color: var(--text-2); +} + +.text-3 { + color: var(--text-3); +} + +.text-4 { + color: var(--text-4); +} + +.text-success { + color: var(--success-color); +} + +.text-primary-1 { + color: var(--background-primary) !important; +} + +.text-primary-2 { + color: var(--background-primary-2) !important; +} + +.text-primary-3 { + color: var(--background-primary-3) !important; +} + +.text-control { + color: var(--control-color) !important; +} + +.text-warning { + color: var(--warn-color) !important; +} + +.wowup-snackbar { + .mdc-snackbar__surface { + background-color: var(--background-secondary-4) !important; + } + + &.text-1 { + * { + color: var(--text-1) !important; + } + } +} + +.addon-summary, +.addon-changelog { + a { + color: var(--control-color); + + &:visited { + color: var(--control-color); + } + } + + h1, + h2 { + border-bottom: 1px solid var(--background-secondary-1); + } + + hr { + background-color: var(--background-secondary-1); + } + + code { + background-color: var(--background-secondary-4); + } +} + +.changelog { + a { + color: var(--control-color); + } +} + +// .tab { +// &:hover { +// svg { +// fill: var(--text-white); +// } +// } +// } + +.tab-icon-inactive { + svg { + transition: fill 0.3s ease 0.1s; + fill: var(--tab-inactive); + } +} + +.tab-icon-active { + svg { + transition: fill 0.3s ease 0.1s; + fill: var(--text-1); + } +} + +.tab-icon-disabled { + svg { + fill: var(--text-disabled); + opacity: 0.25; + } +} + +::-webkit-scrollbar-track { + background-color: var(--scrollbar-track-color); + margin: 8px; +} + +::-webkit-scrollbar-thumb { + border-color: var(--background-primary-3); + background: var(--background-primary); + background: linear-gradient(146deg, var(--background-primary) 0%, var(--background-primary-2) 100%); + min-height: 40px; +} + +div[class*="light-theme"] { + .window-control { + &:hover { + background-color: var(--background-secondary-5); + } + + img { + filter: invert(100%); + } + } + + .logo-img { + filter: invert(100%); + } +} + +// MATERIAL STYLES +.mat-input-element { + caret-color: var(--text-3) !important; + color: var(--text-1) !important; +} + +.mat-form-field.mat-focused .mat-form-field-label { + color: var(--text-2) !important; +} + +.mat-form-field-ripple { + background-color: var(--background-primary-2) !important; +} + +.mat-card { + background-color: var(--background-secondary-3); +} + +.mat-button { + &.mat-primary { + color: var(--control-color) !important; + } +} + +.mat-mdc-icon-button { + &:not([disabled]).mat-accent { + svg { + fill: var(--text-1); + } + } +} + +.mat-option.mat-selected:not(.mat-option-disabled) { + color: var(--text-3) !important; +} + +.sub-accordion .mat-expansion-panel-header, +.sub-accordion .mat-expansion-panel-body { + background-color: var(--background-secondary-2); +} + +// TREE VIEW +div.tree div.tree-children::before { + content: ""; + position: absolute; + border-left: 1px dotted var(--text-2); + height: 100%; + top: -14px; + left: 0; +} + +div.tree { + padding-left: 0; + margin-left: -5px; +} + +div.tree div.tree-children { + position: relative; + padding-left: 0; + margin-left: 16px; +} + +div.tree div.tree-children::before { + left: 5px; +} + +div.tree tree-node > div > .node-wrapper { + margin-left: 24px; +} + +div.tree tree-node > div > .node-wrapper > .node-content-wrapper { + margin-left: 4px; +} + +div.tree tree-node > div.tree-node-leaf > .node-wrapper { + margin-left: 0; +} + +div.tree tree-node > div::before { + content: ""; + position: absolute; + border-bottom: 1px dotted var(--text-2); + width: 7px; + margin-top: 12px; + left: 7px; +} + +// div.tree tree-node > div .toggle-children-wrapper { +// width: 13px; +// height: 13px; +// border: 1px solid var(--text-2); +// position: absolute; +// left: 0; +// margin-top: 5px; +// margin-left: 0; +// display: inline-block; +// z-index: 1; +// } + +// div.tree tree-node > div .toggle-children-wrapper::before { +// content: ""; +// display: inline-block; +// width: 7px; +// border-top: 1px solid var(--text-2); +// position: absolute; +// top: 5px; +// left: 2px; +// } + +// div.tree tree-node > div .toggle-children-wrapper.toggle-children-wrapper-collapsed::after { +// content: ""; +// display: inline-block; +// height: 7px; +// border-left: 1px solid var(--text-2); +// position: absolute; +// top: 2px; +// left: 5px; +// } + +div.tree tree-node > div .toggle-children-wrapper .toggle-children { + display: none; +} + +div.tree tree-node > div .node-content-wrapper { + margin-left: 4px; +} + +div.tree > tree-node > div::before { + left: 14px; +} + +div.tree > tree-node > div > .node-wrapper > tree-node-expander > .toggle-children-wrapper { + left: 22px; +} + +.node-content-wrapper:hover { + background-color: var(--background-secondary-4); +} + +.node-content-wrapper-active, +.node-content-wrapper-focused, +.node-content-wrapper:hover { + box-shadow: none; +} + +.node-content-wrapper-focused, +.node-content-wrapper-active, +.node-content-wrapper.node-content-wrapper-active:hover, +.node-content-wrapper-active.node-content-wrapper-focused { + background-color: var(--background-secondary-3); +} + +.tree-icon { + transition: transform 0.3s; +} +.tree-icon.expanded { + transform: rotate(90deg); +} + +// LIGHT BOX + +.lb-outerContainer { + background-color: var(--background-secondary-4); +} + +.lb-cancel { + width: 80px; + height: 80px; + background-image: url("assets/images/wowup-white-1.png"); + background-position: center; + background-size: cover; + background-repeat: no-repeat; +} +// GRID STYLES +.ag-theme-material { + --ag-foreground-color: var(--text-1); + --ag-disabled-foreground-color: rgba(var(--text-1), 0.38); + --ag-secondary-foreground-color: rgba(var(--text-1), 0.54); + --ag-background-color: var(--background-secondary); + --ag-header-background-color: var(--background-secondary); + --ag-header-cell-hover-background-color: hsl(var(--background-secondary-2), 95%); + --ag-row-hover-color: var(--background-secondary-2); + --ag-border-color: var(--grid-border); + --ag-selected-row-background-color: var(--background-secondary-4); + --ag-range-selection-border-color: transparent; +} diff --git a/WowUp/wowup-electron/src/environments/environment.dev.ts b/WowUp/wowup-electron/src/environments/environment.dev.ts new file mode 100644 index 0000000..021dfce --- /dev/null +++ b/WowUp/wowup-electron/src/environments/environment.dev.ts @@ -0,0 +1,34 @@ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `index.ts`, but if you do +// `ng build --env=prod` then `index.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. + +export const AppConfig = { + production: false, + environment: "DEV", + wowUpWebsiteUrl: "https://dev.wowup.io", + wowUpApiUrl: "https://api.dev.wowup.io", + wowUpHubUrl: "https://hub.dev.wowup.io", + wowupRepositoryUrl: "https://github.com/WowUp/WowUp", + warcraftTavernNewsFeedUrl: + "https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=large", + azure: { + applicationInsightsKey: "4a53e8d9-796c-4f80-b1a6-9a058374dd6d", + }, + wago: { + termsUrl: "https://addons.wago.io/agreements/terms-of-service", + dataConsentUrl: "https://addons.wago.io/agreements/wowup-data-consent", + }, + curseforge: { + httpTimeoutMs: 60000, + apiKey: "{{CURSEFORGE_API_KEY}}", + }, + autoUpdateIntervalMs: 3600000, // 1 hour + appUpdateIntervalMs: 3600000, // 1 hour + defaultHttpTimeoutMs: 10000, + defaultHttpResetTimeoutMs: 30000, + wowUpHubHttpTimeoutMs: 10000, + wagoHttpTimeoutMs: 10000, + newsRefreshIntervalMs: 3600000, // 1 hour + featuredAddonsCacheTimeSec: 30, // 30 sec +}; diff --git a/WowUp/wowup-electron/src/environments/environment.prod.ts b/WowUp/wowup-electron/src/environments/environment.prod.ts new file mode 100644 index 0000000..f6d1e3d --- /dev/null +++ b/WowUp/wowup-electron/src/environments/environment.prod.ts @@ -0,0 +1,29 @@ +export const AppConfig = { + production: true, + environment: "PROD", + wowUpWebsiteUrl: "https://wowup.io", + wowUpApiUrl: "https://api.wowup.io", + wowUpHubUrl: "https://hub.wowup.io", + wowupRepositoryUrl: "https://github.com/WowUp/WowUp", + warcraftTavernNewsFeedUrl: + "https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium", + azure: { + applicationInsightsKey: "4a53e8d9-796c-4f80-b1a6-9a058374dd6d", + }, + wago: { + termsUrl: "https://addons.wago.io/agreements/terms-of-service", + dataConsentUrl: "https://addons.wago.io/agreements/wowup-data-consent", + }, + curseforge: { + httpTimeoutMs: 60000, + apiKey: "{{CURSEFORGE_API_KEY}}", + }, + autoUpdateIntervalMs: 3600000, // 1 hour + appUpdateIntervalMs: 3600000, // 1 hour + defaultHttpTimeoutMs: 10000, + defaultHttpResetTimeoutMs: 30000, + wowUpHubHttpTimeoutMs: 10000, + wagoHttpTimeoutMs: 10000, + newsRefreshIntervalMs: 3600000, // 1 hour + featuredAddonsCacheTimeSec: 30, // 30 sec +}; diff --git a/WowUp/wowup-electron/src/environments/environment.ts b/WowUp/wowup-electron/src/environments/environment.ts new file mode 100644 index 0000000..e3fae29 --- /dev/null +++ b/WowUp/wowup-electron/src/environments/environment.ts @@ -0,0 +1,29 @@ +export const AppConfig = { + production: false, + environment: "LOCAL", + wowUpWebsiteUrl: "https://dev.wowup.io", + wowUpApiUrl: "https://api.dev.wowup.io", + wowUpHubUrl: "https://hub.dev.wowup.io", + wowupRepositoryUrl: "https://github.com/WowUp/WowUp", + warcraftTavernNewsFeedUrl: + "https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium", + azure: { + applicationInsightsKey: "4a53e8d9-796c-4f80-b1a6-9a058374dd6d", + }, + wago: { + termsUrl: "https://addons.wago.io/agreements/terms-of-service", + dataConsentUrl: "https://addons.wago.io/agreements/wowup-data-consent", + }, + curseforge: { + httpTimeoutMs: 60000, + apiKey: "{{CURSEFORGE_API_KEY}}", + }, + autoUpdateIntervalMs: 3600000, // 1 hour + appUpdateIntervalMs: 3600000, // 1 hour + defaultHttpTimeoutMs: 10000, + defaultHttpResetTimeoutMs: 30000, + wowUpHubHttpTimeoutMs: 10000, + wagoHttpTimeoutMs: 10000, + newsRefreshIntervalMs: 3600000, // 1 hour + featuredAddonsCacheTimeSec: 30, // 30 sec +}; diff --git a/WowUp/wowup-electron/src/environments/environment.web.ts b/WowUp/wowup-electron/src/environments/environment.web.ts new file mode 100644 index 0000000..d0ac7cf --- /dev/null +++ b/WowUp/wowup-electron/src/environments/environment.web.ts @@ -0,0 +1,20 @@ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `index.ts`, but if you do +// `ng build --env=prod` then `index.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. + +export const AppConfig = { + production: false, + environment: "DEV", + wowUpWebsiteUrl: "https://dev.wowup.io", + wowUpApiUrl: "https://api.dev.wowup.io", + wowUpHubUrl: "https://hub.dev.wowup.io", + warcraftTavernNewsFeedUrl: + "https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium", + newsRefreshIntervalMs: 3600000, // 1 hour + featuredAddonsCacheTimeSec: 30, // 30 sec + wago: { + termsUrl: "https://addons.wago.io/agreements/terms-of-service", + dataConsentUrl: "https://addons.wago.io/agreements/wowup-data-consent", + }, +}; diff --git a/WowUp/wowup-electron/src/index.html b/WowUp/wowup-electron/src/index.html new file mode 100644 index 0000000..d87bffc --- /dev/null +++ b/WowUp/wowup-electron/src/index.html @@ -0,0 +1,49 @@ + + + + + WowUp.io + + + + + + + + + + +
+ +
+
+ + + diff --git a/WowUp/wowup-electron/src/karma.conf.js b/WowUp/wowup-electron/src/karma.conf.js new file mode 100644 index 0000000..fc65695 --- /dev/null +++ b/WowUp/wowup-electron/src/karma.conf.js @@ -0,0 +1,43 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/0.13/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: "", + frameworks: ["jasmine", "@angular-devkit/build-angular"], + plugins: [ + require("karma-jasmine"), + require("karma-electron"), + require("karma-jasmine-html-reporter"), + require("karma-coverage-istanbul-reporter"), + require("@angular-devkit/build-angular/plugins/karma"), + ], + client: { + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require("path").join(__dirname, "../coverage"), + reports: ["html", "lcovonly"], + fixWebpackSourcePaths: true, + }, + reporters: ["progress", "kjhtml"], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + browsers: ["AngularElectron"], + customLaunchers: { + AngularElectron: { + base: "Electron", + flags: ["--remote-debugging-port=9222"], + browserWindowOptions: { + webPreferences: { + nodeIntegration: true, + nodeIntegrationInSubFrames: true, + allowRunningInsecureContent: true, + contextIsolation: false, + }, + }, + }, + }, + }); +}; diff --git a/WowUp/wowup-electron/src/locales.spec.ts b/WowUp/wowup-electron/src/locales.spec.ts new file mode 100644 index 0000000..3a317e4 --- /dev/null +++ b/WowUp/wowup-electron/src/locales.spec.ts @@ -0,0 +1,143 @@ +/* eslint-disable */ +import { TestBed } from "@angular/core/testing"; +import { TranslateCompiler, TranslateLoader, TranslateModule, TranslateService } from "@ngx-translate/core"; +import fs from "fs"; +import path from "path"; +import flatten from "flat"; +import { catchError, firstValueFrom, Observable, of } from "rxjs"; +import MessageFormat, { MessageFormatOptions } from "@messageformat/core"; + +const LOCALES = ["cs", "de", "en", "es", "fr", "it", "ko", "nb", "pt", "ru", "zh-TW", "zh"]; +const LOCALE_DIR = path.join(__dirname, "..", "..", "..", "..", "..", "..", "src", "assets", "i18n"); + +const LOCALE_SET = {}; + +function loadLocale(code: string) { + const localeJson = fs.readFileSync(path.join(LOCALE_DIR, `${code}.json`), { + encoding: "utf-8", + }); + const localeObj = JSON.parse(localeJson); + + LOCALE_SET[code] = flatten(localeObj); + + console.log(`LOCALE ${code} ${Object.keys(localeObj).length.toString()}`); +} + +for (const code of LOCALES) { + loadLocale(code); +} + +class CustomCompiler extends TranslateCompiler { + private mfCache = new Map(); + private config: MessageFormatOptions<"string">; + + public constructor() { + super(); + + this.config = { customFormatters: undefined, biDiSupport: false, strict: false }; + } + + public compile(value: string, lang: string): (params: any) => string { + return this.getMessageFormatInstance(lang).compile(value); + } + + private doTx(key: string, lang: string): (args: any) => string { + return (args: any) => { + console.log("DO COMP", key, args); + return this.getMessageFormatInstance(lang).compile(key)(args); + }; + } + + compileTranslations(translation: any, lang: string): Object { + const f = flatten(translation) as any; + + Object.keys(f).forEach((k) => { + const ik = f[k]; + f[k] = this.doTx(ik, lang); + }); + + return f; + } + + private getMessageFormatInstance(locale: string): MessageFormat { + if (!this.mfCache.has(locale)) { + this.mfCache.set(locale, new MessageFormat<"string">(locale, this.config)); + } + + return this.mfCache.get(locale)!; + } +} + +class JsonTranslationLoader implements TranslateLoader { + public getTranslation(code: string): Observable { + console.log("LOAD CODE " + code); + return of(LOCALE_SET["en"]); + } +} + +describe("LocaleTest", () => { + let translate: TranslateService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { provide: TranslateLoader, useClass: JsonTranslationLoader }, + compiler: { + provide: TranslateCompiler, + useClass: CustomCompiler, + }, + }), + ], + }); + + translate = TestBed.inject(TranslateService); + }); + + describe("compiler check", () => { + it("should use the correct compiler", () => { + expect(translate).toBeDefined(); + expect(translate.compiler).toBeDefined(); + expect(translate.compiler instanceof CustomCompiler).toBeTruthy(); + }); + }); + + LOCALES.forEach((locale) => { + describe(`${locale}:`, () => { + beforeEach(async () => { + try { + return await firstValueFrom(translate.use(locale)); + } catch (e) { + return console.error(e); + } + }); + + const localeKeys = loadLocaleKeys(locale); + for (const lk of Object.keys(localeKeys)) { + it(`should have translated value ${locale}:${lk}`, (done) => { + // console.log("translations", JSON.stringify(translate.store.translations)); + translate + .get(lk, { d: Date.now(), count: 1, myriadCount: 2, rawCount: 3, addonNames: "test1, test2" }) + .pipe( + catchError((e) => { + console.error(e); + return of(""); + }) + ) + .subscribe((tx) => { + console.log(`TX ${tx}`); + expect(tx === lk).toBeFalse(); + done(); + }); + }); + } + }); + }); + + function loadLocaleKeys(locale: string): { [key: string]: string } { + const localeObj = LOCALE_SET[locale]; + const localeStrs: { [key: string]: string } = flatten(localeObj); + + return localeStrs; + } +}); diff --git a/WowUp/wowup-electron/src/main.ts b/WowUp/wowup-electron/src/main.ts new file mode 100644 index 0000000..54ace9a --- /dev/null +++ b/WowUp/wowup-electron/src/main.ts @@ -0,0 +1,37 @@ +import { enableProdMode } from "@angular/core"; +import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; + +import { AppModule } from "./app/app.module"; +import { AppConfig } from "./environments/environment"; + +if (AppConfig.production) { + enableProdMode(); +} + +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +console.log = function (message?: any, ...optionalParams: any[]) { + window.log.info(message, ...optionalParams); +}; +console.warn = function (message?: any, ...optionalParams: any[]) { + window.log.warn(message, ...optionalParams); +}; +console.error = function (message?: any, ...optionalParams: any[]) { + window.log.error(message, ...optionalParams); +}; +/* eslint-enable @typescript-eslint/no-unsafe-argument */ + +platformBrowserDynamic() + .bootstrapModule(AppModule, { + preserveWhitespaces: false, + }) + .catch((err) => console.error(err)); + +document.addEventListener("click", (evt: any) => { + if (evt.target.tagName === "A" && evt.target.href.startsWith("http")) { + evt.preventDefault(); + } +}); + +// Disable file drop +document.addEventListener("dragover", (event) => event.preventDefault()); +document.addEventListener("drop", (event) => event.preventDefault()); diff --git a/WowUp/wowup-electron/src/markdown.scss b/WowUp/wowup-electron/src/markdown.scss new file mode 100644 index 0000000..983b8d7 --- /dev/null +++ b/WowUp/wowup-electron/src/markdown.scss @@ -0,0 +1,1001 @@ +//This css based on https://github.com/sindresorhus/github-markdown-css + +.markdown-body { + max-width: 979px; + + * { + th { + font-family: Roboto, Helvetica Neue, sans-serif; + font-size: 1.1em; + font-weight: 500; + } + } +} +.markdown-body .octicon { + display: inline-block; + fill: currentColor; + vertical-align: text-bottom; +} + +.markdown-body .anchor { + float: left; + line-height: 1; + margin-left: -20px; + padding-right: 4px; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1b1f23; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: " "; + display: inline-block; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill-rule='evenodd' d='M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z'%3E%3C/path%3E%3C/svg%3E"); +} +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + line-height: 1.5; + color: #24292e; + // font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, + // Segoe UI Emoji; + // font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body details { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body a { + // background-color: initial; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline-width: 0; +} + +.markdown-body strong { + // font-weight: inherit; + // font-weight: bolder; +} + +.markdown-body h1 { + // font-size: 2em; + margin: 0.67em 0; +} + +.markdown-body img { + border-style: none; + display: block; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre { + // font-family: monospace, monospace; + // font-size: 1em; +} + +.markdown-body hr { + box-sizing: initial; + height: 0; + overflow: visible; +} + +.markdown-body input { + font: inherit; + margin: 0; +} + +.markdown-body input { + overflow: visible; +} + +.markdown-body [type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body * { + box-sizing: border-box; +} + +.markdown-body input { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body strong { + // font-weight: 600; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; +} + +.markdown-body hr:after, +.markdown-body hr:before { + display: table; + content: ""; +} + +.markdown-body hr:after { + clear: both; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + // background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body h1 { + // font-size: 32px; +} + +.markdown-body h1, +.markdown-body h2 { + // font-weight: 600; +} + +.markdown-body h2 { + // font-size: 24px; +} + +.markdown-body h3 { + // font-size: 20px; +} + +.markdown-body h3, +.markdown-body h4 { + // font-weight: 600; +} + +.markdown-body h4 { + // font-size: 16px; +} + +.markdown-body h5 { + // font-size: 14px; +} + +.markdown-body h5, +.markdown-body h6 { + // font-weight: 600; +} + +.markdown-body h6 { + // font-size: 12px; +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ol, +.markdown-body ul { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ol ol ol, +.markdown-body ol ul ol, +.markdown-body ul ol ol, +.markdown-body ul ul ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code, +.markdown-body pre { + // font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + // font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body input::-webkit-inner-spin-button, +.markdown-body input::-webkit-outer-spin-button { + margin: 0; + -webkit-appearance: none; + appearance: none; +} + +.markdown-body :checked + .radio-label { + position: relative; + z-index: 1; + border-color: #0366d6; +} + +.markdown-body .border { + border: 1px solid #e1e4e8 !important; +} + +.markdown-body .border-0 { + border: 0 !important; +} + +.markdown-body .border-bottom { + border-bottom: 1px solid #e1e4e8 !important; +} + +.markdown-body .rounded-1 { + border-radius: 3px !important; +} + +.markdown-body .bg-white { + // background-color: #fff !important; +} + +.markdown-body .bg-gray-light { + // background-color: #fafbfc !important; +} + +.markdown-body .text-gray-light { + // color: #6a737d !important; +} + +.markdown-body .mb-0 { + margin-bottom: 0 !important; +} + +.markdown-body .my-2 { + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .py-2 { + padding-top: 8px !important; + padding-bottom: 8px !important; +} + +.markdown-body .pl-3, +.markdown-body .px-3 { + padding-left: 16px !important; +} + +.markdown-body .px-3 { + padding-right: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body .f6 { + // font-size: 12px !important; +} + +.markdown-body .lh-condensed { + line-height: 1.25 !important; +} + +.markdown-body .text-bold { + // font-weight: 600 !important; +} + +.markdown-body .pl-c { + color: #6a737d; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #005cc5; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #6f42c1; +} + +.markdown-body .pl-s .pl-s1, +.markdown-body .pl-smi { + color: #24292e; +} + +.markdown-body .pl-ent { + color: #22863a; +} + +.markdown-body .pl-k { + color: #d73a49; +} + +.markdown-body .pl-pds, +.markdown-body .pl-s, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sra, +.markdown-body .pl-sr .pl-sre { + color: #032f62; +} + +.markdown-body .pl-smw, +.markdown-body .pl-v { + color: #e36209; +} + +.markdown-body .pl-bu { + color: #b31d28; +} + +.markdown-body .pl-ii { + color: #fafbfc; + // background-color: #b31d28; +} + +.markdown-body .pl-c2 { + color: #fafbfc; + // background-color: #d73a49; +} + +.markdown-body .pl-c2:before { + content: "^M"; +} + +.markdown-body .pl-sr .pl-cce { + // font-weight: 700; + color: #22863a; +} + +.markdown-body .pl-ml { + color: #735c0f; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + // font-weight: 700; + color: #005cc5; +} + +.markdown-body .pl-mi { + font-style: italic; + color: #24292e; +} + +.markdown-body .pl-mb { + // font-weight: 700; + color: #24292e; +} + +.markdown-body .pl-md { + color: #b31d28; + // background-color: #ffeef0; +} + +.markdown-body .pl-mi1 { + color: #22863a; + // background-color: #f0fff4; +} + +.markdown-body .pl-mc { + color: #e36209; + // background-color: #ffebda; +} + +.markdown-body .pl-mi2 { + color: #f6f8fa; + // background-color: #005cc5; +} + +.markdown-body .pl-mdr { + // font-weight: 700; + color: #6f42c1; +} + +.markdown-body .pl-ba { + color: #586069; +} + +.markdown-body .pl-sg { + color: #959da5; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #032f62; +} + +.markdown-body .mb-0 { + margin-bottom: 0 !important; +} + +.markdown-body .my-2 { + margin-bottom: 8px !important; +} + +.markdown-body .my-2 { + margin-top: 8px !important; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .py-2 { + padding-top: 8px !important; + padding-bottom: 8px !important; +} + +.markdown-body .pl-3 { + padding-left: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body .pl-7 { + padding-left: 48px !important; +} + +.markdown-body .pl-8 { + padding-left: 64px !important; +} + +.markdown-body .pl-9 { + padding-left: 80px !important; +} + +.markdown-body .pl-10 { + padding-left: 96px !important; +} + +.markdown-body .pl-11 { + padding-left: 112px !important; +} + +.markdown-body .pl-12 { + padding-left: 128px !important; +} + +.markdown-body hr { + border-bottom-color: #eee; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + // background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; +} + +.markdown-body:after, +.markdown-body:before { + display: table; + content: ""; +} + +.markdown-body:after { + clear: both; +} + +.markdown-body > :first-child { + margin-top: 0 !important; +} + +.markdown-body > :last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body blockquote, +.markdown-body details, +.markdown-body dl, +.markdown-body ol, +.markdown-body p, +.markdown-body pre, +.markdown-body table, +.markdown-body ul { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} + +.markdown-body blockquote { + padding: 0 1em; + color: #dfdfdf; + border-left: 0.25em solid #dfe2e5; +} + +.markdown-body blockquote > :first-child { + margin-top: 0; +} + +.markdown-body blockquote > :last-child { + margin-bottom: 0; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + // font-weight: 600; + line-height: 1.25; +} + +.markdown-body h1 { + // font-size: 2em; +} + +.markdown-body h1, +.markdown-body h2 { + padding-bottom: 0.3em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h2 { + // font-size: 1.5em; +} + +.markdown-body h3 { + // font-size: 1.25em; +} + +.markdown-body h4 { + // font-size: 1em; +} + +.markdown-body h5 { + // font-size: 0.875em; +} + +.markdown-body h6 { + // font-size: 0.85em; + color: #6a737d; +} + +.markdown-body ol, +.markdown-body ul { + padding-left: 2em; +} + +.markdown-body ol ol, +.markdown-body ol ul, +.markdown-body ul ol, +.markdown-body ul ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li { + word-wrap: break-all; +} + +.markdown-body li > p { + margin-top: 16px; +} + +.markdown-body li + li { + margin-top: 0.25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + // font-size: 1em; + font-style: italic; + // font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; +} + +.markdown-body table th { + // font-weight: 600; +} + +.markdown-body table td, +.markdown-body table th { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.markdown-body table tr { + // background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + // background-color: #f6f8fa; +} + +.markdown-body img { + max-width: 100%; + box-sizing: initial; + // background-color: #fff; +} + +.markdown-body img[align="right"] { + padding-left: 20px; +} + +.markdown-body img[align="left"] { + padding-right: 20px; +} + +.markdown-body code { + padding: 0.2em 0.4em; + margin: 0; + // font-size: 85%; + background-color: rgba(27, 31, 35, 0.05); + border-radius: 3px; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre > code { + padding: 0; + margin: 0; + // font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + // font-size: 85%; + line-height: 1.45; + // background-color: #f6f8fa; + border-radius: 3px; +} + +.markdown-body pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + // background-color: initial; + border: 0; +} + +.markdown-body .commit-tease-sha { + display: inline-block; + // font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + // font-size: 90%; + color: #444d56; +} + +.markdown-body .full-commit .btn-outline:not(:disabled):hover { + color: #005cc5; + border-color: #005cc5; +} + +.markdown-body .blob-wrapper { + overflow-x: auto; + overflow-y: hidden; +} + +.markdown-body .blob-wrapper-embedded { + max-height: 240px; + overflow-y: auto; +} + +.markdown-body .blob-num { + width: 1%; + min-width: 50px; + padding-right: 10px; + padding-left: 10px; + // font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + // font-size: 12px; + line-height: 20px; + color: rgba(27, 31, 35, 0.3); + text-align: right; + white-space: nowrap; + vertical-align: top; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.markdown-body .blob-num:hover { + color: rgba(27, 31, 35, 0.6); +} + +.markdown-body .blob-num:before { + content: attr(data-line-number); +} + +.markdown-body .blob-code { + position: relative; + padding-right: 10px; + padding-left: 10px; + line-height: 20px; + vertical-align: top; +} + +.markdown-body .blob-code-inner { + overflow: visible; + // font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + // font-size: 12px; + color: #24292e; + word-wrap: normal; + white-space: pre; +} + +.markdown-body .pl-token.active, +.markdown-body .pl-token:hover { + cursor: pointer; + background: #ffea7f; +} + +.markdown-body .tab-size[data-tab-size="1"] { + -moz-tab-size: 1; + tab-size: 1; +} + +.markdown-body .tab-size[data-tab-size="2"] { + -moz-tab-size: 2; + tab-size: 2; +} + +.markdown-body .tab-size[data-tab-size="3"] { + -moz-tab-size: 3; + tab-size: 3; +} + +.markdown-body .tab-size[data-tab-size="4"] { + -moz-tab-size: 4; + tab-size: 4; +} + +.markdown-body .tab-size[data-tab-size="5"] { + -moz-tab-size: 5; + tab-size: 5; +} + +.markdown-body .tab-size[data-tab-size="6"] { + -moz-tab-size: 6; + tab-size: 6; +} + +.markdown-body .tab-size[data-tab-size="7"] { + -moz-tab-size: 7; + tab-size: 7; +} + +.markdown-body .tab-size[data-tab-size="8"] { + -moz-tab-size: 8; + tab-size: 8; +} + +.markdown-body .tab-size[data-tab-size="9"] { + -moz-tab-size: 9; + tab-size: 9; +} + +.markdown-body .tab-size[data-tab-size="10"] { + -moz-tab-size: 10; + tab-size: 10; +} + +.markdown-body .tab-size[data-tab-size="11"] { + -moz-tab-size: 11; + tab-size: 11; +} + +.markdown-body .tab-size[data-tab-size="12"] { + -moz-tab-size: 12; + tab-size: 12; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item + .task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + margin: 0 0.2em 0.25em -1.6em; + vertical-align: middle; +} diff --git a/WowUp/wowup-electron/src/polyfills-test.ts b/WowUp/wowup-electron/src/polyfills-test.ts new file mode 100644 index 0000000..a201b2c --- /dev/null +++ b/WowUp/wowup-electron/src/polyfills-test.ts @@ -0,0 +1 @@ +import "zone.js"; diff --git a/WowUp/wowup-electron/src/polyfills.ts b/WowUp/wowup-electron/src/polyfills.ts new file mode 100644 index 0000000..db6d570 --- /dev/null +++ b/WowUp/wowup-electron/src/polyfills.ts @@ -0,0 +1,52 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import "zone.js"; // Included with Angular CLI. + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/WowUp/wowup-electron/src/styles.scss b/WowUp/wowup-electron/src/styles.scss new file mode 100644 index 0000000..ebbc828 --- /dev/null +++ b/WowUp/wowup-electron/src/styles.scss @@ -0,0 +1,660 @@ +@import "./markdown.scss"; +@import "./custom-theme.scss"; + +:not(input):not(textarea), +:not(input):not(textarea)::after, +:not(input):not(textarea)::before { + -webkit-user-select: none !important; + user-select: none !important; +} + +input, +button, +textarea, +:focus { + outline: none; // You should add some other style for :focus to help UX/a11y +} + +a:not([draggable="true"]), +img:not([draggable="true"]) { + -webkit-user-drag: none; + user-drag: none; + /* Technically not supported in Electron yet */ +} + +a[href^="http://"], +a[href^="https://"], +a[href^="ftp://"] +{ + -webkit-user-drag: auto; + user-drag: auto; + /* Technically not supported in Electron yet */ +} + +.cata-bg { + background-image: url("~assets/images/wow-cata-background.jpg"); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: cover; + background-position: center; + z-index: -10; +} + +.selectable { + user-select: text !important; + -webkit-user-select: text !important; + + * { + user-select: text !important; + -webkit-user-select: text !important; + } +} + +html { + overflow: hidden; + -webkit-app-region: no-drag; +} + +body { + overflow: hidden; + margin: 0; + -webkit-app-region: no-drag; +} + +a, +img { + -webkit-app-region: no-drag; +} + +[hidden] { + display: none !important; +} + +.rounded { + border-radius: 4px; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25em !important; +} + +.p-2 { + padding: 0.5em !important; +} + +.p-3 { + padding: 1em !important; +} + +.pl-1 { + padding-left: 0.25em; +} + +.pl-2 { + padding-left: 0.5em; +} + +.pl-3 { + padding-left: 1em; +} + +.pr-1 { + padding-right: 0.25em; +} + +.pr-2 { + padding-right: 0.5em; +} + +.pr-3 { + padding-right: 1em; +} + +.px-1 { + padding: 0 0.25em; +} + +.px-2 { + padding: 0 0.5em; +} + +.px-3 { + padding: 0 1em; +} + +.pt-1 { + padding-top: 0.25em; +} + +.pt-2 { + padding-top: 0.5em; +} + +.pt-3 { + padding-top: 1em; +} + +.m-0 { + margin: 0 !important; +} + +.mr-1 { + margin-right: 0.25em !important; +} + +.mr-2 { + margin-right: 0.5em !important; +} + +.mr-3 { + margin-right: 1em !important; +} + +.ml-1 { + margin-left: 0.25em !important; +} + +.ml-2 { + margin-left: 0.5em !important; +} + +.ml-3 { + margin-left: 1em !important; +} + +.mt-1 { + margin-top: 0.25em !important; +} + +.mt-2 { + margin-top: 0.5em !important; +} + +.mt-3 { + margin-top: 1em !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} +.mb-1 { + margin-bottom: 0.25em !important; +} + +.mb-2 { + margin-bottom: 0.5em !important; +} + +.mb-3 { + margin-bottom: 1em !important; +} + +.h-100 { + height: 100%; +} + +.w-100 { + width: 100%; +} + +.text-center { + text-align: center; +} + +.pre-wrap { + white-space: pre-wrap; +} + +.no-reg-drag { + -webkit-app-region: no-drag; +} + +.no-link { + pointer-events: none; + text-decoration: none; + color: inherit !important; +} + +.pointer { + pointer-events: all !important; + + &:hover { + cursor: pointer !important; + } +} + +.no-pointer-events { + pointer-events: none; +} + +.option-icon { + width: 17px !important; + height: 17px !important; +} + +mat-icon.success-icon { + color: green; +} + +.grow-tab-content { + .mat-mdc-tab-body-wrapper { + flex-grow: 1; + } +} + +.mat-tab-label, +.mat-tab-label-content, +.mat-select-value, +.mat-form-field-infix { + &:hover { + cursor: pointer !important; + } +} + +.window-appicon { + background-size: 30px !important; + padding-left: 12px !important; +} + +.tab-container { + overflow: auto; + overflow-x: hidden; + height: 100%; + + &.mac { + // height: calc(100vh - 95px); + } + + &.windows { + // height: calc(100vh - 103px); + } + + &.linux { + height: calc(100vh - 103px); + } +} + +.no-pad-select { + .mat-form-field-wrapper { + padding-bottom: 0 !important; + } +} + +.d-inline { + display: inline; +} + +.d-flex { + display: flex; + flex-direction: row; + + &.align-items-center { + align-items: center; + } +} + +.flex-col { + flex-direction: column; +} + +.flex-grow-1 { + flex-grow: 1; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +.col { + display: flex; + flex-direction: column; + + &.align-items-center { + align-items: center; + } + + &.align-items-start { + align-items: flex-start; + } + + &.justify-content-center { + justify-content: center; + } + + &.justify-content-end { + justify-content: flex-end; + } +} + +.row { + display: flex; + flex-direction: row; + + &.align-items-center { + align-items: center; + } + + &.align-items-start { + align-items: flex-start; + } + + &.justify-content-center { + justify-content: center; + } + + &.justify-content-end { + justify-content: flex-end; + } + &.justify-content-between { + justify-content: space-between; + } +} + +.text-right { + text-align: right; +} +.text-left { + text-align: left; +} + +.addon-summary { + img { + max-width: 100%; + } +} + +.addon-summary, +.addon-changelog { + strong, + b { + font-size: 16px; + font-weight: 400; + font-style: normal; + } +} + +.addon-table { + width: 100%; + height: 100%; + + .cell-break-all { + word-break: break-all; + } + + .cell-wrap-text { + white-space: normal; + } +} + +::-webkit-scrollbar-track, +::-webkit-scrollbar-thumb { + border: 4px solid transparent; + border-radius: 8px; +} + +::-webkit-scrollbar-corner { + background: transparent; +} + +.ignored { + color: var(--text-4) !important; +} + +.addon-context-menu { + .addon-context-menu-header { + padding: 0 16px 8px 16px; + display: flex; + flex-direction: row; + align-items: center; + + .addon-name { + word-break: break-all; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + .thumbnail { + width: 30px; + height: 30px; + margin-right: 16px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + overflow: hidden; + flex-shrink: 0; + + img { + height: 100%; + } + + .thumbnail-letter { + color: var(--text-4); + font-size: 1.5em; + font-weight: 400; + } + } + + .addon-name { + font-size: 1em; + } + + .addon-update-available { + mat-icon { + height: 10px; + } + } + } +} + +.light-select { + .mat-form-field-label { + color: var(--text-2) !important; + } +} + +.vertical-radio-group { + display: flex; + flex-direction: column; +} + +.cell-break-all { + word-break: break-all; +} + +.cell-wrap-text { + white-space: normal; + -webkit-line-clamp: 2; + text-overflow: ellipsis; +} + +.max-lines-3 { + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + overflow: hidden; + -webkit-box-orient: vertical; +} + +.cell-center-text { + display: flex; + flex-wrap: nowrap; + align-items: center; +} + +.install-button { + min-width: 120px !important; +} + +.progress-button { + position: relative; + overflow: hidden; + min-width: 120px !important; + + .progress-bar { + height: 1rem; + // border-top-right-radius: 4px; + // border-top-left-radius: 4px; + width: 100%; + // left: 0; + // position: absolute; + } + + .mat-progress-bar-buffer { + background: var(--text-4) !important; + } + + .mat-progress-bar-fill::after { + background: var(--background-primary) !important; + } +} + +.error-text { + color: red; +} + +.no-tabs { + &.mat-tab-group { + .mat-tab-header { + display: none; + } + } +} + +.snackbar-error { + border: 2px solid red; +} + +.snackbar-success { + border: 2px solid green; +} + +.bnet-btn { + background-color: #0074e0; + border: solid 2px transparent; + color: #fff; + font-size: 16px; + font-weight: 500; + transition: + background-color 0.2s, + border-color 0.2s, + color 0.2s; + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + text-decoration: none; + padding-left: 0.5em; + padding-right: 0.5em; +} +.bnet-btn-sm { + font-size: inherit; + border-radius: 4px; + // border: 1px solid white; + cursor: pointer; +} +.bnet-btn:hover { + background-color: #0074e0; + border-color: #47a6ff; + color: #fff; +} + +.update-badge { + background-color: var(--background-primary); + padding: 0.25em 0.5em; + border-radius: 4px; + font-size: 75%; + &.badge-lg { + font-size: 100%; + } +} + +// MATERIAL OVERRIDES + +.no-input-border { + .mat-form-field-infix { + border: none !important; + } +} + +.z-badge { + .mat-badge-content { + z-index: 100; + } +} + +.clear-badge { + .mat-badge-content { + color: transparent; + } +} + +.mat-tooltip { + font-size: 0.95em; +} + +.mat-tab-labels { + z-index: 2; +} + +th.mat-header-cell .mat-sort-header-container.mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + transform: translateY(0) !important; +} + +.mat-menu-content { + .mat-checkbox { + display: block; + } +} + +.header-less-tabs mat-tab-group mat-tab-header { + display: none; +} + +// SCROLLBAR +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + border-radius: 4px; + border-width: 1px; + border-style: solid; +} + +@keyframes pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); + } +} + +@import "./controls.scss"; diff --git a/WowUp/wowup-electron/src/test.ts b/WowUp/wowup-electron/src/test.ts new file mode 100644 index 0000000..6f21a1a --- /dev/null +++ b/WowUp/wowup-electron/src/test.ts @@ -0,0 +1,17 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), { + teardown: { destroyAfterEach: false } + } +); \ No newline at end of file diff --git a/WowUp/wowup-electron/src/tsconfig.app.json b/WowUp/wowup-electron/src/tsconfig.app.json new file mode 100644 index 0000000..409f6ab --- /dev/null +++ b/WowUp/wowup-electron/src/tsconfig.app.json @@ -0,0 +1,21 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "es2015", + "baseUrl": "", + "types": [] + }, + "include": [ + "main.ts", + "polyfills.ts", + "app/**/*.ts", + "common/**/*.ts", + "environments/**/*.ts", + ], + "exclude": [ + "**/*.spec.ts", + "app/**/tests/*.ts", + "app/**/utils/test.utils.ts", + ] +} diff --git a/WowUp/wowup-electron/src/tsconfig.spec.json b/WowUp/wowup-electron/src/tsconfig.spec.json new file mode 100644 index 0000000..f2bbb1e --- /dev/null +++ b/WowUp/wowup-electron/src/tsconfig.spec.json @@ -0,0 +1,23 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills-test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ], + "exclude": [ + "dist", + "release", + "node_modules" + ] +} diff --git a/WowUp/wowup-electron/src/typings.d.ts b/WowUp/wowup-electron/src/typings.d.ts new file mode 100644 index 0000000..7ba4513 --- /dev/null +++ b/WowUp/wowup-electron/src/typings.d.ts @@ -0,0 +1,11 @@ +/* SystemJS module definition */ +declare let nodeModule: NodeModule; +interface NodeModule { + id: string; +} +interface Window { + process: any; + require: any; +} + +declare type DetailsTabType = "description" | "changelog" | "previews"; diff --git a/WowUp/wowup-electron/src/variables.scss b/WowUp/wowup-electron/src/variables.scss new file mode 100644 index 0000000..5739f78 --- /dev/null +++ b/WowUp/wowup-electron/src/variables.scss @@ -0,0 +1,17 @@ +// WOWUP +$wowup-purple-1: #8c9eff; +$wowup-purple-3: #3d5afe; +$wowup-control: #536dfe; + +// HORDE +// Material Color Palette - http://mcg.mbitson.com/#!?horde=%238c1616 +$horde-red-1: #8c1616; // Header Color - Primary 500 +$horde-red-3: #5c0606; // Scroll bar toggle color - Primary 900 +$horde-control: #ff5b5b; // Option toggle color - Accent 200 + +// ALLIANCE +// Material Color Palette - http://mcg.mbitson.com/#!?alliance=%23162c57 +$alliance-blue-1: #162c57; // Header color - Primary 500 +$alliance-blue-2: #394c70; // Secondary color - Primary 400 +$alliance-blue-3: #06102c; // Scroll bar toggle color - Primary 900 +$alliance-control: #335eff; // Option toggle color - Accent 200 diff --git a/WowUp/wowup-electron/test-fixer.js b/WowUp/wowup-electron/test-fixer.js new file mode 100644 index 0000000..d2c73be --- /dev/null +++ b/WowUp/wowup-electron/test-fixer.js @@ -0,0 +1,168 @@ +const fs = require("fs"); +const path = require("path"); +const ignore = require("ignore"); +const { execSync, spawn } = require("child_process"); + +const gitIgnoreFile = fs.readFileSync(".gitignore", { encoding: "utf-8" }); +const gitIgnore = gitIgnoreFile.split("\n"); + +const ig = ignore().add(gitIgnore); + +const shouldClear = process.argv.indexOf("--clear") !== -1; +const shouldRepair = process.argv.indexOf("--repair") !== -1; +const shouldStep = process.argv.indexOf("--step") !== -1; +const shouldFindBreak = process.argv.indexOf("--find-break") !== -1; + +const getAllFiles = function (dirPath, arrayOfFiles) { + files = fs.readdirSync(dirPath); + + arrayOfFiles = arrayOfFiles || []; + + files.forEach(function (file) { + const filePath = path.join(dirPath, file); + + if (ig.ignores(filePath)) { + return; + } + + if (fs.statSync(filePath).isDirectory()) { + arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles); + } else { + arrayOfFiles.push(path.join(__dirname, dirPath, "/", file)); + } + }); + + return arrayOfFiles; +}; + +function repairPath(ogPath) { + const newPath = ogPath.slice(0, -1); + fs.renameSync(ogPath, newPath); + console.log(`${ogPath} -> ${newPath}`); +} + +function executeTests() { + return new Promise((resolve, reject) => { + try { + console.log(`Running ${countTests()} tests`); + const ls = spawn("ng.cmd", ["test", "--watch=false"], { + cwd: __dirname, + }); + + let output = []; + ls.stdout.on("data", (data) => { + // console.log(`stdout: ${data}`); + if (typeof data === "string") { + output.push(data); + } + }); + + ls.stderr.on("data", (data) => { + // console.error(`stderr: ${data}`); + if (typeof data === "string") { + output.push(data); + } + }); + + ls.on("close", (code) => { + if (code !== 0) { + console.log(output.join("\n")); + } + console.log(`child process exited with code ${code}`); + resolve(code); + }); + } catch (error) { + error.status; // Might be 127 in your example. + error.message; // Holds the message you typically want. + error.stderr; // Holds the stderr output. Use `.toString()`. + error.stdout; // Holds the stdout output. Use `.toString()`. + console.error(error.message || error); + } + }); +} + +function getTests() { + const allFiles = getAllFiles(".", []); + return allFiles.filter((f) => f.endsWith(".spec.ts")); +} + +function getTestOverrides() { + const allFiles = getAllFiles(".", []); + return allFiles.filter((f) => f.endsWith(".spec.tsw")); +} + +function countTestsOverrides() { + return getTestOverrides().length; +} + +function countTests() { + return getTests().length; +} + +function runClear() { + console.log(`Running Clear`); + const allFiles = getAllFiles(".", []); + const testFiles = allFiles.filter((f) => f.endsWith(".spec.ts")); + + testFiles.forEach((f) => { + const newPath = f + "w"; + fs.renameSync(f, newPath); + console.log(`${f} -> ${newPath}`); + }); +} + +function runRepair() { + console.log(`Running Repair`); + const allFiles = getAllFiles(".", []); + const testFiles = allFiles.filter((f) => f.endsWith(".spec.tsw")); + + testFiles.forEach((f) => { + repairPath(f); + }); +} + +function runStep() { + console.log(`Running Next`); + const allFiles = getAllFiles(".", []); + const testFiles = allFiles.filter((f) => f.endsWith(".spec.tsw")); + + const firstFile = testFiles[0]; + + repairPath(firstFile); +} + +async function runFindBreak() { + console.log(`Running Find Break`); + runClear(); + + while (countTestsOverrides() > 0) { + runStep(); + const code = await executeTests(); + if (code !== 0) { + throw new Error("executeTests failed"); + } + } +} + +if (shouldClear) { + runClear(); +} + +if (shouldRepair) { + runRepair(); +} + +if (shouldStep) { + runStep(); +} + +if (shouldFindBreak) { + runFindBreak().catch((e) => { + const allTests = getTests(); + const lastTestFile = allTests[allTests.length - 1]; + const relativePath = lastTestFile.substr(lastTestFile.indexOf("src\\") + 4); + console.error(`Failed test found: ${lastTestFile}`); + console.error(`ng test --watch=false --include='${relativePath}'`); + console.error(e); + }); +} diff --git a/WowUp/wowup-electron/tsconfig.json b/WowUp/wowup-electron/tsconfig.json new file mode 100644 index 0000000..0d7a5b4 --- /dev/null +++ b/WowUp/wowup-electron/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "module": "ES2022", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowJs": true, + "downlevelIteration": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "es2016", + "es2015", + "dom", + "ES2019.Array", + "ES2020.String", + "ES2021.String", + "ESNext.String" + ], + "useDefineForClassFields": false + }, + "include": [ + "src/**/*.d.ts" + ], + "exclude": [ + "node_modules" + ], + "angularCompilerOptions": { + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "preserveWhitespaces": true + } +} diff --git a/WowUp/wowup-electron/tsconfig.serve.json b/WowUp/wowup-electron/tsconfig.serve.json new file mode 100644 index 0000000..64023f6 --- /dev/null +++ b/WowUp/wowup-electron/tsconfig.serve.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es2017", + "module": "CommonJS", + "types": [ + "node" + ], + "lib": [ + "es2017", + "es2016", + "es2015", + "dom" + ] + }, + "files": [ + "app/main.ts", + "app/preload.ts" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} + \ No newline at end of file