diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index b63c33a..86004b9 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -24,7 +24,7 @@ jobs: run: | $version = [regex]::Match((Get-Content CMakeLists.txt -Raw), 'project\(GoPieMenu VERSION ([0-9.]+)').Groups[1].Value if ($env:GITHUB_REF_NAME -ne "v$version") { throw "Release tag does not match the application version." } - if (!(Test-Path -LiteralPath "docs/releases/$env:GITHUB_REF_NAME.md")) { throw "Release notes are missing." } + ./scripts/release-notes.ps1 -Version $env:GITHUB_REF_NAME -OutputPath build/release-notes.md - uses: ilammy/msvc-dev-cmd@v1 - uses: jurplel/install-qt-action@v3 with: @@ -67,10 +67,13 @@ jobs: with: name: windows-package path: package + - name: Extract multilingual release notes + shell: pwsh + run: ./scripts/release-notes.ps1 -Version $env:GITHUB_REF_NAME -OutputPath release-notes.md - uses: softprops/action-gh-release@v2 with: name: GoPieMenu ${{ github.ref_name }} - body_path: docs/releases/${{ github.ref_name }}.md + body_path: release-notes.md fail_on_unmatched_files: true files: | package/GoPieMenu_Setup.exe diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7b7588c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,154 @@ +# Changelog + +## [2.1.0] + +### English + + +GoPieMenu now supports English, Traditional Chinese, and Simplified Chinese. New configurations follow the system language by default, falling back to English for unsupported languages. You can choose a language in Settings; apply the change and restart the app for it to take effect. Menu edits still require an explicit Apply, and unapplied changes are handled before the app exits. + +#### What's new + +- Chinese interfaces use Microsoft JhengHei UI. Changing language offers Later or a blue Restart button; Restart saves all pending edits first. +- Manual launches show the editor, including when already running. Windows autostart stays in the tray. + +- GoPieMenu checks for stable releases automatically at startup. When an update is available, it shows a tray notification if the window is hidden or a dialog if the window is open. +- Release notes appear in the current UI language, with English used when a translation is unavailable. +- Skip a release to stop being notified about that version. +- Download the installer and verify its SHA256 checksum. Before installation, confirm the update and resolve any unapplied edits; GoPieMenu then exits so the installer can run. + + + +### 繁體中文 + + +GoPieMenu 現在支援英文、繁體中文與簡體中文。新設定預設依照系統語言顯示,不支援的語言則使用英文。你也可以在「設定」中選擇語言,套用後重新啟動應用程式生效。選單編輯仍須明確按下「套用」,結束應用程式前也會處理尚未套用的變更。 + +#### 新功能 + +- 中文介面使用微軟正黑體 UI。切換語言會提供「稍後」與藍色「重新啟動」按鈕,重新啟動前會套用所有待存變更。 +- 手動開啟會顯示編輯器,已在背景執行時也會喚回;Windows 自動啟動則留在系統匣。 + +- GoPieMenu 會在啟動時自動檢查穩定版更新。若有更新可用,視窗隱藏時會顯示系統匣通知;視窗開啟時則會顯示對話方塊。 +- 發行說明會以目前介面語言顯示;若缺少該語言的翻譯,則改用英文。 +- 可以略過某個版本,之後不再收到該版本的通知。 +- 下載安裝程式並驗證 SHA256 校驗碼。安裝前請確認要更新,並處理尚未套用的變更;接著 GoPieMenu 會結束,讓安裝程式執行。 + + + +### 简体中文 + + +GoPieMenu 现在支持英语、繁体中文和简体中文。新配置默认跟随系统语言显示,不支持的语言则使用英语。你也可以在“设置”中选择语言,应用后重启程序生效。编辑菜单仍需明确点击“应用”,退出应用前也会处理尚未应用的更改。 + +#### 新功能 + +- 中文界面使用 Microsoft JhengHei UI。切换语言会提供“稍后”与蓝色“重新启动”按钮,重新启动前会应用所有待保存更改。 +- 手动打开会显示编辑器,已在后台运行时也会唤回;Windows 自动启动则留在系统托盘。 + +- GoPieMenu 会在启动时自动检查稳定版更新。如果有可用更新,窗口隐藏时会显示系统托盘通知;窗口打开时则会显示对话框。 +- 发布说明会以当前界面语言显示;如果没有对应语言的翻译,则使用英语。 +- 可以跳过某个版本,之后不再收到该版本的通知。 +- 下载安装程序并验证 SHA256 校验和。安装前请确认更新,并处理尚未应用的更改;随后 GoPieMenu 会退出,以便运行安装程序。 + + + + +### Validation + +The local Release build and all seven CTest entries passed, including core and update regression tests, release-note extraction, and UI tests at 100%, 125%, 150%, and 200% scaling. Deployed startup with the SDK removed from PATH, TLS backend loading, and installer packaging also passed. + +Language tests cover Later, save-before-restart, save failure, and launch failure at all four scales. A deployed process also passed the wait-for-parent restart check. Update tests use simulated network responses and installer launches. Actual Windows sign-in startup, notification delivery, and an elevated installation remain manual acceptance checks. + + + +## [2.0.0] + +### English + + +GoPieMenu 2.0 rebuilds the menu editor around a live preview, independent profile appearance, and an explicit Apply/Discard workflow. + +#### What's new + +- Edit actions directly from the pie preview. Switch profiles without losing drafts, and use Undo/Redo before applying changes to the running menu. +- Arrange actions by dragging an entire row, with insertion markers, edge scrolling, and selection preserved. Right-click preview or organizer actions to duplicate or delete them. +- Customize each profile independently: presets, size, colors, opacity, and animation. Duplicating a profile copies its appearance without linking later edits. +- Choose colors with a hue ring, saturation/value square, HSV/opacity controls, RGB/RGBA hex input, and recent colors. +- Use a searchable icon picker with asynchronous loading and embedded icons. Custom SVG, PNG, JPEG, and ICO files remain supported. +- Combine Ctrl, Shift, Alt, and Win modifiers independently with mouse triggers. Keyboard recording keeps the selected activation mode. +- Save empty actions as placeholders; triggering them does nothing. +- Enjoy a refreshed light editor, consistent dialogs and tray menu, new application artwork, and custom window controls. + +#### Upgrading from 1.0 + +1. Export your configuration from Settings, then quit GoPieMenu from the tray before running the installer. +2. Download and run `GoPieMenu_Setup.exe` from this release. This is the Windows x64 installer; Qt dependencies are included. +3. Open Settings, review your profiles, and use **Apply** to save changes. Closing the editor hides it to the tray; quitting prompts about unapplied edits. + +The configuration location and version 1.0 JSON format are retained. Existing appearance values and supported legacy style keys are preserved. Appearance edits now affect only the selected profile. Choose **Frost** to opt into the new, nearly opaque preset. Invalid configurations are reported without overwriting the original file; import is an undoable draft operation. + + + +### 繁體中文 + + +GoPieMenu 2.0 重新打造選單編輯器,提供即時預覽、各設定檔獨立的外觀,以及明確的「套用/捨棄」流程。 + +#### 新功能 + +- 直接在圓餅預覽中編輯動作。切換設定檔時草稿會保留;套用至執行中的選單前,可以使用復原/重做。 +- 拖曳整列即可排列動作,並提供插入標記、邊緣捲動且保留選取狀態。在預覽或整理清單中的動作上按右鍵,即可複製或刪除。 +- 為每個設定檔獨立自訂外觀:預設樣式、大小、顏色、不透明度與動畫。複製設定檔時會一併複製外觀,之後的編輯則彼此獨立。 +- 使用色相環、飽和度/明度方塊、HSV/透明度控制項、RGB/RGBA 十六進位輸入及最近使用的顏色來選色。 +- 使用可搜尋的圖示選擇器,支援非同步載入與內嵌圖示。仍可使用自訂 SVG、PNG、JPEG 與 ICO 檔案。 +- 滑鼠觸發方式可各自搭配 Ctrl、Shift、Alt、Win 修飾鍵。錄製鍵盤快捷鍵時會保留已選的啟用模式。 +- 可以將尚未設定的動作儲存為佔位項目;觸發這類項目不會執行任何操作。 +- 編輯器改用清爽明亮的外觀,並統一對話方塊與系統匣選單樣式,加入全新應用程式圖示及自訂視窗控制按鈕。 + +#### 從 1.0 升級 + +1. 在「設定」中匯出設定,然後先從系統匣結束 GoPieMenu,再執行安裝程式。 +2. 下載並執行此版本中的 `GoPieMenu_Setup.exe`。這是 Windows x64 安裝程式,已包含 Qt 相依元件。 +3. 開啟「設定」並檢查各設定檔,然後按下「套用」儲存變更。關閉編輯器會將它隱藏至系統匣;結束程式時會詢問如何處理尚未套用的變更。 + +設定檔位置與 1.0 版 JSON 格式維持不變,既有外觀值及支援的舊版樣式索引鍵也會保留。外觀變更現在只會影響選取的設定檔。選擇 **Frost** 即可使用全新且接近不透明的預設樣式。遇到無效設定時會顯示錯誤,不會覆寫原始檔案;匯入設定則會建立可復原的草稿。 + + + +### 简体中文 + + +GoPieMenu 2.0 重新打造了菜单编辑器,提供实时预览、各配置独立的外观,以及明确的“应用/放弃”流程。 + +#### 新功能 + +- 直接在饼形菜单预览中编辑操作。切换配置时草稿会保留;应用到正在运行的菜单前,可以使用撤销/重做。 +- 拖动整行即可排列操作,并提供插入标记、边缘滚动且保留选中状态。在预览或整理列表中的操作上单击右键,即可复制或删除。 +- 为每个配置单独自定义外观:预设样式、大小、颜色、不透明度和动画。复制配置时会一并复制外观,后续编辑则互不影响。 +- 使用色相环、饱和度/明度方块、HSV/不透明度控件、RGB/RGBA 十六进制输入和最近使用的颜色来选色。 +- 使用可搜索的图标选择器,支持异步加载和内嵌图标。仍可使用自定义 SVG、PNG、JPEG 和 ICO 文件。 +- 鼠标触发方式可分别搭配 Ctrl、Shift、Alt、Win 修饰键。录制键盘快捷键时会保留已选的激活模式。 +- 可以将尚未设置的操作保存为占位项;触发这类项目不会执行任何操作。 +- 编辑器采用清爽明亮的新外观,并统一对话框和系统托盘菜单样式,加入全新应用图标及自定义窗口控制按钮。 + +#### 从 1.0 升级 + +1. 在“设置”中导出配置,然后先从系统托盘退出 GoPieMenu,再运行安装程序。 +2. 下载并运行此版本中的 `GoPieMenu_Setup.exe`。这是 Windows x64 安装程序,已包含 Qt 依赖项。 +3. 打开“设置”并检查各配置,然后点击“应用”保存更改。关闭编辑器会将其隐藏到系统托盘;退出程序时会询问如何处理尚未应用的更改。 + +配置文件位置和 1.0 版 JSON 格式保持不变,已有外观值及受支持的旧版样式键也会保留。外观更改现在只会影响选中的配置。选择 **Frost** 即可使用全新且近乎不透明的预设样式。遇到无效配置时会显示错误,不会覆盖原始文件;导入配置会创建可撤销的草稿。 + + + + +### Validation + +The release workflow runs core tests and UI tests at 100%, 125%, 150%, and 200% scaling, then checks that the deployed application starts without the Qt SDK on PATH before compiling the installer. `SHA256SUMS.txt` contains the installer's checksum. + +Real input behavior in individual applications, interactions between Win-key combinations and Windows system shortcuts, elevated applications, and moving between monitors with different physical scaling settings remain manual verification areas. + +Full changes: https://github.com/RyuuMeow/GoPieMenu/compare/v1.0.0...v2.0.0 + + diff --git a/CMakeLists.txt b/CMakeLists.txt index 4581850..5238c0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.21) -project(GoPieMenu VERSION 2.0.0 LANGUAGES CXX) +project(GoPieMenu VERSION 2.1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -7,7 +7,7 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) include(CTest) option(GPM_BUILD_DESKTOP "Build the Windows desktop application" ON) -find_package(Qt6 6.9 REQUIRED COMPONENTS Core Gui) +find_package(Qt6 6.9 REQUIRED COMPONENTS Core Gui Network LinguistTools) add_library(gpm_core STATIC src/core/ConfigValidation.cpp @@ -15,9 +15,22 @@ add_library(gpm_core STATIC src/core/EditorSession.cpp src/core/InputState.cpp src/core/Shortcut.cpp + src/core/TranslationManager.cpp ) target_include_directories(gpm_core PUBLIC src) target_link_libraries(gpm_core PUBLIC Qt6::Core Qt6::Gui) + +set(GPM_TRANSLATIONS + resources/i18n/gopiemenu_zh_TW.ts + resources/i18n/gopiemenu_zh_CN.ts +) +qt_add_lrelease(TS_FILES ${GPM_TRANSLATIONS} QM_FILES_OUTPUT_VARIABLE GPM_QM_FILES) +qt_add_resources(gpm_core "translations" PREFIX "/i18n" + BASE "${CMAKE_CURRENT_BINARY_DIR}" FILES ${GPM_QM_FILES}) +add_custom_target(update-translations + COMMAND Qt6::lupdate src -no-obsolete -ts ${GPM_TRANSLATIONS} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Update translation catalogs from C++ and QML sources") target_compile_definitions(gpm_core PUBLIC UNICODE _UNICODE NOMINMAX WIN32_LEAN_AND_MEAN APP_VERSION="${PROJECT_VERSION}") if(MSVC) target_compile_options(gpm_core PUBLIC /utf-8 /W4) @@ -26,6 +39,12 @@ if(WIN32) target_link_libraries(gpm_core PUBLIC user32) endif() +add_library(gpm_updates STATIC src/update/ReleaseInfo.cpp src/update/UpdateManager.cpp) +target_link_libraries(gpm_updates PUBLIC gpm_core Qt6::Network) +if(WIN32) + target_link_libraries(gpm_updates PRIVATE shell32) +endif() + if(GPM_BUILD_DESKTOP) find_package(Qt6 6.9 REQUIRED COMPONENTS Widgets Quick Qml QuickControls2 Svg) add_library(gpm_ui STATIC @@ -41,7 +60,7 @@ if(GPM_BUILD_DESKTOP) src/ui/RunningApplications.cpp ) target_include_directories(gpm_ui PUBLIC src) - target_link_libraries(gpm_ui PUBLIC gpm_core Qt6::Widgets Qt6::Quick Qt6::Qml Qt6::QuickControls2 Qt6::Svg) + target_link_libraries(gpm_ui PUBLIC gpm_core gpm_updates Qt6::Widgets Qt6::Quick Qt6::Qml Qt6::QuickControls2 Qt6::Svg) if(WIN32) target_link_libraries(gpm_ui PRIVATE dwmapi) endif() @@ -78,6 +97,14 @@ if(BUILD_TESTING) add_executable(gpm_core_tests tests/CoreTests.cpp) target_link_libraries(gpm_core_tests PRIVATE gpm_core Qt6::Test) add_test(NAME core COMMAND gpm_core_tests -o artifacts/core.txt,txt) + add_executable(gpm_update_tests tests/UpdateTests.cpp) + target_link_libraries(gpm_update_tests PRIVATE gpm_updates Qt6::Test) + target_compile_definitions(gpm_update_tests PRIVATE GPM_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") + add_test(NAME updates COMMAND gpm_update_tests -o artifacts/updates.txt,txt) + if(WIN32) + find_program(GPM_POWERSHELL NAMES pwsh powershell REQUIRED) + add_test(NAME release-notes COMMAND "${GPM_POWERSHELL}" -NoProfile -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/ReleaseNotesTests.ps1") + endif() if(GPM_BUILD_DESKTOP) add_executable(gpm_ui_tests tests/UiTests.cpp) target_link_libraries(gpm_ui_tests PRIVATE gpm_ui gpm_actions Qt6::Test) diff --git a/README.md b/README.md index 8ae6a78..3bf93cd 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,14 @@ A customizable Windows radial menu for shortcuts, applications, files, websites, and commands. -[Download v2.0.0 for Windows x64](https://github.com/RyuuMeow/GoPieMenu/releases/tag/v2.0.0) · [What's new and upgrading](docs/releases/v2.0.0.md) +[Download v2.1.0 for Windows x64](https://github.com/RyuuMeow/GoPieMenu/releases/tag/v2.1.0) · [What's new and upgrading](CHANGELOG.md#210) ![Light editor](resources/demo/Editor2.png) +The interface supports English, Traditional Chinese, and Simplified Chinese. New configurations follow the system UI language; unsupported languages use English. Settings → Language offers **Later** or a blue **Restart** button. Restart applies all pending changes; Later keeps the draft for further editing. Chinese interfaces use Microsoft JhengHei UI. Existing configurations with an explicit language keep that choice. Your saved menu and action names are not retranslated. + +Opening GoPieMenu manually shows the editor, including when it is already running in the tray. Windows startup keeps it in the tray. + ## Edit your menu 1. Open **Settings** from the tray icon. Use **+** beside the menu selector to create and select a menu. The adjacent three-dot menu contains New, Duplicate, and Delete; deleting selects the first remaining menu. @@ -34,9 +38,17 @@ Configuration remains version 1.0 JSON, stored under Qt's application-data direc The application and [tray menu](resources/demo/TrayMenu.png) use the new Pie Menu artwork and light styling. The GitHub button beside **About** opens this project's repository. +## Updates + +GoPieMenu checks for newer stable GitHub releases at startup. A visible editor shows an update dialog; when hidden or minimized, a tray notification opens the same dialog. **Skip this version** remembers that release; **Settings → Check for updates** can revisit it. + +Release notes use the current interface language, with English fallback and shared validation information. **Update** downloads the Windows installer and verifies `SHA256SUMS.txt`. **Install and exit** asks you to resolve unapplied edits, then opens the Windows installer permission prompt. Cancelling that prompt keeps GoPieMenu running. Closing the update dialog with **Later** does not install anything. + +The [changelog](CHANGELOG.md) is the source for all release notes. Publishing a version extracts only its section into the GitHub Release body, which the application reads. Preview and smoke-test modes do not check for updates. + ## Build and verify -Requires Windows, Visual Studio 2022 with C++ tools, CMake 3.21+, and Qt 6.9 with Widgets, Quick, QML, Quick Controls, SVG, and Test. +Requires Windows, Visual Studio 2022 with C++ tools, CMake 3.21+, and Qt 6.9 with Widgets, Quick, QML, Quick Controls, SVG, Network, Linguist Tools, and Test. ```powershell ./scripts/build.ps1 -QtRoot "C:/Qt/6.9.0/msvc2022_64" diff --git a/README.zh-CN.md b/README.zh-CN.md index bac5953..9c4453f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,10 +4,14 @@ Windows 圆形快捷菜单,支持快捷键、应用程序、文件、网址和命令。 -[下载 v2.0.0(Windows x64)](https://github.com/RyuuMeow/GoPieMenu/releases/tag/v2.0.0) · [更新与升级说明](docs/releases/v2.0.0.md) +[下载 v2.1.0(Windows x64)](https://github.com/RyuuMeow/GoPieMenu/releases/tag/v2.1.0) · [更新与升级说明](CHANGELOG.md#210) ![浅色编辑器](resources/demo/Editor2.png) +界面支持英文、繁体中文及简体中文。新配置默认跟随系统界面语言,不支持的语言使用英文。切换“设置 → 语言”会显示“稍后”及蓝色“重新启动”按钮;重新启动会先应用所有待保存更改,稍后则保留草稿继续编辑。中文界面使用 Microsoft JhengHei UI。已有配置若已指定语言,会保留该选择;已保存的菜单及操作名称不会重新翻译。 + +手动打开 GoPieMenu 会显示编辑器,已在系统托盘运行时也会唤回窗口;随 Windows 自动启动时则留在系统托盘。 + ## 使用方式 1. 从系统托盘打开 **Settings**。顶部菜单左侧的 **+** 可新建并切换到新菜单;三点管理菜单可新建、复制和删除,删除后切换到第一个菜单。 @@ -34,9 +38,17 @@ JSON 配置格式保持 1.0,位于 Qt 应用数据目录;Settings 提供导 程序与[托盘菜单](resources/demo/TrayMenu.png)已采用新版 Pie Menu 标志与浅色风格,**About** 旁的 GitHub 按钮可打开项目页面。 +## 更新 + +GoPieMenu 会在启动时检查 GitHub 是否有更新的稳定版本。编辑器打开时显示更新对话框;隐藏或最小化时显示托盘通知,点击后可查看更新内容。“跳过此版本”会记住该版本,也可从“设置 → 检查更新”重新查看。 + +更新说明按当前界面语言显示,缺少翻译时使用英文,验证信息则共用。“更新”会下载 Windows 安装程序并验证 `SHA256SUMS.txt`;点击“安装并退出”后,会先处理尚未应用的编辑,再打开 Windows 安装权限提示。取消权限提示时 GoPieMenu 会继续运行;点击“稍后”关闭更新窗口不会自动安装。 + +所有更新说明集中在 [CHANGELOG.md](CHANGELOG.md)。发布时只提取该版本区块至 GitHub Release,程序再从 Release 读取内容。预览和启动验证模式不会检查更新。 + ## 构建与测试 -需要 Windows、Visual Studio 2022 C++、CMake 3.21+、Qt 6.9(Widgets、Quick、QML、Quick Controls、SVG、Test)。 +需要 Windows、Visual Studio 2022 C++、CMake 3.21+、Qt 6.9(Widgets、Quick、QML、Quick Controls、SVG、Network、Linguist Tools、Test)。 ```powershell ./scripts/build.ps1 -QtRoot "C:/Qt/6.9.0/msvc2022_64" diff --git a/README.zh-TW.md b/README.zh-TW.md index fd88a12..f9ae18f 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -4,10 +4,14 @@ Windows 圓形快捷選單,可錄製快捷鍵、啟動應用程式、開啟檔案/網址與執行指令。 -[下載 v2.0.0(Windows x64)](https://github.com/RyuuMeow/GoPieMenu/releases/tag/v2.0.0) · [更新與升級說明](docs/releases/v2.0.0.md) +[下載 v2.1.0(Windows x64)](https://github.com/RyuuMeow/GoPieMenu/releases/tag/v2.1.0) · [更新與升級說明](CHANGELOG.md#210) ![淺色編輯器](resources/demo/Editor2.png) +介面支援英文、繁體中文及簡體中文。新設定預設跟隨系統介面語言,不支援的語言使用英文。切換「設定 → 語言」會顯示「稍後」及藍色「重新啟動」按鈕;重新啟動會先套用所有待存變更,稍後則保留草稿繼續編輯。中文介面使用微軟正黑體 UI。既有設定若已指定語言,會保留該選擇;已儲存的選單及動作名稱不會重新翻譯。 + +手動開啟 GoPieMenu 會顯示編輯器,已在系統匣執行時也會喚回視窗;隨 Windows 自動啟動時則留在系統匣。 + ## 使用方式 1. 從系統匣開啟 **Settings**。頂部選單左側的 **+** 可新增並切換到新選單;三點管理選單可新增、複製或刪除,刪除後切換到第一個選單。 @@ -34,9 +38,17 @@ JSON 設定格式維持 1.0,存於 Qt 應用程式資料目錄;Settings 可 程式與[系統匣選單](resources/demo/TrayMenu.png)已採用新版 Pie Menu 標誌與淺色風格,**About** 旁的 GitHub 按鈕可開啟專案頁面。 +## 更新 + +GoPieMenu 會在啟動時檢查 GitHub 是否有較新的穩定版本。編輯器開啟時顯示更新對話窗;隱藏或最小化時顯示系統匣通知,點擊後可查看更新內容。「略過此版本」會記住該版本,也可從「設定 → 檢查更新」重新查看。 + +更新說明依目前介面語言顯示,缺少翻譯時使用英文,驗證資訊則共用。「更新」會下載 Windows 安裝程式並驗證 `SHA256SUMS.txt`;按下「安裝並結束」後,會先處理尚未套用的編輯,再開啟 Windows 安裝權限提示。取消權限提示時 GoPieMenu 會繼續執行;按「稍後」關閉更新視窗不會自動安裝。 + +所有更新說明集中在 [CHANGELOG.md](CHANGELOG.md)。發布時只擷取該版本區塊至 GitHub Release,程式再從 Release 讀取內容。預覽與啟動驗證模式不會檢查更新。 + ## 建置與驗證 -需要 Windows、Visual Studio 2022 C++、CMake 3.21+、Qt 6.9(Widgets、Quick、QML、Quick Controls、SVG、Test)。 +需要 Windows、Visual Studio 2022 C++、CMake 3.21+、Qt 6.9(Widgets、Quick、QML、Quick Controls、SVG、Network、Linguist Tools、Test)。 ```powershell ./scripts/build.ps1 -QtRoot "C:/Qt/6.9.0/msvc2022_64" diff --git a/docs/architecture.md b/docs/architecture.md index c2a64ab..690ab45 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,7 +12,7 @@ Appearance writes always target the current profile's `StyleOverride`, including `SettingsWindow` owns the QML engine, session, recorder, and icon catalog. QML provides the toolbar, canvas, contextual inspector, organizer, and compact drawer. Shared buttons, menus, tooltips, fields, checkboxes, sliders, switches, and dialogs use `Theme.js` tokens. The three-dot menu toggles and suppresses its button tooltip while open. Apply/Discard keep their toolbar positions when disabled. Advanced controls do not replace the canvas. Submenu containers cannot be nested or converted to actions while they contain children. -`AppDialog` supplies the white rounded surface, modal focus, scrollable body, and fixed footer for settings, about, icon selection, color selection, running applications, and exit confirmation. `RunningApplications` only enumerates process names; QML handles search and selection. Quit confirmation is asynchronous, and a failed Apply leaves its dialog and draft open. File/folder selection remains a native dialog; the application requests the light color scheme. The duplicate-instance startup notice remains a light QWidget message box. +`AppDialog` supplies the white rounded surface, modal focus, scrollable body, and fixed footer for settings, about, icon selection, color selection, running applications, and exit confirmation. `RunningApplications` only enumerates process names; QML handles search and selection. Quit confirmation is asynchronous, and a failed Apply leaves its dialog and draft open. File/folder selection remains a native dialog; the application requests the light color scheme. A duplicate launch activates the existing editor over a local socket; if activation is unavailable, a light QWidget notice explains that it is already running. `ColorWheelItem` paints and hit-tests the hue ring and saturation/value square, retaining hue for achromatic colors. `ColorPicker` stages H/S/V/alpha and RGB/RGBA hex changes until confirmation; configuration colors retain Qt's ARGB representation. Selection IDs guard against applying a delayed result to a different item. The last 12 confirmed colors are stored separately in QSettings; isolated preview/test sessions do not write that history. @@ -24,6 +24,30 @@ The QWidget tray menu matches the QML menu's light surface, spacing, corner radi `PieScene` and `ListScene` share drawing, colors, geometry, and hit testing between `PiePreviewItem` and the runtime QWidget menus. Icons are supplied before painting. The preview composes its logical transform with the Qt Quick device-pixel transform and uses fixed label bounds centered on the menu origin, so renaming does not move or scale the menu. Runtime bounds still follow rendered text. Window placement accounts for negative monitor coordinates, label extents, screen edges, and small work areas. Long submenu lists page with the wheel or edge scrolling. Paint requests occur on changes, animation, or input; there is no idle repaint timer. +## Localization + +`TranslationManager` installs an embedded Qt `.qm` catalog before constructing the editor and tray. English uses source strings; `zh-Hant` (including Taiwan, Hong Kong, and Macao) selects Traditional Chinese, and `zh-Hans` (including China and Singapore) selects Simplified Chinese. The first system UI language determines the automatic choice; unsupported languages fall back to English, as do missing translation entries. Explicit script subtags take precedence over region. + +The existing JSON `language` field accepts `system` (new/missing-field default), `en`, `zh-TW`, or `zh-CN`. Existing explicit preferences are retained. Choosing a different language opens a restart prompt: Later retains the undoable draft, while Restart applies all pending changes before launching a replacement process. Save or launch failure keeps the current process open. On Windows the replacement waits for its parent to exit before taking the single-instance lock. Unknown imported language tags fall back to English. User-authored names, action payloads, shortcut tokens, and JSON enum values are never translated. New default names use the current interface language. + +Chinese interfaces use Microsoft JhengHei UI for QML, native controls, and default menu text; custom menu fonts remain explicit overrides. English uses Segoe UI. Manual launches show the editor. The Windows Run entry includes `--autostart` to hide it, and is synchronized on normal startup to migrate older entries. A previously registered command without the flag can show the editor once during migration. A user-scoped local socket lets subsequent manual launches activate the existing editor; duplicate autostart launches exit quietly. + +Translations live in `resources/i18n`. With the Qt Linguist Tools component installed, run `cmake --build build --target update-translations` after changing C++ or QML text, then fill both TS catalogs. Normal builds compile and embed them, so deployment needs no separate application translation files. Core tests cover locale resolution, fallback, embedded catalogs and preference persistence; UI tests open both Chinese editors and exercise the language selector at all four scale factors. + +## Updates and release notes + +`UpdateManager` performs one asynchronous request at normal startup to GitHub's [latest stable release endpoint](https://docs.github.com/en/rest/releases/releases#get-the-latest-release). Preview and smoke-test runs do not create an updater. Numeric version comparison rejects equal/older versions, draft/prereleases, and malformed tags. Offline, rate-limit and parsing failures leave normal operation unaffected; status is available in Settings. Requests have time and size limits and retain HTTPS through redirects. + +`SettingsWindow` chooses a dialog for a visible editor and a native tray notification for a hidden/minimized editor. Clicking the notification or its persistent tray menu entry opens the notes. The dialog waits for existing modal work and recording to finish. Skipping persists the exact version in QSettings; newer releases remain eligible, and a manual Settings check can revisit a skipped version. The notes language is fixed to the interface language chosen at startup, not an unapplied language draft. + +The update action fetches `SHA256SUMS.txt` and `GoPieMenu_Setup.exe` from the same validated repository/tag. Downloads stream to `QSaveFile` in the application cache, enforce the published file size, and must match SHA-256 before being marked ready. Nothing runs without the separate **Install and exit** confirmation. The editor resolves dirty drafts through its existing exit dialog, rechecks the saved installer, and uses Windows elevation to start it. Only successful launch emits the exit signal; save failures, corrupted downloads, and cancelled elevation leave the application open. Cached installers are retained so a running installer is not deleted during app shutdown. + +`CHANGELOG.md` is the only release-note source. Each `## [x.y.z]` section contains ``, `zh-TW`, and `zh-CN` blocks, each closed with ``. Feature/fix/upgrade prose is localized; an optional `shared` block holds validation and comparison links once. Keep heading levels inside language blocks at `####` or deeper; reserve `## [version]` for releases. The application selects one language plus shared content, falls back to English when needed, and supports the unmarked English bodies of older releases. + +To prepare a release, move completed entries from `Unreleased` into a numbered section and update both application/installer versions. Run `scripts/release-notes.ps1 -Version vX.Y.Z -OutputPath build/release-notes.md` to inspect the exact body. CI validates that version and all three nonempty language blocks before packaging; the publish job extracts the same section for `body_path`. It never publishes the whole changelog or the Unreleased section. This preserves the markers in the Release API body without another network request to the repository. + +Update regression tests use fake network replies and fake installer launchers, covering version ordering, localization/fallback, persistent skip/manual revisit, timeout/HTTP errors, checksum/size rejection, retry/cancel, file tampering, and cancelled elevation. UI tests cover tray routing, modal deferral, all three languages at 800×520, and the dirty-draft installation flow at every DPI scale. `ReleaseNotesTests.ps1` checks extraction and rejects missing, duplicate, or malformed language/version blocks. Real Windows toast delivery and an actual UAC/installer run remain manual acceptance checks; tests never install software. + ## Input and asynchronous work `InputState` is a pure state machine behind the Win32 hook adapter. It handles sided modifiers, repeat suppression, matching swallowed releases, app-specific priority, hold/toggle confirmation, and Escape cancellation. The active trigger is a value snapshot. Self-generated input carries `OwnInputTag`, so sending an action does not retrigger the hook. Recording suspends hooks. Runtime widgets consume a selection before dispatch and reject input while closing. @@ -41,7 +65,7 @@ Run `scripts/build.ps1` as described in the README. CI uses the `windows-2022` r - Core: draft isolation across profiles; undo/redo/apply/discard; safe selection after deletion; invalid imports and malformed persisted files; creation and atomic-replacement failures; legacy JSON; hierarchy-preserving moves; trigger state, including left/right Win modifiers with mouse and keyboard triggers; injected events; shortcut parsing. - UI: canvas selection, typing, shortcut/trigger recording, profile creation/deletion and selector synchronization, menu toggle/tooltip behavior, stable preview geometry after renaming, icon centering/search/selection, whole-row drag insertion at both boundaries, autoscroll and cancellation, submenu editing, appearance, and Apply/hide/reopen. - Dialog/window UI: color conversion, ring/square pointer input, hex validation and focus, confirm/cancel isolation and recent colors, modal shortcut blocking, unsaved exit success/failure/cancel, enabled switch, maximize/work-area/restore, running-app selection dialog, and compact color layout. Existing coverage includes the 800×520 drawer, QML warning detection, high-DPI painted coordinates, four-corner/negative-screen placement, a 100-item list, rapid menu reopen, and closing engines during icon loading. -- Packaging: `scripts/package.ps1` checks matching application/installer versions, deploys Qt/QML dependencies, runs `--smoke-test` with the SDK removed from PATH, and compiles the installer plus SHA-256 checksum. The smoke test uses preview isolation and exits after startup. CI uses the same script and retains test screenshots and logs. Version tags must match the application version and have a corresponding file under `docs/releases`; publication waits for the build, tests, and packaging to succeed. +- Packaging: `scripts/package.ps1` checks matching application/installer versions, deploys Qt/QML dependencies, runs `--smoke-test` with the SDK removed from PATH, and compiles the installer plus SHA-256 checksum. The smoke test uses preview isolation, verifies that a deployed TLS backend can load without making a network request, and exits after startup. CI uses the same script and retains test screenshots and logs. Version tags must match the application version and have a corresponding multilingual version section in `CHANGELOG.md`; publication waits for the build, tests, and packaging to succeed. Additional checks cover independent profile appearance (including legacy defaults and custom styles), blank-action save/import, no-op dispatch through fake action handlers, selection-preserving sorting/deletion, all three action context-menu locations, disabled-menu hints, and tray Pause behavior. The GitHub test intercepts URL opening rather than launching a browser. Action dispatch is built as `gpm_actions` so the same production dispatcher can be tested without executing real actions. @@ -58,4 +82,4 @@ Measured on 2026-10-09, Windows 11, Qt 6.9.0 / MSVC 2022, software Qt Quick rend Warm timing includes the test helper's 30ms layout wait. These are local samples, not latency guarantees or cross-device benchmarks. Logs are in `build/artifacts`; timing values are reported, not enforced as brittle CI thresholds. -The local Release build and all five CTest entries passed. Deployed startup passed without an SDK PATH, and the Inno Setup installer compiled successfully. Published build results are available in [GitHub Actions](https://github.com/RyuuMeow/GoPieMenu/actions/workflows/windows-release.yml). Real keyboard/mouse feel in third-party apps, Windows-key combinations interacting with Start/system shortcuts, elevated target apps, and physically moving between monitors with different Windows scaling settings still require hands-on acceptance; uniform scale-factor tests do not simulate that hardware transition. +The local Release build and all CTest entries passed. Deployed startup passed without an SDK PATH, and the Inno Setup installer compiled successfully. Published build results are available in [GitHub Actions](https://github.com/RyuuMeow/GoPieMenu/actions/workflows/windows-release.yml). Real keyboard/mouse feel in third-party apps, Windows-key combinations interacting with Start/system shortcuts, elevated target apps, and physically moving between monitors with different Windows scaling settings still require hands-on acceptance; uniform scale-factor tests do not simulate that hardware transition. diff --git a/docs/releases/v2.0.0.md b/docs/releases/v2.0.0.md index 6f51869..6227fa5 100644 --- a/docs/releases/v2.0.0.md +++ b/docs/releases/v2.0.0.md @@ -1,28 +1,3 @@ -GoPieMenu 2.0 rebuilds the menu editor around a live preview, independent profile appearance, and an explicit Apply/Discard workflow. +# GoPieMenu 2.0.0 -## What's new - -- Edit actions directly from the pie preview. Switch profiles without losing drafts, and use Undo/Redo before applying changes to the running menu. -- Arrange actions by dragging an entire row, with insertion markers, edge scrolling, and selection preserved. Right-click preview or organizer actions to duplicate or delete them. -- Customize each profile independently: presets, size, colors, opacity, and animation. Duplicating a profile copies its appearance without linking later edits. -- Choose colors with a hue ring, saturation/value square, HSV/opacity controls, RGB/RGBA hex input, and recent colors. -- Use a searchable icon picker with asynchronous loading and embedded icons. Custom SVG, PNG, JPEG, and ICO files remain supported. -- Combine Ctrl, Shift, Alt, and Win modifiers independently with mouse triggers. Keyboard recording keeps the selected activation mode. -- Save empty actions as placeholders; triggering them does nothing. -- Enjoy a refreshed light editor, consistent dialogs and tray menu, new application artwork, and custom window controls. - -## Upgrading from 1.0 - -1. Export your configuration from Settings, then quit GoPieMenu from the tray before running the installer. -2. Download and run `GoPieMenu_Setup.exe` from this release. This is the Windows x64 installer; Qt dependencies are included. -3. Open Settings, review your profiles, and use **Apply** to save changes. Closing the editor hides it to the tray; quitting prompts about unapplied edits. - -The configuration location and version 1.0 JSON format are retained. Existing appearance values and supported legacy style keys are preserved. Appearance edits now affect only the selected profile. Choose **Frost** to opt into the new, nearly opaque preset. Invalid configurations are reported without overwriting the original file; import is an undoable draft operation. - -## Validation - -The release workflow runs core tests and UI tests at 100%, 125%, 150%, and 200% scaling, then checks that the deployed application starts without the Qt SDK on PATH before compiling the installer. `SHA256SUMS.txt` contains the installer's checksum. - -Real input behavior in individual applications, interactions between Win-key combinations and Windows system shortcuts, elevated applications, and moving between monitors with different physical scaling settings remain manual verification areas. - -Full changes: https://github.com/RyuuMeow/GoPieMenu/compare/v1.0.0...v2.0.0 +The release notes now live in the [multilingual changelog](../../CHANGELOG.md#200), the single source used by the release workflow and update checker. diff --git a/installer.iss b/installer.iss index b4bdd3b..c8fdf36 100644 --- a/installer.iss +++ b/installer.iss @@ -1,6 +1,6 @@ [Setup] AppName=GoPieMenu -AppVersion=2.0.0 +AppVersion=2.1.0 AppPublisher=RyuuMeow DefaultDirName={autopf}\GoPieMenu DefaultGroupName=GoPieMenu diff --git a/resources/i18n/gopiemenu_zh_CN.ts b/resources/i18n/gopiemenu_zh_CN.ts new file mode 100644 index 0000000..d28e760 --- /dev/null +++ b/resources/i18n/gopiemenu_zh_CN.ts @@ -0,0 +1,1667 @@ + + + + + ActionContextMenu + + + Duplicate + 复制 + + + + Delete action + 删除操作 + + + + ActionExecutor + + + No handler for action type: %1 + 没有可处理动作类型“%1”的处理程序 + + + + Invalid action data for: %1 + “%1”的操作数据无效 + + + + Execution failed + 执行失败 + + + + AppDialog + + + Close + 关闭 + + + + AppearancePane + + + Appearance applies only to this menu. + 外观设置仅应用于此菜单。 + + + + Style + 样式 + + + + Frost + 雾面 + + + + Slate + 石板 + + + + Ocean + 海洋 + + + + Obsidian + 黑曜石 + + + + Size + 大小 + + + + Advanced + 高级 + + + + Center radius + 中心半径 + + + + Icon size + 图标大小 + + + + Text size + 文字大小 + + + + Slice gap + 扇区间距 + + + + Opacity + 不透明度 + + + + Animation + 动画 + + + + Text outline + 文字描边 + + + + Border width + 边框宽度 + + + + Hover size + 悬停缩放 + + + + Automatic text contrast + 自动调整文字对比 + + + + Slices + 扇区 + + + + Highlight + 高亮 + + + + Text + 文字 + + + + Center + 中心 + + + + Border + 边框 + + + + Submenu + 子菜单 + + + + Change %1 color + 更改%1颜色 + + + + Application + + + GoPieMenu is already running. + GoPieMenu 已在运行。 + + + + + Could not install global input hooks. + 无法安装全局输入钩子。 + + + + ColorPicker + + + Choose color + 选择颜色 + + + + Hex · RRGGBB / RRGGBBAA + 十六进制颜色代码 · RRGGBB / RRGGBBAA + + + + Hex color + 十六进制颜色代码 + + + + Enter 6 hex digits, or 8 including opacity. + 请输入 6 位十六进制数字,若包含不透明度则输入 8 位。 + + + + Reset + 重置 + + + + Recent colors + 最近使用的颜色 + + + + Use %1 + 使用 %1 + + + + Colors you use will appear here. + 用过的颜色会显示在这里。 + + + + Cancel + 取消 + + + + Use color + 使用颜色 + + + + ConfigManager + + + Cannot create the configuration folder. + 无法创建配置文件夹。 + + + + ConfigValidation + + + Invalid value type for %1. + “%1”的值类型无效。 + + + + Submenus can contain actions, but cannot contain other submenus. + 子菜单可以包含操作,但不能包含其他子菜单。 + + + + Unknown action type: %1 + 未知的操作类型:%1 + + + + Submenu items must be a list. + 子菜单项必须是列表。 + + + + Each submenu item must be an object. + 每个子菜单项都必须是对象。 + + + + The inner radius must be smaller than the outer radius (80–400). + 内半径必须小于外半径(80–400)。 + + + + Choose an icon size of 12–96 and a font size of 8–32. + 图标大小请设为 12–96,字体大小请设为 8–32。 + + + + The gap must be smaller than each sector. + 间距必须小于每个扇区的角度。 + + + + The appearance contains an out-of-range value. + 外观设置包含超出范围的数值。 + + + + The appearance contains an invalid color. + 外观设置包含无效的颜色。 + + + + Keep at least one menu. + 至少保留一个菜单。 + + + + Menu IDs must be unique. + 菜单 ID 不可重复。 + + + + Give each menu a name. + 请为每个菜单命名。 + + + + A pie can have at most 12 items. + 饼形菜单最多只能有 12 个项目。 + + + + Choose a valid trigger. + 请选择有效的触发方式。 + + + + Add an action or disable this empty menu. + 请添加操作,或停用这个空菜单。 + + + + Item IDs must be unique within a menu. + 同一菜单内的项目 ID 不可重复。 + + + + A submenu can only contain actions. + 子菜单只能包含操作。 + + + + Only submenus can have children. + 只有子菜单可以包含子项。 + + + + An item contains an invalid color. + 项目包含无效的颜色。 + + + + Give each action a name. + 请为每个操作命名。 + + + + : record a valid keyboard shortcut. + : 请录制有效的键盘快捷键。 + + + + : enter a valid URL. + : 请输入有效的 URL。 + + + + Invalid JSON at byte %1: %2 + 字节 %1 处的 JSON 无效:%2 + + + + The configuration must be a JSON object. + 配置必须是 JSON 对象。 + + + + This configuration version is not supported. + 不支持此配置版本。 + + + + The configuration must contain a profiles list. + 配置必须包含 profiles 列表。 + + + + The global appearance must be an object. + 全局外观设置必须是对象。 + + + + Each menu must be an object. + 每个菜单都必须是对象。 + + + + Application filters must be text. + 应用筛选条件必须是文本。 + + + + Each menu must contain an items list. + 每个菜单都必须包含 items 列表。 + + + + Unknown activation mode: %1 + 未知的激活模式:%1 + + + + Invalid trigger modifiers. + 触发修饰键无效。 + + + + Invalid trigger key. + 触发按键无效。 + + + + Each action must be an object. + 每个操作都必须是对象。 + + + + DefaultMenu + + + New Pie Menu + 新建饼形菜单 + + + + Notepad + 记事本 + + + + Calculator + 计算器 + + + + Explorer + 文件资源管理器 + + + + Unnamed + 未命名 + + + + EditorSession + + + Open application + 打开应用 + + + + Keyboard shortcut + 键盘快捷键 + + + + Open file or folder + 打开文件或文件夹 + + + + Open website + 打开网站 + + + + Run command + 运行命令 + + + + Submenu + 子菜单 + + + + Choose an action + 选择操作 + + + + + Not set + 未设置 + + + + Key %1 + %1 键 + + + + Left mouse + 鼠标左键 + + + + Right mouse + 鼠标右键 + + + + Middle mouse + 鼠标中键 + + + + Side button 1 (X1) + 侧键 1 (X1) + + + + Side button 2 (X2) + 侧键 2 (X2) + + + + Your configuration could not be loaded. The original file has not been changed. + 无法加载配置,原始文件未被更改。 + + + + New menu + 新建菜单 + + + + ExitDialog + + + Unsaved changes + 有未保存的更改 + + + + Apply your changes before quitting? + 退出前要先应用更改吗? + + + + Discard + 放弃 + + + + Cancel + 取消 + + + + Apply & quit + 应用并退出 + + + + IconPicker + + + Choose icon + 选择图标 + + + + + Search icons + 搜索图标 + + + + Loading icons… + 正在加载图标… + + + + No matching icons + 没有找到匹配的图标 + + + + Clear icon + 清除图标 + + + + %1 icons + %1 个图标 + + + + InspectorPanel + + + Appearance + 外观 + + + + Menu settings + 菜单设置 + + + + Close panel + 关闭面板 + + + + ItemInspector + + + Choose icon + 选择图标 + + + + + Action name + 操作名称 + + + + Action + 操作 + + + + Open application + 打开应用 + + + + Keyboard shortcut + 键盘快捷键 + + + + Open file or folder + 打开文件或文件夹 + + + + Open website + 打开网站 + + + + Run command + 运行命令 + + + + Submenu + 子菜单 + + + + Action type + 操作类型 + + + + Shortcut + 快捷键 + + + + Website + 网站 + + + + Command + 命令 + + + + Target + 目标 + + + + Press a shortcut… + 请按下快捷键… + + + + Record shortcut + 录制快捷键 + + + + Enter a command + 输入命令 + + + + Choose a target or paste a path + 选择目标或粘贴路径 + + + + Action target + 操作目标 + + + + Choose application + 选择应用 + + + + Choose file + 选择文件 + + + + Choose folder + 选择文件夹 + + + + Edit %1 actions + 编辑 %1 个操作 + + + + Advanced + 高级 + + + + Arguments + 参数 + + + + Optional arguments + 可选参数 + + + + Color + 颜色 + + + + Change color + 更改颜色 + + + + Custom color + 自定义颜色 + + + + Reset + 重置 + + + + Location + 位置 + + + + Duplicate + 复制 + + + + Delete action + 删除操作 + + + + ItemList + + + Arrange actions + 排列操作 + + + + Move selected action up + 将选中的操作上移 + + + + Move selected action down + 将选中的操作下移 + + + + Close list + 关闭列表 + + + + Open submenu + 打开子菜单 + + + + Main + + + + New menu + 新建菜单 + + + + Current menu + 当前菜单 + + + + Manage menus + 管理菜单 + + + + Menu settings + 菜单设置 + + + + Duplicate menu + 复制菜单 + + + + Delete menu + 删除菜单 + + + + Edit trigger and application scope + 编辑触发方式和适用应用 + + + + Undo · Ctrl+Z + 撤销 · Ctrl+Z + + + + Redo · Ctrl+Y + 重做 · Ctrl+Y + + + + Discard + 放弃 + + + + Apply + 应用 + + + + Application settings + 应用设置 + + + + Dismiss message + 关闭消息 + + + + Back to main menu + 返回主菜单 + + + + + Add action + 添加操作 + + + + Keyboard shortcut + 键盘快捷键 + + + + Open application + 打开应用 + + + + Open file or folder + 打开文件或文件夹 + + + + Open website + 打开网站 + + + + Run command + 运行命令 + + + + New submenu + 新建子菜单 + + + + Arrange actions + 排列操作 + + + + + Appearance + 外观 + + + + Add an action to this submenu + 在此子菜单中添加操作 + + + + Build your menu + 创建你的菜单 + + + + Start with a shortcut, an app, or a website. + 先添加快捷键、应用或网站。 + + + + Changes stay in preview until you apply. + 应用前,更改只会显示在预览中。 + + + + Click an action to edit + 点击操作以编辑 + + + + Click a slice to edit + 点击扇区以编辑 + + + + Settings + 设置 + + + + + Language + 语言 + + + + Follow system + 跟随系统 + + + + English + English + + + + 繁體中文 + 繁體中文 + + + + 简体中文 + 简体中文 + + + + Language changes take effect after restarting GoPieMenu. + 重启 GoPieMenu 后,语言更改才会生效。 + + + + Start with Windows + 随 Windows 启动 + + + + Check for updates + 检查更新 + + + + View available update + 查看可用更新 + + + + Import + 导入 + + + + Export + 导出 + + + + Open custom icon folder + 打开自定义图标文件夹 + + + + + About + 关于 + + + + Open GoPieMenu on GitHub + 在 GitHub 上打开 GoPieMenu + + + + Version %1 + 版本 %1 + + + + GoPieMenu · GPL-3.0 + GoPieMenu · GPL-3.0 + + + + Icons by Iconoir · MIT + 图标由 Iconoir 提供 · MIT + + + + Close + 关闭 + + + + MenuSettingsPane + + + + Menu name + 菜单名称 + + + + Open with + 打开方式 + + + + Hold mouse button + 按住鼠标按键 + + + + Hold keyboard shortcut + 按住键盘快捷键 + + + + Press to toggle + 按下以切换 + + + + Activation mode + 激活模式 + + + + Modifiers + 修饰键 + + + + Mouse button + 鼠标按键 + + + + Keyboard shortcut + 键盘快捷键 + + + + Left button + 左键 + + + + Right button + 右键 + + + + Middle button + 中键 + + + + Side button 1 (X1) + 侧键 1 (X1) + + + + Side button 2 (X2) + 侧键 2 (X2) + + + + Trigger mouse button + 触发鼠标按键 + + + + Press a keyboard shortcut… + 请按下键盘快捷键… + + + + Record shortcut + 录制快捷键 + + + + Press the shortcut, point to an action, then click to run it. + 按下快捷键,指向某个操作,再单击即可运行。 + + + + Hold the trigger, point to an action, then release to run it. + 按住触发键,指向某个操作,再松开即可运行。 + + + + Available in + 适用于 + + + + All applications + 所有应用 + + + + Application filter + 应用筛选条件 + + + + Running apps + 正在运行的应用 + + + + Choose application + 选择应用 + + + + Leave empty for every app. Separate application names with commas. + 留空表示适用于所有应用。多个应用名称请用逗号分隔。 + + + + ModifierButtons + + + %1 modifier + %1 修饰键 + + + + RestartDialog + + + Restart required + 需要重新启动 + + + + Restart GoPieMenu to use the new language. Restarting will apply all pending changes. + 重新启动 GoPieMenu 以使用新语言。重新启动也会应用所有待处理的更改。 + + + + Choose Later to keep editing, then apply your changes before restarting manually. + 选择“稍后”即可继续编辑,之后请先应用更改,再手动重新启动。 + + + + Later + 稍后 + + + + Restart + 重新启动 + + + + RunningAppsDialog + + + Running applications + 正在运行的应用 + + + + Search applications + 搜索应用 + + + + Search running applications + 搜索正在运行的应用 + + + + No matching applications + 没有找到匹配的应用 + + + + Refresh + 刷新 + + + + Cancel + 取消 + + + + StatusToggle + + + Enabled + 已启用 + + + + Disabled + 已停用 + + + + Menu enabled + 菜单已启用 + + + + Disable this menu + 停用此菜单 + + + + Enable this menu + 启用此菜单 + + + + Add an action before enabling this menu + 添加操作后才能启用此菜单 + + + + UpdateDialog + + + Update available + 有更新可用 + + + + GoPieMenu %1 → %2 + GoPieMenu %1 → %2 + + + + Release notes are unavailable. You can view this release on GitHub. + 目前无法获取发布说明。你可以在 GitHub 查看此版本。 + + + + This release has no verified Windows installer. View the release on GitHub. + 此版本没有通过验证的 Windows 安装程序。请在 GitHub 查看此版本。 + + + + Downloading and verifying the installer… %1% + 正在下载并验证安装程序… %1% + + + + The installer is verified. Install now? GoPieMenu will close after you resolve any unsaved changes. + 安装程序已通过验证。现在安装吗?处理完尚未保存的更改后,GoPieMenu 将会关闭。 + + + + View release on GitHub + 在 GitHub 查看版本信息 + + + + Skip this version + 跳过此版本 + + + + Cancel download + 取消下载 + + + + Later + 稍后 + + + + Update + 更新 + + + + Install and exit + 安装并退出 + + + + Updates + + + The release response is invalid. + 版本信息响应无效。 + + + + The release version is invalid. + 版本号无效。 + + + + The installer was not started. You may have cancelled the Windows permission prompt. + 安装程序未启动。你可能取消了 Windows 权限提示。 + + + + Installing updates is only supported on Windows. + 仅支持在 Windows 上安装更新。 + + + + WindowTitleBar + + + Minimize + 最小化 + + + + Restore + 还原 + + + + Maximize + 最大化 + + + + Close window + 关闭窗口 + + + + gpm::EditorSession + + + Main menu + 主菜单 + + + + + %1 copy + %1 副本 + + + + Keep at least one menu. + 至少保留一个菜单。 + + + + + Submenus can only contain actions. + 子菜单只能包含操作。 + + + + + + A pie can have at most 12 items. + 饼形菜单最多只能有 12 个项目。 + + + + New submenu + 新建子菜单 + + + + + + New action + 添加操作 + + + + Move or remove the submenu actions before changing its type. + 更改类型前,请先移动或移除子菜单中的操作。 + + + + A submenu cannot be placed inside another submenu. + 子菜单不能放在另一个子菜单中。 + + + + gpm::SettingsWindow + + + Could not restart GoPieMenu. Your changes are saved; please reopen the app manually. + 无法重新启动 GoPieMenu。更改已保存,请手动重新打开应用。 + + + + Could not open the release page. + 无法打开版本信息页面。 + + + + Choose folder + 选择文件夹 + + + + + Choose application + 选择应用 + + + + Choose file + 选择文件 + + + + Applications (*.exe *.bat *.cmd);;All files (*) + 应用程序 (*.exe *.bat *.cmd);;所有文件 (*) + + + + All files (*) + 所有文件 (*) + + + + Applications (*.exe) + 应用程序 (*.exe) + + + + The selection changed. Choose the color again. + 所选项目已更改,请重新选择颜色。 + + + + Color updated, but recent colors could not be saved. + 颜色已更新,但无法保存最近使用的颜色。 + + + + Import configuration + 导入配置 + + + + + JSON files (*.json) + JSON 文件 (*.json) + + + + Export configuration + 导出配置 + + + + Cannot open the icon folder. + 无法打开图标文件夹。 + + + + Settings were saved, but Windows startup could not be updated. + 设置已保存,但无法更新 Windows 启动设置。 + + + + gpm::TrayManager + + + + GoPieMenu - Active + GoPieMenu - 已启用 + + + + Settings + 设置 + + + + + Pause + 暂停 + + + + Resume + 继续 + + + + GoPieMenu - Paused + GoPieMenu - 已暂停 + + + + Quit + 退出 + + + + Update available: %1 + 有可用更新:%1 + + + + Update available + 有更新可用 + + + + GoPieMenu %1 is available. Click to view what's new. + GoPieMenu %1 已有新版本。点击查看更新内容。 + + + + gpm::UpdateManager + + + The update request timed out. Try again later. + 更新请求超时,请稍后再试。 + + + + Checking for updates… + 正在检查更新… + + + + The update response is larger than expected. + 更新响应大小超出预期。 + + + + + + Could not save the installer. Check disk space and try again. + 无法保存安装程序。请确认磁盘空间充足后再试一次。 + + + + No releases are available yet. + 目前尚无可用版本。 + + + + Could not contact the update server: %1 + 无法连接到更新服务器:%1 + + + + You are using the latest version. + 你使用的已是最新版本。 + + + + + Version %1 was skipped. + 已跳过版本 %1。 + + + + GoPieMenu %1 is available. + GoPieMenu %1 已发布。 + + + + The release checksum is missing or invalid. + 版本的 SHA256 校验和缺失或无效。 + + + + Could not create the update download folder. + 无法创建更新下载文件夹。 + + + + + The installer failed verification. Download the update again. + 安装程序验证失败。请重新下载更新。 + + + + The update is ready to install. + 更新已准备就绪,可以安装。 + + + + Could not save the skipped version. Try again. + 无法保存已跳过的版本,请再试一次。 + + + + Could not start the installer. Please try again. + 无法启动安装程序,请再试一次。 + + + diff --git a/resources/i18n/gopiemenu_zh_TW.ts b/resources/i18n/gopiemenu_zh_TW.ts new file mode 100644 index 0000000..8a1f0ea --- /dev/null +++ b/resources/i18n/gopiemenu_zh_TW.ts @@ -0,0 +1,1667 @@ + + + + + ActionContextMenu + + + Duplicate + 複製 + + + + Delete action + 刪除動作 + + + + ActionExecutor + + + No handler for action type: %1 + 沒有可處理動作類型「%1」的處理程序 + + + + Invalid action data for: %1 + 「%1」的動作資料無效 + + + + Execution failed + 執行失敗 + + + + AppDialog + + + Close + 關閉 + + + + AppearancePane + + + Appearance applies only to this menu. + 外觀設定僅套用至此選單。 + + + + Style + 樣式 + + + + Frost + 霧面 + + + + Slate + 石板 + + + + Ocean + 海洋 + + + + Obsidian + 黑曜石 + + + + Size + 大小 + + + + Advanced + 進階 + + + + Center radius + 中心半徑 + + + + Icon size + 圖示大小 + + + + Text size + 文字大小 + + + + Slice gap + 扇形間距 + + + + Opacity + 不透明度 + + + + Animation + 動畫 + + + + Text outline + 文字外框 + + + + Border width + 邊框寬度 + + + + Hover size + 懸停縮放 + + + + Automatic text contrast + 自動調整文字對比 + + + + Slices + 扇形 + + + + Highlight + 醒目顯示 + + + + Text + 文字 + + + + Center + 中心 + + + + Border + 邊框 + + + + Submenu + 子選單 + + + + Change %1 color + 變更%1顏色 + + + + Application + + + GoPieMenu is already running. + GoPieMenu 已在執行中。 + + + + + Could not install global input hooks. + 無法安裝全域輸入攔截器。 + + + + ColorPicker + + + Choose color + 選擇顏色 + + + + Hex · RRGGBB / RRGGBBAA + 十六進位色碼 · RRGGBB / RRGGBBAA + + + + Hex color + 十六進位色碼 + + + + Enter 6 hex digits, or 8 including opacity. + 請輸入 6 位十六進位數字,若包含透明度則輸入 8 位。 + + + + Reset + 重設 + + + + Recent colors + 最近使用的顏色 + + + + Use %1 + 使用 %1 + + + + Colors you use will appear here. + 使用過的顏色會顯示在這裡。 + + + + Cancel + 取消 + + + + Use color + 使用顏色 + + + + ConfigManager + + + Cannot create the configuration folder. + 無法建立設定資料夾。 + + + + ConfigValidation + + + Invalid value type for %1. + 「%1」的值類型無效。 + + + + Submenus can contain actions, but cannot contain other submenus. + 子選單可以包含動作,但不能包含其他子選單。 + + + + Unknown action type: %1 + 未知的動作類型:%1 + + + + Submenu items must be a list. + 子選單項目必須是清單。 + + + + Each submenu item must be an object. + 每個子選單項目都必須是物件。 + + + + The inner radius must be smaller than the outer radius (80–400). + 內半徑必須小於外半徑(80–400)。 + + + + Choose an icon size of 12–96 and a font size of 8–32. + 圖示大小請設為 12–96,字型大小請設為 8–32。 + + + + The gap must be smaller than each sector. + 間距必須小於每個扇形的角度。 + + + + The appearance contains an out-of-range value. + 外觀設定含有超出範圍的數值。 + + + + The appearance contains an invalid color. + 外觀設定含有無效的顏色。 + + + + Keep at least one menu. + 至少保留一個選單。 + + + + Menu IDs must be unique. + 選單 ID 不可重複。 + + + + Give each menu a name. + 請為每個選單命名。 + + + + A pie can have at most 12 items. + 圓餅選單最多只能有 12 個項目。 + + + + Choose a valid trigger. + 請選擇有效的觸發方式。 + + + + Add an action or disable this empty menu. + 請新增動作,或停用這個空選單。 + + + + Item IDs must be unique within a menu. + 同一選單內的項目 ID 不可重複。 + + + + A submenu can only contain actions. + 子選單只能包含動作。 + + + + Only submenus can have children. + 只有子選單可以包含子項目。 + + + + An item contains an invalid color. + 項目含有無效的顏色。 + + + + Give each action a name. + 請為每個動作命名。 + + + + : record a valid keyboard shortcut. + : 請錄製有效的鍵盤快捷鍵。 + + + + : enter a valid URL. + : 請輸入有效的 URL。 + + + + Invalid JSON at byte %1: %2 + 位元組 %1 的 JSON 無效:%2 + + + + The configuration must be a JSON object. + 設定必須是 JSON 物件。 + + + + This configuration version is not supported. + 不支援此設定版本。 + + + + The configuration must contain a profiles list. + 設定必須包含 profiles 清單。 + + + + The global appearance must be an object. + 全域外觀設定必須是物件。 + + + + Each menu must be an object. + 每個選單都必須是物件。 + + + + Application filters must be text. + 應用程式篩選條件必須是文字。 + + + + Each menu must contain an items list. + 每個選單都必須包含 items 清單。 + + + + Unknown activation mode: %1 + 未知的啟用模式:%1 + + + + Invalid trigger modifiers. + 觸發修飾鍵無效。 + + + + Invalid trigger key. + 觸發按鍵無效。 + + + + Each action must be an object. + 每個動作都必須是物件。 + + + + DefaultMenu + + + New Pie Menu + 新圓餅選單 + + + + Notepad + 記事本 + + + + Calculator + 計算機 + + + + Explorer + 檔案總管 + + + + Unnamed + 未命名 + + + + EditorSession + + + Open application + 開啟應用程式 + + + + Keyboard shortcut + 鍵盤快捷鍵 + + + + Open file or folder + 開啟檔案或資料夾 + + + + Open website + 開啟網站 + + + + Run command + 執行命令 + + + + Submenu + 子選單 + + + + Choose an action + 選擇動作 + + + + + Not set + 未設定 + + + + Key %1 + %1 鍵 + + + + Left mouse + 滑鼠左鍵 + + + + Right mouse + 滑鼠右鍵 + + + + Middle mouse + 滑鼠中鍵 + + + + Side button 1 (X1) + 側鍵 1 (X1) + + + + Side button 2 (X2) + 側鍵 2 (X2) + + + + Your configuration could not be loaded. The original file has not been changed. + 無法載入設定,原始檔案未遭變更。 + + + + New menu + 新增選單 + + + + ExitDialog + + + Unsaved changes + 尚有未儲存的變更 + + + + Apply your changes before quitting? + 要先套用變更再結束嗎? + + + + Discard + 捨棄 + + + + Cancel + 取消 + + + + Apply & quit + 套用並結束 + + + + IconPicker + + + Choose icon + 選擇圖示 + + + + + Search icons + 搜尋圖示 + + + + Loading icons… + 正在載入圖示… + + + + No matching icons + 找不到相符的圖示 + + + + Clear icon + 清除圖示 + + + + %1 icons + %1 個圖示 + + + + InspectorPanel + + + Appearance + 外觀 + + + + Menu settings + 選單設定 + + + + Close panel + 關閉面板 + + + + ItemInspector + + + Choose icon + 選擇圖示 + + + + + Action name + 動作名稱 + + + + Action + 動作 + + + + Open application + 開啟應用程式 + + + + Keyboard shortcut + 鍵盤快捷鍵 + + + + Open file or folder + 開啟檔案或資料夾 + + + + Open website + 開啟網站 + + + + Run command + 執行命令 + + + + Submenu + 子選單 + + + + Action type + 動作類型 + + + + Shortcut + 快捷鍵 + + + + Website + 網站 + + + + Command + 命令 + + + + Target + 目標 + + + + Press a shortcut… + 請按下快捷鍵… + + + + Record shortcut + 錄製快捷鍵 + + + + Enter a command + 輸入命令 + + + + Choose a target or paste a path + 選擇目標或貼上路徑 + + + + Action target + 動作目標 + + + + Choose application + 選擇應用程式 + + + + Choose file + 選擇檔案 + + + + Choose folder + 選擇資料夾 + + + + Edit %1 actions + 編輯 %1 個動作 + + + + Advanced + 進階 + + + + Arguments + 引數 + + + + Optional arguments + 選填引數 + + + + Color + 顏色 + + + + Change color + 變更顏色 + + + + Custom color + 自訂顏色 + + + + Reset + 重設 + + + + Location + 位置 + + + + Duplicate + 複製 + + + + Delete action + 刪除動作 + + + + ItemList + + + Arrange actions + 排列動作 + + + + Move selected action up + 將選取的動作上移 + + + + Move selected action down + 將選取的動作下移 + + + + Close list + 關閉清單 + + + + Open submenu + 開啟子選單 + + + + Main + + + + New menu + 新增選單 + + + + Current menu + 目前選單 + + + + Manage menus + 管理選單 + + + + Menu settings + 選單設定 + + + + Duplicate menu + 複製選單 + + + + Delete menu + 刪除選單 + + + + Edit trigger and application scope + 編輯觸發方式與適用應用程式 + + + + Undo · Ctrl+Z + 復原 · Ctrl+Z + + + + Redo · Ctrl+Y + 重做 · Ctrl+Y + + + + Discard + 捨棄 + + + + Apply + 套用 + + + + Application settings + 應用程式設定 + + + + Dismiss message + 關閉訊息 + + + + Back to main menu + 返回主選單 + + + + + Add action + 新增動作 + + + + Keyboard shortcut + 鍵盤快捷鍵 + + + + Open application + 開啟應用程式 + + + + Open file or folder + 開啟檔案或資料夾 + + + + Open website + 開啟網站 + + + + Run command + 執行命令 + + + + New submenu + 新增子選單 + + + + Arrange actions + 排列動作 + + + + + Appearance + 外觀 + + + + Add an action to this submenu + 在此子選單新增動作 + + + + Build your menu + 建立你的選單 + + + + Start with a shortcut, an app, or a website. + 先新增快捷鍵、應用程式或網站。 + + + + Changes stay in preview until you apply. + 套用前,變更只會顯示在預覽中。 + + + + Click an action to edit + 點選動作以編輯 + + + + Click a slice to edit + 點選扇形以編輯 + + + + Settings + 設定 + + + + + Language + 語言 + + + + Follow system + 依照系統 + + + + English + English + + + + 繁體中文 + 繁體中文 + + + + 简体中文 + 简体中文 + + + + Language changes take effect after restarting GoPieMenu. + 重新啟動 GoPieMenu 後,語言變更才會生效。 + + + + Start with Windows + 隨 Windows 啟動 + + + + Check for updates + 檢查更新 + + + + View available update + 查看可用更新 + + + + Import + 匯入 + + + + Export + 匯出 + + + + Open custom icon folder + 開啟自訂圖示資料夾 + + + + + About + 關於 + + + + Open GoPieMenu on GitHub + 在 GitHub 上開啟 GoPieMenu + + + + Version %1 + 版本 %1 + + + + GoPieMenu · GPL-3.0 + GoPieMenu · GPL-3.0 + + + + Icons by Iconoir · MIT + 圖示由 Iconoir 提供 · MIT + + + + Close + 關閉 + + + + MenuSettingsPane + + + + Menu name + 選單名稱 + + + + Open with + 開啟方式 + + + + Hold mouse button + 按住滑鼠按鈕 + + + + Hold keyboard shortcut + 按住鍵盤快捷鍵 + + + + Press to toggle + 按下以切換 + + + + Activation mode + 啟用模式 + + + + Modifiers + 修飾鍵 + + + + Mouse button + 滑鼠按鈕 + + + + Keyboard shortcut + 鍵盤快捷鍵 + + + + Left button + 左鍵 + + + + Right button + 右鍵 + + + + Middle button + 中鍵 + + + + Side button 1 (X1) + 側鍵 1 (X1) + + + + Side button 2 (X2) + 側鍵 2 (X2) + + + + Trigger mouse button + 觸發滑鼠按鈕 + + + + Press a keyboard shortcut… + 請按下鍵盤快捷鍵… + + + + Record shortcut + 錄製快捷鍵 + + + + Press the shortcut, point to an action, then click to run it. + 按下快捷鍵,指向某個動作,再按一下即可執行。 + + + + Hold the trigger, point to an action, then release to run it. + 按住觸發鍵,指向某個動作,再放開即可執行。 + + + + Available in + 適用於 + + + + All applications + 所有應用程式 + + + + Application filter + 應用程式篩選條件 + + + + Running apps + 執行中的應用程式 + + + + Choose application + 選擇應用程式 + + + + Leave empty for every app. Separate application names with commas. + 留空代表適用於所有應用程式。多個應用程式名稱請以逗號分隔。 + + + + ModifierButtons + + + %1 modifier + %1 修飾鍵 + + + + RestartDialog + + + Restart required + 需要重新啟動 + + + + Restart GoPieMenu to use the new language. Restarting will apply all pending changes. + 重新啟動 GoPieMenu 以套用新語言。重新啟動也會套用所有待處理的變更。 + + + + Choose Later to keep editing, then apply your changes before restarting manually. + 選擇「稍後」即可繼續編輯,之後請先套用變更,再手動重新啟動。 + + + + Later + 稍後 + + + + Restart + 重新啟動 + + + + RunningAppsDialog + + + Running applications + 執行中的應用程式 + + + + Search applications + 搜尋應用程式 + + + + Search running applications + 搜尋執行中的應用程式 + + + + No matching applications + 找不到相符的應用程式 + + + + Refresh + 重新整理 + + + + Cancel + 取消 + + + + StatusToggle + + + Enabled + 已啟用 + + + + Disabled + 已停用 + + + + Menu enabled + 選單已啟用 + + + + Disable this menu + 停用此選單 + + + + Enable this menu + 啟用此選單 + + + + Add an action before enabling this menu + 新增動作後才能啟用此選單 + + + + UpdateDialog + + + Update available + 有更新可用 + + + + GoPieMenu %1 → %2 + GoPieMenu %1 → %2 + + + + Release notes are unavailable. You can view this release on GitHub. + 目前無法取得發行說明。你可以在 GitHub 查看此版本。 + + + + This release has no verified Windows installer. View the release on GitHub. + 此版本沒有通過驗證的 Windows 安裝程式。請在 GitHub 查看此版本。 + + + + Downloading and verifying the installer… %1% + 正在下載並驗證安裝程式… %1% + + + + The installer is verified. Install now? GoPieMenu will close after you resolve any unsaved changes. + 安裝程式已通過驗證。要立即安裝嗎?處理完尚未儲存的變更後,GoPieMenu 將會關閉。 + + + + View release on GitHub + 在 GitHub 查看版本資訊 + + + + Skip this version + 略過此版本 + + + + Cancel download + 取消下載 + + + + Later + 稍後 + + + + Update + 更新 + + + + Install and exit + 安裝並結束 + + + + Updates + + + The release response is invalid. + 版本資訊回應無效。 + + + + The release version is invalid. + 版本號無效。 + + + + The installer was not started. You may have cancelled the Windows permission prompt. + 安裝程式未啟動。你可能取消了 Windows 權限提示。 + + + + Installing updates is only supported on Windows. + 僅支援在 Windows 上安裝更新。 + + + + WindowTitleBar + + + Minimize + 最小化 + + + + Restore + 還原 + + + + Maximize + 最大化 + + + + Close window + 關閉視窗 + + + + gpm::EditorSession + + + Main menu + 主選單 + + + + + %1 copy + %1 副本 + + + + Keep at least one menu. + 至少保留一個選單。 + + + + + Submenus can only contain actions. + 子選單只能包含動作。 + + + + + + A pie can have at most 12 items. + 圓餅選單最多只能有 12 個項目。 + + + + New submenu + 新增子選單 + + + + + + New action + 新增動作 + + + + Move or remove the submenu actions before changing its type. + 變更類型前,請先移動或移除子選單中的動作。 + + + + A submenu cannot be placed inside another submenu. + 子選單不能放在另一個子選單中。 + + + + gpm::SettingsWindow + + + Could not restart GoPieMenu. Your changes are saved; please reopen the app manually. + 無法重新啟動 GoPieMenu。變更已儲存,請手動重新開啟應用程式。 + + + + Could not open the release page. + 無法開啟版本資訊頁面。 + + + + Choose folder + 選擇資料夾 + + + + + Choose application + 選擇應用程式 + + + + Choose file + 選擇檔案 + + + + Applications (*.exe *.bat *.cmd);;All files (*) + 應用程式 (*.exe *.bat *.cmd);;所有檔案 (*) + + + + All files (*) + 所有檔案 (*) + + + + Applications (*.exe) + 應用程式 (*.exe) + + + + The selection changed. Choose the color again. + 選取項目已變更,請重新選擇顏色。 + + + + Color updated, but recent colors could not be saved. + 顏色已更新,但無法儲存最近使用的顏色。 + + + + Import configuration + 匯入設定 + + + + + JSON files (*.json) + JSON 檔案 (*.json) + + + + Export configuration + 匯出設定 + + + + Cannot open the icon folder. + 無法開啟圖示資料夾。 + + + + Settings were saved, but Windows startup could not be updated. + 設定已儲存,但無法更新 Windows 啟動設定。 + + + + gpm::TrayManager + + + + GoPieMenu - Active + GoPieMenu - 已啟用 + + + + Settings + 設定 + + + + + Pause + 暫停 + + + + Resume + 繼續 + + + + GoPieMenu - Paused + GoPieMenu - 已暫停 + + + + Quit + 結束 + + + + Update available: %1 + 有可用更新:%1 + + + + Update available + 有更新可用 + + + + GoPieMenu %1 is available. Click to view what's new. + GoPieMenu %1 已推出更新。按一下查看更新內容。 + + + + gpm::UpdateManager + + + The update request timed out. Try again later. + 更新請求逾時,請稍後再試。 + + + + Checking for updates… + 正在檢查更新… + + + + The update response is larger than expected. + 更新回應大小超出預期。 + + + + + + Could not save the installer. Check disk space and try again. + 無法儲存安裝程式。請確認磁碟空間足夠後再試一次。 + + + + No releases are available yet. + 目前尚無可用版本。 + + + + Could not contact the update server: %1 + 無法連線至更新伺服器:%1 + + + + You are using the latest version. + 你使用的已是最新版本。 + + + + + Version %1 was skipped. + 已略過版本 %1。 + + + + GoPieMenu %1 is available. + GoPieMenu %1 已推出。 + + + + The release checksum is missing or invalid. + 版本的 SHA256 校驗碼遺漏或無效。 + + + + Could not create the update download folder. + 無法建立更新下載資料夾。 + + + + + The installer failed verification. Download the update again. + 安裝程式驗證失敗。請重新下載更新。 + + + + The update is ready to install. + 更新已準備就緒,可以安裝。 + + + + Could not save the skipped version. Try again. + 無法儲存略過的版本,請再試一次。 + + + + Could not start the installer. Please try again. + 無法啟動安裝程式,請再試一次。 + + + diff --git a/scripts/release-notes.ps1 b/scripts/release-notes.ps1 new file mode 100644 index 0000000..2547fbc --- /dev/null +++ b/scripts/release-notes.ps1 @@ -0,0 +1,40 @@ +param( + [Parameter(Mandatory = $true)][string]$Version, + [string]$ChangelogPath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'CHANGELOG.md'), + [string]$OutputPath +) +$ErrorActionPreference = 'Stop' +$number = $Version -replace '^v', '' +if ($number -notmatch '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$') { + throw 'A release needs a stable version such as 2.1.0 or v2.1.0.' +} +$source = Get-Content -LiteralPath $ChangelogPath -Raw -Encoding UTF8 +$escaped = [regex]::Escape($number) +$versions = [regex]::Matches($source, "(?ms)^## \[$escaped\][^\r\n]*\r?\n(.*?)(?=^## \[|\z)") +if ($versions.Count -ne 1) { throw "Expected one changelog section for $number." } +$body = $versions[0].Groups[1].Value.Trim() +$sections = [regex]::Matches($body, '(?ms)^[ \t]*[ \t]*\r?\n(.*?)^[ \t]*[ \t]*\r?$') +$languages = @{} +foreach ($section in $sections) { + $language = $section.Groups[1].Value + $content = $section.Groups[2].Value.Trim() + if ($languages.ContainsKey($language) -or !$content -or $content.Contains('').Count -ne $sections.Count) { + throw "Unmatched or unsupported language markers in $number." +} +$notes = "# GoPieMenu $number`n`n$body`n" +if ($OutputPath) { + $absolute = [IO.Path]::GetFullPath($OutputPath) + [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($absolute)) | Out-Null + [IO.File]::WriteAllText($absolute, $notes, [Text.UTF8Encoding]::new($false)) +} else { + $notes +} diff --git a/src/core/ActionExecutor.cpp b/src/core/ActionExecutor.cpp index af215ae..794783f 100644 --- a/src/core/ActionExecutor.cpp +++ b/src/core/ActionExecutor.cpp @@ -1,3 +1,4 @@ +#include // ============================================================================= // GoPieMenu - ActionExecutor Implementation // ============================================================================= @@ -33,7 +34,7 @@ bool ActionExecutor::Execute(const PieItem& Item) auto It = Handlers.find(Item.Action); if (It == Handlers.end()) { - auto Error = QStringLiteral("No handler for action type: %1") + auto Error = QCoreApplication::translate("ActionExecutor", "No handler for action type: %1") .arg(ActionTypeToString(Item.Action)); qWarning() << "[ActionExecutor]" << Error; emit ActionFailed(Item.Name, Error); @@ -43,7 +44,7 @@ bool ActionExecutor::Execute(const PieItem& Item) auto& Handler = It->second; if (!Handler->Validate(Item)) { - auto Error = QStringLiteral("Invalid action data for: %1").arg(Item.Name); + auto Error = QCoreApplication::translate("ActionExecutor", "Invalid action data for: %1").arg(Item.Name); qWarning() << "[ActionExecutor]" << Error; emit ActionFailed(Item.Name, Error); return false; @@ -56,7 +57,7 @@ bool ActionExecutor::Execute(const PieItem& Item) } else { - emit ActionFailed(Item.Name, QStringLiteral("Execution failed")); + emit ActionFailed(Item.Name, QCoreApplication::translate("ActionExecutor", "Execution failed")); } return bOk; } diff --git a/src/core/ConfigManager.cpp b/src/core/ConfigManager.cpp index df218ac..4059f3e 100644 --- a/src/core/ConfigManager.cpp +++ b/src/core/ConfigManager.cpp @@ -1,3 +1,4 @@ +#include #include "ConfigManager.h" #include #include @@ -24,7 +25,7 @@ ConfigLoadResult ConfigManager::Load() { ConfigSaveResult ConfigManager::Save(const AppConfig& config) { if (auto error = ValidateConfig(config); !error.isEmpty()) return {false, error}; if (!QDir().mkpath(QFileInfo(ConfigPath).absolutePath())) - return {false, QStringLiteral("Cannot create the configuration folder.")}; + return {false, QCoreApplication::translate("ConfigManager", "Cannot create the configuration folder.")}; QSaveFile file(ConfigPath); file.setDirectWriteFallback(false); if (!file.open(QIODevice::WriteOnly)) return {false, file.errorString()}; diff --git a/src/core/ConfigValidation.cpp b/src/core/ConfigValidation.cpp index 3a3ccba..7484ba2 100644 --- a/src/core/ConfigValidation.cpp +++ b/src/core/ConfigValidation.cpp @@ -1,3 +1,4 @@ +#include #include "ConfigValidation.h" #include "Shortcut.h" #include @@ -14,7 +15,7 @@ bool InRange(double value, double low, double high) { QString CheckFields(const QJsonObject& object, const QStringList& keys, QJsonValue::Type type) { for (const auto& key : keys) if (object.contains(key) && object[key].type() != type) - return QStringLiteral("Invalid value type for %1.").arg(key); + return QCoreApplication::translate("ConfigValidation", "Invalid value type for %1.").arg(key); return {}; } QString CheckJsonStyle(const QJsonObject& style) { @@ -26,15 +27,15 @@ QString CheckJsonStyle(const QJsonObject& style) { return error.isEmpty() ? CheckFields(style, {"autoContrast", "autoContrastText"}, QJsonValue::Bool) : error; } QString CheckJsonItem(const QJsonObject& item, int depth) { - if (depth > 1) return QStringLiteral("Submenus can contain actions, but cannot contain other submenus."); + if (depth > 1) return QCoreApplication::translate("ConfigValidation", "Submenus can contain actions, but cannot contain other submenus."); if (auto error = CheckFields(item, {"id", "name", "icon", "actionType", "actionData", "actionArgs", "color"}, QJsonValue::String); !error.isEmpty()) return error; const auto action = item.value("actionType").toString("None"); if (action != "None" && ActionTypeToString(StringToActionType(action)) != action) - return QStringLiteral("Unknown action type: %1").arg(action); + return QCoreApplication::translate("ConfigValidation", "Unknown action type: %1").arg(action); if (item.contains("subItems") && !item["subItems"].isArray()) - return QStringLiteral("Submenu items must be a list."); + return QCoreApplication::translate("ConfigValidation", "Submenu items must be a list."); for (const auto& child : item["subItems"].toArray()) { - if (!child.isObject()) return QStringLiteral("Each submenu item must be an object."); + if (!child.isObject()) return QCoreApplication::translate("ConfigValidation", "Each submenu item must be an object."); if (auto error = CheckJsonItem(child.toObject(), depth + 1); !error.isEmpty()) return error; } return {}; @@ -42,49 +43,49 @@ QString CheckJsonItem(const QJsonObject& item, int depth) { } QString ValidateStyle(const StyleConfig& s, int itemCount) { if (!InRange(s.OuterRadius, 80, 400) || !InRange(s.InnerRadius, 10, 150) || s.InnerRadius >= s.OuterRadius - 8) - return QStringLiteral("The inner radius must be smaller than the outer radius (80–400)."); + return QCoreApplication::translate("ConfigValidation", "The inner radius must be smaller than the outer radius (80–400)."); if (!InRange(s.IconSize, 12, 96) || !InRange(s.FontSize, 8, 32)) - return QStringLiteral("Choose an icon size of 12–96 and a font size of 8–32."); + return QCoreApplication::translate("ConfigValidation", "Choose an icon size of 12–96 and a font size of 8–32."); if (!InRange(s.GapAngle, 0, 30) || (itemCount > 1 && s.GapAngle >= 360.0 / itemCount)) - return QStringLiteral("The gap must be smaller than each sector."); + return QCoreApplication::translate("ConfigValidation", "The gap must be smaller than each sector."); if (!InRange(s.Opacity, 0.1, 1) || !InRange(s.BorderWidth, 0, 8) || !InRange(s.TextOutlineThickness, 0, 10) || !InRange(s.HoverScale, 1, 1.25) || s.AnimationDuration < 0 || s.AnimationDuration > 1000) - return QStringLiteral("The appearance contains an out-of-range value."); + return QCoreApplication::translate("ConfigValidation", "The appearance contains an out-of-range value."); for (const auto& color : {s.BackgroundColor, s.SectorColor, s.HoverColor, s.BorderColor, s.TextColor, s.CenterColor, s.CenterDotColor}) - if (!color.isValid()) return QStringLiteral("The appearance contains an invalid color."); + if (!color.isValid()) return QCoreApplication::translate("ConfigValidation", "The appearance contains an invalid color."); return {}; } QString ValidateConfig(const AppConfig& config) { - if (config.Profiles.empty()) return QStringLiteral("Keep at least one menu."); + if (config.Profiles.empty()) return QCoreApplication::translate("ConfigValidation", "Keep at least one menu."); if (auto error = ValidateStyle(config.GlobalStyle); !error.isEmpty()) return error; QSet profileIds; for (const auto& profile : config.Profiles) { const auto prefix = profile.Name + QStringLiteral(": "); - if (profile.Id.isEmpty() || profileIds.contains(profile.Id)) return QStringLiteral("Menu IDs must be unique."); + if (profile.Id.isEmpty() || profileIds.contains(profile.Id)) return QCoreApplication::translate("ConfigValidation", "Menu IDs must be unique."); profileIds.insert(profile.Id); - if (profile.Name.trimmed().isEmpty()) return QStringLiteral("Give each menu a name."); - if (profile.Items.size() > PieMenuConfig::MaxItems) return prefix + QStringLiteral("A pie can have at most 12 items."); + if (profile.Name.trimmed().isEmpty()) return QCoreApplication::translate("ConfigValidation", "Give each menu a name."); + if (profile.Items.size() > PieMenuConfig::MaxItems) return prefix + QCoreApplication::translate("ConfigValidation", "A pie can have at most 12 items."); if (!profile.Trigger.IsValid() || (ModifierKeyToUint(profile.Trigger.Modifiers) & ~15) || profile.Trigger.VKCode > 255) - return prefix + QStringLiteral("Choose a valid trigger."); - if (profile.bEnabled && profile.Items.empty()) return prefix + QStringLiteral("Add an action or disable this empty menu."); + return prefix + QCoreApplication::translate("ConfigValidation", "Choose a valid trigger."); + if (profile.bEnabled && profile.Items.empty()) return prefix + QCoreApplication::translate("ConfigValidation", "Add an action or disable this empty menu."); if (auto error = ValidateStyle(profile.StyleOverride.value_or(config.GlobalStyle), int(profile.Items.size())); !error.isEmpty()) return prefix + error; QSet itemIds; std::function check = [&](const PieItem& item, bool child) -> QString { - if (item.Id.isEmpty() || itemIds.contains(item.Id)) return QStringLiteral("Item IDs must be unique within a menu."); + if (item.Id.isEmpty() || itemIds.contains(item.Id)) return QCoreApplication::translate("ConfigValidation", "Item IDs must be unique within a menu."); itemIds.insert(item.Id); - if (child && item.Action == ActionType::ListMenu) return QStringLiteral("A submenu can only contain actions."); - if (item.Action != ActionType::ListMenu && !item.SubItems.empty()) return QStringLiteral("Only submenus can have children."); - if (item.Color && !item.Color->isValid()) return QStringLiteral("An item contains an invalid color."); + if (child && item.Action == ActionType::ListMenu) return QCoreApplication::translate("ConfigValidation", "A submenu can only contain actions."); + if (item.Action != ActionType::ListMenu && !item.SubItems.empty()) return QCoreApplication::translate("ConfigValidation", "Only submenus can have children."); + if (item.Color && !item.Color->isValid()) return QCoreApplication::translate("ConfigValidation", "An item contains an invalid color."); if (profile.bEnabled) { - if (item.Name.trimmed().isEmpty()) return QStringLiteral("Give each action a name."); + if (item.Name.trimmed().isEmpty()) return QCoreApplication::translate("ConfigValidation", "Give each action a name."); if (!item.IsNoOp() && item.Action == ActionType::SendHotkey && ParseShortcut(item.ActionData).empty()) - return item.Name + QStringLiteral(": record a valid keyboard shortcut."); + return item.Name + QCoreApplication::translate("ConfigValidation", ": record a valid keyboard shortcut."); if (!item.IsNoOp() && item.Action == ActionType::OpenURL) { const auto url = QUrl::fromUserInput(item.ActionData); - if (!url.isValid() || url.scheme().isEmpty()) return item.Name + QStringLiteral(": enter a valid URL."); + if (!url.isValid() || url.scheme().isEmpty()) return item.Name + QCoreApplication::translate("ConfigValidation", ": enter a valid URL."); } } for (const auto& sub : item.SubItems) if (auto error = check(sub, true); !error.isEmpty()) return error; @@ -98,38 +99,38 @@ ConfigLoadResult ParseConfig(const QByteArray& data) { QJsonParseError parseError; const auto document = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError) - return {{}, QStringLiteral("Invalid JSON at byte %1: %2").arg(parseError.offset).arg(parseError.errorString())}; - if (!document.isObject()) return {{}, QStringLiteral("The configuration must be a JSON object.")}; + return {{}, QCoreApplication::translate("ConfigValidation", "Invalid JSON at byte %1: %2").arg(parseError.offset).arg(parseError.errorString())}; + if (!document.isObject()) return {{}, QCoreApplication::translate("ConfigValidation", "The configuration must be a JSON object.")}; const auto root = document.object(); if (auto error = CheckFields(root, {"startWithWindows"}, QJsonValue::Bool); !error.isEmpty()) return {{}, error}; if (auto error = CheckFields(root, {"language"}, QJsonValue::String); !error.isEmpty()) return {{}, error}; if (root.contains("version") && root["version"].toString() != "1.0") - return {{}, QStringLiteral("This configuration version is not supported.")}; - if (!root["profiles"].isArray()) return {{}, QStringLiteral("The configuration must contain a profiles list.")}; + return {{}, QCoreApplication::translate("ConfigValidation", "This configuration version is not supported.")}; + if (!root["profiles"].isArray()) return {{}, QCoreApplication::translate("ConfigValidation", "The configuration must contain a profiles list.")}; if (root.contains("globalStyle") && !root["globalStyle"].isObject()) - return {{}, QStringLiteral("The global appearance must be an object.")}; + return {{}, QCoreApplication::translate("ConfigValidation", "The global appearance must be an object.")}; if (auto error = CheckJsonStyle(root["globalStyle"].toObject()); !error.isEmpty()) return {{}, error}; for (const auto& value : root["profiles"].toArray()) { - if (!value.isObject()) return {{}, QStringLiteral("Each menu must be an object.")}; + if (!value.isObject()) return {{}, QCoreApplication::translate("ConfigValidation", "Each menu must be an object.")}; const auto profile = value.toObject(); if (auto error = CheckFields(profile, {"id", "name"}, QJsonValue::String); !error.isEmpty()) return {{}, error}; if (auto error = CheckFields(profile, {"enabled"}, QJsonValue::Bool); !error.isEmpty()) return {{}, error}; if (auto error = CheckFields(profile, {"trigger", "styleOverride"}, QJsonValue::Object); !error.isEmpty()) return {{}, error}; if (auto error = CheckFields(profile, {"appFilter"}, QJsonValue::Array); !error.isEmpty()) return {{}, error}; - for (const auto& app : profile["appFilter"].toArray()) if (!app.isString()) return {{}, QStringLiteral("Application filters must be text.")}; + for (const auto& app : profile["appFilter"].toArray()) if (!app.isString()) return {{}, QCoreApplication::translate("ConfigValidation", "Application filters must be text.")}; if (auto error = CheckJsonStyle(profile["styleOverride"].toObject()); !error.isEmpty()) return {{}, error}; - if (!profile["items"].isArray()) return {{}, QStringLiteral("Each menu must contain an items list.")}; + if (!profile["items"].isArray()) return {{}, QCoreApplication::translate("ConfigValidation", "Each menu must contain an items list.")}; const auto trigger = profile["trigger"].toObject(); if (auto error = CheckFields(trigger, {"mode", "mouseButton"}, QJsonValue::String); !error.isEmpty()) return {{}, error}; const auto mode = trigger["mode"].toString("MouseHold"); if (ActivationModeToString(StringToActivationMode(mode)) != mode) - return {{}, QStringLiteral("Unknown activation mode: %1").arg(mode)}; + return {{}, QCoreApplication::translate("ConfigValidation", "Unknown activation mode: %1").arg(mode)}; if (trigger.contains("modifiers") && (trigger["modifiers"].toInt(-1) < 0 || trigger["modifiers"].toInt() > 15)) - return {{}, QStringLiteral("Invalid trigger modifiers.")}; + return {{}, QCoreApplication::translate("ConfigValidation", "Invalid trigger modifiers.")}; if (trigger.contains("vkCode") && (trigger["vkCode"].toInt(-1) < 0 || trigger["vkCode"].toInt() > 255)) - return {{}, QStringLiteral("Invalid trigger key.")}; + return {{}, QCoreApplication::translate("ConfigValidation", "Invalid trigger key.")}; for (const auto& item : profile["items"].toArray()) { - if (!item.isObject()) return {{}, QStringLiteral("Each action must be an object.")}; + if (!item.isObject()) return {{}, QCoreApplication::translate("ConfigValidation", "Each action must be an object.")}; if (auto error = CheckJsonItem(item.toObject(), 0); !error.isEmpty()) return {{}, error}; } } diff --git a/src/core/EditorSession.cpp b/src/core/EditorSession.cpp index b6c8eda..533afe3 100644 --- a/src/core/EditorSession.cpp +++ b/src/core/EditorSession.cpp @@ -1,4 +1,6 @@ +#include #include "EditorSession.h" +#include "TranslationManager.h" #include #include #include @@ -15,13 +17,13 @@ void RenewIds(PieItem& item) { } QString ActionName(ActionType type) { switch (type) { - case ActionType::LaunchApp: return QStringLiteral("Open application"); - case ActionType::SendHotkey: return QStringLiteral("Keyboard shortcut"); - case ActionType::OpenFile: return QStringLiteral("Open file or folder"); - case ActionType::OpenURL: return QStringLiteral("Open website"); - case ActionType::RunCommand: return QStringLiteral("Run command"); - case ActionType::ListMenu: return QStringLiteral("Submenu"); - default: return QStringLiteral("Choose an action"); + case ActionType::LaunchApp: return QCoreApplication::translate("EditorSession", "Open application"); + case ActionType::SendHotkey: return QCoreApplication::translate("EditorSession", "Keyboard shortcut"); + case ActionType::OpenFile: return QCoreApplication::translate("EditorSession", "Open file or folder"); + case ActionType::OpenURL: return QCoreApplication::translate("EditorSession", "Open website"); + case ActionType::RunCommand: return QCoreApplication::translate("EditorSession", "Run command"); + case ActionType::ListMenu: return QCoreApplication::translate("EditorSession", "Submenu"); + default: return QCoreApplication::translate("EditorSession", "Choose an action"); } } QString DefaultIcon(ActionType type) { @@ -37,7 +39,7 @@ QString DefaultIcon(ActionType type) { } } QString KeyDisplayName(uint32_t vkCode) { - if (!vkCode) return QStringLiteral("Not set"); + if (!vkCode) return QCoreApplication::translate("EditorSession", "Not set"); #ifdef Q_OS_WIN UINT scan = MapVirtualKeyW(vkCode, MAPVK_VK_TO_VSC); if (vkCode == VK_LEFT || vkCode == VK_RIGHT || vkCode == VK_UP || vkCode == VK_DOWN || @@ -46,10 +48,19 @@ QString KeyDisplayName(uint32_t vkCode) { wchar_t name[128]{}; if (GetKeyNameTextW(LONG(scan << 16), name, 128)) return QString::fromWCharArray(name); #endif - return QStringLiteral("Key %1").arg(vkCode); + return QCoreApplication::translate("EditorSession", "Key %1").arg(vkCode); } QString TriggerDisplayName(const TriggerDef& trigger) { - QString key = trigger.Mode == ActivationMode::MouseHold ? MouseButtonToString(trigger.Button) + " mouse" : KeyDisplayName(trigger.VKCode); + QString mouse; + switch (trigger.Button) { + case MouseButton::Left: mouse = QCoreApplication::translate("EditorSession", "Left mouse"); break; + case MouseButton::Right: mouse = QCoreApplication::translate("EditorSession", "Right mouse"); break; + case MouseButton::Middle: mouse = QCoreApplication::translate("EditorSession", "Middle mouse"); break; + case MouseButton::X1: mouse = QCoreApplication::translate("EditorSession", "Side button 1 (X1)"); break; + case MouseButton::X2: mouse = QCoreApplication::translate("EditorSession", "Side button 2 (X2)"); break; + default: mouse = QCoreApplication::translate("EditorSession", "Not set"); break; + } + QString key = trigger.Mode == ActivationMode::MouseHold ? mouse : KeyDisplayName(trigger.VKCode); const auto mods = ModifierKeyToString(trigger.Modifiers); if (!mods.isEmpty()) key.prepend(mods + " + "); return key; @@ -59,7 +70,7 @@ EditorSession::EditorSession(ConfigManager* manager, QObject* parent) : QObject(parent), Manager(manager), Draft(manager->GetConfig()), AppliedBytes(Draft.Serialize()) { normalizeSelection(); if (!manager->LoadError().isEmpty()) - Error = QStringLiteral("Your configuration could not be loaded. The original file has not been changed. ") + manager->LoadError(); + Error = QCoreApplication::translate("EditorSession", "Your configuration could not be loaded. The original file has not been changed. ") + manager->LoadError(); } const PieMenuConfig* EditorSession::currentProfile() const { auto it = std::ranges::find_if(Draft.Profiles, [&](const auto& p) { return p.Id == CurrentProfile; }); @@ -134,7 +145,7 @@ QVariantMap EditorSession::selectedItem() const { return {}; } QVariantList EditorSession::destinations() const { - QVariantList result{QVariantMap{{"id", ""}, {"name", "Main menu"}}}; + QVariantList result{QVariantMap{{"id", ""}, {"name", tr("Main menu")}}}; if (const auto* p = currentProfile()) for (const auto& item : p->Items) if (item.Action == ActionType::ListMenu) result.append(QVariantMap{{"id", item.Id}, {"name", item.Name}}); return result; @@ -198,7 +209,7 @@ void EditorSession::leaveFolder() { void EditorSession::addProfile() { mutate({}, [&] { PieMenuConfig p; - p.Id = NewId(); p.Name = QStringLiteral("New menu"); p.bEnabled = false; + p.Id = NewId(); p.Name = QCoreApplication::translate("EditorSession", "New menu"); p.bEnabled = false; p.Trigger.Modifiers = ModifierKey::Ctrl; p.StyleOverride = Draft.GlobalStyle; CurrentProfile = p.Id; Selected.clear(); Folder.clear(); Draft.Profiles.push_back(std::move(p)); @@ -209,13 +220,13 @@ void EditorSession::duplicateProfile() { mutate({}, [&] { auto copy = *currentProfile(); copy.StyleOverride = effectiveStyle(); - copy.Id = NewId(); copy.Name += QStringLiteral(" copy"); copy.bEnabled = false; + copy.Id = NewId(); copy.Name = tr("%1 copy").arg(copy.Name); copy.bEnabled = false; for (auto& item : copy.Items) RenewIds(item); CurrentProfile = copy.Id; Selected.clear(); Folder.clear(); Draft.Profiles.push_back(std::move(copy)); }); } void EditorSession::removeProfile() { - if (Draft.Profiles.size() <= 1) { reportError("Keep at least one menu."); return; } + if (Draft.Profiles.size() <= 1) { reportError(tr("Keep at least one menu.")); return; } mutate({}, [&] { std::erase_if(Draft.Profiles, [&](const auto& p) { return p.Id == CurrentProfile; }); CurrentProfile.clear(); Selected.clear(); Folder.clear(); @@ -248,11 +259,11 @@ QString EditorSession::addItem(int action) { auto* values = container(Folder); if (!values) return {}; const auto type = static_cast(std::clamp(action, 1, 6)); - if (type == ActionType::ListMenu && !Folder.isEmpty()) { reportError("Submenus can only contain actions."); return {}; } - if (Folder.isEmpty() && values->size() >= PieMenuConfig::MaxItems) { reportError("A pie can have at most 12 items."); return {}; } + if (type == ActionType::ListMenu && !Folder.isEmpty()) { reportError(tr("Submenus can only contain actions.")); return {}; } + if (Folder.isEmpty() && values->size() >= PieMenuConfig::MaxItems) { reportError(tr("A pie can have at most 12 items.")); return {}; } QString id; mutate({}, [&] { - auto item = PieItem::Create(type == ActionType::ListMenu ? "New submenu" : "New action", type, {}, DefaultIcon(type)); + auto item = PieItem::Create(type == ActionType::ListMenu ? tr("New submenu") : tr("New action"), type, {}, DefaultIcon(type)); id = item.Id; Selected = id; container(Folder)->push_back(std::move(item)); }); return id; @@ -270,9 +281,9 @@ void EditorSession::duplicateItem(const QString& id) { auto* values = owner(id); const auto* item = findItem(id); if (!values || !item) return; - if (values == &mutableProfile()->Items && values->size() >= PieMenuConfig::MaxItems) { reportError("A pie can have at most 12 items."); return; } + if (values == &mutableProfile()->Items && values->size() >= PieMenuConfig::MaxItems) { reportError(tr("A pie can have at most 12 items.")); return; } mutate({}, [&] { - auto copy = *findItem(id); RenewIds(copy); copy.Name += " copy"; + auto copy = *findItem(id); RenewIds(copy); copy.Name = tr("%1 copy").arg(copy.Name); Selected = copy.Id; owner(id)->push_back(std::move(copy)); }); } @@ -280,10 +291,10 @@ void EditorSession::setItemField(const QString& field, const QVariant& value) { auto* item = mutableItem(Selected); if (!item) return; if (field == "action" && item->Action == ActionType::ListMenu && !item->SubItems.empty()) { - reportError("Move or remove the submenu actions before changing its type."); return; + reportError(tr("Move or remove the submenu actions before changing its type.")); return; } if (field == "action" && value.toInt() == int(ActionType::ListMenu) && !Folder.isEmpty()) { - reportError("Submenus can only contain actions."); return; + reportError(tr("Submenus can only contain actions.")); return; } mutate(CurrentProfile + "/" + Selected + "/" + field, [&] { auto& i = *mutableItem(Selected); @@ -306,11 +317,11 @@ void EditorSession::setItemField(const QString& field, const QVariant& value) { if (i.Action == ActionType::SendHotkey) return target; return QFileInfo(target).completeBaseName(); }; - const bool autoName = i.Name.isEmpty() || i.Name == "New action" || i.Name == suggested(i.ActionData); + const bool autoName = i.Name.isEmpty() || (i.Name == "New action" || i.Name == tr("New action")) || i.Name == suggested(i.ActionData); i.ActionData = value.toString(); if (autoName) { i.Name = suggested(i.ActionData); - if (i.Name.isEmpty()) i.Name = "New action"; + if (i.Name.isEmpty()) i.Name = tr("New action"); } } }); @@ -320,10 +331,10 @@ bool EditorSession::moveItem(const QString& id, const QString& destinationFolder auto* from = owner(id); auto* to = container(destinationFolder); if (!item || !from || !to || id == destinationFolder) return false; if (!destinationFolder.isEmpty() && item->Action == ActionType::ListMenu) { - reportError("A submenu cannot be placed inside another submenu."); return false; + reportError(tr("A submenu cannot be placed inside another submenu.")); return false; } if (destinationFolder.isEmpty() && from != to && to->size() >= PieMenuConfig::MaxItems) { - reportError("A pie can have at most 12 items."); return false; + reportError(tr("A pie can have at most 12 items.")); return false; } mutate({}, [&] { auto copy = *findItem(id); @@ -373,6 +384,14 @@ void EditorSession::setStylePreset(const QString& name) { if (next.ToJson() != effectiveStyle().ToJson()) mutableProfile()->StyleOverride = next; }); } +QString EditorSession::language() const { + if (Draft.Language.isEmpty() || Draft.Language == "system") return QStringLiteral("system"); + return ResolveLanguage(Draft.Language).replace('_', '-'); +} +void EditorSession::setLanguage(const QString& language) { + if (language != "system" && language != "en" && language != "zh-TW" && language != "zh-CN") return; + mutate("language", [&] { Draft.Language = language; }); +} void EditorSession::setStartWithWindows(bool value) { mutate({}, [&] { Draft.bStartWithWindows = value; }); } void EditorSession::undo() { if (Undo.empty()) return; diff --git a/src/core/EditorSession.h b/src/core/EditorSession.h index 3effd36..9122ae7 100644 --- a/src/core/EditorSession.h +++ b/src/core/EditorSession.h @@ -26,6 +26,7 @@ class EditorSession : public QObject { Q_PROPERTY(bool dirty READ dirty NOTIFY changed) Q_PROPERTY(bool canUndo READ canUndo NOTIFY changed) Q_PROPERTY(bool canRedo READ canRedo NOTIFY changed) + Q_PROPERTY(QString language READ language NOTIFY changed) Q_PROPERTY(bool startWithWindows READ startWithWindows NOTIFY changed) Q_PROPERTY(QString error READ error NOTIFY errorChanged) public: @@ -44,6 +45,8 @@ class EditorSession : public QObject { bool canUndo() const { return !Undo.empty(); } bool canRedo() const { return !Redo.empty(); } bool startWithWindows() const { return Draft.bStartWithWindows; } + QString language() const; + Q_INVOKABLE void setLanguage(const QString& language); QString error() const { return Error; } const AppConfig& draft() const { return Draft; } const PieMenuConfig* currentProfile() const; diff --git a/src/core/TranslationManager.cpp b/src/core/TranslationManager.cpp new file mode 100644 index 0000000..3a4ba6f --- /dev/null +++ b/src/core/TranslationManager.cpp @@ -0,0 +1,32 @@ +#include "TranslationManager.h" +#include +#include + +static void InitializeTranslations() { Q_INIT_RESOURCE(translations); } + +namespace gpm { +QString ResolveLanguage(const QString& preference, const QStringList& systemLanguages) { + QString tag = preference; + if (tag.isEmpty() || tag == "system") + tag = systemLanguages.isEmpty() ? QStringLiteral("en") : systemLanguages.front(); + const QLocale locale(tag); + if (locale.language() != QLocale::Chinese) return QStringLiteral("en"); + return locale.script() == QLocale::TraditionalHanScript + ? QStringLiteral("zh_TW") : QStringLiteral("zh_CN"); +} +TranslationManager::TranslationManager() { InitializeTranslations(); } +QString InterfaceFontFamily(const QString& preference) { + return ResolveLanguage(preference).startsWith("zh_") + ? QStringLiteral("Microsoft JhengHei UI") : QStringLiteral("Segoe UI"); +} +bool TranslationManager::load(const QString& preference) { + QCoreApplication::removeTranslator(&Translator); + const auto language = ResolveLanguage(preference); + if (language == "en") return true; + if (!Translator.load(":/i18n/gopiemenu_" + language + ".qm")) { + qWarning() << "Could not load translation:" << language; + return false; + } + return QCoreApplication::installTranslator(&Translator); +} +} diff --git a/src/core/TranslationManager.h b/src/core/TranslationManager.h new file mode 100644 index 0000000..cc5e565 --- /dev/null +++ b/src/core/TranslationManager.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include + +namespace gpm { +// Resolve the preferred UI language, falling back to the English source text. +QString ResolveLanguage(const QString& preference, + const QStringList& systemLanguages = QLocale::system().uiLanguages()); +QString InterfaceFontFamily(const QString& preference); + +class TranslationManager { +public: + TranslationManager(); + bool load(const QString& preference); +private: + QTranslator Translator; +}; +} diff --git a/src/main.cpp b/src/main.cpp index fe665a2..f81a562 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include "core/ConfigManager.h" +#include "core/TranslationManager.h" #include "core/HookManager.h" #include "core/ActionExecutor.h" #include "ui/PieMenuWidget.h" @@ -13,6 +14,14 @@ #include #include #include +#include +#include +#include +#include +#include +#ifdef Q_OS_WIN +#include +#endif int main(int argc, char* argv[]) { QApplication app(argc, argv); @@ -24,26 +33,93 @@ int main(int argc, char* argv[]) { app.setStyle("Fusion"); app.styleHints()->setColorScheme(Qt::ColorScheme::Light); QQuickStyle::setStyle("Basic"); + const auto arguments = app.arguments(); +#ifdef Q_OS_WIN + // A restarted child must not contend for the instance lock until its parent exits. + const int parentArgument = arguments.indexOf("--restart-parent"); + if (parentArgument >= 0) { + bool valid = false; + const auto pid = arguments.value(parentArgument + 1).toULong(&valid); + if (!valid || !pid || pid == GetCurrentProcessId()) return 1; + HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, pid); + if (parent) { + const auto result = WaitForSingleObject(parent, 30000); + CloseHandle(parent); + if (result != WAIT_OBJECT_0) return 1; + } else if (GetLastError() != ERROR_INVALID_PARAMETER) return 1; + } +#endif const bool smokeTest = app.arguments().contains("--smoke-test"); + if (smokeTest && !QSslSocket::supportsSsl()) return 1; const bool preview = smokeTest || app.arguments().contains("--preview"); app.setQuitOnLastWindowClosed(preview); + gpm::TranslationManager translations; + translations.load("system"); QSharedMemory instance("GoPieMenu_SingleInstance"); + const bool showSettings = preview || !arguments.contains("--autostart") || arguments.contains("--settings"); + const QString activationName = "GoPieMenu_Activation"; if (!preview && !instance.create(1)) { - QMessageBox::information(nullptr, "GoPieMenu", "GoPieMenu is already running."); + if (showSettings) { + QLocalSocket activation; + activation.connectToServer(activationName); + if (activation.waitForConnected(3000)) { + activation.write("show\n"); + activation.waitForBytesWritten(1000); + } else QMessageBox::information(nullptr, "GoPieMenu", QCoreApplication::translate("Application", "GoPieMenu is already running.")); + } return 0; } + QLocalServer activationServer; + if (!preview) { + QLocalServer::removeServer(activationName); + activationServer.setSocketOptions(QLocalServer::UserAccessOption); + activationServer.listen(activationName); + } QTemporaryDir previewDir; gpm::ConfigManager config(nullptr, preview ? previewDir.filePath("config.json") : QString()); + translations.load(config.GetConfig().Language); + app.setFont(QFont(gpm::InterfaceFontFamily(config.GetConfig().Language))); gpm::ActionExecutor executor; executor.RegisterBuiltins(); gpm::IconService icons; gpm::PieMenuWidget pie(&icons); - gpm::SettingsWindow editor(&config, &icons); + gpm::UpdateOptions updateOptions; + updateOptions.language = gpm::ResolveLanguage(config.GetConfig().Language); + std::unique_ptr updates; + if (!preview) updates = std::make_unique(updateOptions); + gpm::SettingsWindow editor(&config, &icons, nullptr, updates.get()); editor.setPlatformIntegrationEnabled(!preview); editor.setPreviewMode(preview); + editor.setRestartHandler([preview] { + QStringList restartArguments{"--settings"}; + if (preview) restartArguments << "--preview"; +#ifdef Q_OS_WIN + restartArguments << "--restart-parent" << QString::number(QCoreApplication::applicationPid()); +#endif + return QProcess::startDetached(QCoreApplication::applicationFilePath(), restartArguments); + }); + if (!preview) editor.syncAutoStart(); + QObject::connect(&activationServer, &QLocalServer::newConnection, &editor, [&] { + while (auto* socket = activationServer.nextPendingConnection()) { + socket->setParent(&activationServer); + auto receive = [socket, &editor] { + if (!socket->canReadLine()) return; + if (socket->readLine().trimmed() == "show") editor.show(); + socket->disconnectFromServer(); + }; + QObject::connect(socket, &QLocalSocket::readyRead, &editor, receive); + QObject::connect(socket, &QLocalSocket::disconnected, socket, &QObject::deleteLater); + receive(); + } + }); if (!editor.window()) return 1; gpm::HookManager hooks(&config); gpm::TrayManager tray; + QObject::connect(&editor, &gpm::SettingsWindow::updateToastRequested, &tray, &gpm::TrayManager::ShowUpdateNotification); + QObject::connect(&tray, &gpm::TrayManager::UpdateRequested, &editor, &gpm::SettingsWindow::showUpdate); + if (updates) QObject::connect(updates.get(), &gpm::UpdateManager::changed, &tray, [&] { + tray.SetUpdateAvailable(updates->available() ? updates->version() : QString()); + }); QObject::connect(&hooks, &gpm::HookManager::Triggered, &pie, [&](const QString& id, const QPoint& pos) { if (hooks.IsSuspended()) return; if (const auto* profile = config.FindProfile(id)) pie.ShowAt(pos, *profile, config.GetConfig().GlobalStyle); @@ -65,14 +141,15 @@ int main(int argc, char* argv[]) { QObject::connect(&tray, &gpm::TrayManager::SettingsRequested, &editor, &gpm::SettingsWindow::show); QObject::connect(&tray, &gpm::TrayManager::PauseToggled, &hooks, [&](bool paused) { if (paused) hooks.Uninstall(); - else if (!preview && !hooks.Install()) editor.session()->reportError("Could not install global input hooks."); + else if (!preview && !hooks.Install()) editor.session()->reportError(QCoreApplication::translate("Application", "Could not install global input hooks.")); }); QObject::connect(&tray, &gpm::TrayManager::QuitRequested, &editor, &gpm::SettingsWindow::requestExit); QObject::connect(&editor, &gpm::SettingsWindow::exitConfirmed, &app, &QCoreApplication::quit); - if (!preview && !hooks.Install()) editor.session()->reportError("Could not install global input hooks."); + if (!preview && !hooks.Install()) editor.session()->reportError(QCoreApplication::translate("Application", "Could not install global input hooks.")); if (!preview) tray.Show(); - if (preview || app.arguments().contains("--settings") || !QFileInfo::exists(config.GetConfigFilePath()) || !config.LoadError().isEmpty()) + if (showSettings || !config.LoadError().isEmpty()) editor.show(); if (smokeTest) QTimer::singleShot(1000, &app, &QCoreApplication::quit); + if (updates) QTimer::singleShot(0, updates.get(), [&] { updates->check(); }); return app.exec(); } diff --git a/src/models/AppConfig.h b/src/models/AppConfig.h index 91fbe4c..b9904a0 100644 --- a/src/models/AppConfig.h +++ b/src/models/AppConfig.h @@ -25,7 +25,7 @@ struct AppConfig std::vector Profiles; StyleConfig GlobalStyle; // Legacy fallback and initial appearance for new profiles; editor writes are profile-local. bool bStartWithWindows = false; - QString Language = QStringLiteral("en"); + QString Language = QStringLiteral("system"); // === Factory === [[nodiscard]] static AppConfig CreateDefault() @@ -73,7 +73,7 @@ struct AppConfig } Config.bStartWithWindows = Obj["startWithWindows"].toBool(false); - Config.Language = Obj["language"].toString(QStringLiteral("en")); + Config.Language = Obj["language"].toString(QStringLiteral("system")); return Config; } diff --git a/src/models/PieMenuConfig.h b/src/models/PieMenuConfig.h index 8a4c9b2..961f587 100644 --- a/src/models/PieMenuConfig.h +++ b/src/models/PieMenuConfig.h @@ -1,4 +1,5 @@ #pragma once +#include // ============================================================================= // GoPieMenu - Pie Menu Configuration (Profile) @@ -41,19 +42,19 @@ struct PieMenuConfig { PieMenuConfig Config; Config.Id = QUuid::createUuid().toString(QUuid::WithoutBraces); - Config.Name = QStringLiteral("New Pie Menu"); + Config.Name = QCoreApplication::translate("DefaultMenu", "New Pie Menu"); Config.Trigger.Modifiers = ModifierKey::Ctrl; Config.Trigger.Button = MouseButton::Right; // Add some demo items Config.Items.push_back(PieItem::Create( - QStringLiteral("Notepad"), ActionType::LaunchApp, + QCoreApplication::translate("DefaultMenu", "Notepad"), ActionType::LaunchApp, QStringLiteral("notepad.exe"), QStringLiteral("page.svg"))); Config.Items.push_back(PieItem::Create( - QStringLiteral("Calculator"), ActionType::LaunchApp, + QCoreApplication::translate("DefaultMenu", "Calculator"), ActionType::LaunchApp, QStringLiteral("calc.exe"), QStringLiteral("calculator"))); Config.Items.push_back(PieItem::Create( - QStringLiteral("Explorer"), ActionType::LaunchApp, + QCoreApplication::translate("DefaultMenu", "Explorer"), ActionType::LaunchApp, QStringLiteral("explorer.exe"), QStringLiteral("folder"))); Config.Items.push_back(PieItem::Create( QStringLiteral("CMD"), ActionType::RunCommand, @@ -107,7 +108,7 @@ struct PieMenuConfig { PieMenuConfig Config; Config.Id = Obj["id"].toString(QUuid::createUuid().toString(QUuid::WithoutBraces)); - Config.Name = Obj["name"].toString(QStringLiteral("Unnamed")); + Config.Name = Obj["name"].toString(QCoreApplication::translate("DefaultMenu", "Unnamed")); Config.bEnabled = Obj["enabled"].toBool(true); // Trigger diff --git a/src/ui/SettingsWindow.cpp b/src/ui/SettingsWindow.cpp index 6723ad6..d7ad345 100644 --- a/src/ui/SettingsWindow.cpp +++ b/src/ui/SettingsWindow.cpp @@ -2,6 +2,7 @@ #include "quick/PiePreviewItem.h" #include "quick/ColorWheelItem.h" #include "RunningApplications.h" +#include "core/TranslationManager.h" #include #include #include @@ -23,9 +24,10 @@ static void InitializeEditorResources() { Q_INIT_RESOURCE(editor); } namespace gpm { -SettingsWindow::SettingsWindow(ConfigManager* manager, IconService* icons, QObject* parent) - : QObject(parent), Manager(manager), Icons(icons), Session(manager), Recorder(&Session), Catalog(icons) { +SettingsWindow::SettingsWindow(ConfigManager* manager, IconService* icons, QObject* parent, UpdateManager* updates) + : QObject(parent), Manager(manager), Icons(icons), Updates(updates), Session(manager), Recorder(&Session), Catalog(icons) { InitializeEditorResources(); + FontFamily = InterfaceFontFamily(manager->GetConfig().Language); QGuiApplication::styleHints()->setColorScheme(Qt::ColorScheme::Light); for (const auto& value : QSettings().value("editor/recentColors").toStringList()) { if (QColor(value).isValid() && !RecentColors.contains(value)) RecentColors.append(value); @@ -36,6 +38,7 @@ SettingsWindow::SettingsWindow(ConfigManager* manager, IconService* icons, QObje qmlRegisterType("GoPieMenu", 1, 0, "ColorWheel"); qmlRegisterUncreatableType("GoPieMenu", 1, 0, "EditorSession", "Provided by the application"); qmlRegisterUncreatableType("GoPieMenu", 1, 0, "IconService", "Provided by the application"); + qmlRegisterUncreatableType("GoPieMenu", 1, 0, "UpdateManager", "Provided by the application"); return true; }(); Q_UNUSED(registered); @@ -50,6 +53,12 @@ SettingsWindow::SettingsWindow(ConfigManager* manager, IconService* icons, QObje Engine.load(QUrl("qrc:/editor/Main.qml")); if (!Engine.rootObjects().isEmpty()) Window = qobject_cast(Engine.rootObjects().first()); if (Window) connect(Window, &QWindow::windowStateChanged, this, &SettingsWindow::windowStateChanged); + if (Window) connect(Window, &QWindow::visibilityChanged, this, &SettingsWindow::presentPendingUpdate); + if (Updates) connect(Updates, &UpdateManager::updateAvailable, this, [this] { + PendingUpdate = true; + presentPendingUpdate(); + if (PendingUpdate) emit updateToastRequested(Updates->version()); + }); #ifdef Q_OS_WIN if (Window) { const auto handle = reinterpret_cast(Window->winId()); @@ -80,7 +89,9 @@ void SettingsWindow::show() { } } FirstShow = false; - Window->show(); Window->raise(); Window->requestActivate(); + if (Window->windowState() == Qt::WindowMinimized) Window->showNormal(); + else Window->show(); + Window->raise(); Window->requestActivate(); } void SettingsWindow::raise() { if (Window) Window->raise(); } void SettingsWindow::activateWindow() { if (Window) Window->requestActivate(); } @@ -106,7 +117,8 @@ void SettingsWindow::toggleMaximized() { emit windowStateChanged(); } void SettingsWindow::requestExit() { - if (!Session.dirty()) { emit exitConfirmed(); return; } + InstallOnExit = false; + if (!Session.dirty()) { finishExit(); return; } show(); emit confirmExitRequested(); } @@ -114,7 +126,56 @@ bool SettingsWindow::resolveExit(const QString& choice) { if (choice == "apply") { if (!Session.apply()) return false; } else if (choice == "discard") Session.discard(); else return false; - emit exitConfirmed(); return true; + return finishExit(); +} +void SettingsWindow::cancelExit() { InstallOnExit = false; } +bool SettingsWindow::chooseLanguage(const QString& language) { + const auto previous = Session.language(); + Session.setLanguage(language); + return Session.language() != previous; +} +bool SettingsWindow::restartForLanguage() { + if (!Session.apply()) return false; + if (!RestartHandler || !RestartHandler()) { + Session.reportError(tr("Could not restart GoPieMenu. Your changes are saved; please reopen the app manually.")); + return false; + } + emit exitConfirmed(); + return true; +} +bool SettingsWindow::finishExit() { + if (InstallOnExit && (!Updates || !Updates->launchInstaller())) { + if (Updates) Session.reportError(Updates->error()); + return false; + } + InstallOnExit = false; + emit exitConfirmed(); + return true; +} +void SettingsWindow::requestUpdateInstall() { + if (!Updates || !Updates->ready()) return; + InstallOnExit = true; + if (Session.dirty()) { emit confirmExitRequested(); return; } + if (!finishExit()) { InstallOnExit = false; emit updateDialogRequested(); } +} +void SettingsWindow::presentPendingUpdate() { + if (!PendingUpdate || !Updates || !Updates->available() || !Window || !Window->isVisible() || + Window->visibility() == QWindow::Minimized) return; + PendingUpdate = false; + emit updateDialogRequested(); +} +void SettingsWindow::showUpdate() { + if (!Updates || !Updates->available()) return; + PendingUpdate = true; + show(); + presentPendingUpdate(); +} +void SettingsWindow::openReleasePage() { + if (Updates && Updates->available()) openReleaseLink(Updates->releasePage()); +} +void SettingsWindow::openReleaseLink(const QUrl& url) { + if (url.scheme() != "https" || url.host().isEmpty()) return; + if (!QDesktopServices::openUrl(url)) Session.reportError(tr("Could not open the release page.")); } void SettingsWindow::chooseTarget(bool directory) { const auto type = static_cast(Session.selectedItem()["action"].toInt()); @@ -207,7 +268,7 @@ void SettingsWindow::installAutoStart() { #ifdef Q_OS_WIN QSettings registry("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", QSettings::NativeFormat); if (Manager->GetConfig().bStartWithWindows) - registry.setValue("GoPieMenu", "\"" + QDir::toNativeSeparators(QCoreApplication::applicationFilePath()) + "\""); + registry.setValue("GoPieMenu", "\"" + QDir::toNativeSeparators(QCoreApplication::applicationFilePath()) + "\" --autostart"); else registry.remove("GoPieMenu"); registry.sync(); if (registry.status() != QSettings::NoError) Session.reportError(tr("Settings were saved, but Windows startup could not be updated.")); diff --git a/src/ui/SettingsWindow.h b/src/ui/SettingsWindow.h index 4e375ee..aaa3b95 100644 --- a/src/ui/SettingsWindow.h +++ b/src/ui/SettingsWindow.h @@ -3,32 +3,47 @@ #include #include #include +#include #include "core/EditorSession.h" #include "icons/IconCatalog.h" #include "InputRecorder.h" +#include "update/UpdateManager.h" namespace gpm { class SettingsWindow : public QObject { Q_OBJECT Q_PROPERTY(QString iconDirectory READ iconDirectory CONSTANT) Q_PROPERTY(QString version READ version CONSTANT) + Q_PROPERTY(QString fontFamily READ fontFamily CONSTANT) Q_PROPERTY(bool previewMode READ previewMode WRITE setPreviewMode NOTIFY previewModeChanged) Q_PROPERTY(QStringList recentColors READ recentColors NOTIFY recentColorsChanged) Q_PROPERTY(QStringList runningApplications READ runningApplications NOTIFY runningApplicationsChanged) Q_PROPERTY(bool maximized READ maximized NOTIFY windowStateChanged) + Q_PROPERTY(gpm::UpdateManager* updater READ updater CONSTANT) public: - explicit SettingsWindow(ConfigManager* manager, IconService* icons, QObject* parent = nullptr); + explicit SettingsWindow(ConfigManager* manager, IconService* icons, QObject* parent = nullptr, UpdateManager* updates = nullptr); ~SettingsWindow() override; void show(); void raise(); void activateWindow(); Q_INVOKABLE void requestExit(); Q_INVOKABLE bool resolveExit(const QString& choice); + Q_INVOKABLE void cancelExit(); + Q_INVOKABLE bool chooseLanguage(const QString& language); + Q_INVOKABLE bool restartForLanguage(); + void setRestartHandler(std::function handler) { RestartHandler = std::move(handler); } + QString fontFamily() const { return FontFamily; } + Q_INVOKABLE void showUpdate(); + Q_INVOKABLE void requestUpdateInstall(); + Q_INVOKABLE void openReleasePage(); + Q_INVOKABLE void openReleaseLink(const QUrl& url); + UpdateManager* updater() const { return Updates; } Q_INVOKABLE void toggleMaximized(); bool maximized() const; QQuickWindow* window() const { return Window; } EditorSession* session() { return &Session; } void setPlatformIntegrationEnabled(bool enabled) { PlatformIntegration = enabled; } + void syncAutoStart() { installAutoStart(); } bool previewMode() const { return PreviewMode; } void setPreviewMode(bool enabled) { PreviewMode = enabled; emit previewModeChanged(); } QString iconDirectory() const { return Icons->directory(); } @@ -57,11 +72,16 @@ class SettingsWindow : public QObject { void recentColorsChanged(); void runningApplicationsChanged(); void windowStateChanged(); + void updateDialogRequested(); + void updateToastRequested(const QString& version); private: + void presentPendingUpdate(); + bool finishExit(); void addAppFilter(const QString& name); void installAutoStart(); ConfigManager* Manager; IconService* Icons; + QPointer Updates; EditorSession Session; InputRecorder Recorder; IconCatalog Catalog; @@ -70,6 +90,10 @@ class SettingsWindow : public QObject { bool PlatformIntegration = true; bool PreviewMode = false; bool FirstShow = true; + bool PendingUpdate = false; + bool InstallOnExit = false; + QString FontFamily; + std::function RestartHandler; QString ColorField, ColorProfile, ColorItem; QStringList RecentColors, Applications; }; diff --git a/src/ui/TrayManager.cpp b/src/ui/TrayManager.cpp index 6b63c79..95ddbd0 100644 --- a/src/ui/TrayManager.cpp +++ b/src/ui/TrayManager.cpp @@ -29,7 +29,7 @@ void TrayManager::CreateTrayIcon() { TrayIcon = new QSystemTrayIcon(this); TrayIcon->setIcon(QIcon(QStringLiteral(":/logo/tray-color.ico"))); - TrayIcon->setToolTip(QStringLiteral("GoPieMenu - Active")); + TrayIcon->setToolTip(tr("GoPieMenu - Active")); TrayMenu = new QMenu; TrayMenu->setObjectName(QStringLiteral("trayContextMenu")); @@ -38,34 +38,43 @@ void TrayManager::CreateTrayIcon() TrayMenu->setStyleSheet(QStringLiteral( // Match the QML AppMenu/AppMenuItem spacing, colors, and corner radii. "QMenu { background: #ffffff; color: #29374b; border: 1px solid #e2e7ef; " - " border-radius: 8px; padding: 6px; min-width: 212px; font-family: 'Segoe UI'; font-size: 14px; }" + " border-radius: 8px; padding: 6px; min-width: 212px; font-family: '%1'; font-size: 14px; }" "QMenu::item { padding: 10px 12px; min-height: 20px; border-radius: 5px; margin: 0; }" "QMenu::item:selected { background: #e2ebf8; color: #29374b; }" "QMenu::item:pressed { background: #d2e1f5; }" "QMenu::separator { background: #e2e7ef; height: 1px; margin: 6px; }" "QMenu::indicator { width: 14px; height: 14px; border: 1px solid #e2e7ef; border-radius: 3px; }" "QMenu::indicator:checked { background: #4e80c8; border-color: #4e80c8; }" - )); + ).arg(QApplication::font().family())); - auto* LocalSettingsAction = TrayMenu->addAction(QStringLiteral("Settings")); + auto* LocalSettingsAction = TrayMenu->addAction(tr("Settings")); connect(LocalSettingsAction, &QAction::triggered, this, &TrayManager::SettingsRequested); + UpdateAction = TrayMenu->addAction(QString()); + UpdateAction->setObjectName("trayUpdateAction"); + UpdateAction->setVisible(false); + connect(UpdateAction, &QAction::triggered, this, &TrayManager::UpdateRequested); + connect(TrayIcon, &QSystemTrayIcon::messageClicked, this, [this] { + if (UpdateAction->isVisible()) emit UpdateRequested(); + }); + TrayMenu->addSeparator(); - PauseAction = TrayMenu->addAction(QStringLiteral("Pause")); + PauseAction = TrayMenu->addAction(tr("Pause")); + PauseAction->setObjectName("trayPauseAction"); PauseAction->setCheckable(true); connect(PauseAction, &QAction::toggled, this, [this](bool bChecked) { bIsPaused = bChecked; - PauseAction->setText(bChecked ? QStringLiteral("Resume") : QStringLiteral("Pause")); - TrayIcon->setToolTip(bChecked ? QStringLiteral("GoPieMenu - Paused") - : QStringLiteral("GoPieMenu - Active")); + PauseAction->setText(bChecked ? tr("Resume") : tr("Pause")); + TrayIcon->setToolTip(bChecked ? tr("GoPieMenu - Paused") + : tr("GoPieMenu - Active")); emit PauseToggled(bChecked); }); TrayMenu->addSeparator(); - auto* LocalQuitAction = TrayMenu->addAction(QStringLiteral("Quit")); + auto* LocalQuitAction = TrayMenu->addAction(tr("Quit")); connect(LocalQuitAction, &QAction::triggered, this, &TrayManager::QuitRequested); TrayIcon->setContextMenu(TrayMenu); @@ -77,6 +86,18 @@ void TrayManager::Show() TrayIcon->show(); } +void TrayManager::SetUpdateAvailable(const QString& version) { + UpdateAction->setText(tr("Update available: %1").arg(version)); + UpdateAction->setVisible(!version.isEmpty()); +} + +void TrayManager::ShowUpdateNotification(const QString& version) { + SetUpdateAvailable(version); + TrayIcon->showMessage(tr("Update available"), + tr("GoPieMenu %1 is available. Click to view what's new.").arg(version), + QSystemTrayIcon::Information, 10000); +} + void TrayManager::OnTrayActivated(QSystemTrayIcon::ActivationReason Reason) { if (Reason == QSystemTrayIcon::DoubleClick) diff --git a/src/ui/TrayManager.h b/src/ui/TrayManager.h index d39e63d..f5bae4a 100644 --- a/src/ui/TrayManager.h +++ b/src/ui/TrayManager.h @@ -21,11 +21,14 @@ class TrayManager : public QObject /** Makes the tray icon visible */ void Show(); + void SetUpdateAvailable(const QString& version); + void ShowUpdateNotification(const QString& version); signals: void SettingsRequested(); void PauseToggled(bool bPaused); void QuitRequested(); + void UpdateRequested(); private: // === Initialization === @@ -36,6 +39,7 @@ class TrayManager : public QObject QSystemTrayIcon* TrayIcon = nullptr; QMenu* TrayMenu = nullptr; QAction* PauseAction = nullptr; + QAction* UpdateAction = nullptr; // === State === bool bIsPaused = false; diff --git a/src/ui/qml/ActionContextMenu.qml b/src/ui/qml/ActionContextMenu.qml index 4176b22..3df71fc 100644 --- a/src/ui/qml/ActionContextMenu.qml +++ b/src/ui/qml/ActionContextMenu.qml @@ -14,12 +14,12 @@ AppMenu { } AppMenuItem { objectName: "contextDuplicateAction" - text: "Duplicate" + text: qsTr("Duplicate") onTriggered: if (root.profileId === editor.profileId) editor.duplicateItem(root.itemId) } AppMenuItem { objectName: "contextDeleteAction" - text: "Delete action" + text: qsTr("Delete action") destructive: true onTriggered: if (root.profileId === editor.profileId) editor.removeItem(root.itemId) } diff --git a/src/ui/qml/AppDialog.qml b/src/ui/qml/AppDialog.qml index b7bc0fe..1ce5279 100644 --- a/src/ui/qml/AppDialog.qml +++ b/src/ui/qml/AppDialog.qml @@ -32,7 +32,7 @@ Popup { id: titleRow Layout.fillWidth: true Label { text: root.title; color: T.ink; font.pixelSize: 19; font.weight: Font.DemiBold; Layout.fillWidth: true } - AppButton { visible: root.dismissible; iconName: "cancel.svg"; hint: "Close"; compact: true; onClicked: root.close() } + AppButton { visible: root.dismissible; iconName: "cancel.svg"; hint: qsTr("Close"); compact: true; onClicked: root.close() } } ScrollView { id: scroll diff --git a/src/ui/qml/AppearancePane.qml b/src/ui/qml/AppearancePane.qml index 143f8eb..a915e04 100644 --- a/src/ui/qml/AppearancePane.qml +++ b/src/ui/qml/AppearancePane.qml @@ -10,39 +10,42 @@ ColumnLayout { spacing: 14 Label { Layout.fillWidth: true - text: "Appearance applies only to this menu." + text: qsTr("Appearance applies only to this menu.") color: T.muted; font.pixelSize: 12; wrapMode: Text.WordWrap } - FieldLabel { text: "Style" } + FieldLabel { text: qsTr("Style") } Flow { Layout.fillWidth: true spacing: 6 Repeater { - model: ["Frost", "Slate", "Ocean", "Obsidian"] - AppButton { required property string modelData; text: modelData; kind: "secondary"; onClicked: editor.setStylePreset(modelData) } + model: [ + {label: qsTr("Frost"), value: "Frost"}, {label: qsTr("Slate"), value: "Slate"}, + {label: qsTr("Ocean"), value: "Ocean"}, {label: qsTr("Obsidian"), value: "Obsidian"} + ] + AppButton { required property var modelData; text: modelData.label; kind: "secondary"; onClicked: editor.setStylePreset(modelData.value) } } } - NumberSetting { Layout.fillWidth: true; label: "Size"; field: "outerRadius"; minimum: 80; maximum: 400; step: 5; value: root.style.outerRadius || 150 } - AppButton { text: "Advanced"; kind: "text"; iconName: root.advanced ? "nav-arrow-down.svg" : "nav-arrow-right.svg"; onClicked: root.advanced = !root.advanced } + NumberSetting { Layout.fillWidth: true; label: qsTr("Size"); field: "outerRadius"; minimum: 80; maximum: 400; step: 5; value: root.style.outerRadius || 150 } + AppButton { text: qsTr("Advanced"); kind: "text"; iconName: root.advanced ? "nav-arrow-down.svg" : "nav-arrow-right.svg"; onClicked: root.advanced = !root.advanced } ColumnLayout { Layout.fillWidth: true visible: root.advanced spacing: 16 - NumberSetting { Layout.fillWidth: true; label: "Center radius"; field: "innerRadius"; minimum: 10; maximum: Math.min(150, (root.style.outerRadius || 150) - 9); value: root.style.innerRadius || 45 } - NumberSetting { Layout.fillWidth: true; label: "Icon size"; field: "iconSize"; minimum: 12; maximum: 96; value: root.style.iconSize || 28 } - NumberSetting { Layout.fillWidth: true; label: "Text size"; field: "fontSize"; minimum: 8; maximum: 32; value: root.style.fontSize || 11 } - NumberSetting { Layout.fillWidth: true; label: "Slice gap"; field: "gapAngle"; minimum: 0; maximum: 20; step: .5; value: root.style.gapAngle || 0; suffix: "°" } - NumberSetting { Layout.fillWidth: true; label: "Opacity"; field: "opacity"; minimum: .1; maximum: 1; step: .05; value: root.style.opacity || .95 } - NumberSetting { Layout.fillWidth: true; label: "Animation"; field: "animationDuration"; minimum: 0; maximum: 500; step: 10; value: root.style.animationDuration || 0; suffix: " ms" } - NumberSetting { Layout.fillWidth: true; label: "Text outline"; field: "textOutlineThickness"; minimum: 0; maximum: 10; step: .5; value: root.style.textOutlineThickness || 0 } - NumberSetting { Layout.fillWidth: true; label: "Border width"; field: "borderWidth"; minimum: 0; maximum: 5; step: .5; value: root.style.borderWidth || 0 } - NumberSetting { Layout.fillWidth: true; label: "Hover size"; field: "hoverScale"; minimum: 1; maximum: 1.25; step: .01; value: root.style.hoverScale || 1 } - AppCheckBox { text: "Automatic text contrast"; checked: root.style.autoContrast || false; onClicked: editor.setStyleField("autoContrast", checked) } + NumberSetting { Layout.fillWidth: true; label: qsTr("Center radius"); field: "innerRadius"; minimum: 10; maximum: Math.min(150, (root.style.outerRadius || 150) - 9); value: root.style.innerRadius || 45 } + NumberSetting { Layout.fillWidth: true; label: qsTr("Icon size"); field: "iconSize"; minimum: 12; maximum: 96; value: root.style.iconSize || 28 } + NumberSetting { Layout.fillWidth: true; label: qsTr("Text size"); field: "fontSize"; minimum: 8; maximum: 32; value: root.style.fontSize || 11 } + NumberSetting { Layout.fillWidth: true; label: qsTr("Slice gap"); field: "gapAngle"; minimum: 0; maximum: 20; step: .5; value: root.style.gapAngle || 0; suffix: "°" } + NumberSetting { Layout.fillWidth: true; label: qsTr("Opacity"); field: "opacity"; minimum: .1; maximum: 1; step: .05; value: root.style.opacity || .95 } + NumberSetting { Layout.fillWidth: true; label: qsTr("Animation"); field: "animationDuration"; minimum: 0; maximum: 500; step: 10; value: root.style.animationDuration || 0; suffix: " ms" } + NumberSetting { Layout.fillWidth: true; label: qsTr("Text outline"); field: "textOutlineThickness"; minimum: 0; maximum: 10; step: .5; value: root.style.textOutlineThickness || 0 } + NumberSetting { Layout.fillWidth: true; label: qsTr("Border width"); field: "borderWidth"; minimum: 0; maximum: 5; step: .5; value: root.style.borderWidth || 0 } + NumberSetting { Layout.fillWidth: true; label: qsTr("Hover size"); field: "hoverScale"; minimum: 1; maximum: 1.25; step: .01; value: root.style.hoverScale || 1 } + AppCheckBox { text: qsTr("Automatic text contrast"); checked: root.style.autoContrast || false; onClicked: editor.setStyleField("autoContrast", checked) } Repeater { model: [ - {name: "Slices", key: "sectorColor"}, {name: "Highlight", key: "hoverColor"}, - {name: "Text", key: "textColor"}, {name: "Center", key: "centerColor"}, - {name: "Border", key: "borderColor"}, {name: "Submenu", key: "backgroundColor"} + {name: qsTr("Slices"), key: "sectorColor"}, {name: qsTr("Highlight"), key: "hoverColor"}, + {name: qsTr("Text"), key: "textColor"}, {name: qsTr("Center"), key: "centerColor"}, + {name: qsTr("Border"), key: "borderColor"}, {name: qsTr("Submenu"), key: "backgroundColor"} ] RowLayout { required property var modelData @@ -50,7 +53,7 @@ ColumnLayout { Label { text: modelData.name; font.pixelSize: 14; color: T.ink; Layout.fillWidth: true } AppButton { kind: "secondary"; implicitWidth: 64 - hint: "Change " + modelData.name.toLowerCase() + " color" + hint: qsTr("Change %1 color").arg(modelData.name.toLowerCase()) onClicked: appController.chooseColor(modelData.key) Rectangle { width: 28; height: 18; anchors.centerIn: parent; color: root.style[modelData.key] || "transparent"; radius: 4; border.color: T.line } } diff --git a/src/ui/qml/ColorPicker.qml b/src/ui/qml/ColorPicker.qml index 407c898..951a3e3 100644 --- a/src/ui/qml/ColorPicker.qml +++ b/src/ui/qml/ColorPicker.qml @@ -7,7 +7,7 @@ import "Theme.js" as T AppDialog { id: root objectName: "colorPicker" - title: "Choose color" + title: qsTr("Choose color") width: Math.min(600, parent.width - 32) property color original: T.accent property bool hexValid: true @@ -37,7 +37,7 @@ AppDialog { ColorChannel { label: "S"; value: wheel.saturation * 100; onEdited: function(value) { wheel.saturation = value / 100 } } ColorChannel { label: "V"; value: wheel.value * 100; onEdited: function(value) { wheel.value = value / 100 } } ColorChannel { label: "A"; value: wheel.alpha * 100; onEdited: function(value) { wheel.alpha = value / 100 } } - FieldLabel { text: "Hex · RRGGBB / RRGGBBAA"; topPadding: root.compact ? 0 : 8 } + FieldLabel { text: qsTr("Hex · RRGGBB / RRGGBBAA"); topPadding: root.compact ? 0 : 8 } AppField { id: hexField objectName: "colorHexField" @@ -51,20 +51,20 @@ AppDialog { root.hexValid = valid } onEditingFinished: if (root.hexValid) text = wheel.hex - Accessible.name: "Hex color" + Accessible.name: qsTr("Hex color") } } } - Label { visible: !root.hexValid; text: "Enter 6 hex digits, or 8 including opacity."; color: T.danger; font.pixelSize: 12 } + Label { visible: !root.hexValid; text: qsTr("Enter 6 hex digits, or 8 including opacity."); color: T.danger; font.pixelSize: 12 } RowLayout { Layout.fillWidth: true ColorSwatch { value: root.original; Layout.preferredWidth: 64; Layout.preferredHeight: 32 } IconImage { name: "arrow-right.svg"; width: 16; height: 16; tint: T.muted } ColorSwatch { value: wheel.color; Layout.preferredWidth: 64; Layout.preferredHeight: 32 } Item { Layout.fillWidth: true } - AppButton { text: "Reset"; kind: "danger"; compact: root.compact; onClicked: { wheel.color = root.original; hexField.text = wheel.hex } } + AppButton { text: qsTr("Reset"); kind: "danger"; compact: root.compact; onClicked: { wheel.color = root.original; hexField.text = wheel.hex } } } - FieldLabel { text: "Recent colors"; topPadding: root.compact ? 0 : 8 } + FieldLabel { text: qsTr("Recent colors"); topPadding: root.compact ? 0 : 8 } Flow { Layout.fillWidth: true spacing: 8 @@ -75,16 +75,16 @@ AppDialog { required property int index objectName: "recentColor" + index implicitWidth: 36; implicitHeight: 36 - hint: "Use " + modelData + hint: qsTr("Use %1").arg(modelData) ColorSwatch { anchors.fill: parent; anchors.margins: 3; value: parent.modelData } onClicked: { wheel.color = modelData; hexField.text = wheel.hex } } } - Label { visible: appController.recentColors.length === 0; text: "Colors you use will appear here."; color: T.muted; font.pixelSize: 13 } + Label { visible: appController.recentColors.length === 0; text: qsTr("Colors you use will appear here."); color: T.muted; font.pixelSize: 13 } } footer: RowLayout { Item { Layout.fillWidth: true } - AppButton { objectName: "cancelColorButton"; text: "Cancel"; onClicked: root.close() } - AppButton { objectName: "useColorButton"; text: "Use color"; kind: "primary"; enabled: root.hexValid; onClicked: { appController.acceptColor(wheel.color); root.close() } } + AppButton { objectName: "cancelColorButton"; text: qsTr("Cancel"); onClicked: root.close() } + AppButton { objectName: "useColorButton"; text: qsTr("Use color"); kind: "primary"; enabled: root.hexValid; onClicked: { appController.acceptColor(wheel.color); root.close() } } } } diff --git a/src/ui/qml/ExitDialog.qml b/src/ui/qml/ExitDialog.qml index 307bc18..04c92ef 100644 --- a/src/ui/qml/ExitDialog.qml +++ b/src/ui/qml/ExitDialog.qml @@ -6,13 +6,14 @@ import "Theme.js" as T AppDialog { id: root objectName: "exitDialog" - title: "Unsaved changes" - Label { Layout.fillWidth: true; text: "Apply your changes before quitting?"; wrapMode: Text.WordWrap; color: T.ink; font.pixelSize: 14 } + title: qsTr("Unsaved changes") + onClosed: appController.cancelExit() + Label { Layout.fillWidth: true; text: qsTr("Apply your changes before quitting?"); wrapMode: Text.WordWrap; color: T.ink; font.pixelSize: 14 } Label { Layout.fillWidth: true; visible: editor.error.length > 0; text: editor.error; wrapMode: Text.WordWrap; color: T.danger; font.pixelSize: 13 } footer: RowLayout { - AppButton { objectName: "exitDiscardButton"; text: "Discard"; kind: "danger"; onClicked: { root.close(); appController.resolveExit("discard") } } + AppButton { objectName: "exitDiscardButton"; text: qsTr("Discard"); kind: "danger"; onClicked: if (appController.resolveExit("discard")) root.close() } Item { Layout.fillWidth: true } - AppButton { objectName: "exitCancelButton"; text: "Cancel"; onClicked: root.close() } - AppButton { objectName: "exitApplyButton"; text: "Apply & quit"; kind: "primary"; onClicked: if (appController.resolveExit("apply")) root.close() } + AppButton { objectName: "exitCancelButton"; text: qsTr("Cancel"); onClicked: root.close() } + AppButton { objectName: "exitApplyButton"; text: qsTr("Apply & quit"); kind: "primary"; onClicked: if (appController.resolveExit("apply")) root.close() } } } diff --git a/src/ui/qml/IconPicker.qml b/src/ui/qml/IconPicker.qml index 57f3619..8bb20c1 100644 --- a/src/ui/qml/IconPicker.qml +++ b/src/ui/qml/IconPicker.qml @@ -6,7 +6,7 @@ import "Theme.js" as T AppDialog { id: root objectName: "iconPicker" - title: "Choose icon" + title: qsTr("Choose icon") width: Math.min(520, parent.width - 32) onOpened: { search.text = ""; iconCatalog.query = ""; search.forceActiveFocus() } onClosed: { searchDelay.stop(); iconCatalog.query = "" } @@ -19,9 +19,9 @@ AppDialog { id: search objectName: "iconSearch" Layout.fillWidth: true - placeholderText: "Search icons" + placeholderText: qsTr("Search icons") onTextEdited: searchDelay.restart() - Accessible.name: "Search icons" + Accessible.name: qsTr("Search icons") } } Timer { id: searchDelay; interval: 16; onTriggered: iconCatalog.query = search.text } @@ -58,16 +58,16 @@ AppDialog { Label { anchors.centerIn: parent visible: grid.count === 0 - text: iconCatalog.loading ? "Loading icons…" : "No matching icons" + text: iconCatalog.loading ? qsTr("Loading icons…") : qsTr("No matching icons") color: T.muted; font.pixelSize: 14 } } RowLayout { Layout.fillWidth: true - AppButton { text: "Clear icon"; onClicked: { editor.setItemField("icon", ""); root.close() } } + AppButton { text: qsTr("Clear icon"); onClicked: { editor.setItemField("icon", ""); root.close() } } Item { Layout.fillWidth: true } BusyIndicator { running: iconCatalog.loading; visible: running; implicitWidth: 22; implicitHeight: 22 } - Label { text: iconCatalog.count + " icons"; color: T.muted; font.pixelSize: 12 } + Label { text: qsTr("%1 icons").arg(iconCatalog.count); color: T.muted; font.pixelSize: 12 } } } } diff --git a/src/ui/qml/InspectorPanel.qml b/src/ui/qml/InspectorPanel.qml index a5e8660..bac2b47 100644 --- a/src/ui/qml/InspectorPanel.qml +++ b/src/ui/qml/InspectorPanel.qml @@ -17,11 +17,11 @@ Rectangle { Layout.leftMargin: 22; Layout.rightMargin: 14 Layout.topMargin: 14; Layout.bottomMargin: 8 Label { - text: root.mode === "appearance" ? "Appearance" : root.mode === "menu" ? "Menu settings" : "" + text: root.mode === "appearance" ? qsTr("Appearance") : root.mode === "menu" ? qsTr("Menu settings") : "" font.pixelSize: 17; font.weight: Font.DemiBold; color: T.ink Layout.fillWidth: true } - AppButton { iconName: "cancel.svg"; hint: "Close panel"; compact: true; onClicked: root.closePanel() } + AppButton { iconName: "cancel.svg"; hint: qsTr("Close panel"); compact: true; onClicked: root.closePanel() } } ScrollView { id: scroll diff --git a/src/ui/qml/ItemInspector.qml b/src/ui/qml/ItemInspector.qml index 1bcb91a..019e13b 100644 --- a/src/ui/qml/ItemInspector.qml +++ b/src/ui/qml/ItemInspector.qml @@ -16,7 +16,7 @@ ColumnLayout { objectName: "chooseIconButton" kind: "secondary" iconName: root.item.icon || "media-image.svg" - hint: "Choose icon" + hint: qsTr("Choose icon") implicitHeight: 46; implicitWidth: 46 onClicked: root.chooseIcon() } @@ -26,33 +26,33 @@ ColumnLayout { Layout.minimumWidth: 0 font.pixelSize: 17 font.weight: Font.DemiBold - placeholderText: "Action name" + placeholderText: qsTr("Action name") text: root.item.name || "" onTextEdited: editor.setItemField("name", text) - Accessible.name: "Action name" + Accessible.name: qsTr("Action name") } } - FieldLabel { text: "Action" } + FieldLabel { text: qsTr("Action") } AppCombo { objectName: "actionTypeCombo" Layout.fillWidth: true textRole: "label"; valueRole: "value" model: [ - {label: "Open application", value: 1}, - {label: "Keyboard shortcut", value: 3}, - {label: "Open file or folder", value: 4}, - {label: "Open website", value: 5}, - {label: "Run command", value: 2}, - {label: "Submenu", value: 6} + {label: qsTr("Open application"), value: 1}, + {label: qsTr("Keyboard shortcut"), value: 3}, + {label: qsTr("Open file or folder"), value: 4}, + {label: qsTr("Open website"), value: 5}, + {label: qsTr("Run command"), value: 2}, + {label: qsTr("Submenu"), value: 6} ] selectionValue: root.item.action onActivated: editor.setItemField("action", currentValue) enabled: !(root.item.action === 6 && root.item.childCount > 0) - Accessible.name: "Action type" + Accessible.name: qsTr("Action type") } FieldLabel { visible: root.item.action !== 6 - text: root.item.action === 3 ? "Shortcut" : root.item.action === 5 ? "Website" : root.item.action === 2 ? "Command" : "Target" + text: root.item.action === 3 ? qsTr("Shortcut") : root.item.action === 5 ? qsTr("Website") : root.item.action === 2 ? qsTr("Command") : qsTr("Target") } AppButton { objectName: "actionRecorderButton" @@ -60,7 +60,7 @@ ColumnLayout { Layout.fillWidth: true kind: "secondary" iconName: "key-command.svg" - text: recorder.active && recorder.target === "action" ? "Press a shortcut…" : root.item.target || "Record shortcut" + text: recorder.active && recorder.target === "action" ? qsTr("Press a shortcut…") : root.item.target || qsTr("Record shortcut") onClicked: recorder.active ? recorder.cancel() : recorder.start("action") } AppField { @@ -68,50 +68,50 @@ ColumnLayout { visible: root.item.action !== 3 && root.item.action !== 6 Layout.fillWidth: true text: root.item.target || "" - placeholderText: root.item.action === 5 ? "https://example.com" : root.item.action === 2 ? "Enter a command" : "Choose a target or paste a path" + placeholderText: root.item.action === 5 ? "https://example.com" : root.item.action === 2 ? qsTr("Enter a command") : qsTr("Choose a target or paste a path") onTextEdited: editor.setItemField("target", text) - Accessible.name: "Action target" + Accessible.name: qsTr("Action target") } RowLayout { visible: root.item.action === 1 || root.item.action === 4 Layout.fillWidth: true AppButton { - text: root.item.action === 1 ? "Choose application" : "Choose file" + text: root.item.action === 1 ? qsTr("Choose application") : qsTr("Choose file") iconName: root.item.action === 1 ? "app-window.svg" : "page.svg" kind: "secondary" onClicked: appController.chooseTarget(false) } - AppButton { visible: root.item.action === 4; iconName: "folder.svg"; hint: "Choose folder"; kind: "secondary"; onClicked: appController.chooseTarget(true) } + AppButton { visible: root.item.action === 4; iconName: "folder.svg"; hint: qsTr("Choose folder"); kind: "secondary"; onClicked: appController.chooseTarget(true) } } AppButton { objectName: "enterSubmenuButton" visible: root.item.action === 6 Layout.fillWidth: true kind: "secondary" - text: "Edit " + (root.item.childCount || 0) + " actions" + text: qsTr("Edit %1 actions").arg(root.item.childCount || 0) iconName: "nav-arrow-right.svg" onClicked: editor.enterFolder(root.item.id) } - AppButton { objectName: "itemAdvancedButton"; text: "Advanced"; kind: "text"; iconName: root.advanced ? "nav-arrow-down.svg" : "nav-arrow-right.svg"; onClicked: root.advanced = !root.advanced } + AppButton { objectName: "itemAdvancedButton"; text: qsTr("Advanced"); kind: "text"; iconName: root.advanced ? "nav-arrow-down.svg" : "nav-arrow-right.svg"; onClicked: root.advanced = !root.advanced } ColumnLayout { visible: root.advanced Layout.fillWidth: true spacing: 8 - FieldLabel { visible: root.item.action === 1 || root.item.action === 2; text: "Arguments" } + FieldLabel { visible: root.item.action === 1 || root.item.action === 2; text: qsTr("Arguments") } AppField { visible: root.item.action === 1 || root.item.action === 2 Layout.fillWidth: true text: root.item.arguments || "" - placeholderText: "Optional arguments" + placeholderText: qsTr("Optional arguments") onTextEdited: editor.setItemField("arguments", text) } - FieldLabel { text: "Color" } + FieldLabel { text: qsTr("Color") } RowLayout { - AppButton { text: root.item.color ? "Change color" : "Custom color"; kind: "text"; onClicked: appController.chooseColor("color", true) } + AppButton { text: root.item.color ? qsTr("Change color") : qsTr("Custom color"); kind: "text"; onClicked: appController.chooseColor("color", true) } Rectangle { visible: !!root.item.color; width: 22; height: 22; radius: 5; color: root.item.color || "transparent"; border.color: T.line } - AppButton { objectName: "resetItemColorButton"; visible: !!root.item.color; text: "Reset"; kind: "danger"; onClicked: editor.setItemField("color", "") } + AppButton { objectName: "resetItemColorButton"; visible: !!root.item.color; text: qsTr("Reset"); kind: "danger"; onClicked: editor.setItemField("color", "") } } - FieldLabel { visible: root.item.action !== 6; text: "Location" } + FieldLabel { visible: root.item.action !== 6; text: qsTr("Location") } AppCombo { visible: root.item.action !== 6 Layout.fillWidth: true @@ -125,8 +125,8 @@ ColumnLayout { Rectangle { Layout.fillWidth: true; implicitHeight: 1; color: T.line } RowLayout { Layout.fillWidth: true - AppButton { text: "Duplicate"; iconName: "copy.svg"; onClicked: editor.duplicateItem(root.item.id) } + AppButton { text: qsTr("Duplicate"); iconName: "copy.svg"; onClicked: editor.duplicateItem(root.item.id) } Item { Layout.fillWidth: true } - AppButton { iconName: "trash.svg"; hint: "Delete action"; kind: "danger"; onClicked: editor.removeItem(root.item.id) } + AppButton { iconName: "trash.svg"; hint: qsTr("Delete action"); kind: "danger"; onClicked: editor.removeItem(root.item.id) } } } diff --git a/src/ui/qml/ItemList.qml b/src/ui/qml/ItemList.qml index 145d9c6..69133a8 100644 --- a/src/ui/qml/ItemList.qml +++ b/src/ui/qml/ItemList.qml @@ -73,10 +73,10 @@ Rectangle { spacing: 0 RowLayout { Layout.fillWidth: true; Layout.topMargin: 5 - Label { text: "Arrange actions"; font.pixelSize: 13; color: T.muted; Layout.fillWidth: true } - AppButton { iconName: "nav-arrow-up.svg"; compact: true; hint: "Move selected action up"; enabled: root.selectedIndex > 0; onClicked: editor.moveSelected(-1) } - AppButton { iconName: "nav-arrow-down.svg"; compact: true; hint: "Move selected action down"; enabled: root.selectedIndex >= 0 && root.selectedIndex < list.count - 1; onClicked: editor.moveSelected(1) } - AppButton { iconName: "cancel.svg"; compact: true; hint: "Close list"; onClicked: root.closeList() } + Label { text: qsTr("Arrange actions"); font.pixelSize: 13; color: T.muted; Layout.fillWidth: true } + AppButton { iconName: "nav-arrow-up.svg"; compact: true; hint: qsTr("Move selected action up"); enabled: root.selectedIndex > 0; onClicked: editor.moveSelected(-1) } + AppButton { iconName: "nav-arrow-down.svg"; compact: true; hint: qsTr("Move selected action down"); enabled: root.selectedIndex >= 0 && root.selectedIndex < list.count - 1; onClicked: editor.moveSelected(1) } + AppButton { iconName: "cancel.svg"; compact: true; hint: qsTr("Close list"); onClicked: root.closeList() } } Item { id: viewport @@ -126,7 +126,7 @@ Rectangle { IconImage { name: row.modelData.icon; width: 19; height: 19 } Label { text: row.modelData.name; Layout.fillWidth: true; Layout.minimumWidth: 0; color: T.ink; font.pixelSize: 14; elide: Text.ElideRight } Label { visible: row.modelData.action === 6; text: row.modelData.childCount; color: T.muted; font.pixelSize: 12 } - AppButton { visible: row.modelData.action === 6; iconName: "nav-arrow-right.svg"; compact: true; hint: "Open submenu"; onClicked: editor.enterFolder(row.modelData.id) } + AppButton { visible: row.modelData.action === 6; iconName: "nav-arrow-right.svg"; compact: true; hint: qsTr("Open submenu"); onClicked: editor.enterFolder(row.modelData.id) } } Accessible.role: Accessible.ListItem Accessible.name: modelData.name diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index 3457fa3..031a905 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -13,7 +13,7 @@ ApplicationWindow { title: "GoPieMenu" flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowMinMaxButtonsHint color: T.background - font.family: "Segoe UI" + font.family: appController.fontFamily font.pixelSize: 14 palette.window: T.background palette.base: T.surface @@ -27,7 +27,20 @@ ApplicationWindow { property bool compactMode: width < 1000 property bool listVisible: false property string previousSelection: "" - readonly property bool dialogOpen: iconPicker.visible || colorPicker.visible || exitDialog.visible || runningApps.visible || settings.visible || about.visible + property bool pendingUpdate: false + readonly property bool dialogOpen: iconPicker.visible || colorPicker.visible || exitDialog.visible || runningApps.visible || settings.visible || about.visible || updateDialog.visible || restartDialog.visible + function tryOpenUpdate() { + if (pendingUpdate && visible && !dialogOpen && !recorder.active && appController.updater && appController.updater.available) { + pendingUpdate = false + updateDialog.open() + } + } + onDialogOpenChanged: Qt.callLater(tryOpenUpdate) + onVisibleChanged: Qt.callLater(tryOpenUpdate) + Connections { + target: recorder + function onChanged() { Qt.callLater(root.tryOpenUpdate) } + } onClosing: function(event) { root.contentItem.forceActiveFocus() recorder.cancel() @@ -71,7 +84,7 @@ ApplicationWindow { spacing: 8 AppButton { objectName: "newMenuButton" - iconName: "plus.svg"; hint: "New menu" + iconName: "plus.svg"; hint: qsTr("New menu") onClicked: { editor.addProfile(); root.panelMode = "menu" } } AppCombo { @@ -81,20 +94,20 @@ ApplicationWindow { textRole: "name"; valueRole: "id" selectionValue: editor.profileId onActivated: editor.selectProfile(currentValue) - Accessible.name: "Current menu" + Accessible.name: qsTr("Current menu") } AppMenuButton { objectName: "manageMenusButton" - iconName: "more-horiz.svg"; hint: "Manage menus" + iconName: "more-horiz.svg"; hint: qsTr("Manage menus") menu: profileMenu AppMenu { id: profileMenu objectName: "profileMenu" - AppMenuItem { text: "New menu"; onTriggered: { editor.addProfile(); root.panelMode = "menu" } } - AppMenuItem { text: "Menu settings"; onTriggered: root.panelMode = "menu" } - AppMenuItem { text: "Duplicate menu"; onTriggered: { editor.duplicateProfile(); root.panelMode = "menu" } } + AppMenuItem { text: qsTr("New menu"); onTriggered: { editor.addProfile(); root.panelMode = "menu" } } + AppMenuItem { text: qsTr("Menu settings"); onTriggered: root.panelMode = "menu" } + AppMenuItem { text: qsTr("Duplicate menu"); onTriggered: { editor.duplicateProfile(); root.panelMode = "menu" } } AppMenuSeparator {} - AppMenuItem { text: "Delete menu"; destructive: true; enabled: editor.profiles.length > 1; onTriggered: editor.removeProfile() } + AppMenuItem { text: qsTr("Delete menu"); destructive: true; enabled: editor.profiles.length > 1; onTriggered: editor.removeProfile() } } } Rectangle { Layout.preferredHeight: 24; implicitWidth: 1; color: T.line; Layout.leftMargin: 3; Layout.rightMargin: 3 } @@ -102,16 +115,16 @@ ApplicationWindow { objectName: "triggerSummaryButton" iconName: "key-command.svg" text: root.width < 1000 ? "" : editor.profile.triggerSummary || "" - hint: "Edit trigger and application scope" + hint: qsTr("Edit trigger and application scope") onClicked: root.panelMode = root.panelMode === "menu" ? "" : "menu" checked: root.panelMode === "menu" } Item { Layout.fillWidth: true } - AppButton { objectName: "undoButton"; iconName: "undo.svg"; hint: "Undo · Ctrl+Z"; enabled: editor.canUndo; onClicked: editor.undo() } - AppButton { iconName: "redo.svg"; hint: "Redo · Ctrl+Y"; enabled: editor.canRedo; onClicked: editor.redo() } - AppButton { objectName: "discardButton"; enabled: editor.dirty; text: "Discard"; onClicked: { root.contentItem.forceActiveFocus(); editor.discard() } } - AppButton { objectName: "applyButton"; enabled: editor.dirty; text: "Apply"; iconName: "check.svg"; kind: "primary"; onClicked: root.applyDraft() } - AppButton { objectName: "applicationSettingsButton"; iconName: "settings.svg"; hint: "Application settings"; onClicked: settings.open() } + AppButton { objectName: "undoButton"; iconName: "undo.svg"; hint: qsTr("Undo · Ctrl+Z"); enabled: editor.canUndo; onClicked: editor.undo() } + AppButton { iconName: "redo.svg"; hint: qsTr("Redo · Ctrl+Y"); enabled: editor.canRedo; onClicked: editor.redo() } + AppButton { objectName: "discardButton"; enabled: editor.dirty; text: qsTr("Discard"); onClicked: { root.contentItem.forceActiveFocus(); editor.discard() } } + AppButton { objectName: "applyButton"; enabled: editor.dirty; text: qsTr("Apply"); iconName: "check.svg"; kind: "primary"; onClicked: root.applyDraft() } + AppButton { objectName: "applicationSettingsButton"; iconName: "settings.svg"; hint: qsTr("Application settings"); onClicked: settings.open() } } } @@ -129,7 +142,7 @@ ApplicationWindow { anchors.left: parent.left; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 20; anchors.rightMargin: 12 Label { text: editor.error; color: "#954c41"; font.pixelSize: 13; wrapMode: Text.WordWrap; Layout.fillWidth: true } - AppButton { iconName: "cancel.svg"; compact: true; hint: "Dismiss message"; onClicked: editor.clearError() } + AppButton { iconName: "cancel.svg"; compact: true; hint: qsTr("Dismiss message"); onClicked: editor.clearError() } } } RowLayout { @@ -145,36 +158,36 @@ ApplicationWindow { AppButton { objectName: "backToMenuButton" visible: editor.folderId.length > 0 - iconName: "arrow-left.svg"; hint: "Back to main menu" + iconName: "arrow-left.svg"; hint: qsTr("Back to main menu") text: editor.folderName maximumTextWidth: 140 onClicked: editor.leaveFolder() } AppMenuButton { objectName: "addActionButton" - iconName: "plus.svg"; text: "Add action"; kind: "secondary" + iconName: "plus.svg"; text: qsTr("Add action"); kind: "secondary" menu: addMenu AppMenu { id: addMenu - AppMenuItem { text: "Keyboard shortcut"; onTriggered: editor.addItem(3) } - AppMenuItem { text: "Open application"; onTriggered: editor.addItem(1) } - AppMenuItem { text: "Open file or folder"; onTriggered: editor.addItem(4) } - AppMenuItem { text: "Open website"; onTriggered: editor.addItem(5) } - AppMenuItem { text: "Run command"; onTriggered: editor.addItem(2) } + AppMenuItem { text: qsTr("Keyboard shortcut"); onTriggered: editor.addItem(3) } + AppMenuItem { text: qsTr("Open application"); onTriggered: editor.addItem(1) } + AppMenuItem { text: qsTr("Open file or folder"); onTriggered: editor.addItem(4) } + AppMenuItem { text: qsTr("Open website"); onTriggered: editor.addItem(5) } + AppMenuItem { text: qsTr("Run command"); onTriggered: editor.addItem(2) } AppMenuSeparator { visible: editor.folderId.length === 0 } - AppMenuItem { text: "New submenu"; visible: editor.folderId.length === 0; onTriggered: editor.addItem(6) } + AppMenuItem { text: qsTr("New submenu"); visible: editor.folderId.length === 0; onTriggered: editor.addItem(6) } } } Item { Layout.fillWidth: true } AppButton { objectName: "arrangeButton" - iconName: "layers.svg"; hint: "Arrange actions"; checked: root.listVisible + iconName: "layers.svg"; hint: qsTr("Arrange actions"); checked: root.listVisible onClicked: root.listVisible = !root.listVisible } AppButton { objectName: "appearanceButton" - iconName: "palette.svg"; text: canvasColumn.width > 680 ? "Appearance" : "" - hint: "Appearance"; checked: root.panelMode === "appearance" + iconName: "palette.svg"; text: canvasColumn.width > 680 ? qsTr("Appearance") : "" + hint: qsTr("Appearance"); checked: root.panelMode === "appearance" onClicked: root.panelMode = root.panelMode === "appearance" ? "" : "appearance" } } @@ -196,9 +209,9 @@ ApplicationWindow { anchors.centerIn: parent spacing: 14 IconImage { name: "add-circle.svg"; width: 40; height: 40; tint: "#91a7c5"; Layout.alignment: Qt.AlignHCenter } - Label { text: editor.folderId.length ? "Add an action to this submenu" : "Build your menu"; color: T.ink; font.pixelSize: 20; font.weight: Font.DemiBold; Layout.alignment: Qt.AlignHCenter } - Label { text: "Start with a shortcut, an app, or a website."; color: T.muted; font.pixelSize: 14; Layout.alignment: Qt.AlignHCenter } - AppButton { text: "Add action"; iconName: "plus.svg"; kind: "primary"; Layout.alignment: Qt.AlignHCenter; onClicked: addMenu.open() } + Label { text: editor.folderId.length ? qsTr("Add an action to this submenu") : qsTr("Build your menu"); color: T.ink; font.pixelSize: 20; font.weight: Font.DemiBold; Layout.alignment: Qt.AlignHCenter } + Label { text: qsTr("Start with a shortcut, an app, or a website."); color: T.muted; font.pixelSize: 14; Layout.alignment: Qt.AlignHCenter } + AppButton { text: qsTr("Add action"); iconName: "plus.svg"; kind: "primary"; Layout.alignment: Qt.AlignHCenter; onClicked: addMenu.open() } } DropArea { anchors.fill: parent @@ -220,7 +233,7 @@ ApplicationWindow { } Label { objectName: "previewHint" - text: editor.selectedId.length ? "Changes stay in preview until you apply." : editor.folderId.length ? "Click an action to edit" : "Click a slice to edit" + text: editor.selectedId.length ? qsTr("Changes stay in preview until you apply.") : editor.folderId.length ? qsTr("Click an action to edit") : qsTr("Click a slice to edit") color: T.muted; font.pixelSize: 13 } } @@ -273,34 +286,77 @@ ApplicationWindow { ColorPicker { id: colorPicker } ExitDialog { id: exitDialog } RunningAppsDialog { id: runningApps } + UpdateDialog { id: updateDialog } + RestartDialog { id: restartDialog } Connections { target: appController function onColorRequested(color) { colorPicker.begin(color) } function onConfirmExitRequested() { exitDialog.open() } function onRunningAppsRequested() { runningApps.open() } + function onUpdateDialogRequested() { root.pendingUpdate = true; Qt.callLater(root.tryOpenUpdate) } } WindowResizeHandles { anchors.fill: parent; targetWindow: root; z: 1000 } AppDialog { id: settings - title: "Settings" + title: qsTr("Settings") width: Math.min(440, root.width - 32) ColumnLayout { id: settingsContent Layout.fillWidth: true spacing: 14 - AppCheckBox { text: "Start with Windows"; checked: editor.startWithWindows; onClicked: editor.setStartWithWindows(checked) } + FieldLabel { text: qsTr("Language") } + AppCombo { + objectName: "languageCombo" + Layout.fillWidth: true + textRole: "label"; valueRole: "value" + model: [ + {label: qsTr("Follow system"), value: "system"}, + {label: qsTr("English"), value: "en"}, + {label: qsTr("繁體中文"), value: "zh-TW"}, + {label: qsTr("简体中文"), value: "zh-CN"} + ] + selectionValue: editor.language + onActivated: { + if (appController.chooseLanguage(currentValue)) { + settings.close() + restartDialog.open() + } + } + Accessible.name: qsTr("Language") + } + Label { Layout.fillWidth: true; text: qsTr("Language changes take effect after restarting GoPieMenu."); color: T.muted; font.pixelSize: 12; wrapMode: Text.WordWrap } + AppCheckBox { text: qsTr("Start with Windows"); checked: editor.startWithWindows; onClicked: editor.setStartWithWindows(checked) } + AppButton { + objectName: "checkUpdatesButton" + visible: appController.updater !== null + enabled: appController.updater && !appController.updater.checking && !appController.updater.downloading && !appController.updater.ready + text: qsTr("Check for updates"); kind: "secondary" + onClicked: appController.updater.check(true) + } + AppButton { + objectName: "viewUpdateButton" + visible: appController.updater && appController.updater.available + text: qsTr("View available update") + onClicked: { settings.close(); appController.showUpdate() } + } + Label { + Layout.fillWidth: true + visible: appController.updater && appController.updater.status.length > 0 + text: appController.updater ? appController.updater.status : "" + wrapMode: Text.WordWrap; color: T.muted; font.pixelSize: 12 + } Rectangle { Layout.fillWidth: true; implicitHeight: 1; color: T.line } RowLayout { - AppButton { text: "Import"; iconName: "import.svg"; kind: "secondary"; onClicked: { settings.close(); appController.importConfig() } } - AppButton { text: "Export"; iconName: "upload.svg"; kind: "secondary"; onClicked: appController.exportConfig() } + AppButton { text: qsTr("Import"); iconName: "import.svg"; kind: "secondary"; onClicked: { settings.close(); appController.importConfig() } } + AppButton { text: qsTr("Export"); iconName: "upload.svg"; kind: "secondary"; onClicked: appController.exportConfig() } } - AppButton { text: "Open custom icon folder"; iconName: "folder.svg"; onClicked: appController.openIconDirectory() } + AppButton { text: qsTr("Open custom icon folder"); iconName: "folder.svg"; onClicked: appController.openIconDirectory() } Rectangle { Layout.fillWidth: true; implicitHeight: 1; color: T.line } RowLayout { - AppButton { text: "About"; onClicked: { settings.close(); about.open() } } + AppButton { text: qsTr("About"); onClicked: { settings.close(); about.open() } } AppButton { objectName: "projectGithubButton" - iconName: "github.svg"; hint: "Open GoPieMenu on GitHub" + iconName: "github.svg"; hint: qsTr("Open GoPieMenu on GitHub") onClicked: Qt.openUrlExternally("https://github.com/RyuuMeow/GoPieMenu") } } @@ -308,15 +364,15 @@ ApplicationWindow { } AppDialog { id: about - title: "About" + title: qsTr("About") width: 340 ColumnLayout { id: aboutContent Layout.fillWidth: true; spacing: 12 Label { text: "GoPieMenu"; font.pixelSize: 22; font.weight: Font.DemiBold; color: T.ink } - Label { text: "Version " + appController.version; color: T.muted } - Label { text: "GoPieMenu · GPL-3.0\nIcons by Iconoir · MIT"; color: T.muted; font.pixelSize: 13 } - AppButton { text: "Close"; kind: "secondary"; onClicked: about.close() } + Label { text: qsTr("Version %1").arg(appController.version); color: T.muted } + Label { text: qsTr("GoPieMenu · GPL-3.0") + "\n" + qsTr("Icons by Iconoir · MIT"); color: T.muted; font.pixelSize: 13 } + AppButton { text: qsTr("Close"); kind: "secondary"; onClicked: about.close() } } } } diff --git a/src/ui/qml/MenuSettingsPane.qml b/src/ui/qml/MenuSettingsPane.qml index 47d0298..b94286f 100644 --- a/src/ui/qml/MenuSettingsPane.qml +++ b/src/ui/qml/MenuSettingsPane.qml @@ -5,63 +5,63 @@ import "Theme.js" as T ColumnLayout { spacing: 10 - FieldLabel { text: "Menu name" } - AppField { Layout.fillWidth: true; text: editor.profile.name || ""; onTextEdited: editor.setProfileField("name", text); Accessible.name: "Menu name" } + FieldLabel { text: qsTr("Menu name") } + AppField { Layout.fillWidth: true; text: editor.profile.name || ""; onTextEdited: editor.setProfileField("name", text); Accessible.name: qsTr("Menu name") } StatusToggle { objectName: "settingsEnabledToggle" checked: editor.profile.enabled || false canEnable: (editor.profile.itemCount || 0) > 0 onToggled: editor.setProfileField("enabled", checked) } - FieldLabel { text: "Open with" } + FieldLabel { text: qsTr("Open with") } AppCombo { objectName: "activationModeCombo" Layout.fillWidth: true - model: ["Hold mouse button", "Hold keyboard shortcut", "Press to toggle"] + model: [qsTr("Hold mouse button"), qsTr("Hold keyboard shortcut"), qsTr("Press to toggle")] currentIndex: editor.profile.triggerMode || 0 onActivated: { recorder.cancel(); editor.setProfileField("triggerMode", currentIndex) } - Accessible.name: "Activation mode" + Accessible.name: qsTr("Activation mode") } - FieldLabel { text: "Modifiers" } + FieldLabel { text: qsTr("Modifiers") } ModifierButtons { modifiers: editor.profile.modifiers || 0; onEdited: function(value) { editor.setProfileField("modifiers", value) } } - FieldLabel { text: editor.profile.triggerMode === 0 ? "Mouse button" : "Keyboard shortcut" } + FieldLabel { text: editor.profile.triggerMode === 0 ? qsTr("Mouse button") : qsTr("Keyboard shortcut") } AppCombo { objectName: "mouseButtonCombo" visible: editor.profile.triggerMode === 0 Layout.fillWidth: true textRole: "label"; valueRole: "value" model: [ - {label: "Left button", value: "Left"}, {label: "Right button", value: "Right"}, - {label: "Middle button", value: "Middle"}, {label: "Side button 1 (X1)", value: "X1"}, - {label: "Side button 2 (X2)", value: "X2"} + {label: qsTr("Left button"), value: "Left"}, {label: qsTr("Right button"), value: "Right"}, + {label: qsTr("Middle button"), value: "Middle"}, {label: qsTr("Side button 1 (X1)"), value: "X1"}, + {label: qsTr("Side button 2 (X2)"), value: "X2"} ] selectionValue: editor.profile.mouseButton onActivated: editor.setProfileField("mouseButton", currentValue) - Accessible.name: "Trigger mouse button" + Accessible.name: qsTr("Trigger mouse button") } AppButton { objectName: "triggerRecorderButton" visible: editor.profile.triggerMode !== 0 Layout.fillWidth: true; kind: "secondary"; iconName: "key-command.svg" - text: recorder.active && recorder.target === "trigger" ? "Press a keyboard shortcut…" : editor.profile.vkCode ? editor.profile.triggerSummary : "Record shortcut" + text: recorder.active && recorder.target === "trigger" ? qsTr("Press a keyboard shortcut…") : editor.profile.vkCode ? editor.profile.triggerSummary : qsTr("Record shortcut") onClicked: recorder.active ? recorder.cancel() : recorder.start("trigger") } Label { Layout.fillWidth: true - text: editor.profile.triggerMode === 2 ? "Press the shortcut, point to an action, then click to run it." : "Hold the trigger, point to an action, then release to run it." + text: editor.profile.triggerMode === 2 ? qsTr("Press the shortcut, point to an action, then click to run it.") : qsTr("Hold the trigger, point to an action, then release to run it.") wrapMode: Text.WordWrap; color: T.muted; font.pixelSize: 13 } - FieldLabel { text: "Available in" } + FieldLabel { text: qsTr("Available in") } AppField { Layout.fillWidth: true text: editor.profile.appFilter || "" - placeholderText: "All applications" + placeholderText: qsTr("All applications") onEditingFinished: editor.setProfileField("appFilter", text) - Accessible.name: "Application filter" + Accessible.name: qsTr("Application filter") } RowLayout { - AppButton { text: "Running apps"; iconName: "app-window.svg"; kind: "secondary"; onClicked: appController.pickRunningApplication() } - AppButton { iconName: "folder.svg"; hint: "Choose application"; kind: "secondary"; onClicked: appController.chooseApplicationFilter() } + AppButton { text: qsTr("Running apps"); iconName: "app-window.svg"; kind: "secondary"; onClicked: appController.pickRunningApplication() } + AppButton { iconName: "folder.svg"; hint: qsTr("Choose application"); kind: "secondary"; onClicked: appController.chooseApplicationFilter() } } - Label { Layout.fillWidth: true; text: "Leave empty for every app. Separate application names with commas."; color: T.muted; font.pixelSize: 12; wrapMode: Text.WordWrap } + Label { Layout.fillWidth: true; text: qsTr("Leave empty for every app. Separate application names with commas."); color: T.muted; font.pixelSize: 12; wrapMode: Text.WordWrap } } diff --git a/src/ui/qml/ModifierButtons.qml b/src/ui/qml/ModifierButtons.qml index fc1a7a3..0ef6388 100644 --- a/src/ui/qml/ModifierButtons.qml +++ b/src/ui/qml/ModifierButtons.qml @@ -15,7 +15,7 @@ RowLayout { compact: true checkable: true checked: (root.modifiers & modelData.bit) !== 0 - Accessible.name: modelData.label + " modifier" + Accessible.name: qsTr("%1 modifier").arg(modelData.label) onClicked: root.edited(root.modifiers ^ modelData.bit) } } diff --git a/src/ui/qml/RestartDialog.qml b/src/ui/qml/RestartDialog.qml new file mode 100644 index 0000000..c5103c8 --- /dev/null +++ b/src/ui/qml/RestartDialog.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import "Theme.js" as T + +AppDialog { + id: root + objectName: "restartDialog" + title: qsTr("Restart required") + Label { + Layout.fillWidth: true + text: qsTr("Restart GoPieMenu to use the new language. Restarting will apply all pending changes.") + wrapMode: Text.WordWrap; color: T.ink + } + Label { + Layout.fillWidth: true + text: qsTr("Choose Later to keep editing, then apply your changes before restarting manually.") + wrapMode: Text.WordWrap; color: T.muted; font.pixelSize: 12 + } + Label { + Layout.fillWidth: true + visible: editor.error.length > 0 + text: editor.error; wrapMode: Text.WordWrap; color: T.danger + } + footer: RowLayout { + Item { Layout.fillWidth: true } + AppButton { objectName: "restartLaterButton"; text: qsTr("Later"); onClicked: root.close() } + AppButton { + objectName: "restartNowButton" + text: qsTr("Restart"); kind: "primary" + onClicked: if (appController.restartForLanguage()) root.close() + } + } +} diff --git a/src/ui/qml/RunningAppsDialog.qml b/src/ui/qml/RunningAppsDialog.qml index 046d4be..10c3a5e 100644 --- a/src/ui/qml/RunningAppsDialog.qml +++ b/src/ui/qml/RunningAppsDialog.qml @@ -6,9 +6,9 @@ import "Theme.js" as T AppDialog { id: root objectName: "runningAppsDialog" - title: "Running applications" + title: qsTr("Running applications") onOpened: { search.text = ""; search.forceActiveFocus() } - AppField { id: search; objectName: "runningAppsSearch"; Layout.fillWidth: true; placeholderText: "Search applications"; Accessible.name: "Search running applications" } + AppField { id: search; objectName: "runningAppsSearch"; Layout.fillWidth: true; placeholderText: qsTr("Search applications"); Accessible.name: qsTr("Search running applications") } ListView { Layout.fillWidth: true; Layout.preferredHeight: 300 clip: true; spacing: 3 @@ -20,11 +20,11 @@ AppDialog { text: modelData onTriggered: { appController.useRunningApplication(modelData); root.close() } } - Label { anchors.centerIn: parent; visible: parent.count === 0; text: "No matching applications"; color: T.muted; font.pixelSize: 13 } + Label { anchors.centerIn: parent; visible: parent.count === 0; text: qsTr("No matching applications"); color: T.muted; font.pixelSize: 13 } } footer: RowLayout { - AppButton { text: "Refresh"; kind: "text"; onClicked: appController.refreshRunningApplications() } + AppButton { text: qsTr("Refresh"); kind: "text"; onClicked: appController.refreshRunningApplications() } Item { Layout.fillWidth: true } - AppButton { text: "Cancel"; onClicked: root.close() } + AppButton { text: qsTr("Cancel"); onClicked: root.close() } } } diff --git a/src/ui/qml/StatusToggle.qml b/src/ui/qml/StatusToggle.qml index f5cdb8a..73881b3 100644 --- a/src/ui/qml/StatusToggle.qml +++ b/src/ui/qml/StatusToggle.qml @@ -10,8 +10,8 @@ Switch { padding: 0 spacing: 10 hoverEnabled: true - text: checked ? "Enabled" : "Disabled" - Accessible.name: "Menu enabled" + text: checked ? qsTr("Enabled") : qsTr("Disabled") + Accessible.name: qsTr("Menu enabled") indicator: Rectangle { width: 42; height: 24 y: (root.height - height)/2 @@ -29,5 +29,5 @@ Switch { font.pixelSize: 13 verticalAlignment: Text.AlignVCenter } - AppToolTip { visible: root.hovered; text: root.checked ? "Disable this menu" : root.canEnable ? "Enable this menu" : "Add an action before enabling this menu" } + AppToolTip { visible: root.hovered; text: root.checked ? qsTr("Disable this menu") : root.canEnable ? qsTr("Enable this menu") : qsTr("Add an action before enabling this menu") } } diff --git a/src/ui/qml/UpdateDialog.qml b/src/ui/qml/UpdateDialog.qml new file mode 100644 index 0000000..5402b5d --- /dev/null +++ b/src/ui/qml/UpdateDialog.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import "Theme.js" as T + +AppDialog { + id: root + objectName: "updateDialog" + readonly property var updater: appController.updater + title: qsTr("Update available") + width: Math.min(640, parent.width - 32) + Label { + Layout.fillWidth: true + text: qsTr("GoPieMenu %1 → %2").arg(appController.version).arg(root.updater ? root.updater.version : "") + color: T.ink; font.pixelSize: 16; font.weight: Font.DemiBold + wrapMode: Text.WordWrap + } + TextArea { + objectName: "updateReleaseNotes" + Layout.fillWidth: true + readOnly: true; selectByMouse: true + textFormat: TextEdit.MarkdownText + text: root.updater && root.updater.notes.length ? root.updater.notes : qsTr("Release notes are unavailable. You can view this release on GitHub.") + wrapMode: TextEdit.Wrap + color: T.ink; font.pixelSize: 14 + padding: 0; background: null + onLinkActivated: function(link) { appController.openReleaseLink(link) } + } + Label { + Layout.fillWidth: true + visible: root.updater && !root.updater.canDownload + text: qsTr("This release has no verified Windows installer. View the release on GitHub.") + color: T.muted; wrapMode: Text.WordWrap + } + ProgressBar { + objectName: "updateProgress" + Layout.fillWidth: true + visible: root.updater && root.updater.downloading + indeterminate: value === 0 + value: root.updater ? root.updater.progress : 0 + } + Label { + Layout.fillWidth: true + visible: root.updater && root.updater.downloading + text: qsTr("Downloading and verifying the installer… %1%").arg(Math.round(root.updater ? root.updater.progress * 100 : 0)) + color: T.muted; wrapMode: Text.WordWrap + } + Label { + objectName: "updateReadyMessage" + Layout.fillWidth: true + visible: root.updater && root.updater.ready + text: qsTr("The installer is verified. Install now? GoPieMenu will close after you resolve any unsaved changes.") + color: T.ink; wrapMode: Text.WordWrap + } + Label { + Layout.fillWidth: true + visible: root.updater && root.updater.error.length > 0 + text: root.updater ? root.updater.error : "" + color: T.danger; wrapMode: Text.WordWrap + } + AppButton { text: qsTr("View release on GitHub"); kind: "text"; onClicked: appController.openReleasePage() } + footer: RowLayout { + AppButton { + objectName: "skipUpdateButton" + visible: root.updater && !root.updater.downloading && !root.updater.ready + text: qsTr("Skip this version") + onClicked: if (root.updater.skipVersion()) root.close() + } + AppButton { + objectName: "cancelUpdateDownloadButton" + visible: root.updater && root.updater.downloading + text: qsTr("Cancel download") + onClicked: root.updater.cancelDownload() + } + Item { Layout.fillWidth: true } + AppButton { text: qsTr("Later"); onClicked: root.close() } + AppButton { + objectName: "downloadUpdateButton" + visible: root.updater && !root.updater.ready + enabled: root.updater && root.updater.canDownload && !root.updater.downloading && !root.updater.checking + text: qsTr("Update"); kind: "primary" + onClicked: root.updater.download() + } + AppButton { + objectName: "installUpdateButton" + visible: root.updater && root.updater.ready + text: qsTr("Install and exit"); kind: "primary" + onClicked: { root.close(); appController.requestUpdateInstall() } + } + } +} diff --git a/src/ui/qml/WindowTitleBar.qml b/src/ui/qml/WindowTitleBar.qml index 0deddcb..7d6c5cc 100644 --- a/src/ui/qml/WindowTitleBar.qml +++ b/src/ui/qml/WindowTitleBar.qml @@ -18,8 +18,8 @@ Rectangle { } Row { anchors.right: parent.right; anchors.rightMargin: 6; anchors.verticalCenter: parent.verticalCenter - WindowControlButton { objectName: "windowMinimizeButton"; iconName: "window-minimize.svg"; hint: "Minimize"; onClicked: root.targetWindow.showMinimized() } - WindowControlButton { objectName: "windowMaximizeButton"; iconName: appController.maximized ? "window-restore.svg" : "window-maximize.svg"; hint: appController.maximized ? "Restore" : "Maximize"; onClicked: root.toggleMaximized() } - WindowControlButton { objectName: "windowCloseButton"; iconName: "window-close.svg"; hint: "Close window"; onClicked: root.targetWindow.close() } + WindowControlButton { objectName: "windowMinimizeButton"; iconName: "window-minimize.svg"; hint: qsTr("Minimize"); onClicked: root.targetWindow.showMinimized() } + WindowControlButton { objectName: "windowMaximizeButton"; iconName: appController.maximized ? "window-restore.svg" : "window-maximize.svg"; hint: appController.maximized ? qsTr("Restore") : qsTr("Maximize"); onClicked: root.toggleMaximized() } + WindowControlButton { objectName: "windowCloseButton"; iconName: "window-close.svg"; hint: qsTr("Close window"); onClicked: root.targetWindow.close() } } } diff --git a/src/ui/rendering/MenuScene.cpp b/src/ui/rendering/MenuScene.cpp index 66803d1..9076744 100644 --- a/src/ui/rendering/MenuScene.cpp +++ b/src/ui/rendering/MenuScene.cpp @@ -1,5 +1,6 @@ #include "MenuScene.h" #include +#include #include #include #include @@ -72,7 +73,7 @@ void PieScene::build(const PieMenuConfig& config, const StyleConfig& globalStyle if (Config.Items.empty()) return; const auto span = 2 * Pi / Config.Items.size(); const auto gap = std::min(Style.GapAngle * Pi / 180, span * .8); - QFont font(Style.FontFamily); font.setPointSizeF(Style.FontSize); font.setWeight(QFont::Medium); + QFont font(Style.FontFamily == "Segoe UI" ? QGuiApplication::font().family() : Style.FontFamily); font.setPointSizeF(Style.FontSize); font.setWeight(QFont::Medium); auto bold = font; bold.setWeight(QFont::DemiBold); for (int i = 0; i < Sectors.size(); ++i) { auto& sector = Sectors[i]; @@ -182,7 +183,7 @@ void ListScene::draw(QPainter& p, int first, int visibleRows, int hovered, qreal const QRectF bounds(QPointF(), size(visibleRows)); p.setPen(QPen(Style.BorderColor, Style.BorderWidth)); p.setBrush(Style.BackgroundColor); p.drawRoundedRect(bounds, 10, 10); - QFont font(Style.FontFamily); font.setPointSizeF(Style.FontSize); font.setWeight(QFont::Medium); + QFont font(Style.FontFamily == "Segoe UI" ? QGuiApplication::font().family() : Style.FontFamily); font.setPointSizeF(Style.FontSize); font.setWeight(QFont::Medium); for (int row = 0; row < visibleRows; ++row) { const int index = first + row; if (index < 0 || index >= count()) break; diff --git a/src/update/ReleaseInfo.cpp b/src/update/ReleaseInfo.cpp new file mode 100644 index 0000000..d9d4d66 --- /dev/null +++ b/src/update/ReleaseInfo.cpp @@ -0,0 +1,101 @@ +#include "ReleaseInfo.h" +#include "core/TranslationManager.h" +#include +#include +#include +#include +#include +#include +#include + +namespace gpm { +namespace { +std::optional StableVersion(const QString& text) { + static const QRegularExpression pattern(R"(^v?((?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*))$)"); + const auto match = pattern.match(text); + if (!match.hasMatch()) return {}; + qsizetype suffix = 0; + const auto version = QVersionNumber::fromString(match.captured(1), &suffix); + if (suffix != match.capturedLength(1) || version.segmentCount() != 3) return {}; + return version; +} +} + +QString LocalizedReleaseNotes(const QString& body, const QString& language) { + // Older releases had an English-only body without language markers. + if (!body.contains("[ \t]*\r?\n(.*?)^[ \t]*[ \t]*\r?$)", + QRegularExpression::MultilineOption | QRegularExpression::DotMatchesEverythingOption); + QMap sections; + auto matches = blocks.globalMatch(body); + while (matches.hasNext()) { + const auto match = matches.next(); + const auto content = match.captured(2).trimmed(); + // Don't display another language if a marker was accidentally left unclosed. + if (content.contains("")) { throw "Missing language: $language" } +} +if (!$notes.Contains('rebuilds the menu editor') -or !$notes.Contains('SHA256SUMS.txt')) { throw 'Release content was lost.' } +function MustFail([scriptblock]$Operation) { + $failed = $false + try { & $Operation | Out-Null } catch { $failed = $true } + if (!$failed) { throw 'Invalid changelog was accepted.' } +} +MustFail { & $extractor -Version 99.99.99 } +MustFail { & $extractor -Version '2.0.0-beta' } +$testDirectory = Join-Path ([IO.Path]::GetTempPath()) ('gpm-release-notes-' + [guid]::NewGuid().ToString('N')) +[IO.Directory]::CreateDirectory($testDirectory) | Out-Null +$fixture = Join-Path $testDirectory 'CHANGELOG.md' +$output = Join-Path $testDirectory 'notes.md' +try { + $valid = "# Changelog`n## [2.1.0]`n" + foreach ($language in @('en', 'zh-TW', 'zh-CN', 'shared')) { + $valid += "`nContent for $language.`n`n" + } + [IO.File]::WriteAllText($fixture, $valid) + & $extractor -Version 2.1.0 -ChangelogPath $fixture -OutputPath $output + $result = [IO.File]::ReadAllText($output) + if (!$result.Contains('Content for zh-TW.') -or !$result.Contains('Content for shared.')) { throw 'Missing output content.' } + [IO.File]::WriteAllText($fixture, $valid.Replace('gpm:lang zh-CN', 'gpm:lang unsupported')) + MustFail { & $extractor -Version 2.1.0 -ChangelogPath $fixture } + [IO.File]::WriteAllText($fixture, $valid + "`n`nDuplicate`n") + MustFail { & $extractor -Version 2.1.0 -ChangelogPath $fixture } + [IO.File]::WriteAllText($fixture, $valid + "`n`nUnclosed") + MustFail { & $extractor -Version 2.1.0 -ChangelogPath $fixture } + [IO.File]::WriteAllText($fixture, $valid + "`n" + $valid) + MustFail { & $extractor -Version 2.1.0 -ChangelogPath $fixture } +} finally { + # These are the two exact temporary files created above; no recursive cleanup. + if (Test-Path -LiteralPath $fixture) { Remove-Item -LiteralPath $fixture } + if (Test-Path -LiteralPath $output) { Remove-Item -LiteralPath $output } + Remove-Item -LiteralPath $testDirectory +} +Write-Output 'Release note extraction, language validation, and failure cases passed.' diff --git a/tests/UiTests.cpp b/tests/UiTests.cpp index c3db74e..1758463 100644 --- a/tests/UiTests.cpp +++ b/tests/UiTests.cpp @@ -17,6 +17,8 @@ #include "ui/rendering/MenuScene.h" #include "ui/TrayManager.h" #include "core/ActionExecutor.h" +#include "core/TranslationManager.h" +#include "UpdateTestSupport.h" using namespace gpm; class UrlReceiver : public QObject { @@ -82,6 +84,166 @@ private slots: for (auto* screen : QGuiApplication::screens()) qInfo() << "Screen" << screen->name() << screen->geometry() << screen->availableGeometry() << screen->devicePixelRatio(); } void init() { QTest::failOnWarning(QRegularExpression(".*(qrc:/editor|QQml|Binding loop|ReferenceError|TypeError).*")); } + void hiddenUpdatesUseTrayAndReopenTheirNotes() { + QTemporaryDir dir; update_test::Network network; network.responses = {{update_test::Release()}}; + UpdateManager updates(update_test::Options(dir.path()), &network); + ConfigManager config(nullptr, dir.filePath("config.json")); IconService icons; + SettingsWindow editor(&config, &icons, nullptr, &updates); editor.setPlatformIntegrationEnabled(false); + QSignalSpy toast(&editor, &SettingsWindow::updateToastRequested); + QSignalSpy dialog(&editor, &SettingsWindow::updateDialogRequested); + updates.check(); QTRY_COMPARE(toast.count(), 1); QCOMPARE(dialog.count(), 0); + QCOMPARE(toast[0][0].toString(), "2.1.0"); + QVERIFY(showEditor(editor)); QTRY_COMPARE(dialog.count(), 1); + auto* popup = editor.window()->findChild("updateDialog"); QVERIFY(popup); + QTRY_VERIFY(popup->property("visible").toBool()); + QVERIFY(click(editor.window(), "skipUpdateButton")); + QVERIFY(!updates.available()); QTRY_VERIFY(!popup->property("visible").toBool()); + TrayManager tray; + auto* icon = tray.findChild(); QVERIFY(icon); + auto* action = icon->contextMenu()->findChild("trayUpdateAction"); QVERIFY(action); + QVERIFY(!action->isVisible()); tray.SetUpdateAvailable("2.1.0"); QVERIFY(action->isVisible()); + QSignalSpy clicked(&tray, &TrayManager::UpdateRequested); + action->trigger(); QCOMPARE(clicked.count(), 1); + QVERIFY(QMetaObject::invokeMethod(icon, "messageClicked")); QCOMPARE(clicked.count(), 2); + tray.SetUpdateAvailable({}); QVERIFY(QMetaObject::invokeMethod(icon, "messageClicked")); + QCOMPARE(clicked.count(), 2); + } + void visibleUpdatesWaitForDialogsAndUseTheInterfaceLanguage() { + for (const auto& language : {QString("en"), QString("zh-TW"), QString("zh-CN")}) { + TranslationManager translations; QVERIFY(translations.load(language)); + QTemporaryDir dir; update_test::Network network; network.responses = {{update_test::Release()}}; + UpdateManager updates(update_test::Options(dir.path(), language), &network); + ConfigManager config(nullptr, dir.filePath("config.json")); IconService icons; + SettingsWindow editor(&config, &icons, nullptr, &updates); editor.setPlatformIntegrationEnabled(false); + QVERIFY(showEditor(editor)); auto* w = editor.window(); + QSignalSpy toast(&editor, &SettingsWindow::updateToastRequested); + QVERIFY(click(w, "applicationSettingsButton")); + updates.check(); QTRY_VERIFY(updates.available()); + auto* popup = w->findChild("updateDialog"); QVERIFY(popup); + QVERIFY(!popup->property("visible").toBool()); QCOMPARE(toast.count(), 0); + QTest::keyClick(w, Qt::Key_Escape); + QTRY_VERIFY(popup->property("visible").toBool()); + QCOMPARE(popup->property("title").toString(), QCoreApplication::translate("UpdateDialog", "Update available")); + auto* notes = find(w, "updateReleaseNotes"); QVERIFY(notes); + // TextArea canonicalizes Markdown paragraph spacing when exposing text. + QCOMPARE(notes->property("text").toString().simplified(), updates.notes().simplified()); + QVERIFY(!updates.notes().contains("gpm:lang")); + w->resize(800, 520); QTRY_COMPARE(w->size(), QSize(800, 520)); + QVERIFY(snapshot(w, "update-" + language)); + QVERIFY(click(w, "skipUpdateButton")); QVERIFY(!updates.available()); + } + } + void installingUpdatesProtectsDraftsAndHandlesCancellation() { + QTemporaryDir dir; update_test::Network network; + network.responses = {{update_test::Release()}, {update_test::Checksum()}, {update_test::Installer}}; + auto options = update_test::Options(dir.path()); int launches = 0; bool allowInstall = false; + options.launchInstaller = [&](const QString&, QString* error) { + ++launches; if (!allowInstall) *error = "Simulated cancelled permission prompt"; return allowInstall; + }; + UpdateManager updates(options, &network); + ConfigManager config(nullptr, dir.filePath("config.json")); IconService icons; + SettingsWindow editor(&config, &icons, nullptr, &updates); editor.setPlatformIntegrationEnabled(false); + QVERIFY(showEditor(editor)); auto* w = editor.window(); + auto* session = editor.session(); session->setProfileField("name", "Draft name"); + updates.check(); QTRY_VERIFY(updates.available()); + QTRY_VERIFY(find(w, "downloadUpdateButton")); QVERIFY(click(w, "downloadUpdateButton")); + QTRY_VERIFY(updates.ready()); QCOMPARE(launches, 0); + QSignalSpy exits(&editor, &SettingsWindow::exitConfirmed); + QVERIFY(click(w, "installUpdateButton")); + QTRY_VERIFY(find(w, "exitCancelButton")); QVERIFY(click(w, "exitCancelButton")); + QTest::qWait(100); QCOMPARE(launches, 0); QCOMPARE(exits.count(), 0); QVERIFY(session->dirty()); + // Cancelling installation must not turn a subsequent ordinary Quit into an install. + editor.requestExit(); QVERIFY(editor.resolveExit("discard")); QCOMPARE(launches, 0); QCOMPARE(exits.count(), 1); + auto* exitPopup = w->findChild("exitDialog"); QVERIFY(exitPopup); + QVERIFY(QMetaObject::invokeMethod(exitPopup, "close")); QTest::qWait(100); + exits.clear(); session->setProfileField("name", ""); // Applying this draft is invalid. + editor.showUpdate(); QTRY_VERIFY(find(w, "installUpdateButton")); QVERIFY(click(w, "installUpdateButton")); + QTRY_VERIFY(find(w, "exitApplyButton")); QVERIFY(click(w, "exitApplyButton")); + QCOMPARE(exits.count(), 0); QCOMPARE(launches, 0); QVERIFY(session->dirty()); + session->setProfileField("name", "Saved name"); + QVERIFY(click(w, "exitApplyButton")); QCOMPARE(exits.count(), 0); QCOMPARE(launches, 1); + QVERIFY(!session->dirty()); QVERIFY(updates.ready()); + QVERIFY(click(w, "exitCancelButton")); QTest::qWait(100); + allowInstall = true; editor.showUpdate(); QTRY_VERIFY(find(w, "installUpdateButton")); + QVERIFY(click(w, "installUpdateButton")); QCOMPARE(exits.count(), 1); QCOMPARE(launches, 2); + } + void longReleaseNotesKeepUpdateActionsVisible() { + QTemporaryDir dir; update_test::Network network; + auto body = update_test::Notes; + body.replace("- English update.", QString("- A detailed release change with enough text to wrap in a compact window.\n").repeated(80)); + network.responses = {{update_test::Release("v2.1.0", body)}}; + UpdateManager updates(update_test::Options(dir.path()), &network); + ConfigManager config(nullptr, dir.filePath("config.json")); IconService icons; + SettingsWindow editor(&config, &icons, nullptr, &updates); editor.setPlatformIntegrationEnabled(false); + QVERIFY(showEditor(editor)); auto* w = editor.window(); w->resize(800, 520); + updates.check(); QTRY_VERIFY(find(w, "skipUpdateButton")); + QVERIFY(snapshot(w, "update-long-notes")); + QVERIFY(click(w, "skipUpdateButton")); QVERIFY(!updates.available()); + } + void chineseEditorsAndLanguageSelection() { + const auto previousFont = QGuiApplication::font(); + for (const auto& language : {QString("zh-TW"), QString("zh-CN")}) { + TranslationManager translations; QVERIFY(translations.load(language)); + QGuiApplication::setFont(QFont(InterfaceFontFamily(language))); + QTemporaryDir dir; ConfigManager config(nullptr, dir.filePath("config.json")); IconService icons; + auto initial = config.GetConfig(); initial.Language = language; QVERIFY(config.Commit(initial)); + SettingsWindow editor(&config, &icons); editor.setPlatformIntegrationEnabled(false); + QCOMPARE(editor.fontFamily(), "Microsoft JhengHei UI"); + QVERIFY(showEditor(editor)); auto* w = editor.window(); + auto* apply = find(w, "applyButton"); QVERIFY(apply); + QCOMPARE(apply->property("text").toString(), QCoreApplication::translate("Main", "Apply")); + QVERIFY(apply->property("text").toString() != "Apply"); + QVERIFY(click(w, "applicationSettingsButton")); + auto* combo = find(w, "languageCombo"); QVERIFY(combo); + QTRY_COMPARE(combo->property("currentValue").toString(), language); + combo->setProperty("currentIndex", 1); + QVERIFY(QMetaObject::invokeMethod(combo, "activated", Q_ARG(int, combo->property("currentIndex").toInt()))); + QCOMPARE(editor.session()->language(), "en"); + QVERIFY(editor.session()->dirty()); + QCOMPARE(config.GetConfig().Language, language); + QTRY_VERIFY(find(w, "restartNowButton")); + QCOMPARE(find(w, "restartNowButton")->property("kind").toString(), "primary"); + QVERIFY(snapshot(w, "restart-" + language)); + QVERIFY(click(w, "restartLaterButton")); + QVERIFY(editor.session()->dirty()); + QCOMPARE(config.GetConfig().Language, language); + } + QGuiApplication::setFont(previousFont); + } + void languageRestartSavesBeforeLaunching() { + QTemporaryDir dir; ConfigManager config(nullptr, dir.filePath("config.json")); IconService icons; + SettingsWindow editor(&config, &icons); editor.setPlatformIntegrationEnabled(false); + QSignalSpy exited(&editor, &SettingsWindow::exitConfirmed); + QVERIFY(editor.chooseLanguage("zh-TW")); + QVERIFY(!editor.chooseLanguage("zh-TW")); + int launches = 0; + editor.setRestartHandler([&] { + ++launches; + return config.GetConfig().Language == "zh-TW" && !editor.session()->dirty(); + }); + QVERIFY(editor.restartForLanguage()); + QCOMPARE(launches, 1); QCOMPARE(exited.count(), 1); + ConfigManager saved(nullptr, dir.filePath("config.json")); + QCOMPARE(saved.GetConfig().Language, "zh-TW"); + QVERIFY(editor.chooseLanguage("en")); + editor.setRestartHandler([] { return false; }); + QVERIFY(!editor.restartForLanguage()); + QCOMPARE(exited.count(), 1); + QCOMPARE(config.GetConfig().Language, "en"); + QVERIFY(!editor.session()->error().isEmpty()); + } + void languageRestartSaveFailureKeepsDraft() { + QTemporaryDir dir; ConfigManager config(nullptr, dir.path()); IconService icons; + SettingsWindow editor(&config, &icons); editor.setPlatformIntegrationEnabled(false); + QSignalSpy exited(&editor, &SettingsWindow::exitConfirmed); + bool launched = false; + editor.setRestartHandler([&] { launched = true; return true; }); + QVERIFY(editor.chooseLanguage("zh-TW")); + QVERIFY(!editor.restartForLanguage()); + QVERIFY(!launched); QCOMPARE(exited.count(), 0); + QVERIFY(editor.session()->dirty()); + QCOMPARE(config.GetConfig().Language, "system"); + } void appearanceFollowsTheSelectedProfile() { QTemporaryDir dir; ConfigManager config(nullptr, dir.filePath("config.json")); IconService icons; auto initial = config.GetConfig(); @@ -199,7 +361,8 @@ private slots: menu->popup(w->mapToGlobal(QPoint(100,160))); QTRY_VERIFY(menu->isVisible()); menu->setActiveAction(menu->actions()[0]); QTest::qWait(30); QVERIFY(menu->grab().save("artifacts/tray-menu-" + QString::number(w->devicePixelRatio()) + ".png")); - QTest::mouseClick(menu, Qt::LeftButton, Qt::NoModifier, menu->actionGeometry(menu->actions()[2]).center()); + auto* pauseAction = menu->findChild("trayPauseAction"); QVERIFY(pauseAction); + QTest::mouseClick(menu, Qt::LeftButton, Qt::NoModifier, menu->actionGeometry(pauseAction).center()); QCOMPARE(pause.count(), 1); QVERIFY(pause[0][0].toBool()); QVERIFY(!menu->isVisible()); } diff --git a/tests/UpdateTestSupport.h b/tests/UpdateTestSupport.h new file mode 100644 index 0000000..6047ec0 --- /dev/null +++ b/tests/UpdateTestSupport.h @@ -0,0 +1,85 @@ +#pragma once +#include "update/UpdateManager.h" +#include +#include +#include +#include +#include + +namespace update_test { +struct Response { + QByteArray body; + int status = 200; + QNetworkReply::NetworkError error = QNetworkReply::NoError; + int delayMs = 0; // Negative simulates a server that never responds. +}; + +class Reply : public QNetworkReply { +public: + Reply(const QNetworkRequest& request, Response response, QObject* parent) : QNetworkReply(parent), Data(response.body) { + setRequest(request); setUrl(request.url()); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, response.status); + open(QIODevice::ReadOnly | QIODevice::Unbuffered); + if (response.delayMs < 0) return; + QTimer::singleShot(response.delayMs, this, [this, response] { + if (isFinished()) return; + if (response.error != NoError) setError(response.error, "Simulated network failure"); + emit readyRead(); + if (isFinished()) return; + setFinished(true); emit finished(); + }); + } + void abort() override { + if (isFinished()) return; + setError(OperationCanceledError, "Cancelled"); setFinished(true); emit finished(); + } + qint64 bytesAvailable() const override { return Data.size() - Offset + QNetworkReply::bytesAvailable(); } +protected: + qint64 readData(char* destination, qint64 maxSize) override { + const auto count = std::min(maxSize, qint64(Data.size()) - Offset); + if (count <= 0) return -1; + std::memcpy(destination, Data.constData() + Offset, size_t(count)); Offset += count; return count; + } +private: + QByteArray Data; + qint64 Offset = 0; +}; + +class Network : public QNetworkAccessManager { +public: + QList responses; + QList requests; +protected: + QNetworkReply* createRequest(Operation, const QNetworkRequest& request, QIODevice*) override { + requests.append(request); + const auto response = responses.isEmpty() + ? Response{{}, 500, QNetworkReply::UnknownNetworkError} : responses.takeFirst(); + return new Reply(request, response, this); + } +}; + +inline const QByteArray Installer = "This is test data, not an executable."; +inline const QString Notes = "### English\n\n#### What's new\n- English update.\n\n" + "### 繁體中文\n\n#### 新功能\n- 繁體更新內容。\n\n" + "### 简体中文\n\n#### 新功能\n- 简体更新内容。\n\n" + "\n### Validation\nShared validation.\n\n"; +inline QByteArray Release(const QString& version = "v2.1.0", const QString& notes = Notes) { + return QJsonDocument(QJsonObject{{"tag_name", version}, {"draft", false}, {"prerelease", false}, {"body", notes}, + {"html_url", "https://untrusted.invalid/ignore"}, + {"assets", QJsonArray{ + QJsonObject{{"name", "GoPieMenu_Setup.exe"}, {"state", "uploaded"}, {"size", Installer.size()}}, + QJsonObject{{"name", "SHA256SUMS.txt"}, {"state", "uploaded"}, {"size", 100}}}}}).toJson(); +} +inline QByteArray Checksum(const QByteArray& bytes = Installer) { + return QCryptographicHash::hash(bytes, QCryptographicHash::Sha256).toHex() + " GoPieMenu_Setup.exe\n"; +} +inline gpm::UpdateOptions Options(const QString& directory, const QString& language = "en") { + gpm::UpdateOptions options; + options.currentVersion = "2.0.0"; + options.language = language; + options.settingsFile = directory + "/updates.ini"; + options.downloadDirectory = directory + "/downloads"; + options.launchInstaller = [](const QString&, QString*) { return true; }; + return options; +} +} diff --git a/tests/UpdateTests.cpp b/tests/UpdateTests.cpp new file mode 100644 index 0000000..3cac3da --- /dev/null +++ b/tests/UpdateTests.cpp @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include "UpdateTestSupport.h" + +using namespace gpm; +using namespace update_test; + +class UpdateTests : public QObject { + Q_OBJECT +private slots: + void comparesVersionsNumericallyAndRejectsPrereleases() { + QString error; + QVERIFY(ParseRelease(Release("v2.0.10"), "2.0.9", "en", &error)); + QVERIFY(error.isEmpty()); + QVERIFY(!ParseRelease(Release("v2.0.9"), "2.0.10", "en")); + QVERIFY(!ParseRelease(Release("v2.0.0"), "2.0.0", "en")); + QVERIFY(!ParseRelease(Release("v1.9.9"), "2.0.0", "en")); + QVERIFY(!ParseRelease(Release("v3.0.0-beta.1"), "2.0.0", "en")); + QVERIFY(!ParseRelease(Release("v2.1.0/../../file"), "2.0.0", "en")); + QVERIFY(!ParseRelease("not JSON", "2.0.0", "en", &error)); QVERIFY(!error.isEmpty()); + auto json = QJsonDocument::fromJson(Release()).object(); + json["draft"] = true; QVERIFY(!ParseRelease(QJsonDocument(json).toJson(), "2.0.0", "en")); + json["draft"] = false; json["prerelease"] = true; + QVERIFY(!ParseRelease(QJsonDocument(json).toJson(), "2.0.0", "en")); + const auto release = ParseRelease(Release(), "2.0.0", "en"); QVERIFY(release); + QCOMPARE(release->pageUrl, QUrl("https://github.com/RyuuMeow/GoPieMenu/releases/tag/v2.1.0")); + QCOMPARE(release->installerUrl, QUrl("https://github.com/RyuuMeow/GoPieMenu/releases/download/v2.1.0/GoPieMenu_Setup.exe")); + } + void selectsLocalizedNotesAndSharedValidation() { + const auto traditional = LocalizedReleaseNotes(Notes, "zh-HK"); + QVERIFY(traditional.contains("繁體更新內容")); + QVERIFY(traditional.contains("Shared validation")); + QVERIFY(!traditional.contains("English update")); + QVERIFY(!traditional.contains("简体更新内容")); + QVERIFY(!traditional.contains("gpm:lang")); + QVERIFY(LocalizedReleaseNotes(Notes, "zh-SG").contains("简体更新内容")); + QVERIFY(LocalizedReleaseNotes(Notes, "fr-FR").contains("English update")); + const QString english = "\nFallback\n"; + QCOMPARE(LocalizedReleaseNotes(english, "zh-TW"), "Fallback"); + QCOMPARE(LocalizedReleaseNotes("Legacy English notes", "zh-TW"), "Legacy English notes"); + QVERIFY(LocalizedReleaseNotes("\nBroken", "en").isEmpty()); + QVERIFY(LocalizedReleaseNotes(english + "\n" + english, "en").isEmpty()); + auto crlf = Notes; crlf.replace("\n", "\r\n"); + QCOMPARE(LocalizedReleaseNotes(crlf, "zh-TW").replace("\r\n", "\n"), traditional); + } + void extractsRealChangelogLanguageBlocks() { + QFile file(QStringLiteral(GPM_SOURCE_DIR) + "/CHANGELOG.md"); QVERIFY(file.open(QIODevice::ReadOnly)); + const auto text = QString::fromUtf8(file.readAll()); + const QRegularExpression section( + "^## \\[" + QRegularExpression::escape(QStringLiteral(APP_VERSION)) + "\\][^\\r\\n]*\\r?\\n(.*?)(?=^## \\[|\\z)", + QRegularExpression::MultilineOption | QRegularExpression::DotMatchesEverythingOption); + const auto match = section.match(text); + QVERIFY2(match.hasMatch(), "The application version must have release notes."); + const auto current = match.captured(1); + for (const auto& language : {"en", "zh-TW", "zh-CN"}) { + const auto notes = LocalizedReleaseNotes(current, language); + QVERIFY(!notes.isEmpty()); QVERIFY(notes.contains("Validation")); QVERIFY(!notes.contains("gpm:lang")); + } + } + void parsesOnlyTheInstallerChecksum() { + QCOMPARE(InstallerChecksum(Checksum()), QCryptographicHash::hash(Installer, QCryptographicHash::Sha256).toHex()); + QCOMPARE(InstallerChecksum(Checksum().toUpper().replace("GOPIEMENU_SETUP.EXE", "GoPieMenu_Setup.exe")), InstallerChecksum(Checksum())); + QVERIFY(InstallerChecksum("bad GoPieMenu_Setup.exe").isEmpty()); + QVERIFY(InstallerChecksum(Checksum() + Checksum()).isEmpty()); + QVERIFY(InstallerChecksum(Checksum().replace("GoPieMenu_Setup.exe", "../GoPieMenu_Setup.exe")).isEmpty()); + } + void checksOncePerRequestAndRemembersSkippedVersion() { + QTemporaryDir dir; Network network; network.responses = {{Release()}, {Release()}, {Release("v2.2.0")}}; + auto options = Options(dir.path()); + UpdateManager first(options, &network); + QSignalSpy offered(&first, &UpdateManager::updateAvailable); + first.check(); first.check(); + QTRY_COMPARE(offered.count(), 1); QCOMPARE(network.requests.size(), 1); + QVERIFY(first.available()); QVERIFY(first.skipVersion()); QVERIFY(!first.available()); + UpdateManager second(options, &network); + QSignalSpy next(&second, &UpdateManager::updateAvailable); + second.check(); QTRY_VERIFY(!second.checking()); QCOMPARE(next.count(), 0); + second.check(); QTRY_COMPARE(next.count(), 1); QCOMPARE(second.version(), "2.2.0"); + } + void manualCheckCanRevisitASkippedRelease() { + QTemporaryDir dir; Network network; network.responses = {{Release()}, {Release()}}; + UpdateManager manager(Options(dir.path()), &network); + manager.check(); QTRY_VERIFY(manager.available()); QVERIFY(manager.skipVersion()); + manager.check(true); QTRY_VERIFY(manager.available()); + } + void downloadsVerifiesThenRequiresExplicitInstall() { + QTemporaryDir dir; Network network; network.responses = {{Release()}, {Checksum()}, {Installer}}; + auto options = Options(dir.path(), "zh-TW"); int launches = 0; + options.launchInstaller = [&](const QString& path, QString*) { ++launches; return QFile::exists(path); }; + UpdateManager manager(options, &network); + manager.check(); QTRY_VERIFY(manager.available()); QVERIFY(manager.notes().contains("繁體")); + QVERIFY(!manager.launchInstaller()); QCOMPARE(launches, 0); + manager.download(); QTRY_VERIFY(manager.ready()); QCOMPARE(manager.progress(), 1.0); + QVERIFY(!manager.downloading()); QCOMPARE(launches, 0); + QFile file(manager.installerPath()); QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), Installer); file.close(); + QVERIFY(manager.launchInstaller()); QCOMPARE(launches, 1); + QCOMPARE(network.requests.size(), 3); + QCOMPARE(network.requests[0].url().host(), "api.github.com"); + QCOMPARE(network.requests[1].url().fileName(), "SHA256SUMS.txt"); + QCOMPARE(network.requests[2].url().fileName(), "GoPieMenu_Setup.exe"); + } + void rejectsCorruptedDownloadAndAllowsRetry() { + QTemporaryDir dir; Network network; network.responses = {{Release()}, {Checksum("wrong")}, {Installer}, {Checksum()}, {Installer}}; + UpdateManager manager(Options(dir.path()), &network); + manager.check(); QTRY_VERIFY(manager.available()); manager.download(); + QTRY_VERIFY(!manager.downloading()); QVERIFY(!manager.ready()); QVERIFY(!manager.error().isEmpty()); + QVERIFY(!QFile::exists(manager.installerPath())); QVERIFY(!manager.launchInstaller()); + manager.download(); QTRY_VERIFY(manager.ready()); QVERIFY(manager.error().isEmpty()); + } + void rechecksTheFileBeforeLaunchingAndKeepsAppOnUacCancel() { + QTemporaryDir dir; Network network; network.responses = {{Release()}, {Checksum()}, {Installer}}; + auto options = Options(dir.path()); int launches = 0; + options.launchInstaller = [&](const QString&, QString* error) { ++launches; *error = "UAC cancelled"; return false; }; + UpdateManager manager(options, &network); + manager.check(); QTRY_VERIFY(manager.available()); manager.download(); QTRY_VERIFY(manager.ready()); + QVERIFY(!manager.launchInstaller()); QVERIFY(manager.ready()); QCOMPARE(launches, 1); + QFile file(manager.installerPath()); QVERIFY(file.open(QIODevice::WriteOnly)); file.write("changed"); file.close(); + QVERIFY(!manager.launchInstaller()); QVERIFY(!manager.ready()); QCOMPARE(launches, 1); + } + void handlesNetworkFailuresAndTimeoutsWithoutOfferingAnUpdate() { + QTemporaryDir dir; Network network; + network.responses = {{{}, 404, QNetworkReply::ContentNotFoundError}, {{}, 403, QNetworkReply::ContentAccessDenied}, + {"invalid JSON"}, {{}, 200, QNetworkReply::NoError, -1}}; + auto options = Options(dir.path()); options.requestTimeoutMs = 30; + UpdateManager manager(options, &network); QSignalSpy offered(&manager, &UpdateManager::updateAvailable); + for (int attempt = 0; attempt < 4; ++attempt) { + manager.check(); QTRY_VERIFY(!manager.checking()); QVERIFY(!manager.available()); + if (attempt == 0) QVERIFY(manager.error().isEmpty()); else QVERIFY(!manager.error().isEmpty()); + } + QCOMPARE(offered.count(), 0); + } + void cancelsDownloadsAndRejectsMissingOrOversizedFiles() { + QTemporaryDir dir; Network network; network.responses = {{Release()}, {Checksum(), 200, QNetworkReply::NoError, -1}, + {"invalid manifest"}, {Checksum()}, {Installer + "extra"}}; + UpdateManager manager(Options(dir.path()), &network); + manager.check(); QTRY_VERIFY(manager.available()); manager.download(); QVERIFY(manager.downloading()); + manager.cancelDownload(); QVERIFY(!manager.downloading()); QVERIFY(!manager.ready()); + manager.download(); QTRY_VERIFY(!manager.downloading()); QVERIFY(!manager.ready()); + manager.download(); QTRY_VERIFY(!manager.downloading()); QVERIFY(!manager.ready()); + QVERIFY(!QFile::exists(manager.installerPath())); + } + void missingWindowsAssetsStillShowsNotes() { + auto json = QJsonDocument::fromJson(Release()).object(); json["assets"] = QJsonArray(); + QTemporaryDir dir; Network network; network.responses = {{QJsonDocument(json).toJson()}}; + UpdateManager manager(Options(dir.path()), &network); + manager.check(); QTRY_VERIFY(manager.available()); QVERIFY(!manager.canDownload()); + manager.download(); QCOMPARE(network.requests.size(), 1); QVERIFY(!manager.downloading()); + } +}; +QTEST_GUILESS_MAIN(UpdateTests) +#include "UpdateTests.moc" diff --git a/video/.gitignore b/video/.gitignore new file mode 100644 index 0000000..b62197b --- /dev/null +++ b/video/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +.DS_Store +.env + +# Ignore the output video from Git but not videos you import into src/. +out diff --git a/video/.prettierrc b/video/.prettierrc new file mode 100644 index 0000000..37d5071 --- /dev/null +++ b/video/.prettierrc @@ -0,0 +1,5 @@ +{ + "useTabs": false, + "bracketSpacing": true, + "tabWidth": 2 +} diff --git a/video/README.md b/video/README.md new file mode 100644 index 0000000..5e3b671 --- /dev/null +++ b/video/README.md @@ -0,0 +1,62 @@ +# GoPieMenu 功能与设置导览 + +简体中文视频草稿,1920 × 1080 / 30 fps / 54 秒。主合成为 `GoPieMenu-Bilibili`。全片统一采用白色、浅灰、蓝色强调和深灰文字,对齐软件界面。Slate、Ocean、Obsidian 的深色仅出现在真实菜单预设中。 + +每个镜头只保留一个短标题与一个操作焦点,不叠加段落说明、步骤编号或重复标语。主要设置段以 React / SVG 重建为简化操作示意:下拉展开、增加扇形、录入按键、拖动让位、输入应用名、开关切换、Apply 完成反馈。复合主题按时间拆成单独镜头。风格段采用源码预设色值连续插值,直接改变矢量菜单外观。详细说明保留在旁白稿,不铺满画面。 + +短字采用约 0.2 秒整体入场,标题位置跟随主角或操作控件:开场左右呼应、手势按拍切字、设置提示靠近高亮、风格名与菜单并置、结尾居中。短段落 1–2 秒完成一个动作,复杂设置保留辨识时间。 + +## 启动与验证 + +在 `video` 目录运行 `npm ci`,然后 `npm run dev -- --no-open --port=3001`。预览地址为 http://localhost:3001/GoPieMenu-Bilibili 。独立于其他 Remotion 示例项目。 + +`npm run lint` 检查 ESLint 与 TypeScript;`npm run build` 验证 bundle;`npm run render` 输出 `out/gopiemenu-bilibili.mp4`(H.264)。中文版保留 Studio 草稿;英文版提供本地 MP4,未发布至平台。已加入背景配乐与操作音效;无配音与最终逐句字幕。 + +## 配乐与音效 + +`public/audio/` 为本项目原创程序合成素材,无外部采样。120 BPM 轻电子背景包含柔和键盘和弦、低音、轻打击与短旋律;pop、tick、swipe、confirm 分别对应弹出、选择、滑动及完成。运行 `npm run audio` 可用 Node 标准库重建全部 WAV,生成源码为 `scripts/generate-audio.mjs`。 + +`src/Soundtrack.tsx` 管理混音音量和相对场景的帧级音效时点。背景音量为 0.48,音效为 0.3–0.5;音乐开头淡入、末尾 1.5 秒淡出。检测到某段旁白 WAV 时,该段背景自动降低 55%,边界平滑过渡。独立场景仍用于画面编辑;声音在主合成播放。 + +## 导览顺序与配音槽 + +| 场景 / WAV 文件名 | 时间 | 介绍 | +| ----------------- | -------- | -------------------------------- | +| hook | 0–2 秒 | 产品开场 | +| gesture | 2–5 秒 | 默认按住、划过、松开 | +| overview | 5–8 秒 | 切换、新建、管理菜单 | +| actions | 8–12 秒 | Add action 与右侧动作设置 | +| arrange | 12–14 秒 | Arrange actions 拖动排序 | +| submenu | 14–20 秒 | 分类、子动作与二级菜单编辑 | +| trigger | 20–24 秒 | 触发方式、快捷键、指定应用 | +| style | 24–30 秒 | Frost → Slate → Ocean → Obsidian | +| settings | 30–33 秒 | 开机启动、导入导出、自定义图标 | +| apply | 33–35 秒 | 预览、Apply、Discard | +| workflow | 35–51 秒 | 返回绘图工作流 | +| outro | 51–54 秒 | 项目入口 | + +旁白与时长的单一来源为 `src/timeline.ts`,主合成、独立场景和配音起点自动从这里生成。组件映射在 `src/sceneRegistry.ts`。GPT-SoVITS 生成的 PCM WAV 放入 `public/voice/<场景名>.wav`,缺失旁白文件时保留配乐和音效。音频必须短于对应场景,建议留 0.5 秒余量;实际配音完成后再调整节奏和字幕。独立场景不包含旁白。 + +实际操作片段按 1 倍速完整播放 16 秒,画面放大至接近全高,标题移至侧边。 + +## 素材与真实性 + +- 主导览不再使用静态界面截图。`DemoUI.tsx` 提供可动画的控件与菜单,`Tour.tsx` 和 `Submenu.tsx` 编排操作演示。画面标注「操作示意」,不是录屏或对原生动画的承诺。 +- Frost / Slate / Ocean / Obsidian 色值来自 `StyleConfig.h` 与 `EditorSession::setStylePreset`,使用矢量插值演示变化;结构和标签为减少阅读负担做了简化。 +- 展示的选单管理、添加动作、快捷键、排序、子菜单、应用匹配、开机启动、导入导出与 Apply 均依据实际功能。所有演出仅发生在影片,不操作用户设置或文件。 +- `public/assets/preset-*.png`、`tour-*.png`、`submenu-*.png` 留作取材参考,主合成不再引用。 +- Logo 与唯一保留的实际绘图 GIF 来自本仓库 `resources`。GIF 源为 720 × 553,发布前可替换高清录屏。 +- 默认手势示意使用 Ctrl + 右键,触发设置演示使用可自定义的 F12。 +- 中文字体优先使用 Microsoft YaHei,跨系统渲染需安装对应字体或 Noto Sans CJK SC。 + +## 发布文案草稿 + +标题:给鼠标装一个快捷菜单!快速上手 GoPieMenu + +简介:从新建菜单、添加动作,到二级菜单、触发键与应用匹配,再到 Frost、Slate、Ocean、Obsidian 四种外观预设。快速认识 GoPieMenu 的简单设置界面。Windows 开源项目,下载和说明:https://github.com/RyuuMeow/GoPieMenu 。绘图软件仅为使用场景示例。 + +## English version + +`GoPieMenu-English` shares the 54-second animation, music, and full-speed drawing demo with the Chinese composition. Titles, illustrative menu labels, and captions are localized through `src/Language.tsx`. The original drawing recording retains its captured application UI. No narration is included; optional English narration belongs in `public/voice/en/.wav`, separate from Chinese audio. + +Preview: http://localhost:3001/GoPieMenu-English . Run `npm run render:en` to export `out/gopiemenu-english.mp4` (1080p, 30 fps, H.264). diff --git a/video/eslint.config.mjs b/video/eslint.config.mjs new file mode 100644 index 0000000..13b44a0 --- /dev/null +++ b/video/eslint.config.mjs @@ -0,0 +1,3 @@ +import { config } from "@remotion/eslint-config-flat"; + +export default config; diff --git a/video/package-lock.json b/video/package-lock.json new file mode 100644 index 0000000..52e2db8 --- /dev/null +++ b/video/package-lock.json @@ -0,0 +1,5281 @@ +{ + "name": "gopiemenu-promo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gopiemenu-promo", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "@remotion/cli": "4.0.534", + "react": "19.2.3", + "react-dom": "19.2.3", + "remotion": "4.0.534" + }, + "devDependencies": { + "@remotion/eslint-config-flat": "4.0.534", + "@types/react": "19.2.7", + "@types/web": "0.0.166", + "eslint": "9.39.5", + "prettier": "3.8.1", + "typescript": "5.9.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "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/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "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==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "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==", + "license": "MIT", + "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/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediabunny/aac-encoder": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.56.1.tgz", + "integrity": "sha512-a+OY/BctP7zC4G/prHikooOVaoWXMD4+bUyEwHg2dtQiQdIwC6TDJdVYJu5dfrdSNOiAFkv1HKcYEeROdwMezg==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@mediabunny/flac-encoder": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.56.1.tgz", + "integrity": "sha512-zbmorMVlF1izwtlz5mWnu7A1rJLM+V26I0pEFr9Td1EDTwE5uAGRSW+/UZlnjwj1t00Woe3Zonfh9kb7lRRBqg==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@mediabunny/mp3-encoder": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.56.1.tgz", + "integrity": "sha512-p66ovEq6mZt3ejje45NOju84hIJI4v8rHfsmc1D2J34ziV0USE8AdJBzmiwqxl6nZkIAQLNB19HzbVRcQfc51g==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@module-federation/error-codes": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", + "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", + "license": "MIT" + }, + "node_modules/@module-federation/runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", + "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/runtime-core": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", + "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-tools": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", + "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/webpack-bundler-runtime": "0.22.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", + "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", + "license": "MIT" + }, + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", + "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remotion/bundler": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.534.tgz", + "integrity": "sha512-8PdxU94g8Y0RErUYzQFzEbPBwBU6x1QwVRCL+vqr/sW+LrEZvWrqpMxp6QPkRUpIGA+8yWhYXl8R3D72xY8qEg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/media-parser": "4.0.534", + "@remotion/studio": "4.0.534", + "@remotion/studio-shared": "4.0.534", + "@remotion/timeline-utils": "4.0.534", + "@rspack/core": "1.7.11", + "@rspack/plugin-react-refresh": "1.6.1", + "css-loader": "7.1.4", + "esbuild": "0.28.1", + "react-refresh": "0.18.0", + "remotion": "4.0.534", + "style-loader": "4.0.0", + "webpack": "5.105.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/captions": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/captions/-/captions-4.0.534.tgz", + "integrity": "sha512-Auxsj2ZuvYKiljZ7g/FwWArZbwR7XvkIseDmT4UE0Vfj+9c57//hjNGp4JFn2uKIv7BeHN8zxOi0W0xUzVd2lA==", + "license": "MIT" + }, + "node_modules/@remotion/cli": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.534.tgz", + "integrity": "sha512-Eqqbg98XqisuetU4OMezj11G6Gesn9hRZrGjQoSpJNVIHfgzbR4deqVj5yAbddaDmmlsHBEFVfdOjayE2q+Yww==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@babel/parser": "7.24.1", + "@remotion/bundler": "4.0.534", + "@remotion/codemods": "4.0.534", + "@remotion/media-utils": "4.0.534", + "@remotion/player": "4.0.534", + "@remotion/renderer": "4.0.534", + "@remotion/studio": "4.0.534", + "@remotion/studio-server": "4.0.534", + "@remotion/studio-shared": "4.0.534", + "dotenv": "17.3.1", + "mediabunny": "1.56.1", + "minimist": "1.2.6", + "prompts": "2.4.2", + "remotion": "4.0.534", + "semver": "7.5.3" + }, + "bin": { + "remotion": "remotion-cli.js", + "remotionb": "remotionb-cli.js", + "remotiond": "remotiond-cli.js" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/codemods": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/codemods/-/codemods-4.0.534.tgz", + "integrity": "sha512-TDu9NTIQZafUF+qAQcLlIuud4fvghiiQGtvcFJ5yk3s8elBPamEiP8nwhbApsSL5c8Pz6ztGIJcfKcPR9Q5gWg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@babel/types": "7.24.0", + "@remotion/studio-protocol": "4.0.534", + "@remotion/studio-shared": "4.0.534", + "ast-types": "0.16.1", + "recast": "0.23.21", + "remotion": "4.0.534" + } + }, + "node_modules/@remotion/compositor-darwin-arm64": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.534.tgz", + "integrity": "sha512-q/FW5u8KaA/VIks8kPpOByIuJGaxH6myHG3Xyx5qAfxhiBjbimp2WEYmF09GivBdHp3CT/f5RrRLT+8Jrpi1ow==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.534.tgz", + "integrity": "sha512-yz5FtNA2k7Q1i8DBkHAMoBFkH+mCfwdYTxIQYROyZ640vDlZoXTqcDjKq0NLO1y/dGDpjpVWbYHjH1u4D+Dqeg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.534.tgz", + "integrity": "sha512-7yeVbpaSdgymw1Fs4xm08ZOI55oL2Uq0WyJrqYIhQIbg6d7Ecd6ePNbPyaeQTIa9Dq5Rxay+qbF/DV3pqOb68w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.534.tgz", + "integrity": "sha512-rJd/ZPEvQaLgh6ULy9svPwQ9QRNW2jX03PkX8heoIC1JUh196XupQ9QMRTMOFNz/Fu7yWgyVbXxQVkRtJb1EvA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.534.tgz", + "integrity": "sha512-o7vW9vld+PmbjazKJl66sYdJZxCxW/adQd3zozV355Vb5NVqZmV8BHvSuZ1Xv210r0Vj0qtEv50ig2x0nAPmLA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.534.tgz", + "integrity": "sha512-6uMmkP143LF/AwjdU997xdwg7o7NCdb2eEtwt+/1qYK8/HTkd6KupqLFLBevuIQVyJ06iCwmUhZSxNL9tbWHoQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.534.tgz", + "integrity": "sha512-+hp7c5ZAnomRe4U+s9tG36kmjMo0V43xV7WG353QCCHLRjKiR5KdkrU1TJQOAq53oyLdsD7BHUkEEWIBSSzSLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@remotion/elevenlabs": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/elevenlabs/-/elevenlabs-4.0.534.tgz", + "integrity": "sha512-TPd1XFSUfausKxeIljPqWOslJpeYARAeZFT2TSmvrmQBpz3i8yoG96gk3qXkqj7ebzQAqzv75bzd8jegwRU1Gw==", + "license": "MIT", + "dependencies": { + "@remotion/captions": "4.0.534", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/eslint-config-flat": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/eslint-config-flat/-/eslint-config-flat-4.0.534.tgz", + "integrity": "sha512-ERw/E9+gLk7+PYMaGP1yIPJ1I8jV9ZSJCXni3Ms271AZ/AjC0Bppx9ozhztItJtZF+91WmEdstB3wZAIEA8iBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "typescript-eslint": "8.21.0" + }, + "peerDependencies": { + "eslint": ">=9" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", + "integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/type-utils": "8.21.0", + "@typescript-eslint/utils": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/@typescript-eslint/parser": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz", + "integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/@typescript-eslint/type-utils": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz", + "integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/utils": "8.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", + "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/@typescript-eslint/utils": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz", + "integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@remotion/eslint-config-flat/node_modules/typescript-eslint": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.21.0.tgz", + "integrity": "sha512-txEKYY4XMKwPXxNkN8+AxAdX6iIJAPiJbHE/FpQccs/sxw8Lf26kqwC3cn0xkHlW8kEbLhkhCsjWuMveaY9Rxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.21.0", + "@typescript-eslint/parser": "8.21.0", + "@typescript-eslint/utils": "8.21.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@remotion/licensing": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.534.tgz", + "integrity": "sha512-pNP8OfY+vbDbjNBLYGoxGyE0SsReRy85G/QTF17mQC86dt2+pzsYlOkG2mq6R4tkt27xaQv8O5EFiJfYYfVkkw==", + "license": "MIT" + }, + "node_modules/@remotion/media": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/media/-/media-4.0.534.tgz", + "integrity": "sha512-xdipJG5ApdwIeu5YS1+iYzUxKybFwsKIjYKPtM5ywBUZiAiaBwK8nMiGRU73xchiLygBlxCpnPQ33a7L3+btMg==", + "dependencies": { + "mediabunny": "1.56.1", + "remotion": "4.0.534", + "zod": "4.5.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/media-parser": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.534.tgz", + "integrity": "sha512-FE/Ww9wBOrxiMCqGwHWbPXZBxs27WzDCDhOxkYyxnOSzSePzYseDrDdwuqCGApr1cDxiwkAm91EcqvP5acPziQ==", + "license": "Remotion License https://remotion.dev/license" + }, + "node_modules/@remotion/media-utils": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.534.tgz", + "integrity": "sha512-ANj7kfYvO/IL4kGxfG4Cv14J45jgpoKHQ0zrYEZoGdiaiLeM/Hj+L3WyNqTySyVS3n6RzU+aqBMDiijOPpOMtQ==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.56.1", + "remotion": "4.0.534" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/openai-whisper": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/openai-whisper/-/openai-whisper-4.0.534.tgz", + "integrity": "sha512-YKUkbL7PXTtlIwiBEDD1k/3cqMJXTKh9T8jRvIr/q6Bjbv26Ed0FfAd6ZY3kKX/3x4ozlHF7OTOG/3HMBgKZeA==", + "license": "MIT", + "dependencies": { + "@remotion/captions": "4.0.534", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/paths": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/paths/-/paths-4.0.534.tgz", + "integrity": "sha512-bc/kgZrpadDK95zc/qZ0x7RFN5R2p6/NceIKhLY8jhCLx6SL3BxAprBaNAFmOPX4JQ42uZp1qMAdzw7JI0GCPQ==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.534" + } + }, + "node_modules/@remotion/player": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.534.tgz", + "integrity": "sha512-uXkg9qsuYP85eykXTayr8jjSzFJYwtPzYprMfjxnAx9u5C/H726wTE800wGUcDPCDn1SPf6TAg71pmo882Z+jA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "remotion": "4.0.534" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.534.tgz", + "integrity": "sha512-CLZiNRibavsEpDWSm1WsD//xLGCUA/OwhFektjiAbRNkX75Ee3bOV4cjpyihaEd0HaiUv3bUT0QLnvdwynor/w==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/licensing": "4.0.534", + "@remotion/streaming": "4.0.534", + "execa": "5.1.1", + "remotion": "4.0.534", + "source-map": "0.8.0", + "ws": "8.21.0" + }, + "optionalDependencies": { + "@remotion/compositor-darwin-arm64": "4.0.534", + "@remotion/compositor-darwin-x64": "4.0.534", + "@remotion/compositor-linux-arm64-gnu": "4.0.534", + "@remotion/compositor-linux-arm64-musl": "4.0.534", + "@remotion/compositor-linux-x64-gnu": "4.0.534", + "@remotion/compositor-linux-x64-musl": "4.0.534", + "@remotion/compositor-win32-x64-msvc": "4.0.534" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/sdk": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/sdk/-/sdk-4.0.534.tgz", + "integrity": "sha512-XMh394qeaEsbo6Q2HCaxx4uYBauNutMiQiFwBvMnIzjRr7j+SRKSv8x4d7RWlAWDTdlEeithNgwSJxm0lXgwVQ==", + "license": "Remotion License", + "dependencies": { + "@remotion/player": "4.0.534", + "@remotion/studio-shared": "4.0.534", + "remotion": "4.0.534" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/streaming": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.534.tgz", + "integrity": "sha512-GE7wbEbQH15Ul4wCJYv81bjpbVIBKTxkcfCJgtYV8kiydG8v0OduYRyeUk1NTAAm80MK1oGjPOqabwRMxFGOfg==", + "license": "MIT" + }, + "node_modules/@remotion/studio": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.534.tgz", + "integrity": "sha512-sWteZLvzTMCAvpqVcXvZ7PF9gbadK7KL8RsTFf+diyHs4g1lB2Enb4vuyd7qkdFOBctE0BG4K/Srcd0Mej/axw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@remotion/captions": "4.0.534", + "@remotion/codemods": "4.0.534", + "@remotion/elevenlabs": "4.0.534", + "@remotion/media": "4.0.534", + "@remotion/media-utils": "4.0.534", + "@remotion/openai-whisper": "4.0.534", + "@remotion/paths": "4.0.534", + "@remotion/player": "4.0.534", + "@remotion/renderer": "4.0.534", + "@remotion/sdk": "4.0.534", + "@remotion/studio-protocol": "4.0.534", + "@remotion/studio-shared": "4.0.534", + "@remotion/timeline-utils": "4.0.534", + "@remotion/web-renderer": "4.0.534", + "@remotion/zod-types": "4.0.534", + "@tanstack/react-virtual": "3.14.9", + "mediabunny": "1.56.1", + "memfs": "3.4.3", + "open": "8.4.2", + "prismjs": "1.30.0", + "remotion": "4.0.534", + "semver": "7.5.3", + "zod": "4.5.4" + }, + "peerDependencies": { + "@remotion/video-matting": "4.0.534", + "@remotion/whisper-webgpu": "4.0.534", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@remotion/video-matting": { + "optional": true + }, + "@remotion/whisper-webgpu": { + "optional": true + } + } + }, + "node_modules/@remotion/studio-protocol": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/studio-protocol/-/studio-protocol-4.0.534.tgz", + "integrity": "sha512-bNUlqQS3iU64FXJKOfb+irlC3U6H3pAIDEWUvyqza05KYtSF26rrR93ogtOMvBbP0aDtXFwLAOjtsH0xj4KvqQ==", + "license": "Remotion License", + "dependencies": { + "zod": "4.5.4" + } + }, + "node_modules/@remotion/studio-server": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.534.tgz", + "integrity": "sha512-lEA2XaEH99HJYXrpbyO2n2SI0o1iw2Daua9eEYwBwTDBGGc5bngmaS2lkAfTO4rcW3c2aqxqNkBQEKzIXPcIeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@babel/types": "7.24.0", + "@remotion/bundler": "4.0.534", + "@remotion/codemods": "4.0.534", + "@remotion/renderer": "4.0.534", + "@remotion/studio-protocol": "4.0.534", + "@remotion/studio-shared": "4.0.534", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "kiwi-schema": "0.5.0", + "memfs": "3.4.3", + "open": "8.4.2", + "recast": "0.23.21", + "remotion": "4.0.534", + "semver": "7.5.3", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/studio-shared": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.534.tgz", + "integrity": "sha512-0lLHhyN7xJZGB/MnCIDHaSVp4/Oarf/M+xflr6z3Lh1UGBU9TD5V6IWAwn/hcMuXxQWs/3VSRvwZXPtsBPCpTQ==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.534" + } + }, + "node_modules/@remotion/timeline-utils": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.534.tgz", + "integrity": "sha512-4qx/e4LV/FurqLBZKbOte18zi9OzRDQE4aa+uuUvxAwnil7mN8Usg+suamPYogwq6pZ1+ZjuMbB7pQqqAU1KLQ==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.56.1", + "remotion": "4.0.534" + } + }, + "node_modules/@remotion/web-renderer": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.534.tgz", + "integrity": "sha512-5JlbHvU4eAA/ymvHLmHHvR+YfkTMnVcInwaNVu2kLyXGEO6HcRmqDwp2hhAGtj212/LvZKKLPLBXbfhHygelIQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@mediabunny/aac-encoder": "1.56.1", + "@mediabunny/flac-encoder": "1.56.1", + "@mediabunny/mp3-encoder": "1.56.1", + "@remotion/licensing": "4.0.534", + "mediabunny": "1.56.1", + "remotion": "4.0.534" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@remotion/zod-types": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.534.tgz", + "integrity": "sha512-WYIHdVFGrOBIX7v7v5GIJ1LCjv8oyruJD1sl06ZpDBfMzJspSTQrXH2hoT37YZcA+RUpyUomHkQrdiqxk/Jxgg==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.534" + } + }, + "node_modules/@rspack/binding": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz", + "integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==", + "license": "MIT", + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.7.11", + "@rspack/binding-darwin-x64": "1.7.11", + "@rspack/binding-linux-arm64-gnu": "1.7.11", + "@rspack/binding-linux-arm64-musl": "1.7.11", + "@rspack/binding-linux-x64-gnu": "1.7.11", + "@rspack/binding-linux-x64-musl": "1.7.11", + "@rspack/binding-wasm32-wasi": "1.7.11", + "@rspack/binding-win32-arm64-msvc": "1.7.11", + "@rspack/binding-win32-ia32-msvc": "1.7.11", + "@rspack/binding-win32-x64-msvc": "1.7.11" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.11.tgz", + "integrity": "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.11.tgz", + "integrity": "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.11.tgz", + "integrity": "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.11.tgz", + "integrity": "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.11.tgz", + "integrity": "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.11.tgz", + "integrity": "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.11.tgz", + "integrity": "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "1.0.7" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.11.tgz", + "integrity": "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.11.tgz", + "integrity": "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.11.tgz", + "integrity": "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/core": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.11.tgz", + "integrity": "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime-tools": "0.22.0", + "@rspack/binding": "1.7.11", + "@rspack/lite-tapable": "1.1.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", + "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", + "license": "MIT" + }, + "node_modules/@rspack/plugin-react-refresh": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.1.tgz", + "integrity": "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==", + "license": "MIT", + "dependencies": { + "error-stack-parser": "^2.1.4", + "html-entities": "^2.6.0" + }, + "peerDependencies": { + "react-refresh": ">=0.10.0 <1.0.0", + "webpack-hot-middleware": "2.x" + }, + "peerDependenciesMeta": { + "webpack-hot-middleware": { + "optional": true + } + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.14.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.9.tgz", + "integrity": "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.7", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.7.tgz", + "integrity": "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/dom-mediacapture-transform": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.12.tgz", + "integrity": "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/web": { + "version": "0.0.166", + "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.166.tgz", + "integrity": "sha512-qvY/TzK1WuxfeACL3Zzw+gMivGiIynRKH98nLET7ACzTRTX8CWMA6LQJ9WayIHvTBU1JeFCBRIBjsxhGz4TfHQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz", + "integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", + "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", + "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.21.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "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==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "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/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/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==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", + "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "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, + "license": "MIT", + "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/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.452", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.452.tgz", + "integrity": "sha512-cVvk1YBSj6571VmV6hdJTqj94Y79xSu8cj6Y9t6ae4Y4431z8/7EJFvu3rVp90xhfuv9BpPKUmaLfTo4+FOGBg==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "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/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "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==", + "license": "MIT", + "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/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==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "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.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "license": "Unlicense" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.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==", + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.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==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kiwi-schema": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/kiwi-schema/-/kiwi-schema-0.5.0.tgz", + "integrity": "sha512-X+FpfU0yTEtc6aTHS7VwbOpvQwRt70+pXXWRI5fd6CvWhe7pSVC854TVo4Zo0x5/wwcWj+/9KUlXpdcP0dY9AA==", + "license": "MIT", + "bin": { + "kiwic": "cli.js" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mediabunny": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.56.1.tgz", + "integrity": "sha512-DNOm0bRBmeOxJgCK/g3LxNWbxY5morTauNdlychujGmJ9L37ezVezkNoI1WtO2Xdb4WlWWJlYfIKnOEzd5phRw==", + "license": "MPL-2.0", + "workspaces": [ + ".", + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, + "node_modules/memfs": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "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==", + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "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/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "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/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "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==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remotion": { + "version": "4.0.534", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.534.tgz", + "integrity": "sha512-gSfMreFJgncWEHGsIWksicmEGsTrGMIAgZ87WS4e+bIB+nVsAN67UXWbQ88xGwUONm3xzwu9Ty786kFDcgZoaQ==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.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" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/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==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/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==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=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==", + "license": "ISC", + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.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==", + "license": "MIT", + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svg-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.1.0.tgz", + "integrity": "sha512-bwLf38YmY+TDYHJw1Ex0Co8c4yeXuJAo8YnXGZrscxrvYoVVIvLeniEkV1Ks/54VteMnL4FtyN1+P+TZJdUPmQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.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-sources": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.6.0.tgz", + "integrity": "sha512-EIMmPVvNI0CYXyRPp34F2Qk7W37/BZVZIofS6LAUnrNjCVahS+hO4mxwFJDjQjODTGZDtVzLYoN2+1dYkhk9qA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/video/package.json b/video/package.json new file mode 100644 index 0000000..b4b4f36 --- /dev/null +++ b/video/package.json @@ -0,0 +1,32 @@ +{ + "name": "gopiemenu-promo", + "version": "1.0.0", + "description": "GoPieMenu Bilibili feature showcase with GPT-SoVITS voiceover slots", + "repository": {}, + "license": "UNLICENSED", + "private": true, + "dependencies": { + "@remotion/cli": "4.0.534", + "react": "19.2.3", + "react-dom": "19.2.3", + "remotion": "4.0.534" + }, + "devDependencies": { + "@remotion/eslint-config-flat": "4.0.534", + "@types/react": "19.2.7", + "@types/web": "0.0.166", + "eslint": "9.39.5", + "prettier": "3.8.1", + "typescript": "5.9.3" + }, + "scripts": { + "audio": "node scripts/generate-audio.mjs", + "dev": "remotion studio", + "render": "remotion render src/index.ts GoPieMenu-Bilibili out/gopiemenu-bilibili.mp4 --codec=h264 --crf=18 --pixel-format=yuv420p", + "build": "remotion bundle", + "upgrade": "remotion upgrade", + "lint": "eslint src && tsc", + "format": "prettier --write src README.md package.json", + "render:en": "remotion render src/index.ts GoPieMenu-English out/gopiemenu-english.mp4 --codec=h264 --crf=18 --pixel-format=yuv420p" + } +} diff --git a/video/public/assets/colors.png b/video/public/assets/colors.png new file mode 100644 index 0000000..ce9eca6 Binary files /dev/null and b/video/public/assets/colors.png differ diff --git a/video/public/assets/drawing-demo.gif b/video/public/assets/drawing-demo.gif new file mode 100644 index 0000000..796fd74 Binary files /dev/null and b/video/public/assets/drawing-demo.gif differ diff --git a/video/public/assets/drawing.png b/video/public/assets/drawing.png new file mode 100644 index 0000000..251ebdf Binary files /dev/null and b/video/public/assets/drawing.png differ diff --git a/video/public/assets/editor.png b/video/public/assets/editor.png new file mode 100644 index 0000000..de48626 Binary files /dev/null and b/video/public/assets/editor.png differ diff --git a/video/public/assets/logo.png b/video/public/assets/logo.png new file mode 100644 index 0000000..7b5736c Binary files /dev/null and b/video/public/assets/logo.png differ diff --git a/video/public/assets/preset-frost.png b/video/public/assets/preset-frost.png new file mode 100644 index 0000000..34acbfc Binary files /dev/null and b/video/public/assets/preset-frost.png differ diff --git a/video/public/assets/preset-obsidian.png b/video/public/assets/preset-obsidian.png new file mode 100644 index 0000000..488a7a1 Binary files /dev/null and b/video/public/assets/preset-obsidian.png differ diff --git a/video/public/assets/preset-ocean.png b/video/public/assets/preset-ocean.png new file mode 100644 index 0000000..be2acc6 Binary files /dev/null and b/video/public/assets/preset-ocean.png differ diff --git a/video/public/assets/preset-slate.png b/video/public/assets/preset-slate.png new file mode 100644 index 0000000..04c05af Binary files /dev/null and b/video/public/assets/preset-slate.png differ diff --git a/video/public/assets/submenu-editor.png b/video/public/assets/submenu-editor.png new file mode 100644 index 0000000..108cb59 Binary files /dev/null and b/video/public/assets/submenu-editor.png differ diff --git a/video/public/assets/submenu-live.png b/video/public/assets/submenu-live.png new file mode 100644 index 0000000..582bc1b Binary files /dev/null and b/video/public/assets/submenu-live.png differ diff --git a/video/public/assets/submenu-parent.png b/video/public/assets/submenu-parent.png new file mode 100644 index 0000000..d4be5ef Binary files /dev/null and b/video/public/assets/submenu-parent.png differ diff --git a/video/public/assets/tour-add.png b/video/public/assets/tour-add.png new file mode 100644 index 0000000..dafbd03 Binary files /dev/null and b/video/public/assets/tour-add.png differ diff --git a/video/public/assets/tour-arrange.png b/video/public/assets/tour-arrange.png new file mode 100644 index 0000000..f52a8b8 Binary files /dev/null and b/video/public/assets/tour-arrange.png differ diff --git a/video/public/assets/tour-menus.png b/video/public/assets/tour-menus.png new file mode 100644 index 0000000..2a00203 Binary files /dev/null and b/video/public/assets/tour-menus.png differ diff --git a/video/public/assets/tour-settings.png b/video/public/assets/tour-settings.png new file mode 100644 index 0000000..f5adabc Binary files /dev/null and b/video/public/assets/tour-settings.png differ diff --git a/video/public/assets/tour-trigger.png b/video/public/assets/tour-trigger.png new file mode 100644 index 0000000..bc89b63 Binary files /dev/null and b/video/public/assets/tour-trigger.png differ diff --git a/video/public/audio/confirm.wav b/video/public/audio/confirm.wav new file mode 100644 index 0000000..df6cc5c Binary files /dev/null and b/video/public/audio/confirm.wav differ diff --git a/video/public/audio/music.wav b/video/public/audio/music.wav new file mode 100644 index 0000000..e3264c8 Binary files /dev/null and b/video/public/audio/music.wav differ diff --git a/video/public/audio/pop.wav b/video/public/audio/pop.wav new file mode 100644 index 0000000..c8c778c Binary files /dev/null and b/video/public/audio/pop.wav differ diff --git a/video/public/audio/swipe.wav b/video/public/audio/swipe.wav new file mode 100644 index 0000000..45a3627 Binary files /dev/null and b/video/public/audio/swipe.wav differ diff --git a/video/public/audio/tick.wav b/video/public/audio/tick.wav new file mode 100644 index 0000000..d2b1371 Binary files /dev/null and b/video/public/audio/tick.wav differ diff --git a/video/public/voice/.gitkeep b/video/public/voice/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/video/public/voice/.gitkeep @@ -0,0 +1 @@ + diff --git a/video/remotion.config.ts b/video/remotion.config.ts new file mode 100644 index 0000000..31d58ab --- /dev/null +++ b/video/remotion.config.ts @@ -0,0 +1,12 @@ +/** + * Note: When using the Node.JS APIs, the config file + * doesn't apply. Instead, pass options directly to the APIs. + * + * All configuration options: https://remotion.dev/docs/config + */ + +import { Config } from "@remotion/cli/config"; + +Config.setRspack(true); +Config.setVideoImageFormat("jpeg"); +Config.setOverwriteOutput(true); diff --git a/video/scripts/generate-audio.mjs b/video/scripts/generate-audio.mjs new file mode 100644 index 0000000..5a65cd9 --- /dev/null +++ b/video/scripts/generate-audio.mjs @@ -0,0 +1,48 @@ +// Original procedural score and UI sounds; no samples or third-party recordings. +// Rebuild with npm run audio. Uses only Node's standard library. +import { mkdirSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +const out = fileURLToPath(new URL("../public/audio/", import.meta.url)); +mkdirSync(out, {recursive:true}); +const sr=44100, tau=2*Math.PI; +let seed=713; +const noise=()=>{seed=(1664525*seed+1013904223)>>>0;return seed/2147483648-1;}; +const hz=n=>440*2**((n-69)/12); +const track=seconds=>[new Float32Array(Math.ceil(seconds*sr)),new Float32Array(Math.ceil(seconds*sr))]; +function add(buf,start,duration,fn,gain=1,pan=0){ + const offset=Math.round(start*sr), length=Math.floor(duration*sr); + const l=Math.sqrt((1-pan)/2),r=Math.sqrt((1+pan)/2); + for(let i=0;iMath.sin(tau*(48*x+7*(1-Math.exp(-x*24))))*Math.exp(-x*20),.24); + add(music,t+.25,.07,x=>noise()*Math.exp(-x*65),.018,beat%2?.35:-.35); + if(beat%4===2)add(music,t,.13,x=>(noise()*.5+Math.sin(tau*185*x)*.2)*Math.exp(-x*32),.065); + if(beat%2===0)add(music,t,.65,x=>Math.sin(tau*hz(chord[0]-24)*x)*Math.exp(-x*5),.19); + if(beat%4===0)chord.forEach((n,j)=>add(music,t,2.3,x=>(Math.sin(tau*hz(n)*x)+.18*Math.sin(tau*hz(n)*2*x))*Math.exp(-x*1.8),.075,(j-1.5)*.28)); + if(beat%2===0){const n=chord[(Math.floor(beat/2)+1)%4]+12; + const pluck=x=>(Math.sin(tau*hz(n)*x)+.2*Math.sin(tau*hz(n)*3*x))*Math.exp(-x*8); + add(music,t+.125,.7,pluck,.09,-.2);add(music,t+.5,.55,pluck,.024,.5); + } +} +save("music",music,.62); +const pop=track(.22);add(pop,0,.22,t=>Math.sin(tau*(620*t+32*(1-Math.exp(-t*18))))*Math.exp(-t*24),.8);save("pop",pop,.55); +const tick=track(.1);add(tick,0,.1,t=>(Math.sin(tau*1350*t)*.7+noise()*.15)*Math.exp(-t*65),.6);save("tick",tick,.5); +const swipe=track(.26);let low=0;add(swipe,0,.26,t=>{low=.88*low+.12*noise();return low*Math.sin(Math.PI*t/.26)**2;},1);save("swipe",swipe,.36); +const confirm=track(.65);[74,78,81].forEach((n,i)=>add(confirm,i*.065,.43,t=>Math.sin(tau*hz(n)*t)*Math.exp(-t*10),.28,(i-1)*.3));save("confirm",confirm,.6); diff --git a/video/src/DemoUI.tsx b/video/src/DemoUI.tsx new file mode 100644 index 0000000..99df362 --- /dev/null +++ b/video/src/DemoUI.tsx @@ -0,0 +1,176 @@ +import { Copy } from "./Language"; +import type { CSSProperties, ReactNode } from "react"; +import { interpolateColors, useCurrentFrame } from "remotion"; +import { MotionStage, snap } from "./motion"; + +export const panel: CSSProperties = { + position: "absolute", + background: "white", + border: "2px solid #dce4ef", + borderRadius: 24, + boxShadow: "0 22px 65px #33547a15", + color: "#253348", +}; +export const field: CSSProperties = { + background: "#f7f9fc", + border: "2px solid #dce4ef", + borderRadius: 14, + padding: "22px 28px", + fontSize: 36, +}; +export const DemoStage = ({ children }: { children: ReactNode }) => ( + + {children} +
+ 操作示意 +
+
+); +export const Keycap = ({ + children, + active = false, +}: { + children: ReactNode; + active?: boolean; +}) => ( + + {children} + +); + +// Colors use StyleConfig/EditorSession presets. The geometry is a simplified animated demonstration. +export const presets = [ + { + name: "Frost", + sector: "rgba(230,235,242,0.98)", + hover: "rgba(108,150,218,0.99)", + center: "#f9fafc", + text: "#252f3e", + }, + { + name: "Slate", + sector: "rgba(78,88,103,0.88)", + hover: "rgba(105,140,188,0.94)", + center: "#2d2d37", + text: "#f0f0f5", + }, + { + name: "Ocean", + sector: "rgba(40,66,99,0.88)", + hover: "rgba(60,130,210,0.94)", + center: "#2d2d37", + text: "#f0f0f5", + }, + { + name: "Obsidian", + sector: "rgba(55,55,65,0.78)", + hover: "rgba(80,120,200,0.86)", + center: "#2d2d37", + text: "#f0f0f5", + }, +]; +export const LivePie = ({ + x = 650, + y = 560, + size = 650, + preset = 0, + changeAt = 0, + active = -1, + labels = ["变形", "调色", "图层"], +}: { + x?: number; + y?: number; + size?: number; + preset?: number; + changeAt?: number; + active?: number; + labels?: string[]; +}) => { + const f = useCurrentFrame(); + const p = presets[preset], + old = presets[Math.max(0, preset - 1)]; + const progress = snap(f, changeAt, changeAt + 10, 0, 1); + const color = (key: "sector" | "hover" | "center" | "text") => + interpolateColors(progress, [0, 1], [old[key], p[key]]); + const point = (r: number, a: number) => [ + 300 + r * Math.cos((a * Math.PI) / 180), + 300 + r * Math.sin((a * Math.PI) / 180), + ]; + return ( + + {labels.map((label, i) => { + const mid = -90 + (i * 360) / labels.length, + start = mid - 180 / labels.length + 2, + end = mid + 180 / labels.length - 2; + const a = point(272, start), + b = point(272, end), + c = point(94, end), + d = point(94, start), + t = point(185, mid); + return ( + + + + {label} + + + ); + })} + + + + ); +}; diff --git a/video/src/FocalText.tsx b/video/src/FocalText.tsx new file mode 100644 index 0000000..cb5ea16 --- /dev/null +++ b/video/src/FocalText.tsx @@ -0,0 +1,51 @@ +import { useCopy, useLanguage } from "./Language"; +import { useCurrentFrame } from "remotion"; +import { snap } from "./motion"; + +// Coordinates describe the visual center, so short titles stay aligned to their subject. +export const FocalText = ({ + text, + x = 960, + y = 540, + size = 100, + delay = 0, + badge = false, +}: { + text: string; + x?: number; + y?: number; + size?: number; + delay?: number; + badge?: boolean; +}) => { + const copy = useCopy(); + const english = useLanguage() === "en"; + const title = copy(text); + const frame = useCurrentFrame() - delay; + return ( +
+ {title} +
+ ); +}; diff --git a/video/src/Language.tsx b/video/src/Language.tsx new file mode 100644 index 0000000..a4d8c33 --- /dev/null +++ b/video/src/Language.tsx @@ -0,0 +1,48 @@ +import { createContext, useContext } from "react"; + +export const LanguageContext = createContext<"zh" | "en">("zh"); +const english: Record = { + 就在手边: "Within reach", + 动效示意: "Animated demo", + 按住: "Hold", + 划过: "Swipe", + 执行: "Release", + "Ctrl + 右键 · 操作示意": "Ctrl + right-click · Animated demo", + 回到创作: "Back to creating", + "实际操作 GIF": "Live demo", + 原速播放: "Original speed", + "Windows 开源快捷菜单": "An open-source radial menu for Windows", + 项目地址见简介: "Download link in the description", + 操作示意: "Animated demo", + 选个菜单: "Pick a menu", + 添加动作: "Add an action", + 设个快捷键: "Set a shortcut", + 拖动排序: "Drag to reorder", + 指定应用: "Choose an app", + 设置触发键: "Set a trigger", + "导入 · 导出": "Import · Export", + 开机启动: "Start with Windows", + 完成: "Done", + 双击编辑: "Double-click to edit", + 松开执行: "Release to run", + 再选动作: "Pick an action", + 打开分类: "Open a submenu", + 应用: "Apps", + 网址: "Links", + 文件: "Files", + 快捷键: "Shortcuts", + 变形: "Transform", + 调色: "Color", + 图层: "Layers", + "图层 ›": "Layers ›", + 新图层: "New layer", + 切换图层显示: "Toggle visibility", + 锁定透明度: "Lock alpha", + "✓ 新图层": "✓ New layer", +}; +export const useLanguage = () => useContext(LanguageContext); +export const useCopy = () => { + const language = useLanguage(); + return (text: string) => (language === "en" ? (english[text] ?? text) : text); +}; +export const Copy = ({ children }: { children: string }) => useCopy()(children); diff --git a/video/src/Promo.tsx b/video/src/Promo.tsx new file mode 100644 index 0000000..bcf0f50 --- /dev/null +++ b/video/src/Promo.tsx @@ -0,0 +1,64 @@ +import { useLanguage } from "./Language"; +import { + AbsoluteFill, + Audio, + getStaticFiles, + Sequence, + Series, +} from "remotion"; +import { Cut } from "./motion"; +import { FPS, scenes } from "./timeline"; +import { sceneComponents } from "./sceneRegistry"; +import { Soundtrack } from "./Soundtrack"; +export const Promo = () => { + const files = getStaticFiles(); + const voiceFolder = useLanguage() === "en" ? "voice/en" : "voice"; + return ( + + + files.some( + (file) => file.name === `${voiceFolder}/${scene.id}.wav`, + ), + ) + .map((scene) => scene.id)} + /> + + {scenes.map((scene, index) => { + const Component = sceneComponents[scene.id]; + return ( + + {scene.id === "outro" ? ( + + ) : ( + + + + )} + + ); + })} + + {scenes.map((scene) => { + const voice = files.find( + (file) => file.name === `${voiceFolder}/${scene.id}.wav`, + ); + return voice ? ( + + + ) : null; + })} + + ); +}; diff --git a/video/src/Root.tsx b/video/src/Root.tsx new file mode 100644 index 0000000..9b6c873 --- /dev/null +++ b/video/src/Root.tsx @@ -0,0 +1,43 @@ +import { LanguageContext } from "./Language"; +import { Composition, Folder } from "remotion"; +import { Promo } from "./Promo"; +import { sceneComponents } from "./sceneRegistry"; +import { FPS, scenes, TOTAL_FRAMES } from "./timeline"; +const EnglishPromo = () => ( + + + +); +export const RemotionRoot = () => ( + <> + + + + {scenes.map((scene) => ( + + ))} + + +); diff --git a/video/src/Soundtrack.tsx b/video/src/Soundtrack.tsx new file mode 100644 index 0000000..b1ab6f0 --- /dev/null +++ b/video/src/Soundtrack.tsx @@ -0,0 +1,96 @@ +import { Audio, interpolate, Sequence, staticFile } from "remotion"; +import { FPS, scenes, TOTAL_FRAMES, type SceneId } from "./timeline"; + +type Sound = "pop" | "tick" | "swipe" | "confirm"; +// Frame offsets follow the visual events within each scene, independent of scene starts. +const cues: Partial>> = { + hook: [[4, "pop"]], + gesture: [ + [0, "tick"], + [30, "swipe"], + [60, "confirm"], + ], + overview: [ + [9, "pop"], + [40, "tick"], + ], + actions: [ + [8, "pop"], + [27, "pop"], + [67, "tick"], + ], + arrange: [[10, "swipe"]], + submenu: [ + [5, "pop"], + [32, "pop"], + [67, "tick"], + [87, "confirm"], + [107, "swipe"], + ], + trigger: [ + [9, "tick"], + [69, "tick"], + ], + style: [ + [4, "pop"], + [49, "pop"], + [94, "pop"], + [139, "pop"], + ], + settings: [ + [8, "tick"], + [53, "tick"], + ], + apply: [[15, "confirm"]], + workflow: [[3, "swipe"]], + outro: [[8, "confirm"]], +}; +const lengths: Record = { + pop: 7, + tick: 3, + swipe: 8, + confirm: 20, +}; +export const Soundtrack = ({ voiceIds }: { voiceIds: string[] }) => ( + <> +