diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8eb50b6eb..4c10092201 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,6 @@ on: ref: required: false type: string - default: ${{ github.ref }} mobile: required: false type: string @@ -39,24 +38,27 @@ on: jobs: build: runs-on: ${{ inputs.platform }} + permissions: + contents: write steps: - name: Checkout repository uses: actions/checkout@v4 with: + fetch-depth: 0 repository: ${{ inputs.repository }} ref: ${{ inputs.ref }} - fetch-depth: 0 - name: Install Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: rustflags: '' + target: ${{ inputs.target }} - name: Rust Cache uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - key: ${{ inputs.mobile == '' && inputs.platform || format('{0}-{1}', inputs.platform, inputs.mobile) }} + key: ${{ inputs.mobile != '' && format('{0}-{1}', inputs.platform, inputs.mobile) || inputs.platform }}${{ inputs.target != '' && format('-{0}', inputs.target) || '' }} - name: Set up Node.js uses: actions/setup-node@v4 @@ -79,6 +81,18 @@ jobs: librsvg2-dev \ patchelf + - name: Install LLVM and Clang (Windows) + if: runner.os == 'Windows' + uses: KyleMayes/install-llvm-action@v2 + with: + version: "19" + directory: ${{ runner.temp }}/llvm + + - name: Set LIBCLANG_PATH (Windows) + if: runner.os == 'Windows' + shell: bash + run: echo "LIBCLANG_PATH=${{ runner.temp }}/llvm/bin" >> $GITHUB_ENV + # ------------- Android Setup ------------- - name: Set up JDK if: inputs.mobile == 'android' @@ -100,65 +114,79 @@ jobs: link-to-sdk: true - name: Install frontend dependencies - run: npm install + run: npm ci - name: Setup Android signing if: inputs.mobile == 'android' && github.event_name != 'pull_request' run: | cd src-tauri/gen/android - echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties - echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties - echo "${{ secrets.ANDROID_KEY_BASE64 }}" | base64 -d > $RUNNER_TEMP/keystore.jks - echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties - - - name: rustup install target - if: ${{ inputs.target != '' }} - run: rustup target add ${{ inputs.target }} + keytool -genkey -v \ + -keystore "$RUNNER_TEMP/rapidraw-release.jks" \ + -keyalg RSA -keysize 2048 -validity 36500 \ + -alias rapidraw \ + -storepass android123 \ + -keypass android123 \ + -dname "CN=RAW工坊, OU=Dev, O=RAW工坊, ST=China, C=CN" \ + -noprompt + echo "keyAlias=rapidraw" > keystore.properties + echo "password=android123" >> keystore.properties + echo "storeFile=$RUNNER_TEMP/rapidraw-release.jks" >> keystore.properties - id: patch-release-name shell: bash - if: ${{ inputs.release-id != '' }} run: | platform="${{ inputs.platform }}" if [[ "${{ inputs.mobile }}" == "android" ]]; then replacement="android" else - replacement="$(echo ${platform} | sed -E 's/-latest//')" + replacement="$(echo "${platform}" | sed -E 's/-latest//' | sed -E 's/windows-11-arm/windows-arm64/' | sed -E 's/windows$/windows-x64/')" + fi + if [[ -n "${{ inputs.asset-name-pattern }}" ]]; then + patched_platform=$(echo '${{ inputs.asset-name-pattern }}' | sed -E "s/\[platform\]/${replacement}/") + else + patched_platform="${replacement}" fi - patched_platform=$(echo '${{ inputs.asset-name-pattern }}' | sed -E "s/\[platform\]/${replacement}/") if [[ -n "${{ inputs.asset-prefix }}" ]]; then patched_platform="${{ inputs.asset-prefix }}_${patched_platform}" fi - echo "platform=${patched_platform}" >> $GITHUB_OUTPUT + echo "platform=${patched_platform}" >> "$GITHUB_OUTPUT" - id: tauri-build name: Build with tauri-action - if: ${{ inputs.mobile == '' }} - # FIXME set this back to a release major version, ex. v0 . This is a commit from the `dev` branch which includes PR - # https://github.com/tauri-apps/tauri-action/pull/1099 , adding the assetNamePattern feature. - uses: tauri-apps/tauri-action@cf3eb9b18add8548a40584695215c80ab7274f31 + if: inputs.mobile == '' + uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NO_STRIP: ${{ startsWith(inputs.platform, 'ubuntu') }} with: - args: --verbose ${{ inputs.build-args }} ${{ inputs.target != '' && '--target' || '' }} ${{ inputs.target }} + args: --verbose${{ inputs.build-args && format(' {0}', inputs.build-args) || '' }}${{ inputs.target != '' && format(' --target {0}', inputs.target) || '' }} assetNamePattern: ${{ steps.patch-release-name.outputs.platform }} releaseId: ${{ inputs.release-id }} retryAttempts: 3 - name: Build Android - if: ${{ inputs.mobile == 'android' }} + if: inputs.mobile == 'android' shell: bash env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} ORT_SKIP_DOWNLOAD: '1' ORT_LIB_LOCATION: ${{ format('{0}/src-tauri/libs/arm64-v8a', github.workspace) }} ORT_STRATEGY: manual run: | - npx tauri android build --verbose ${{ inputs.build-args }} + # tauri android build --target accepts short names (aarch64, armv7, i686, x86_64) + # while Rust toolchain setup needs the full triple (e.g. aarch64-linux-android) + target_arg="" + if [[ -n "${{ inputs.target }}" ]]; then + tauri_target="${{ inputs.target }}" + # Strip everything after the first hyphen to get the short name + tauri_target="${tauri_target%%-*}" + target_arg="--target $tauri_target" + fi + npx tauri android build --verbose ${target_arg}${{ inputs.build-args && format(' {0}', inputs.build-args) || '' }} - name: Prepare Android Release Assets - if: ${{ inputs.mobile == 'android' && inputs.release-id != '' }} + if: inputs.mobile == 'android' && inputs.release-id != '' shell: bash run: | set -euo pipefail @@ -182,19 +210,18 @@ jobs: done - name: Upload Android Release Assets - if: ${{ inputs.mobile == 'android' && inputs.release-id != '' }} + if: inputs.mobile == 'android' && inputs.release-id != '' shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail - tag_ref="${{ inputs.ref }}" - tag_name="${tag_ref#refs/tags/}" + tag_name="${{ github.event.release.tag_name || github.ref_name }}" gh release upload "$tag_name" "$RUNNER_TEMP"/android-release-assets/* --clobber - name: Upload Android Artifacts - if: ${{ inputs.mobile == 'android' && inputs.release-id == '' }} + if: inputs.mobile == 'android' && inputs.release-id == '' uses: actions/upload-artifact@v4 with: name: ${{ inputs.asset-prefix }}_android${{ inputs.target && format('_{0}', inputs.target) || '' }}_artifacts @@ -206,13 +233,15 @@ jobs: if-no-files-found: warn - name: Upload Windows Artifacts - if: ${{ startsWith(inputs.platform, 'windows') && inputs.release-id == '' && inputs.mobile == '' }} + if: startsWith(inputs.platform, 'windows') && inputs.release-id == '' && inputs.mobile == '' uses: actions/upload-artifact@v4 with: name: ${{ inputs.asset-prefix }}_${{ inputs.platform }}${{ inputs.target && format('_{0}', inputs.target) || '' }}_artifacts path: | - src-tauri/target/${{ inputs.target || 'release' }}/bundle/**/*.exe - src-tauri/target/${{ inputs.target || 'release' }}/release/bundle/**/*.exe + src-tauri/target/${{ inputs.target }}/release/bundle/nsis/*.exe + src-tauri/target/${{ inputs.target }}/release/bundle/nsis/*.nsis.zip + src-tauri/target/release/bundle/nsis/*.exe + src-tauri/target/release/bundle/nsis/*.nsis.zip retention-days: 7 compression-level: 6 if-no-files-found: warn diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f689dd317..59227b7e97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,36 +11,37 @@ jobs: fail-fast: false matrix: include: + # Windows x64 - platform: 'windows-latest' - builds-args: '--bundles nsis' + args: '--bundles nsis' target: '' asset-prefix: '01' + # Windows ARM64 (native build on ARM runner) - platform: 'windows-11-arm' - builds-args: '--bundles nsis' - target: 'aarch64-pc-windows-msvc' - asset-prefix: '01' + args: '--bundles nsis' + target: '' + asset-prefix: '01_arm' + # macOS Apple Silicon - platform: 'macos-14' target: aarch64-apple-darwin asset-prefix: '02' - - platform: 'macos-15-intel' + # macOS Intel (use macos-13) + - platform: 'macos-13' target: x86_64-apple-darwin - asset-prefix: '02' + asset-prefix: '02_intel' + # Ubuntu x64 - platform: 'ubuntu-22.04' target: '' asset-prefix: '03' - - platform: 'ubuntu-22.04-arm' - target: '' - asset-prefix: '03' + # Ubuntu 24.04 x64 - platform: 'ubuntu-24.04' target: '' - asset-prefix: '03' - - platform: 'ubuntu-24.04-arm' - target: '' - asset-prefix: '03' + asset-prefix: '03_24' + # Android ARM64 - platform: 'ubuntu-latest' mobile: 'android' target: 'aarch64-linux-android' - args: '--target aarch64' + args: '' asset-prefix: '04' uses: ./.github/workflows/build.yml with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dea8e99de3..a942a89efc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' - name: Install dependencies @@ -66,9 +66,14 @@ jobs: sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ + build-essential \ + curl \ + wget \ + file \ libssl-dev \ libayatana-appindicator3-dev \ - librsvg2-dev + librsvg2-dev \ + patchelf - name: Clippy working-directory: src-tauri diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 8227ea5dd0..58d1bfaef8 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -10,35 +10,29 @@ jobs: matrix: include: - platform: 'windows-latest' - builds-args: '--bundles nsis' + args: '--bundles nsis' target: '' asset-prefix: '01' - platform: 'windows-11-arm' - builds-args: '--bundles nsis' - target: 'aarch64-pc-windows-msvc' - asset-prefix: '01' + args: '--bundles nsis' + target: '' + asset-prefix: '01_arm' - platform: 'macos-14' target: aarch64-apple-darwin asset-prefix: '02' - - platform: 'macos-15-intel' + - platform: 'macos-13' target: x86_64-apple-darwin - asset-prefix: '02' + asset-prefix: '02_intel' - platform: 'ubuntu-22.04' target: '' asset-prefix: '03' - - platform: 'ubuntu-22.04-arm' - target: '' - asset-prefix: '03' - platform: 'ubuntu-24.04' target: '' - asset-prefix: '03' - - platform: 'ubuntu-24.04-arm' - target: '' - asset-prefix: '03' + asset-prefix: '03_24' - platform: 'ubuntu-latest' mobile: 'android' target: 'aarch64-linux-android' - args: '--debug --target aarch64' + args: '--debug' asset-prefix: '04' uses: ./.github/workflows/build.yml with: @@ -47,3 +41,6 @@ jobs: build-args: ${{ matrix.args }} asset-prefix: ${{ matrix.asset-prefix }} mobile: ${{ matrix.mobile }} + secrets: inherit + permissions: + contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c47901160..728c1c38a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,43 +10,48 @@ jobs: fail-fast: false matrix: include: + # Windows x64 - platform: 'windows-latest' - builds-args: '--bundles nsis' + build-args: '--bundles nsis' target: '' asset-prefix: '01' + # Windows ARM64 (native build on ARM runner) - platform: 'windows-11-arm' - builds-args: '--bundles nsis' - target: 'aarch64-pc-windows-msvc' - asset-prefix: '01' + build-args: '--bundles nsis' + target: '' + asset-prefix: '01_arm' + # macOS Apple Silicon (M1/M2/M3) - platform: 'macos-14' + build-args: '' target: aarch64-apple-darwin asset-prefix: '02' - - platform: 'macos-15-intel' + # macOS Intel (use macos-13 for Intel) + - platform: 'macos-13' + build-args: '' target: x86_64-apple-darwin - asset-prefix: '02' + asset-prefix: '02_intel' + # Ubuntu x64 - platform: 'ubuntu-22.04' + build-args: '' target: '' asset-prefix: '03' - - platform: 'ubuntu-22.04-arm' - target: '' - asset-prefix: '03' + # Ubuntu 24.04 x64 - platform: 'ubuntu-24.04' + build-args: '' target: '' - asset-prefix: '03' - - platform: 'ubuntu-24.04-arm' - target: '' - asset-prefix: '03' + asset-prefix: '03_24' + # Android ARM64 - platform: 'ubuntu-latest' mobile: 'android' target: 'aarch64-linux-android' - args: '--target aarch64' + build-args: '' asset-prefix: '04' uses: ./.github/workflows/build.yml with: release-id: ${{ github.event.release.id }} platform: ${{ matrix.platform }} target: ${{ matrix.target }} - build-args: ${{ matrix.args }} + build-args: ${{ matrix.build-args }} asset-name-pattern: '[name]_v[version]_[platform]_[arch][ext]' asset-prefix: ${{ matrix.asset-prefix }} mobile: ${{ matrix.mobile }} diff --git a/.gitignore b/.gitignore index 1236ed3aa3..54f07af58b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* -/target/ node_modules dist dist-ssr @@ -16,25 +15,10 @@ dist-ssr # Editor directories and files .vscode/* !.vscode/extensions.json -!.vscode/settings.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln -*.sw? - -# dynamic libraries downloaded by 'ort' crate at build time -src-tauri/resources/libonnxruntime.so -src-tauri/resources/libonnxruntime.dylib -src-tauri/resources/onnxruntime.dll -src-tauri/gen/* -!src-tauri/gen/android/ -# Ignore Android assets generated by Tauri -src-tauri/gen/android/app/src/main/assets/ -src-tauri/libs/ - -**/keystore.properties -**/keystore.jks -**/key.properties \ No newline at end of file +*.sw? \ No newline at end of file diff --git a/.trae/documents/prd.md b/.trae/documents/prd.md new file mode 100644 index 0000000000..d0cfceb6ce --- /dev/null +++ b/.trae/documents/prd.md @@ -0,0 +1,89 @@ +# RapidRAW 应用介绍页面 — 产品需求文档 + +## 1. 产品概述 +RapidRAW 是一款美观、非破坏性、GPU 加速的 RAW 图像编辑器,定位为 Adobe Lightroom 的现代高性能替代品。本页面旨在以哈苏橙(Hasselblad Orange)为视觉核心,打造一个极致精美的应用介绍与使用指南落地页,向摄影爱好者和专业用户传达产品的高端、专业、极致体验感。 + +- 目标用户:摄影爱好者、专业摄影师、后期修图从业者、追求高性能工具的创作者 +- 核心价值:以顶级视觉设计传递 RapidRAW 的轻量、高性能、跨平台、非破坏性编辑等产品亮点 + +## 2. 核心功能 + +### 2.1 页面模块 +1. **首屏 Hero 区域**:全屏视觉冲击,产品名称、核心标语、CTA 下载按钮,配合哈苏橙渐变光效动画 +2. **产品特性展示区**:GPU 加速、非破坏性编辑、跨平台支持、轻量 < 20MB 等核心卖点 +3. **编辑器预览区**:RapidRAW 编辑器界面展示,模拟编辑工作流 +4. **技术架构展示**:Rust + wgpu + React + Tauri 技术栈可视化 +5. **使用指南区**:快速上手步骤指引,从安装到导出的完整流程 +6. **版本更新区**:v1.8.13 更新日志展示 +7. **下载与社区区**:多平台下载入口、Discord/Instagram 社区链接、GitHub 仓库 + +### 2.2 页面详情 + +| 页面区域 | 模块名称 | 功能描述 | +|---------|---------|---------| +| Hero 首屏 | 动态标题 | 产品名 + 核心标语,文字逐字显现动画,哈苏橙光晕背景 | +| Hero 首屏 | CTA 按钮 | 下载按钮,hover 时橙色脉冲扩散效果 | +| 产品特性 | 特性卡片 | 4 张玻璃态卡片,hover 时橙色边框光效 + 3D 倾斜 | +| 编辑器预览 | 界面展示 | 编辑器截图 + 浮动标注动画,展示核心编辑功能 | +| 技术架构 | 技术标签 | Rust/wgpu/React/Tauri 技术徽章,旋转悬浮动画 | +| 使用指南 | 步骤流程 | 4 步骤时间轴,滚动触发逐步显现 | +| 版本更新 | 更新日志 | v1.8.13 修复项列表,代码风格展示 | +| 下载社区 | 下载入口 | Windows/macOS/Linux/Android 四平台下载 | +| 下载社区 | 社区链接 | Discord/Instagram/Github 链接按钮 | + +## 3. 核心流程 + +用户访问页面 → 被 Hero 区域视觉冲击吸引 → 向下滚动浏览产品特性 → 查看编辑器预览了解功能 → 阅读使用指南快速上手 → 点击下载按钮获取应用 + +```mermaid +graph TD + A["用户访问页面"] --> B["Hero 首屏视觉冲击"] + B --> C["浏览产品特性"] + C --> D["编辑器预览"] + D --> E["技术架构展示"] + E --> F["使用指南"] + F --> G["版本更新"] + G --> H["下载 / 社区"] +``` + +## 4. 用户界面设计 + +### 4.1 设计风格 +- **主色调**:哈苏橙 #CF4E24(Hasselblad 经典橙),辅以暗色系 #0A0A0A / #1A1A1A 背景 +- **辅助色**:暖白 #FAFAF5、浅橙 #FF8C42、深橙 #9E3A12 +- **按钮风格**:圆角胶囊型,哈苏橙渐变填充,hover 时光晕扩散 + 微缩放 +- **字体方案**: + - 标题:Playfair Display(优雅衬线体,摄影/艺术感) + - 正文:DM Sans(现代几何无衬线,清晰可读) + - 代码/技术:JetBrains Mono(技术感等宽字体) +- **布局风格**:深色全屏沉浸式,大留白 + 不对称构图,滚动叙事式 +- **动画效果**: + - 页面加载:文字逐字显现 + 哈苏橙光晕从中心扩散 + - 滚动触发:IntersectionObserver 驱动,元素从下方滑入 + 淡入 + - 特性卡片:hover 时 3D 透视倾斜 + 橙色边框光效 + - 背景层:缓慢流动的橙黑渐变 mesh + 细腻噪点纹理 + - 微交互:按钮 hover 脉冲、链接下划线动画、滚动进度条 + +### 4.2 页面设计概览 + +| 页面区域 | 模块名称 | UI 元素 | +|---------|---------|---------| +| Hero 首屏 | 动态标题 | Playfair Display 72px,文字 clip 橙色渐变,逐字淡入 | +| Hero 首屏 | 光晕背景 | 径向渐变橙黑 mesh,缓慢脉动动画 | +| Hero 首屏 | CTA 按钮 | 胶囊型,bg-gradient 橙色,hover scale + glow | +| 产品特性 | 特性卡片 | glassmorphism 卡片,backdrop-blur,橙色边框光效 | +| 编辑器预览 | 界面展示 | 深色圆角容器,内嵌截图,浮动标注动画 | +| 使用指南 | 步骤时间轴 | 垂直时间轴,橙色节点,滚动逐步显现 | +| 下载社区 | 平台按钮 | 方形图标 + 平台名,hover 橙色背景渐显 | + +### 4.3 响应式设计 +- 桌面优先(1440px 基准) +- 平板适配(768px):卡片改为双列,时间轴左侧对齐 +- 移动端适配(375px):单列堆叠,字号缩小,触摸优化按钮尺寸 + +### 4.4 动效设计要点 +- 首屏:1.5s 文字逐字淡入 + 光晕从中心扩散 +- 特性卡片:hover 时 transform: perspective(1000px) rotateX/Y,box-shadow 橙色光晕 +- 滚动:全局 IntersectionObserver,元素 translateY(40px) → translateY(0) + opacity 0→1 +- 背景:CSS @keyframes 缓慢色相偏移,mesh gradient 流动 +- 进度条:顶部固定 2px 橙色线条,随滚动位置变化 diff --git a/.trae/documents/tech-architecture.md b/.trae/documents/tech-architecture.md new file mode 100644 index 0000000000..838c6e92f3 --- /dev/null +++ b/.trae/documents/tech-architecture.md @@ -0,0 +1,100 @@ +# RapidRAW 应用介绍页面 — 技术架构文档 + +## 1. 架构设计 + +```mermaid +graph TB + subgraph "前端层" + A["React 18 + TypeScript"] + B["Tailwind CSS 3"] + C["Vite 构建工具"] + end + subgraph "动画层" + D["CSS Animations / Keyframes"] + E["IntersectionObserver API"] + F["CSS Transform 3D"] + end + subgraph "资源层" + G["Google Fonts CDN"] + H["GitHub Assets 图片"] + I["Trae Text-to-Image API"] + end + A --> B + A --> C + A --> D + A --> E + A --> F + A --> G + A --> H + A --> I +``` + +## 2. 技术说明 +- **前端**:React@18 + TypeScript + Tailwind CSS@3 + Vite +- **初始化工具**:vite-init (react-ts 模板) +- **后端**:无(纯前端静态页面) +- **数据库**:无 +- **动画方案**:CSS Keyframes + IntersectionObserver,不引入额外动画库 +- **字体**:Google Fonts CDN (Playfair Display, DM Sans, JetBrains Mono) +- **图片**:GitHub 仓库 Assets + Trae Text-to-Image API 生成装饰图 + +## 3. 路由定义 + +| 路由 | 用途 | +|------|------| +| / | 单页应用,所有内容通过滚动叙事呈现 | + +## 4. 组件结构 + +``` +src/ +├── components/ +│ ├── Hero.tsx # 首屏 Hero 区域 +│ ├── Features.tsx # 产品特性卡片 +│ ├── EditorPreview.tsx # 编辑器预览展示 +│ ├── TechStack.tsx # 技术架构展示 +│ ├── UserGuide.tsx # 使用指南步骤 +│ ├── Changelog.tsx # 版本更新日志 +│ ├── Download.tsx # 下载与社区 +│ ├── ScrollProgress.tsx # 滚动进度条 +│ └── NoiseOverlay.tsx # 噪点纹理覆盖层 +├── pages/ +│ └── Home.tsx # 主页面组合所有组件 +├── App.tsx +└── main.tsx +``` + +## 5. CSS 设计令牌 + +```css +:root { + /* 哈苏橙色彩系统 */ + --hasselblad-orange: #CF4E24; + --hasselblad-light: #FF8C42; + --hasselblad-dark: #9E3A12; + --hasselblad-glow: rgba(207, 78, 36, 0.4); + + /* 中性色系 */ + --bg-primary: #0A0A0A; + --bg-secondary: #1A1A1A; + --bg-card: rgba(26, 26, 26, 0.6); + --text-primary: #FAFAF5; + --text-secondary: #A0A0A0; + + /* 字体 */ + --font-display: 'Playfair Display', serif; + --font-body: 'DM Sans', sans-serif; + --font-mono: 'JetBrains Mono', monospace; +} +``` + +## 6. 动画规范 + +| 动画类型 | 实现方式 | 参数 | +|---------|---------|------| +| 文字逐字显现 | CSS @keyframes + animation-delay | opacity 0→1, translateY 20px→0, delay: 0.08s/字 | +| 光晕脉动 | CSS @keyframes | radial-gradient scale 1→1.2, opacity 0.6→0.3 | +| 滚动触发 | IntersectionObserver | threshold: 0.15, translateY 40px→0, opacity 0→1, duration 0.8s | +| 卡片 3D 倾斜 | onMouseMove + CSS transform | perspective 1000px, rotateX/Y ±5deg | +| 滚动进度条 | scroll event | width: scrollY / (docHeight - winHeight) * 100% | +| 噪点纹理 | CSS background-image | SVG noise filter, opacity 0.03 | diff --git a/README.md b/README.md index 7581c6278f..7515059c25 100644 --- a/README.md +++ b/README.md @@ -1,629 +1,57 @@ -

- RapidRAW Editor -

- -
- -[![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white)](https://www.rust-lang.org/) -[![wgpu](https://img.shields.io/badge/wgpu-%23282C34.svg?style=for-the-badge&logo=webgpu&logoColor=white)](https://wgpu.rs/) -[![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)](https://react.dev/) -[![Tauri](https://img.shields.io/badge/Tauri-24C8DB?style=for-the-badge&logo=tauri&logoColor=white)](https://tauri.app/) -[![AGPL-3.0](https://img.shields.io/badge/License-AGPL_v3-blue.svg?style=for-the-badge)](https://opensource.org/licenses/AGPL-3.0) -[![GitHub stars](https://img.shields.io/github/stars/CyberTimon/RapidRAW?style=for-the-badge&logo=github&label=Stars)](https://github.com/CyberTimon/RapidRAW/stargazers) -
-[![www.getrapidraw.com](https://img.shields.io/badge/getrapidraw.com-%232ea44f?style=for-the-badge&logo=safari&logoColor=white)](https://www.getrapidraw.com) -[![Instagram](https://img.shields.io/badge/Instagram-%23E4405F.svg?style=for-the-badge&logo=Instagram&logoColor=white)](https://www.instagram.com/getrapidraw/) -[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/cvFugZ2Hw8) - -
- -# RapidRAW - -> A beautiful, non-destructive, and GPU-accelerated RAW image editor built with performance in mind. - -RapidRAW is a modern, high-performance alternative to Adobe Lightroom®. It delivers a simple, beautiful editing experience in a lightweight package (under 20MB) for Windows, macOS, Linux, and Android. - -I started developing this project as a personal challenge when I was 18. My goal was to create a high-performance tool for my own photography workflow while deepening my understanding of React, WGSL and Rust. - - - - - - -
-
- - Download RapidRAW - -

Download RapidRAW

-

Get the latest release for Windows, macOS, Linux, and Android. Packaged and ready to run.

- Download Latest Version → -

-
-
- - Read the Docs - -

Read the Docs

-

Learn how RapidRAW works with step-by-step tutorials, from adjustments to masking.

- View Tutorials & Docs → -

-
- -
-For Who Is This? -RapidRAW is for photographers who love to edit their photos in a clean, fast, and simple workflow. It prioritizes speed, a beautiful user interface, and powerful tools that let you achieve your creative color vision quickly. -

-RapidRAW is still in active development and isn't yet as polished as mature tools like Darktable, RawTherapee, or Adobe Lightroom®. Right now, the focus is on building a fast, enjoyable core editing experience. You may encounter bugs - if you do, please report them so I can fix them :) Your feedback really helps! -

-
-
-Recent Changes - -- **2026-07-11:** Added new local Clone and Heal cleanup tools with highly optimized, parallelized processing. Also fixed Android back-button navigation and resolved an issue causing freezes with iCloud -- **2026-07-08:** Improved thumbnail loading speeds using native file transfers and updated core rendering engines for better overall performance and compatibility -- **2026-07-06:** Fixed copying adjustments directly from the filmstrip, resolved AI model and LUT download issues on Android, and fixed several Windows-specific bugs (including offscreen windows and folder exports) -- **2026-07-05:** Implemented advanced HDR deghosting with new grayscale image alignment and warping mechanics to prevent visual artifacts during HDR merges -- **2026-07-03:** Added "Open With" external editor support and implemented a fallback to embedded previews for undecodable or unsupported RAW files -- **2026-06-29:** Completely reworked the shadows and blacks adjustments, and introduced a new LUT preview panel with hover-to-test functionality, easy importing, and removal support -- **2026-06-25:** Implemented folder sorting, reliable image/album counts, and fixed folder expansion race conditions -- **2026-06-20:** Added quick filters to the bottom bar and integrated global hue shifts into the copy-paste system -- **2026-06-18:** New preset intensity slider -- **2026-06-14:** Added Korean translation support and integrated the global hue slider - -
-Expand further - -- **2026-06-12:** Refined and standardized Traditional Chinese translations -- **2026-06-10:** Completed i18next configuration and added Traditional Chinese locale support -- **2026-06-08:** Resolved infinite indexing loops, brightness bugs, and general compiler warnings -- **2026-06-07:** Fixed copy-pasting, improved library performance & eight new languages -- **2026-06-01:** Improved thumbnail performance, polished metadata panel & non-blocking exif reading -- **2026-05-30:** Implemented reliable edited status, sorting & filtering options -- **2026-05-29:** Refactor exporting to be resource aware -- **2026-05-27:** Added German language -- **2026-05-26:** Converted all components to support full internalization (multilingual / i18n support) -- **2026-05-25:** Implemented dynamic high-resolution rendering for the canvas UI and added copy/pasting of lens correction parameters -- **2026-05-24:** Added advanced library filtering capabilities (queries) -- **2026-05-20:** Introduced a dedicated EXIF data overlay display directly inside the library and list views -- **2026-05-18:** Added global image preprocessing settings, numpad support for customizable keyboard shortcuts, and updated the "Grey" theme color variables -- **2026-05-16:** Initial backend implementation of the cloud service functionality alongside a preview worker backpressure mechanism for better handling of high-quality live previews -- **2026-05-15:** Added the ability to assign custom icons to individual folders in the library tree -- **2026-05-14:** Expanded the library architecture to support multi-root folders and introduced a custom album system -- **2026-05-11:** Improved brush tool -- **2026-05-05:** Major refactor to zustand... -- **2026-05-04:** Added EXIF editing to the metadata panel, accumulating shader execution order, and improved UI responsiveness with triple buffering -- **2026-05-03:** Introduced a "focus mode" for distraction-free editing and enhanced filmic exposure. Batch editing now correctly respects copy/paste settings -- **2026-05-01:** Implemented manual noise reduction with separate controls for luma and color. Optimized the thumbnail generation and request system for better performance -- **2026-04-30:** Major backend refactoring for improved stability and performance. Fixed key issues with cropping, including preserving position when changing aspect ratios -- **2026-04-29:** Added a tonemapper override option and significantly improved the UI on vertical/mobile screens -- **2026-04-27:** Implemented parametric curves tool and introduced thumbnail workers to speed up library browsing -- **2026-04-24:** Overhauled the controls system, adding a dedicated settings section for fully customizable keyboard shortcuts -- **2026-04-22:** Improved auto-adjustment logic, fixed lens correction on Android, and added an import button for mobile devices -- **2026-04-21:** Signed Android APKs, added canvas shortcuts to keybinds, added reset adjustments confirm submenu, and fixed WGPU renderer bugs -- **2026-04-20:** Added style/tool preset mode, improved auto-adjustments via thumbnail caching, and optimized WGPU renderer with custom transform wrapper -- **2026-04-19:** Added brightness to auto-adjust and replicated pixelated rendering logic in WGPU display -- **2026-04-18:** Implemented direct WGPU renderer and fixed macOS GPU context initialization -- **2026-04-17:** Added comprehensive touch support for masks, curves, sliders, and scrolling -- **2026-04-16:** Presets and copy/paste settings now support masks and crops; added mask intersect mode -- **2026-04-15:** Native rotation slider, mask duplication improvements, and Android AI mask fixes -- **2026-04-14:** Implemented `.rrexif` format to keep EXIF when denoising/stitching and added batch denoising -- **2026-04-13:** Added option to preserve folder structure when batch exporting and removed mask limit -- **2026-04-12:** Implemented option to keep export file timestamps from EXIF capture date -- **2026-04-11:** Added flow mask controls/rasterization and dynamic gradient sliders for color grading wheels -- **2026-04-10:** Improved downscaling algorithm, optimized zoom handling, and implemented global UI text layout upgrades -- **2026-04-09:** Fixed Linux touchpad pinch zoom scaling and optimized Masks/AI panel space efficiency -- **2026-04-08:** Redesigned color grading wheels for a minimalistic, consistent look -- **2026-04-07:** Added AVIF export support and fixed adjustment race conditions on fast image switching -- **2026-04-04:** Fixed filmstrip additive multi-range selection -- **2026-04-02:** Added Android URI support and Android file management integration -- **2026-04-01:** Added depth masking with depth anything v2 & improved ROI rendering performance -- **2026-03-30:** LaMa inpainting for lightweight local content-aware fill and object removal -- **2026-03-26:** Performance improvements & new flat list mode for library -- **2026-03-25:** Optimize folder loading & tree fetching -- **2026-03-23:** Generate thumbnails only for visible viewport items -- **2026-03-22:** Dependency migrations and other bug fixes -- **2026-03-21:** Colored sliders for temperature and tint -- **2026-03-18:** Implemented AI NIND denoising -- **2026-03-16:** LRU cache for instant image loading -- **2026-03-15:** Improved high quality subject mask models, various UI improvements and shader improvements -- **2026-03-14:** New image analytics panel which can display vectorscopes, waveforms, parades & histograms -- **2026-03-13:** JPEG XL, WebP, and additional format support, including the ability to export LUTs -- **2026-03-12:** Added parametric color & luminance masks -- **2026-03-10:** Implement region of interest rendering to improve performance when zooming in -- **2026-03-07:** Batch negative conversion & various shader improvements -- **2026-03-06:** Performance optimizations and UI cleanup -- **2026-03-05:** Initial draw support for linear & radial masks -- **2026-03-04:** Real-time mask overlay rendering & pixel perfect zooming -- **2026-03-03:** Instant image rendering & real-time histogram update -- **2026-03-02:** Remember last export settings & lens correction auto cropping -- **2026-03-01:** Optimized pixelated interpolation at maximum zoom level -- **2026-02-27:** Refactored fullscreen handling, smooth and integrated fullscreen viewer -- **2026-02-24:** Improved tonal adjustments using detail masks, remember zoom level & faster fullscreen preview -- **2026-02-23:** Custom AI tag lists, clear button for tag settings & improved window state restoration -- **2026-02-23:** Improved RAW processing, incorrect thumbnail crop scaling & improved mask handles -- **2026-02-21:** XMP metadata read/sync -- **2026-02-20:** Main window size/position persistence, right-click history dropdown & new library organization panel -- **2026-02-19:** Exponential zoom scaling, right-click to delete curve points & selected image count display -- **2026-02-18:** Added a setting for Linear RAW mode for advanced processing & improved right panel switcher -- **2026-02-17:** Display RAW image counts in the folder tree & improved folder reading performance -- **2026-02-16:** New composition guide overlays for cropping -- **2026-02-16:** Added the ability to export masks as separate images -- **2026-02-13:** Optimized live previews, instant metadata loading and new jpeg encoder -- **2026-02-13:** Added ability to merge multiple bracketed images to a HDR -- **2026-02-12:** Straight brush mask lines using shift click and enhanced Lensfun DB parsing -- **2026-02-10:** Improved image loading performance -- **2026-02-06:** Refactored negative conversion logic using characteristic curves. -- **2026-02-04:** Global tooltips & major UI polish -- **2026-02-03:** New creative effects: Glow, Halation & Lens Flares -- **2026-01-31:** Accurate color noise reduction for RAW images & improved image loading -- **2026-01-30:** Enhanced Lensfun DB parsing and improved lens matching logic -- **2026-01-29:** Add cross-channel copy/paste & flat-line clipping logic for curves -- **2026-01-26:** Favorite lens saving, improved rotation controls (finer grid), better local contrast adjustments -- **2026-01-25:** Filmstrip performance boost, improved sorting, lens distortion fixes for AI masks & crop -- **2026-01-24:** Added automatic lens, TCA & vignette correction using lensfun -- **2026-01-22:** Improved and centralized EXIF data handling for greater accuracy and support -- **2026-01-21:** Inpainting now works correctly on images with geometry transformations -- **2026-01-20:** Export preset management for saving export settings -- **2026-01-19:** Preload library for faster startup & automatic geometry transformation helper lines -- **2026-01-18:** Implement image geometry transformation utils -- **2026-01-17:** Refactor AI panel to correctly work with the new masking system -- **2026-01-16:** Major masking system overhaul with drag & drop, per-mask opacity/invert & UI improvements -- **2026-01-13:** New python middleware client for external generative AI integration (ComfyUI) -- **2026-01-12:** Created a RapidRAW community discord server -- **2026-01-11:** Separate preview worker, optional high-quality live previews & mask/ai patch caching -- **2026-01-10:** Enhanced EXIF UI, optimized color wheels/curves & rawler update -- **2026-01-09:** Live previews for all adjustments & masks with optimized GPU processing -- **2026-01-05:** Collage maker upgrade (drag & drop, zoom, ratio options) -- **2026-01-05:** 'Prefer RAW' filter option added to library -- **2026-01-05:** Support for uppercase file extensions -- **2026-01-05:** Flush thumbnail cache on folder switch -- **2025-12-27:** Fix LUT banding issues with improved sampling -- **2025-12-26:** AI masking stability improvements under load -- **2025-12-23:** Metadata card in toolbar & context menu export -- **2025-12-23:** Monochromatic grain & white balance picker improvements -- **2025-12-22:** BM3D Denoising with comparison slider -- **2025-12-20:** Batch export stability improvements & RAM optimization -- **2025-12-14:** Exposure slider added to masking tools -- **2025-12-14:** Improved delete workflow -- **2025-12-08:** Improved mask eraser tool behavior & ORT v2 migration -- **2025-12-07:** Write EXIF metadata to file -- **2025-12-07:** Color picker for white balance -- **2025-11-30:** HSL luminance artifacts fix -- **2025-11-29:** Improved mask stacking & many bug fixes -- **2025-11-28:** QOI support -- **2025-11-25:** Update rawler -- **2025-11-23:** Recursive library view to display images from all subfolders -- **2025-11-22:** DNG loader improvements -- **2025-11-18:** Improved vibrancy adjustment -- **2025-11-15:** Virtual copies & library improvements -- **2025-11-14:** Open-with-file cross plattform compatibilty & single instance lock -- **2025-11-13:** Rewritten tagging system to support pill-like image tagging -- **2025-11-10:** Improved folder tree with search functionality -- **2025-11-08:** Added EXR file format support -- **2025-11-XX:** Improving AgX -- **2025-11-02:** Optimize image loading & add processing engine settings -- **2025-10-31:** Expose highlights compression point to user & improve keybinds detection -- **2025-10-28:** Copy paste settings & brightness adjustment -- **2025-10-XX:** Working on tonemapping - ongoing... -- **2025-10-24:** Getting AgX right isn't as easy as it seems :=) -- **2025-10-22:** AgX tone mapping -- **2025-10-19:** Whole image mask component & organize mask components better -- **2025-10-19:** You can now apply presets to masks & improved auto adjustments -- **2025-10-17:** New centré adjustment, rawler now as a submodule & improved logger -- **2025-10-15:** Ability to pin folders, improved session handling & smooth library thumbnail updating -- **2025-10-11:** Realistic, complex & non-dulling exposure & highlights slider -- **2025-10-11:** Smooth filmstrip thumbnail updates -- **2025-10-07:** New watermarking support -- **2025-10-06:** Improve crop quality by transforming before scaling -- **2025-10-XX:** Many small improvements - ongoing... -- **2025-09-27:** Sort library by exif metadata & release cleanup / bug fixes -- **2025-09-26:** Collage maker to create unique collages with many different layouts, spacing & border radius -- **2025-09-23:** Color calibration tool to adjust RGB primaries & adjustments visibility settings -- **2025-09-22:** Issue template & CI/CD improvements -- **2025-09-20:** Universal presets importer, prioritize dGPU & improved local contrast tools (sharpness, clarity etc.) -- **2025-09-17:** Automatic image culling (duplicate & blur detection) -- **2025-09-14:** Grid previews in community panel & improved ComfyUi workflow -- **2025-09-12:** New community presets panel to share & showcase presets -- **2025-09-10:** Extended generative AI roadmap & started building RapidRAW website -- **2025-09-09:** Many shader improvements & bug fixes, invert tint slider -- **2025-09-06:** New update notifier that alerts users when a new version becomes available -- **2025-09-04:** Added toggleable clipping warnings (blue = shadows, red = highlights) -- **2025-09-02:** Transition to Rust 2024 & Cache image on GPU -- **2025-08-31:** Cancel thumbnail generation on folder change & optimized ai patch saving -- **2025-08-30:** Optimize ComfyUI image transfer & speed -- **2025-08-28:** Chromatic aberration correction & Shader improvements -- **2025-08-26:** User customisable ComfyUI workflow selection -- **2025-08-25:** Make LUTs parser more robust (support more advanced formats) -- **2025-08-24:** Improved keyboard shortcuts -- **2025-08-23:** Estimate file size before exporting -- **2025-08-21:** Added LUTs (.cube, .3dl, .png, .jpg, .jpeg, .tiff) support -- **2025-08-16:** Fast AI sky masks -- **2025-08-15:** Show full resolution image when zooming in -- **2025-08-15:** Implement Tauri's IPC as a replacement for the slow Base64 image transfer -- **2025-08-12:** Relative zoom indicator -- **2025-08-11:** TypeScript cleanup & many bug fixes -- **2025-08-09:** Local inpainting without the need for ComfyUI, ability to change thumbnail aspect ratio -- **2025-08-09:** Frontend refactored to TypeScript thanks to @varjolintu -- **2025-08-08:** New onnxruntime download strategy & the base for local inpainting -- **2025-08-05:** Improved HSL cascading, UI & animation improvements, ability to grow & shrink / feather AI masks -- **2025-08-03:** New high performance, seamless image panorama stitcher (without any dependencies on OpenCV) -- **2025-08-02:** Added an image straightening tool and improved crop & rotation functionality (especially on portrait images) -- **2025-08-02:** A new dedicated image importer, ability to rename and batch rename files, improved dark theme, and other fixes -- **2025-07-31:** Ability to tag & filter images by color labels, refactored image right clicking -- **2025-07-31:** Reimplemented the functionality of GPU processing (GPU cropping, etc.) -> No longer dependent on TEXTURE_BINDING_ARRAY -- **2025-07-29:** Refactored generative AI foundation, many small fixes -- **2025-07-27:** Automatic AI image tagging, overall mask transparency setting per mask -- **2025-07-25:** Fuji RAF X-Trans sensor support (new x-trans demosaicing algo) -- **2025-07-24:** Auto crop when cropping an image (to prevent black borders), added drag & drop sort abilty to presets panel -- **2025-07-22:** Significant improvements to the shader: More accurate exposure slider, better tone mapper (simplified ACES) -- **2025-07-21:** Remember scroll position when going into the editing section -- **2025-07-20:** Ability to add presets to folders, export preset folders etc, preset _animations_ -- **2025-07-20:** Tutorials on how to use RapidRAW -- **2025-07-19:** Initial color negative conversion implementation, shader improvements -- **2025-07-19:** New color wheels, persistent collapsed / expanded state for UI elements -- **2025-07-19:** Fixed banding & purple artefacts on RAW images, better color noise reduction, show exposure in stops -- **2025-07-18:** Smooth zoom slider, new adaptive editor theme setting -- **2025-07-18:** New export functionality: Export with metadata, GPS metadata remover, batch export file naming scheme using tags -- **2025-07-18:** Ability to delete the associated RAW/JPEG in right click delete operations -- **2025-07-17:** Small bug fixes -- **2025-07-13:** Native looking titlebar and ability to input precise number into sliders -- **2025-07-13:** Huge update to masks: You can now add multiple masks to a mask containers, subtract / add / combine masks etc. -- **2025-07-12:** Improved curves tool, more shader improvements, improved handling of very large files -- **2025-07-11:** More accurate shader, reorganized main library preferences dropdown, smoother histogram, more realistic film grain -- **2025-07-11:** Added a HUD-like waveform overlay toggle to display specific channel waveforms (w-key) -- **2025-07-10:** Rewritten batch export system and async thumbnail generation (makes the loading of large folders a lot more fluid) -- **2025-07-10:** Window transparency can now be toggled in the settings, thanks to @andrewazores -- **2025-07-08:** Ability to toggle the visibility of individual adjustments sections -- **2025-07-08:** Fixed top-left zoom bug, corrected scale behavior in crop panel, keep default original aspect ratio -- **2025-07-08:** Added image rating filter and redesigned the metadata panel with improved layout, clearer sections, and an embedded GPS map -- **2025-07-07:** Improved generative AI features and updated [AI Roadmap](#ai-roadmap) -- **2025-07-06:** Initial generative AI integration with [ComfyUI](https://github.com/comfyanonymous/ComfyUI) - for more details, checkout the [AI Roadmap](#ai-roadmap) -- **2025-07-05:** Ability to overwrite preset with current settings -- **2025-07-04:** High speed and precise cache to significantly accelerate large image editing -- **2025-07-04:** Greatly improved shader with better dehaze, more accurate curves etc -- **2025-07-04:** Predefined 90° clockwise rotation and ability to flip images -- **2025-07-03:** Switched from [rawloader](https://github.com/pedrocr/rawloader) to [rawler](https://github.com/dnglab/dnglab/tree/main/rawler) to support a wider range of RAW formats -- **2025-07-02:** AI-powered foreground / background masking -- **2025-06-30:** AI-powered subject masking -- **2025-06-30:** Precompiled Linux builds -- **2025-06-29:** New 5:4 aspect ratio, new low contrast grey theme and more cameras support (DJI Mavic lineup) -- **2025-06-28:** Release cleanup, CI/CD improvements and minor fixes -- **2025-06-27:** Initial release. For more information about the earlier progress, look at the [Initial Development Log](#initial-development-log) - -
-
-
- -**Table of Contents** - -- [Key Features](#key-features) -- [Demo & Screenshots](#demo--screenshots) -- [The Idea](#the-idea) -- [Current Priorities](#current-priorities) -- [AI Roadmap](#ai-roadmap) -- [Initial Development Log](#initial-development-log) -- [Getting Started](#getting-started) -- [System Requirements](#system-requirements) -- [Contributing](#contributing) -- [Special Thanks](#special-thanks) -- [Support the Project](#support-the-project) -- [License & Philosophy](#license--philosophy) - ---- - -## Key Features - - - - - - -
-

Core Editing Engine

-
    -
  • GPU-Accelerated: Full 32-bit image processing pipeline written in WGSL for instant feedback.
  • -
  • Masking: Layer-based masking with AI subject, depth, sky, and foreground detection. Combine with traditional masks for great control.
  • -
  • Generative Edits: Remove or add elements using text prompts, powered by an optional AI backend.
  • -
  • Full RAW Support: Supports a wide range of RAW camera formats through rawler, with JPEG support included.
  • -
  • Non-Destructive Workflow: All edits are stored in a .rrdata sidecar file, leaving your original images untouched.
  • -
  • Lens Correction: Automatic distortion, TCA, and vignette correction powered by Lensfun.
  • -
-

Professional Grade Adjustments

-
    -
  • Tonal Controls: Exposure, Tone Mapping (including AgX!), Contrast, Highlights, Shadows, Whites, and Blacks.
  • -
  • Tone Curves: Full control over Luma/RGB channels.
  • -
  • Color Grading: Temperature, Tint, Vibrance, Saturation, color wheels and a full HSL color mixer.
  • -
  • Detail Enhancement: Sharpening, Clarity, Structure, and Noise Reduction.
  • -
  • Effects: LUTs, Dehaze, Vignette, Glow, Halation, Flares and Film Grain.
  • -
  • Transform Tools: Perspective correction, rotation, straightening, crop, and warping tools.
  • -
-
-

Library & Workflow

-
    -
  • Image Library: Effortlessly manage and cull your entire photo collection for a streamlined and efficient workflow.
  • -
  • Organization: Recursive folder view, virtual copies, color labels, star ratings, tags and more.
  • -
  • File Operations: Import, copy, move, rename, and duplicate images/folders.
  • -
  • Filmstrip View: Quickly navigate between all the images in your current folder while editing.
  • -
  • Batch Operations: Save significant time by applying a consistent set of adjustments or exporting entire batches of images simultaneously.
  • -
  • EXIF Data Viewer: Gain insights by inspecting the complete metadata from your camera.
  • -
-

Productivity & UI

-
    -
  • Preset System: Create, save, import, and share your favorite looks.
  • -
  • Copy & Paste Settings: Quickly transfer adjustments between images.
  • -
  • Undo/Redo History: A robust history system for every edit.
  • -
  • Customizable UI: Modern, multilingual UI with resizable panels and smooth animations.
  • -
  • Compositions: Built-in seamless Panorama Stitcher, flexible Collage Maker, and Film Negative Converter.
  • -
  • Exporting: Control file format, watermarking, naming scheme, metadata, resizing options on export.
  • -
-
- -## Demo & Screenshots - -Here's RapidRAW in action. - -

- The main editor interface in action
- The main editor interface in action. -

-
- - - - - - - - - - - - - -
- Powerful batch operations and export -
- Powerful batch operations and export. -
- Customizable editor layout and panels -
- Customizable editor layout and panels. -
- Advanced masking to speedup workflow -
- Advanced masking to speedup workflow. -
- Experimental generative AI features -
- Experimental generative AI features. -
- Library navigation and folder management -
- Library navigation and folder management. -
- Beautiful themes and UI customization -
- Beautiful themes and UI customization. -
- -> If you like the theme images and want to see more of my own images, checkout my Instagram: [**@timonkaech.photography**](https://www.instagram.com/timonkaech.photography/) - -## The Idea - -As a photography enthusiast, I often found existing software to be sluggish and resource-heavy on my machine. Born from the desire for a more responsive and streamlined photo editing experience, I set out to build my own. The goal was to create a tool that was not only fast but **also helped me learn the details of digital image processing and camera technology**. - -I set an ambitious goal to rapidly build a functional, feature-rich application from an empty folder. This personal challenge pushed me to learn quickly and focus intensely on the core architecture and user experience. - -The foundation is built on Rust for its safety and performance, and Tauri for its ability to create lightweight, cross-platform desktop apps with a web frontend. The entire image processing pipeline is offloaded to the GPU via WGPU and a custom WGSL shader, ensuring that even on complex edits with multiple masks, the UI remains fluid. - -I am immensely grateful for Google's Gemini suite of AI models. As a young developer without a formal background in advanced mathematics or image science, Google's AI Studio was an invaluable assistant, helping me research and implement complex concepts in record time. - -## Current Priorities - -While the core functionality is in place, I'm actively working on improving several key areas. Here's a transparent look at the current focus: - -| Task | Priority | Difficulty | Status | -| ---------------------------------------------------------------------------------------------------------- | -------- | ---------- | ------ | -| Find a better X-Trans demosaicing algorithm | Medium | High | [ ] | -| Refactoring the frontend (reduce prop drilling in React components) | Low | Medium | [X] | -| Write a tutorial on how to connect ComfyUI with RapidRAW | Medium | Medium | [ ] | -| Centralize Coordinate Transformation Logic - See [#245](https://github.com/CyberTimon/RapidRAW/issues/245) | Medium | High | [X] | -| Improve speed on older systems (e.g. Pascal GPUs) | Medium | High | [X] | -| Implement warping tools | Low | High | [X] | - -## AI Roadmap - -I've designed RapidRAW's AI features with flexibility in mind. You have three ways to use them, giving you the choice between fast local tools, powerful self-hosting, and simple cloud convenience. - -### 1. Built-in AI Tools (Local & Free) - -These features are integrated directly into RapidRAW and run entirely on your computer. They are fast, free, and require no setup from you. - -- **AI Masking:** Instantly detect and mask subjects, skies, and foregrounds. -- **Automatic Tagging:** The image library is automatically tagged with keywords using a local CLIP model, making your photos easy to search. -- **Simple Generative Replace:** A basic, CPU-based inpainting tool for removing small distractions. - -### 2. Self-Hosted Integration with ComfyUI (Local & Free) - -For users with a capable GPU who want maximum control, RapidRAW can connect to your own local [ComfyUI](https://github.com/comfyanonymous/ComfyUI) server. This is managed by the [**RapidRAW-AI-Connector**](https://github.com/CyberTimon/RapidRAW-AI-Connector), a lightweight middleware that bridges RapidRAW and ComfyUI. Its purpose is to manage image caching, workflow injection, and AI coordination. - -**Why this approach?** This new architecture makes generative edits much more efficient. Instead of sending the entire high-resolution image for every single change, the AI Connector intelligently caches it. The full image is sent only once; for every subsequent edit, only the tiny mask and text are transferred. This makes the process significantly faster and more responsive. - -This setup gives you the best of both worlds: a highly efficient workflow while retaining full control to use your own hardware and any custom Diffusion models or workflows you choose. - -- **Full Control:** Use your own hardware and any custom Diffusion model or workflow you choose. -- **Cost-Free Power:** Utilise your existing hardware for advanced generative edits at no extra cost. -- **Custom Workflow Selection:** Import your own ComfyUI workflows and use your custom nodes. - -### 3. Optional Cloud Service (Subscription) - -To be clear, **I won't lock features behind a paywall.** All of RapidRAW's functionality is available for free if you use the built-in tools or self-host. - -However, I realize that not everyone has the powerful hardware or technical desire to set up and maintain their own ComfyUI server. For those who want a simpler solution, I will be offering an optional **$TBD/month subscription**. - -This is purely a **convenience service**. It provides the **same high-quality results** as a self-hosted setup without any of the hassle - just log in, and it works. Subscribing is also the best way to support the project and help me dedicate more time to its development. - -| Feature | Built-in AI (Free) | Self-Hosted (ComfyUI) | Optional Cloud Service | -| ------------ | ------------------------------ | ----------------------------------- | ---------------------- | -| **Cost** | Free, included | Free (requires your own hardware) | $TBD / month | -| **Setup** | None | Manual ComfyUI / AI Connector setup | None (Just log in) | -| **Use Case** | Everyday workflow acceleration | Full control for technical users | Maximum convenience | -| **Status** | **Available** | **Available** | Coming Soon | - -
-Click to see the Generative AI features in action -
-

- Experimental generative AI features -
- Generative Replace, which can be powered by either a local ComfyUI backend or the upcoming optional cloud service. -

-
- -## Initial Development Log - -This project began as an intensive sprint to build the core functionality. Here's a summary of the initial progress and key milestones: - -
-Click to expand the day-by-day development log - -- **Day 1: June 13th, 2025** - Project inception, basic Tauri setup, and initial brightness/contrast shader implementation. -- **Day 2: June 14th** - Core architecture refactor, full library support (folder tree, image list), and optimized image loading. Implemented histogram and curve editor support. Added UI themes. -- **Day 3: June 15th** - Implemented a working crop tool, preset system, and context menus. Enabled auto-saving of edits to sidecar files and auto-thumbnail generation. Refined color adjustments. -- **Day 4: June 16th** - Initial prototype for local adjustments with masking. Added mask support to presets. Bug-free image preview switching. -- **Day 5: June 17th** - Major UI overhaul. Created the filmstrip and resizable panel layout. Fixed mask scaling issues and improved the library/welcome screen. -- **Day 6: June 18th** - Performance tuning. Reduced GPU calls for adjustments, leading to a much smoother cropping and editing experience. Implemented saving of panel UI state. -- **Day 7: June 19th** - Enhanced library functionality. Added multi-selection and the ability to copy/paste adjustments across multiple images. -- **Day 8: June 20th** - Implemented initial RAW file support and an EXIF metadata viewer. -- **Day 9: June 21st** - Added advanced detail adjustments (Clarity, Sharpening, Dehaze, etc.) and film grain. Developed a linear RAW processing pipeline. -- **Day 10: June 22nd** - Implemented layer stacking for smooth preview transitions. Built a robust export panel with batch export capabilities. Added import/export for presets. -- **Day 11: June 23rd** - Added full undo/redo functionality integrated with a custom history hook. Improved context menus and completed the settings panel. -- **Day 12: June 24th** - Implemented image rotation and fixed all mask scaling/alignment issues related to cropping and rotation. -- **Day 13: June 25th** - Rewrote the mask system to be bitmap-based. Implemented brush and linear gradient tools, with semi-transparent visualization. -- **Day 14: June 26th-27th** - Final polish. Added universal keyboard shortcuts, full adjustment support for masks, theme management, and final UI/UX improvements. This ReadMe. - -
- -## Getting Started - -You have two options to run RapidRAW: - -**1. Download the Latest Release (Recommended)** - -**Windows & macOS:** - -- Grab the pre-built installer or application bundle for your operating system from the [**Releases**](https://github.com/CyberTimon/RapidRAW/releases) page. - -**Linux:** - -- The official Flatpak package supports all Linux distributions and is available on [**Flathub**](https://flathub.org/apps/io.github.CyberTimon.RapidRAW). -- On Debian-based distributions, install the `.deb` package from the [**Releases**](https://github.com/CyberTimon/RapidRAW/releases) page. -- On Arch-based distributions, use the [`rapidraw-bin`](https://aur.archlinux.org/packages/rapidraw-bin) package from the AUR. - -**2. Build from Source** - -If you want to build the project yourself, you'll need to have [Rust](https://www.rust-lang.org/tools/install) and [Node.js](https://nodejs.org/) installed. - -```bash -# 1. Clone the repository -git clone https://github.com/CyberTimon/RapidRAW.git -cd RapidRAW - -# 2. Install frontend dependencies -npm install - -# 3. Build and run the application -npm start +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config({ + extends: [ + // Remove ...tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) ``` -## System Requirements - -RapidRAW is built to be lightweight and cross-platform. The minimum (tested) requirements are: - -**Operating System:** - -- **Windows:** Windows 10 or newer -- **macOS:** macOS 13 (Ventura) or newer -- **Linux:** Ubuntu 22.04+ or a compatible modern distribution - -**Hardware Recommendations:** - -- **RAM:** **16GB or more is highly recommended.** While the application may run on systems with less memory, performance is best with 16GB+ to handle high-resolution RAW files, undo history, and complex layer masking without slowdowns. -- **GPU:** A dedicated GPU is recommended. RapidRAW relies heavily on GPU acceleration for its processing pipeline. Very old GPU architectures (generally pre-2015) or older integrated graphics may struggle, leading to instability or graphical artifacts. - -### Common Problems - -
-App crashes when opening an image / entering edit mode - -If the application crashes immediately when you try to start editing a picture, it is often due to the automatic selection of the GPU backend. - -1. Open **Settings** on the **Home Screen** (Gear icon). -2. Navigate to the **Processing** tab. -3. Locate the **Processing Backend** setting. -4. Change it from **Auto** to a specific backend supported by your OS (e.g., **Vulkan**, **DirectX12**, **OpenGL**, or **Metal**). -5. Restart the application and try opening the image again. Experiment with different backends if the first one doesn't work. -
- -
-Linux Wayland/WebKit Crash - -If RapidRAW crashes on Wayland (e.g. GNOME + NVIDIA), try launching it with: - -```bash -WEBKIT_DISABLE_DMABUF_RENDERER=1 RapidRAW -``` - -or - -```bash -WEBKIT_DISABLE_COMPOSITING_MODE=1 RapidRAW +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config({ + extends: [ + // other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) ``` - -This issue is related to **WebKit** and **NVIDIA drivers**, not RapidRAW directly. Switching to **X11** or using **AMD / Intel GPUs** may also help. - -See [#306](https://github.com/CyberTimon/RapidRAW/issues/306) for more information. - -
- -## Contributing - -I’m really grateful for any contributions you make to RapidRAW! Whether you’re reporting a bug, suggesting a new feature, or submitting a pull request - your input helps shape the project and makes it better for everyone. Don’t hesitate to open an issue or share your ideas. - -### Image format issues - -If your camera’s RAW files aren’t supported, please open a issue here first: [rawler issues](https://github.com/dnglab/dnglab/issues). Once support is added in rawler, create a issue for RapidRAW so I can update the packages and keep everything in sync. - -## Special Thanks - -A huge thank you to the following projects and tools that were very important in the development of RapidRAW: - -- **[Google AI Studio](https://aistudio.google.com):** For providing amazing assistance in researching, implementing image processing algorithms and giving an overall speed boost. -- **[rawler](https://github.com/dnglab/dnglab/tree/main/rawler):** For the excellent Rust crate that provides the foundation for RAW file processing in this project. -- **[lensfun](https://lensfun.github.io/):** For its invaluable open-source library and comprehensive database for automatic lens correction. -- **[LaMa](https://github.com/advimman/lama):** For the powerful & simple image inpainting model, which enables content-aware fill and object removal. -- **[SAM 2](https://github.com/facebookresearch/sam2):** For providing the foundation model used for the AI subject detection capabilities. -- **[U-2-Net](https://github.com/xuebinqin/U-2-Net):** For providing the robust architecture used for the AI sky and foreground detection capabilities. -- **[Depth Anything V2](https://github.com/DepthAnything/Depth-Anything-V2):** For the powerful monocular depth estimation model that enables the AI depth masking capabilities. -- **[nind-denoise](https://github.com/trougnouf/nind-denoise):** For providing AI models that power the AI noise reduction capabilities in RapidRAW. -- **[NegPy](https://github.com/marcinz606/NegPy):** For the inspiration behind the negative conversion logic, particularly the mathematical approach to film inversion using characteristic curves. -- **[pixls.us](https://discuss.pixls.us/):** For being an incredible community full of knowledgeable people who offered inspiration, advice, and ideas. -- **[darktable & co.](https://github.com/darktable-org/darktable):** For some reference implementations that guided parts of this work. -- **You:** For using and supporting RapidRAW. Your interest keeps this project alive and evolving. - -## Support the Project - -As a young developer balancing this project with an apprenticeship, your support means the world. If you find RapidRAW useful or exciting, please consider donating to help me dedicate more time to its development and cover any associated costs. - -- **Ko-fi:** [Donate on Ko-fi](https://ko-fi.com/cybertimon) -- **Crypto:** - - BTC: `36yHjo2dkBwQ63p3YwtqoYAohoZhhUTkCJ` (min. 0.0001) - - ETH: `0x597e6bdb97f3d0f1602b5efc8f3b7beb21eaf74a` (min. 0.005) - - SOL: `CkXM3C777S8iJX9h3MGSfwGxb85Yx7GHmynQUFSbZXUL` (min. 0.01) - -## License & Philosophy - -This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**. I chose this license to ensure that RapidRAW and any of its derivatives will always remain open-source and free for the community. It protects the project from being used in closed-source commercial software, ensuring that improvements benefit everyone. - -See the [LICENSE](LICENSE) file for more details. diff --git a/coverage/base.css b/coverage/base.css new file mode 100644 index 0000000000..f418035b46 --- /dev/null +++ b/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js new file mode 100644 index 0000000000..530d1ed2ba --- /dev/null +++ b/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/components/panel/right/Masks.tsx.html b/coverage/components/panel/right/Masks.tsx.html new file mode 100644 index 0000000000..d7f27f1b47 --- /dev/null +++ b/coverage/components/panel/right/Masks.tsx.html @@ -0,0 +1,964 @@ + + + + + + Code coverage report for components/panel/right/Masks.tsx + + + + + + + + + +
+
+

All files / components/panel/right Masks.tsx

+
+ +
+ 47.76% + Statements + 32/67 +
+ + +
+ 0% + Branches + 0/36 +
+ + +
+ 50% + Functions + 3/6 +
+ + +
+ 61.53% + Lines + 32/52 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  + 
import {
+  Brush,
+  BringToFront,
+  Circle,
+  Cloud,
+  Droplet,
+  Droplets,
+  Eraser,
+  MoreHorizontal,
+  RectangleHorizontal,
+  Sparkles,
+  TriangleRight,
+  User,
+  Sun,
+  Stamp,
+  Bandage,
+} from 'lucide-react';
+import i18n from 'i18next';
+ 
+export enum Mask {
+  AiDepth = 'ai-depth',
+  AiForeground = 'ai-foreground',
+  AiSky = 'ai-sky',
+  AiSubject = 'ai-subject',
+  All = 'all',
+  Brush = 'brush',
+  Flow = 'flow',
+  Color = 'color',
+  Linear = 'linear',
+  Luminance = 'luminance',
+  QuickEraser = 'quick-eraser',
+  Radial = 'radial',
+  Clone = 'clone',
+  Heal = 'heal',
+}
+ 
+export enum SubMaskMode {
+  Additive = 'additive',
+  Subtractive = 'subtractive',
+  Intersect = 'intersect',
+}
+ 
+export enum ToolType {
+  AiSeletor = 'ai-selector',
+  Brush = 'brush',
+  Eraser = 'eraser',
+  GenerativeReplace = 'generative-replace',
+  SelectSubject = 'select-subject',
+}
+ 
+export interface MaskType {
+  disabled: boolean;
+  icon: any;
+  id?: string;
+  name: string;
+  type: Mask;
+}
+ 
+export interface SubMask {
+  id: string;
+  invert: boolean;
+  mode: SubMaskMode;
+  name?: string;
+  opacity: number;
+  parameters?: any;
+  type: Mask;
+  visible: boolean;
+}
+ 
+export function formatMaskTypeName(type: string) {
+  if (type === Mask.AiDepth) return i18n.t('masks.types.depth');
+  if (type === Mask.AiSubject) return i18n.t('masks.types.subject');
+  if (type === Mask.AiForeground) return i18n.t('masks.types.foreground');
+  if (type === Mask.AiSky) return i18n.t('masks.types.sky');
+  if (type === Mask.All) return i18n.t('masks.types.all');
+  if (type === Mask.QuickEraser) return i18n.t('masks.types.quickEraser');
+  if (type === Mask.Brush) return i18n.t('masks.types.brush');
+  if (type === Mask.Flow) return i18n.t('masks.types.flow');
+  if (type === Mask.Color) return i18n.t('masks.types.color');
+  if (type === Mask.Linear) return i18n.t('masks.types.linear');
+  if (type === Mask.Luminance) return i18n.t('masks.types.luminance');
+  if (type === Mask.Radial) return i18n.t('masks.types.radial');
+  if (type === Mask.Clone) return i18n.t('masks.types.clone');
+  if (type === Mask.Heal) return i18n.t('masks.types.heal');
+  return type.charAt(0).toUpperCase() + type.slice(1);
+}
+ 
+export function getMaskTypeName(mask: MaskType) {
+  if (mask.id === 'others') return i18n.t('masks.types.others');
+  if (mask.type === Mask.QuickEraser && mask.name === 'Quick Erase') {
+    return i18n.t('masks.types.quickErase');
+  }
+  return formatMaskTypeName(mask.type);
+}
+ 
+export function getSubMaskName(subMask: Pick<SubMask, 'name' | 'type'>) {
+  return subMask.name?.trim() || formatMaskTypeName(subMask.type);
+}
+ 
+export const MASK_ICON_MAP: Record<Mask, any> = {
+  [Mask.AiDepth]: BringToFront,
+  [Mask.AiForeground]: User,
+  [Mask.AiSky]: Cloud,
+  [Mask.AiSubject]: Sparkles,
+  [Mask.All]: RectangleHorizontal,
+  [Mask.Brush]: Brush,
+  [Mask.Flow]: Droplets,
+  [Mask.Color]: Droplet,
+  [Mask.Linear]: TriangleRight,
+  [Mask.Luminance]: Sparkles,
+  [Mask.QuickEraser]: Eraser,
+  [Mask.Radial]: Circle,
+  [Mask.Clone]: Stamp,
+  [Mask.Heal]: Bandage,
+};
+ 
+export const MASK_PANEL_CREATION_TYPES: Array<MaskType> = [
+  {
+    disabled: false,
+    icon: Sparkles,
+    name: 'Subject',
+    type: Mask.AiSubject,
+  },
+  {
+    disabled: false,
+    icon: Cloud,
+    name: 'Sky',
+    type: Mask.AiSky,
+  },
+  {
+    disabled: false,
+    icon: User,
+    name: 'Foreground',
+    type: Mask.AiForeground,
+  },
+  {
+    disabled: false,
+    icon: TriangleRight,
+    name: 'Linear',
+    type: Mask.Linear,
+  },
+  {
+    disabled: false,
+    icon: Circle,
+    name: 'Radial',
+    type: Mask.Radial,
+  },
+  {
+    disabled: false,
+    icon: MoreHorizontal,
+    id: 'others',
+    name: 'Others',
+    type: null as any,
+  },
+];
+ 
+export const AI_MANUAL_CLEANUP_TYPES: Array<MaskType> = [
+  {
+    disabled: false,
+    icon: Stamp,
+    name: 'Clone',
+    type: Mask.Clone,
+  },
+  {
+    disabled: false,
+    icon: Bandage,
+    name: 'Heal',
+    type: Mask.Heal,
+  },
+];
+ 
+export const AI_GENERATIVE_CREATION_TYPES: Array<MaskType> = [
+  {
+    disabled: false,
+    icon: Eraser,
+    name: 'Quick Erase',
+    type: Mask.QuickEraser,
+  },
+  {
+    disabled: false,
+    icon: Sparkles,
+    name: 'Subject',
+    type: Mask.AiSubject,
+  },
+  {
+    disabled: false,
+    icon: User,
+    name: 'Foreground',
+    type: Mask.AiForeground,
+  },
+  {
+    disabled: false,
+    icon: Brush,
+    name: 'Brush',
+    type: Mask.Brush,
+  },
+  {
+    disabled: false,
+    icon: TriangleRight,
+    name: 'Linear',
+    type: Mask.Linear,
+  },
+  {
+    disabled: false,
+    icon: Circle,
+    name: 'Radial',
+    type: Mask.Radial,
+  },
+];
+ 
+export const SUB_MASK_COMPONENT_TYPES: Array<MaskType> = [
+  {
+    disabled: false,
+    icon: Sparkles,
+    name: 'Subject',
+    type: Mask.AiSubject,
+  },
+  {
+    disabled: false,
+    icon: Cloud,
+    name: 'Sky',
+    type: Mask.AiSky,
+  },
+  {
+    disabled: false,
+    icon: User,
+    name: 'Foreground',
+    type: Mask.AiForeground,
+  },
+  {
+    disabled: false,
+    icon: TriangleRight,
+    name: 'Linear',
+    type: Mask.Linear,
+  },
+  {
+    disabled: false,
+    icon: Circle,
+    name: 'Radial',
+    type: Mask.Radial,
+  },
+  {
+    disabled: false,
+    icon: MoreHorizontal,
+    id: 'others',
+    name: 'Others',
+    type: null as any,
+  },
+];
+ 
+export const OTHERS_MASK_TYPES: Array<MaskType> = [
+  {
+    disabled: false,
+    icon: BringToFront,
+    name: 'Depth',
+    type: Mask.AiDepth,
+  },
+  {
+    disabled: false,
+    icon: Droplet,
+    name: 'Color',
+    type: Mask.Color,
+  },
+  {
+    disabled: false,
+    icon: Sun,
+    name: 'Luminance',
+    type: Mask.Luminance,
+  },
+  {
+    disabled: false,
+    icon: Brush,
+    name: 'Brush',
+    type: Mask.Brush,
+  },
+  {
+    disabled: false,
+    icon: Droplets,
+    name: 'Flow',
+    type: Mask.Flow,
+  },
+  {
+    disabled: false,
+    icon: RectangleHorizontal,
+    name: 'Whole Image',
+    type: Mask.All,
+  },
+];
+ 
+export const AI_SUB_MASK_COMPONENT_TYPES: Array<MaskType> = [
+  ...AI_MANUAL_CLEANUP_TYPES,
+  ...AI_GENERATIVE_CREATION_TYPES,
+];
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/components/panel/right/index.html b/coverage/components/panel/right/index.html new file mode 100644 index 0000000000..22539e15e7 --- /dev/null +++ b/coverage/components/panel/right/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for components/panel/right + + + + + + + + + +
+
+

All files components/panel/right

+
+ +
+ 47.76% + Statements + 32/67 +
+ + +
+ 0% + Branches + 0/36 +
+ + +
+ 50% + Functions + 3/6 +
+ + +
+ 61.53% + Lines + 32/52 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
Masks.tsx +
+
47.76%32/670%0/3650%3/661.53%32/52
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/components/ui/AndroidBottomNav.tsx.html b/coverage/components/ui/AndroidBottomNav.tsx.html new file mode 100644 index 0000000000..b133a8f3d2 --- /dev/null +++ b/coverage/components/ui/AndroidBottomNav.tsx.html @@ -0,0 +1,262 @@ + + + + + + Code coverage report for components/ui/AndroidBottomNav.tsx + + + + + + + + + +
+
+

All files / components/ui AndroidBottomNav.tsx

+
+ +
+ 92.85% + Statements + 13/14 +
+ + +
+ 80% + Branches + 8/10 +
+ + +
+ 100% + Functions + 5/5 +
+ + +
+ 90.9% + Lines + 10/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +3x +3x +3x +  +3x +  +2x +  +  +10x +10x +  +  +  +  +  +  +  +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  + 
import { Home, SlidersHorizontal, Palette, UserCircle, FileInput } from 'lucide-react';
+import clsx from 'clsx';
+import { useTranslation } from 'react-i18next';
+ 
+import { Panel } from './AppProperties';
+import { useUIStore } from '../../store/useUIStore';
+ 
+interface AndroidBottomNavProps {
+  isAndroid: boolean;
+}
+ 
+interface NavItem {
+  panel: Panel | null;
+  icon: typeof Home;
+  labelKey: string;
+}
+ 
+const navItems: NavItem[] = [
+  { panel: null, icon: Home, labelKey: 'editor.android.bottomNav.library' },
+  { panel: Panel.Adjustments, icon: SlidersHorizontal, labelKey: 'editor.android.bottomNav.basic' },
+  { panel: Panel.Color, icon: Palette, labelKey: 'editor.android.bottomNav.color' },
+  { panel: Panel.Portrait, icon: UserCircle, labelKey: 'editor.android.bottomNav.portrait' },
+  { panel: Panel.Export, icon: FileInput, labelKey: 'editor.android.bottomNav.export' },
+];
+ 
+export default function AndroidBottomNav({ isAndroid }: AndroidBottomNavProps) {
+  const { t } = useTranslation();
+  const activeRightPanel = useUIStore((s) => s.activeRightPanel);
+  const setRightPanel = useUIStore((s) => s.setRightPanel);
+ 
+  if (!isAndroid) return null;
+ 
+  return (
+    <div className="flex items-center justify-around shrink-0 h-14 bg-bg-secondary border-t border-border-color">
+      {navItems.map(({ panel, icon: Icon, labelKey }) => {
+        const isActive = panel ? activeRightPanel === panel : activeRightPanel === null;
+        return (
+          <button
+            key={labelKey}
+            className={clsx(
+              'flex flex-col items-center justify-center gap-0.5 px-3 py-1 rounded-md transition-colors',
+              isActive ? 'text-accent' : 'text-text-secondary',
+            )}
+            onClick={() => {
+              Iif (panel === null) {
+                setRightPanel(null);
+              } else {
+                setRightPanel(activeRightPanel === panel ? null : panel);
+              }
+            }}
+          >
+            <Icon size={22} strokeWidth={1.8} />
+            <span className="text-[11px] leading-tight font-medium tracking-wide">{t(labelKey as any)}</span>
+          </button>
+        );
+      })}
+    </div>
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/components/ui/AndroidShareSheet.tsx.html b/coverage/components/ui/AndroidShareSheet.tsx.html new file mode 100644 index 0000000000..35edf4016a --- /dev/null +++ b/coverage/components/ui/AndroidShareSheet.tsx.html @@ -0,0 +1,436 @@ + + + + + + Code coverage report for components/ui/AndroidShareSheet.tsx + + + + + + + + + +
+
+

All files / components/ui AndroidShareSheet.tsx

+
+ +
+ 86.66% + Statements + 13/15 +
+ + +
+ 75% + Branches + 3/4 +
+ + +
+ 100% + Functions + 4/4 +
+ + +
+ 92.85% + Lines + 13/14 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +6x +6x +  +6x +  +1x +1x +1x +1x +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +20x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import React, { useCallback, useState } from 'react';
+import { invoke } from '@tauri-apps/api/core';
+import { Share2, MessageCircle, X } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { motion, AnimatePresence } from 'framer-motion';
+import Text from './Text';
+import { TextVariants } from '../../types/typography';
+ 
+interface AndroidShareSheetProps {
+  filePath: string;
+  mimeType: string;
+  visible: boolean;
+  onClose: () => void;
+}
+ 
+interface ShareTarget {
+  id: string;
+  labelKey: string;
+  icon: React.ReactNode;
+}
+ 
+const SHARE_TARGETS: ShareTarget[] = [
+  { id: 'wechat', labelKey: 'androidShare.wechat', icon: <MessageCircle size={20} /> },
+  { id: 'qq', labelKey: 'androidShare.qq', icon: <MessageCircle size={20} /> },
+  { id: 'weibo', labelKey: 'androidShare.weibo', icon: <MessageCircle size={20} /> },
+  { id: 'more', labelKey: 'androidShare.more', icon: <Share2 size={20} /> },
+];
+ 
+export default function AndroidShareSheet({
+  filePath,
+  mimeType,
+  visible,
+  onClose,
+}: AndroidShareSheetProps) {
+  const { t } = useTranslation();
+  const [sharing, setSharing] = useState(false);
+ 
+  const handleShare = useCallback(
+    async (targetId: string) => {
+      Iif (sharing) return;
+      setSharing(true);
+      try {
+        await invoke('share_image', {
+          filePath,
+          mimeType,
+          title: t('androidShare.title' as any, { target: t(`androidShare.${targetId}` as any) }),
+        });
+      } catch (err) {
+        console.error('Share failed:', err);
+      } finally {
+        setSharing(false);
+        onClose();
+      }
+    },
+    [filePath, mimeType, sharing, t, onClose],
+  );
+ 
+  return (
+    <AnimatePresence>
+      {visible && (
+        <>
+          <motion.div
+            className="fixed inset-0 bg-black/40 z-40"
+            initial={{ opacity: 0 }}
+            animate={{ opacity: 1 }}
+            exit={{ opacity: 0 }}
+            onClick={onClose}
+          />
+          <motion.div
+            className="fixed bottom-0 left-0 right-0 bg-bg-primary rounded-t-2xl z-50 shadow-lg border-t border-surface"
+            initial={{ y: '100%' }}
+            animate={{ y: 0 }}
+            exit={{ y: '100%' }}
+            transition={{ type: 'spring', damping: 25, stiffness: 300 }}
+          >
+            <div className="flex items-center justify-between p-4 border-b border-surface">
+              <Text variant={TextVariants.title}>{t('androidShare.titleDefault' as any)}</Text>
+              <button
+                onClick={onClose}
+                className="p-1 rounded-full hover:bg-surface transition-colors"
+              >
+                <X size={20} className="text-text-secondary" />
+              </button>
+            </div>
+            <div className="p-4">
+              <div className="grid grid-cols-4 gap-4">
+                {SHARE_TARGETS.map((target) => (
+                  <button
+                    key={target.id}
+                    onClick={() => handleShare(target.id)}
+                    disabled={sharing}
+                    className="flex flex-col items-center gap-2 p-3 rounded-xl hover:bg-surface transition-colors disabled:opacity-50"
+                  >
+                    <div className="w-12 h-12 rounded-full bg-surface flex items-center justify-center text-text-primary">
+                      {target.icon}
+                    </div>
+                    <Text variant={TextVariants.small} className="text-text-secondary">
+                      {t(target.labelKey as any)}
+                    </Text>
+                  </button>
+                ))}
+              </div>
+            </div>
+            <div className="p-4 pt-0">
+              <button
+                onClick={onClose}
+                className="w-full py-3 rounded-xl bg-surface text-text-secondary font-medium text-sm hover:bg-card-active transition-colors"
+              >
+                {t('androidShare.cancel' as any)}
+              </button>
+            </div>
+          </motion.div>
+        </>
+      )}
+    </AnimatePresence>
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/components/ui/AppProperties.tsx.html b/coverage/components/ui/AppProperties.tsx.html new file mode 100644 index 0000000000..5d9df44d5c --- /dev/null +++ b/coverage/components/ui/AppProperties.tsx.html @@ -0,0 +1,1288 @@ + + + + + + Code coverage report for components/ui/AppProperties.tsx + + + + + + + + + +
+
+

All files / components/ui AppProperties.tsx

+
+ +
+ 100% + Statements + 129/129 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 10/10 +
+ + +
+ 100% + Lines + 129/129 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { ExportPreset } from './ExportImportProperties';
+import { Adjustments, CopyPasteSettings } from '../../utils/adjustments';
+import { ToolType } from '../panel/right/Masks';
+ 
+export const GLOBAL_KEYS = [
+  ' ',
+  'ArrowUp',
+  'ArrowDown',
+  'ArrowLeft',
+  'ArrowRight',
+  'f',
+  'b',
+  'a',
+  's',
+  'd',
+  'r',
+  'm',
+  'k',
+  'p',
+  'i',
+  'e',
+  '0',
+  '1',
+  '2',
+  '3',
+  '4',
+  '5',
+  'Enter',
+];
+export const OPTION_SEPARATOR = 'separator';
+ 
+export enum Invokes {
+  AddTagForPaths = 'add_tag_for_paths',
+  ApplyAdjustments = 'apply_adjustments',
+  ApplyAdjustmentsToPaths = 'apply_adjustments_to_paths',
+  ApplyAutoAdjustmentsToPaths = 'apply_auto_adjustments_to_paths',
+  ApplyDenoising = 'apply_denoising',
+  CalculateAutoAdjustments = 'calculate_auto_adjustments',
+  CancelExport = 'cancel_export',
+  CheckAIConnectorStatus = 'check_ai_connector_status',
+  ClearAllSidecars = 'clear_all_sidecars',
+  ClearAiTags = 'clear_ai_tags',
+  ClearAllTags = 'clear_all_tags',
+  ClearThumbnailCache = 'clear_thumbnail_cache',
+  CopyFiles = 'copy_files',
+  CreateFolder = 'create_folder',
+  CreateVirtualCopy = 'create_virtual_copy',
+  CullImages = 'cull_images',
+  DeleteFolder = 'delete_folder',
+  DuplicateFile = 'duplicate_file',
+  EstimateExportSizes = 'estimate_export_sizes',
+  ExportImages = 'export_images',
+  FrontendLog = 'frontend_log',
+  GenerateAiForegroundMask = 'generate_ai_foreground_mask',
+  GenerateAiSkyMask = 'generate_ai_sky_mask',
+  GenerateAiSubjectMask = 'generate_ai_subject_mask',
+  GenerateAiRating = 'generate_ai_rating',
+  GenerateAiRatingsBatch = 'generate_ai_ratings_batch',
+  GenerateFullscreenPreview = 'generate_fullscreen_preview',
+  GeneratePreviewForPath = 'generate_preview_for_path',
+  GenerateMaskOverlay = 'generate_mask_overlay',
+  GeneratePresetPreview = 'generate_preset_preview',
+  GenerateThumbnailsProgressive = 'generate_thumbnails_progressive',
+  GenerateUncroppedPreview = 'generate_uncropped_preview',
+  GetFolderTree = 'get_folder_tree',
+  GetFolderChildren = 'get_folder_children',
+  GetLogFilePath = 'get_log_file_path',
+  GetOrCreateInternalLibraryRoot = 'get_or_create_internal_library_root',
+  GetPinnedFolderTrees = 'get_pinned_folder_trees',
+  GetSupportedFileTypes = 'get_supported_file_types',
+  HandleExportPresetsToFile = 'handle_export_presets_to_file',
+  HandleImportPresetsFromFile = 'handle_import_presets_from_file',
+  HandleImportLegacyPresetsFromFile = 'handle_import_legacy_presets_from_file',
+  ImportFiles = 'import_files',
+  InvokeGenerativeReplace = 'invoke_generative_replace',
+  InvokeGenerativeReplaseWithMaskDef = 'invoke_generative_replace_with_mask_def',
+  ListImagesInDir = 'list_images_in_dir',
+  ListImagesRecursive = 'list_images_recursive',
+  LoadImage = 'load_image',
+  LoadMetadata = 'load_metadata',
+  LoadPresets = 'load_presets',
+  LoadSettings = 'load_settings',
+  MoveFiles = 'move_files',
+  ReadExifForPaths = 'read_exif_for_paths',
+  RemoveTagForPaths = 'remove_tag_for_paths',
+  RenameFiles = 'rename_files',
+  RenameFolder = 'rename_folder',
+  ResetAdjustmentsForPaths = 'reset_adjustments_for_paths',
+  SaveMetadataAndUpdateThumbnail = 'save_metadata_and_update_thumbnail',
+  SaveCollage = 'save_collage',
+  SaveDenoisedImage = 'save_denoised_image',
+  SavePanorama = 'save_panorama',
+  SaveHdr = 'save_hdr',
+  SavePresets = 'save_presets',
+  SaveSettings = 'save_settings',
+  SetColorLabelForPaths = 'set_color_label_for_paths',
+  SetRatingForPaths = 'set_rating_for_paths',
+  ShowInFinder = 'show_in_finder',
+  StartBackgroundIndexing = 'start_background_indexing',
+  StitchPanorama = 'stitch_panorama',
+  MergeHdr = 'merge_hdr',
+  TestAIConnectorConnection = 'test_ai_connector_connection',
+  UpdateWgpuTransform = 'update_wgpu_transform',
+  UpdateExifFields = 'update_exif_fields',
+  FetchCommunityPresets = 'fetch_community_presets',
+  GenerateAllCommunityPreviews = 'generate_all_community_previews',
+  SaveCommunityPreset = 'save_community_preset',
+  SaveTempFile = 'save_temp_file',
+  GetAlbums = 'get_albums',
+  SaveAlbums = 'save_albums',
+  AddToAlbum = 'add_to_album',
+  GetAlbumImages = 'get_album_images',
+}
+ 
+export enum ExifOverlay {
+  Off = 'off',
+  Hover = 'hover',
+  Always = 'always',
+}
+ 
+export enum Panel {
+  Adjustments = 'adjustments',
+  Ai = 'ai',
+  Color = 'color',
+  Crop = 'crop',
+  Export = 'export',
+  Masks = 'masks',
+  Metadata = 'metadata',
+  Portrait = 'portrait',
+  Presets = 'presets',
+}
+ 
+export enum RawStatus {
+  All = 'all',
+  NonRawOnly = 'nonRawOnly',
+  RawOnly = 'rawOnly',
+  RawOverNonRaw = 'rawOverNonRaw',
+}
+ 
+export enum SortDirection {
+  Ascending = 'asc',
+  Descending = 'desc',
+}
+ 
+export type FolderSortKey = 'name' | 'modified' | 'created' | 'imageCount';
+ 
+export interface FolderTreeSort {
+  key: FolderSortKey;
+  order: SortDirection;
+}
+ 
+export enum Theme {
+  Arctic = 'arctic',
+  Blue = 'blue',
+  Dark = 'dark',
+  Grey = 'grey',
+  Light = 'light',
+  MutedGreen = 'muted-green',
+  Sepia = 'sepia',
+  Snow = 'snow',
+}
+ 
+export enum ThumbnailAspectRatio {
+  Cover = 'cover',
+  Contain = 'contain',
+}
+ 
+export interface AppSettings {
+  aiConnectorAddress?: string;
+  aiProvider?: string;
+  decorations?: any;
+  editorPreviewResolution?: number;
+  enableZoomHifi?: boolean;
+  useFullDpiRendering?: boolean;
+  highResZoomMultiplier?: number;
+  enableLivePreviews?: boolean;
+  livePreviewQuality?: string;
+  enableAiTagging?: boolean;
+  filterCriteria?: FilterCriteria;
+  lastFolderState?: any;
+  pinnedFolders?: any;
+  rootFolders?: string[];
+  taggingShortcuts?: string[];
+  lastRootPath: string | null;
+  libraryViewMode?: LibraryViewMode;
+  sortCriteria?: SortCriteria;
+  theme: Theme;
+  thumbnailSize?: ThumbnailSize;
+  thumbnailAspectRatio?: ThumbnailAspectRatio;
+  uiVisibility?: UiVisibility;
+  adjustmentVisibility?: { [key: string]: boolean };
+  rawHighlightCompression?: number;
+  processingBackend?: string;
+  linuxGpuOptimization?: boolean;
+  exportPresets?: ExportPreset[];
+  myLenses?: any;
+  enableFolderImageCounts?: boolean;
+  displayEditIcon?: boolean;
+  linearRawMode?: string;
+  enableXmpSync?: boolean;
+  createXmpIfMissing?: boolean;
+  isWaveformVisible?: boolean;
+  waveformHeight?: number;
+  activeWaveformChannel?: string;
+  useWgpuRenderer?: boolean;
+  canvasInputMode?: 'mouse' | 'trackpad';
+  zoomSpeedMultiplier?: number;
+  keybinds?: { [action: string]: string[] };
+  tonemapperOverrideEnabled?: boolean;
+  defaultRawTonemapper?: string;
+  defaultNonRawTonemapper?: string;
+  copyPasteSettings?: CopyPasteSettings;
+  enableFocusMode?: boolean;
+  openTreeSections?: string[];
+  folderIcons?: Record<string, string>;
+  exifOverlay?: ExifOverlay;
+  language?: string;
+  folderTreeSort?: FolderTreeSort;
+}
+ 
+export interface BrushSettings {
+  feather: number;
+  size: number;
+  tool: ToolType;
+}
+ 
+export enum LibraryViewMode {
+  Flat = 'flat',
+  Recursive = 'recursive',
+}
+ 
+export const EditedStatus = {
+  All: 'all',
+  EditedOnly: 'editedOnly',
+  UneditedOnly: 'uneditedOnly',
+} as const;
+ 
+export type EditedStatus = (typeof EditedStatus)[keyof typeof EditedStatus];
+ 
+export interface FilterCriteria {
+  colors: Array<string>;
+  rating: number;
+  rawStatus: RawStatus;
+  editedStatus?: EditedStatus;
+}
+ 
+export interface Folder {
+  children: any;
+  id?: string | undefined;
+  name?: string | undefined;
+  imageCount?: number;
+}
+ 
+export interface ImageFile {
+  is_edited: boolean;
+  modified: number;
+  path: string;
+  rating: number;
+  tags: Array<string> | null;
+  exif: { [key: string]: string } | null;
+  is_virtual_copy: boolean;
+  is_cloud_placeholder: boolean;
+}
+ 
+export interface Option {
+  color?: string;
+  disabled?: boolean;
+  icon?: any;
+  isDestructive?: boolean;
+  label?: string;
+  onClick?(): void;
+  onRightClick?(): void;
+  submenu?: any;
+  type?: string;
+}
+ 
+export enum Orientation {
+  Horizontal = 'horizontal',
+  Vertical = 'vertical',
+}
+ 
+export interface Preset {
+  adjustments: Partial<Adjustments>;
+  folder?: Folder;
+  id: string;
+  name: string;
+  includeMasks?: boolean;
+  includeCropTransform?: boolean;
+  presetType?: 'tool' | 'style' | 'portrait' | 'color' | 'ai-color' | 'combined';
+}
+ 
+export interface Progress {
+  completed?: number;
+  current?: number;
+  total: number;
+}
+ 
+export interface SelectedImage {
+  exif: any;
+  height: number;
+  isRaw: boolean;
+  isReady: boolean;
+  metadata?: any;
+  original_base64?: string;
+  originalUrl: string | null;
+  path: string;
+  thumbnailUrl: string;
+  width: number;
+}
+ 
+export interface SortCriteria {
+  key: string;
+  label?: string;
+  order: string;
+}
+ 
+export interface SupportedTypes {
+  nonRaw: Array<string>;
+  raw: Array<string>;
+}
+ 
+export enum ThumbnailSize {
+  Large = 'large',
+  Medium = 'medium',
+  Small = 'small',
+  List = 'list',
+}
+ 
+export interface TransformState {
+  positionX: number;
+  positionY: number;
+  scale: number;
+}
+ 
+export interface UiVisibility {
+  folderTree: boolean;
+  filmstrip: boolean;
+}
+ 
+export interface WaveformData {
+  blue: string;
+  green: string;
+  height: number;
+  luma: string;
+  red: string;
+  rgb: string;
+  parade: string;
+  vectorscope: string;
+  width: number;
+}
+ 
+export interface CullingSettings {
+  similarityThreshold: number;
+  blurThreshold: number;
+  groupSimilar: boolean;
+  filterBlurry: boolean;
+}
+ 
+export interface ImageAnalysisResult {
+  path: string;
+  qualityScore: number;
+  sharpnessMetric: number;
+  centerFocusMetric: number;
+  exposureMetric: number;
+  width: number;
+  height: number;
+}
+ 
+export interface CullGroup {
+  representative: ImageAnalysisResult;
+  duplicates: ImageAnalysisResult[];
+}
+ 
+export interface CullingSuggestions {
+  similarGroups: CullGroup[];
+  blurryImages: ImageAnalysisResult[];
+  failedPaths: string[];
+}
+ 
+export interface KeybindHandler {
+  shouldFire?: () => boolean;
+  execute: (event: KeyboardEvent) => void;
+}
+ 
+export type AlbumItem = Album | AlbumGroup;
+ 
+export interface Album {
+  type: 'album';
+  id: string;
+  name: string;
+  icon?: string;
+  images: string[];
+}
+ 
+export interface AlbumGroup {
+  type: 'group';
+  id: string;
+  name: string;
+  icon?: string;
+  children: AlbumItem[];
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/components/ui/Text.tsx.html b/coverage/components/ui/Text.tsx.html new file mode 100644 index 0000000000..84b935cc43 --- /dev/null +++ b/coverage/components/ui/Text.tsx.html @@ -0,0 +1,214 @@ + + + + + + Code coverage report for components/ui/Text.tsx + + + + + + + + + +
+
+

All files / components/ui Text.tsx

+
+ +
+ 100% + Statements + 4/4 +
+ + +
+ 100% + Branches + 7/7 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 4/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +25x +  +25x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  + 
import React, { forwardRef } from 'react';
+import clsx from 'clsx';
+import {
+  TextWeight,
+  TextColor,
+  VariantConfig,
+  TEXT_WEIGHT_KEYS,
+  TEXT_COLOR_KEYS,
+  TextVariants,
+} from '../../types/typography';
+ 
+export interface TextProps extends React.HTMLAttributes<HTMLElement> {
+  variant?: VariantConfig;
+  weight?: TextWeight;
+  color?: TextColor;
+  as?: React.ElementType;
+  children: React.ReactNode;
+}
+ 
+export const Text = forwardRef<HTMLElement, TextProps>(
+  ({ variant = TextVariants.body, weight, color, as, className, children, ...props }, ref) => {
+    const Component = as || variant.defaultElement;
+ 
+    return (
+      <Component
+        ref={ref}
+        className={clsx(
+          variant.size,
+          TEXT_WEIGHT_KEYS[weight ?? variant.defaultWeight],
+          TEXT_COLOR_KEYS[color ?? variant.defaultColor],
+          variant.extraClasses,
+          className,
+        )}
+        {...props}
+      >
+        {children}
+      </Component>
+    );
+  },
+);
+ 
+Text.displayName = 'Text';
+export default Text;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/components/ui/index.html b/coverage/components/ui/index.html new file mode 100644 index 0000000000..c93b239184 --- /dev/null +++ b/coverage/components/ui/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for components/ui + + + + + + + + + +
+
+

All files components/ui

+
+ +
+ 98.14% + Statements + 159/162 +
+ + +
+ 85.71% + Branches + 18/21 +
+ + +
+ 100% + Functions + 20/20 +
+ + +
+ 98.73% + Lines + 156/158 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
AndroidBottomNav.tsx +
+
92.85%13/1480%8/10100%5/590.9%10/11
AndroidShareSheet.tsx +
+
86.66%13/1575%3/4100%4/492.85%13/14
AppProperties.tsx +
+
100%129/129100%0/0100%10/10100%129/129
Text.tsx +
+
100%4/4100%7/7100%1/1100%4/4
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json new file mode 100644 index 0000000000..aeea068f12 --- /dev/null +++ b/coverage/coverage-final.json @@ -0,0 +1,11 @@ +{"/workspace/src/components/panel/right/Masks.tsx": {"path":"/workspace/src/components/panel/right/Masks.tsx","statementMap":{"0":{"start":{"line":20,"column":7},"end":{"line":35,"column":null}},"1":{"start":{"line":21,"column":2},"end":{"line":21,"column":null}},"2":{"start":{"line":22,"column":2},"end":{"line":22,"column":null}},"3":{"start":{"line":23,"column":2},"end":{"line":23,"column":null}},"4":{"start":{"line":24,"column":2},"end":{"line":24,"column":null}},"5":{"start":{"line":25,"column":2},"end":{"line":25,"column":null}},"6":{"start":{"line":26,"column":2},"end":{"line":26,"column":null}},"7":{"start":{"line":27,"column":2},"end":{"line":27,"column":null}},"8":{"start":{"line":28,"column":2},"end":{"line":28,"column":null}},"9":{"start":{"line":29,"column":2},"end":{"line":29,"column":null}},"10":{"start":{"line":30,"column":2},"end":{"line":30,"column":null}},"11":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"12":{"start":{"line":32,"column":2},"end":{"line":32,"column":null}},"13":{"start":{"line":33,"column":2},"end":{"line":33,"column":null}},"14":{"start":{"line":34,"column":2},"end":{"line":34,"column":null}},"15":{"start":{"line":37,"column":7},"end":{"line":41,"column":null}},"16":{"start":{"line":38,"column":2},"end":{"line":38,"column":null}},"17":{"start":{"line":39,"column":2},"end":{"line":39,"column":null}},"18":{"start":{"line":40,"column":2},"end":{"line":40,"column":null}},"19":{"start":{"line":43,"column":7},"end":{"line":49,"column":null}},"20":{"start":{"line":44,"column":2},"end":{"line":44,"column":null}},"21":{"start":{"line":45,"column":2},"end":{"line":45,"column":null}},"22":{"start":{"line":46,"column":2},"end":{"line":46,"column":null}},"23":{"start":{"line":47,"column":2},"end":{"line":47,"column":null}},"24":{"start":{"line":48,"column":2},"end":{"line":48,"column":null}},"25":{"start":{"line":71,"column":2},"end":{"line":71,"column":null}},"26":{"start":{"line":71,"column":29},"end":{"line":71,"column":null}},"27":{"start":{"line":72,"column":2},"end":{"line":72,"column":null}},"28":{"start":{"line":72,"column":31},"end":{"line":72,"column":null}},"29":{"start":{"line":73,"column":2},"end":{"line":73,"column":null}},"30":{"start":{"line":73,"column":34},"end":{"line":73,"column":null}},"31":{"start":{"line":74,"column":2},"end":{"line":74,"column":null}},"32":{"start":{"line":74,"column":27},"end":{"line":74,"column":null}},"33":{"start":{"line":75,"column":2},"end":{"line":75,"column":null}},"34":{"start":{"line":75,"column":25},"end":{"line":75,"column":null}},"35":{"start":{"line":76,"column":2},"end":{"line":76,"column":null}},"36":{"start":{"line":76,"column":33},"end":{"line":76,"column":null}},"37":{"start":{"line":77,"column":2},"end":{"line":77,"column":null}},"38":{"start":{"line":77,"column":27},"end":{"line":77,"column":null}},"39":{"start":{"line":78,"column":2},"end":{"line":78,"column":null}},"40":{"start":{"line":78,"column":26},"end":{"line":78,"column":null}},"41":{"start":{"line":79,"column":2},"end":{"line":79,"column":null}},"42":{"start":{"line":79,"column":27},"end":{"line":79,"column":null}},"43":{"start":{"line":80,"column":2},"end":{"line":80,"column":null}},"44":{"start":{"line":80,"column":28},"end":{"line":80,"column":null}},"45":{"start":{"line":81,"column":2},"end":{"line":81,"column":null}},"46":{"start":{"line":81,"column":31},"end":{"line":81,"column":null}},"47":{"start":{"line":82,"column":2},"end":{"line":82,"column":null}},"48":{"start":{"line":82,"column":28},"end":{"line":82,"column":null}},"49":{"start":{"line":83,"column":2},"end":{"line":83,"column":null}},"50":{"start":{"line":83,"column":27},"end":{"line":83,"column":null}},"51":{"start":{"line":84,"column":2},"end":{"line":84,"column":null}},"52":{"start":{"line":84,"column":26},"end":{"line":84,"column":null}},"53":{"start":{"line":85,"column":2},"end":{"line":85,"column":null}},"54":{"start":{"line":89,"column":2},"end":{"line":89,"column":null}},"55":{"start":{"line":89,"column":28},"end":{"line":89,"column":null}},"56":{"start":{"line":90,"column":2},"end":{"line":92,"column":null}},"57":{"start":{"line":91,"column":4},"end":{"line":91,"column":null}},"58":{"start":{"line":93,"column":2},"end":{"line":93,"column":null}},"59":{"start":{"line":97,"column":2},"end":{"line":97,"column":null}},"60":{"start":{"line":100,"column":48},"end":{"line":115,"column":null}},"61":{"start":{"line":117,"column":58},"end":{"line":155,"column":null}},"62":{"start":{"line":157,"column":56},"end":{"line":170,"column":null}},"63":{"start":{"line":172,"column":61},"end":{"line":209,"column":null}},"64":{"start":{"line":211,"column":57},"end":{"line":249,"column":null}},"65":{"start":{"line":251,"column":50},"end":{"line":288,"column":null}},"66":{"start":{"line":290,"column":60},"end":{"line":293,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":20,"column":7},"end":{"line":20,"column":12}},"loc":{"start":{"line":20,"column":7},"end":{"line":35,"column":null}},"line":20},"1":{"name":"(anonymous_1)","decl":{"start":{"line":37,"column":7},"end":{"line":37,"column":12}},"loc":{"start":{"line":37,"column":7},"end":{"line":41,"column":null}},"line":37},"2":{"name":"(anonymous_2)","decl":{"start":{"line":43,"column":7},"end":{"line":43,"column":12}},"loc":{"start":{"line":43,"column":7},"end":{"line":49,"column":null}},"line":43},"3":{"name":"formatMaskTypeName","decl":{"start":{"line":70,"column":16},"end":{"line":70,"column":35}},"loc":{"start":{"line":70,"column":49},"end":{"line":86,"column":null}},"line":70},"4":{"name":"getMaskTypeName","decl":{"start":{"line":88,"column":16},"end":{"line":88,"column":32}},"loc":{"start":{"line":88,"column":48},"end":{"line":94,"column":null}},"line":88},"5":{"name":"getSubMaskName","decl":{"start":{"line":96,"column":16},"end":{"line":96,"column":31}},"loc":{"start":{"line":96,"column":72},"end":{"line":98,"column":null}},"line":96}},"branchMap":{"0":{"loc":{"start":{"line":71,"column":2},"end":{"line":71,"column":null}},"type":"if","locations":[{"start":{"line":71,"column":2},"end":{"line":71,"column":null}},{"start":{},"end":{}}],"line":71},"1":{"loc":{"start":{"line":72,"column":2},"end":{"line":72,"column":null}},"type":"if","locations":[{"start":{"line":72,"column":2},"end":{"line":72,"column":null}},{"start":{},"end":{}}],"line":72},"2":{"loc":{"start":{"line":73,"column":2},"end":{"line":73,"column":null}},"type":"if","locations":[{"start":{"line":73,"column":2},"end":{"line":73,"column":null}},{"start":{},"end":{}}],"line":73},"3":{"loc":{"start":{"line":74,"column":2},"end":{"line":74,"column":null}},"type":"if","locations":[{"start":{"line":74,"column":2},"end":{"line":74,"column":null}},{"start":{},"end":{}}],"line":74},"4":{"loc":{"start":{"line":75,"column":2},"end":{"line":75,"column":null}},"type":"if","locations":[{"start":{"line":75,"column":2},"end":{"line":75,"column":null}},{"start":{},"end":{}}],"line":75},"5":{"loc":{"start":{"line":76,"column":2},"end":{"line":76,"column":null}},"type":"if","locations":[{"start":{"line":76,"column":2},"end":{"line":76,"column":null}},{"start":{},"end":{}}],"line":76},"6":{"loc":{"start":{"line":77,"column":2},"end":{"line":77,"column":null}},"type":"if","locations":[{"start":{"line":77,"column":2},"end":{"line":77,"column":null}},{"start":{},"end":{}}],"line":77},"7":{"loc":{"start":{"line":78,"column":2},"end":{"line":78,"column":null}},"type":"if","locations":[{"start":{"line":78,"column":2},"end":{"line":78,"column":null}},{"start":{},"end":{}}],"line":78},"8":{"loc":{"start":{"line":79,"column":2},"end":{"line":79,"column":null}},"type":"if","locations":[{"start":{"line":79,"column":2},"end":{"line":79,"column":null}},{"start":{},"end":{}}],"line":79},"9":{"loc":{"start":{"line":80,"column":2},"end":{"line":80,"column":null}},"type":"if","locations":[{"start":{"line":80,"column":2},"end":{"line":80,"column":null}},{"start":{},"end":{}}],"line":80},"10":{"loc":{"start":{"line":81,"column":2},"end":{"line":81,"column":null}},"type":"if","locations":[{"start":{"line":81,"column":2},"end":{"line":81,"column":null}},{"start":{},"end":{}}],"line":81},"11":{"loc":{"start":{"line":82,"column":2},"end":{"line":82,"column":null}},"type":"if","locations":[{"start":{"line":82,"column":2},"end":{"line":82,"column":null}},{"start":{},"end":{}}],"line":82},"12":{"loc":{"start":{"line":83,"column":2},"end":{"line":83,"column":null}},"type":"if","locations":[{"start":{"line":83,"column":2},"end":{"line":83,"column":null}},{"start":{},"end":{}}],"line":83},"13":{"loc":{"start":{"line":84,"column":2},"end":{"line":84,"column":null}},"type":"if","locations":[{"start":{"line":84,"column":2},"end":{"line":84,"column":null}},{"start":{},"end":{}}],"line":84},"14":{"loc":{"start":{"line":89,"column":2},"end":{"line":89,"column":null}},"type":"if","locations":[{"start":{"line":89,"column":2},"end":{"line":89,"column":null}},{"start":{},"end":{}}],"line":89},"15":{"loc":{"start":{"line":90,"column":2},"end":{"line":92,"column":null}},"type":"if","locations":[{"start":{"line":90,"column":2},"end":{"line":92,"column":null}},{"start":{},"end":{}}],"line":90},"16":{"loc":{"start":{"line":90,"column":6},"end":{"line":90,"column":69}},"type":"binary-expr","locations":[{"start":{"line":90,"column":6},"end":{"line":90,"column":40}},{"start":{"line":90,"column":40},"end":{"line":90,"column":69}}],"line":90},"17":{"loc":{"start":{"line":97,"column":9},"end":{"line":97,"column":null}},"type":"binary-expr","locations":[{"start":{"line":97,"column":9},"end":{"line":97,"column":33}},{"start":{"line":97,"column":33},"end":{"line":97,"column":null}}],"line":97}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":1,"61":1,"62":1,"63":1,"64":1,"65":1,"66":1},"f":{"0":1,"1":1,"2":1,"3":0,"4":0,"5":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0],"8":[0,0],"9":[0,0],"10":[0,0],"11":[0,0],"12":[0,0],"13":[0,0],"14":[0,0],"15":[0,0],"16":[0,0],"17":[0,0]},"meta":{"lastBranch":18,"lastFunction":6,"lastStatement":67,"seen":{"s:20:7:35:Infinity":0,"f:20:7:20:12":0,"s:21:2:21:Infinity":1,"s:22:2:22:Infinity":2,"s:23:2:23:Infinity":3,"s:24:2:24:Infinity":4,"s:25:2:25:Infinity":5,"s:26:2:26:Infinity":6,"s:27:2:27:Infinity":7,"s:28:2:28:Infinity":8,"s:29:2:29:Infinity":9,"s:30:2:30:Infinity":10,"s:31:2:31:Infinity":11,"s:32:2:32:Infinity":12,"s:33:2:33:Infinity":13,"s:34:2:34:Infinity":14,"s:37:7:41:Infinity":15,"f:37:7:37:12":1,"s:38:2:38:Infinity":16,"s:39:2:39:Infinity":17,"s:40:2:40:Infinity":18,"s:43:7:49:Infinity":19,"f:43:7:43:12":2,"s:44:2:44:Infinity":20,"s:45:2:45:Infinity":21,"s:46:2:46:Infinity":22,"s:47:2:47:Infinity":23,"s:48:2:48:Infinity":24,"f:70:16:70:35":3,"b:71:2:71:Infinity:undefined:undefined:undefined:undefined":0,"s:71:2:71:Infinity":25,"s:71:29:71:Infinity":26,"b:72:2:72:Infinity:undefined:undefined:undefined:undefined":1,"s:72:2:72:Infinity":27,"s:72:31:72:Infinity":28,"b:73:2:73:Infinity:undefined:undefined:undefined:undefined":2,"s:73:2:73:Infinity":29,"s:73:34:73:Infinity":30,"b:74:2:74:Infinity:undefined:undefined:undefined:undefined":3,"s:74:2:74:Infinity":31,"s:74:27:74:Infinity":32,"b:75:2:75:Infinity:undefined:undefined:undefined:undefined":4,"s:75:2:75:Infinity":33,"s:75:25:75:Infinity":34,"b:76:2:76:Infinity:undefined:undefined:undefined:undefined":5,"s:76:2:76:Infinity":35,"s:76:33:76:Infinity":36,"b:77:2:77:Infinity:undefined:undefined:undefined:undefined":6,"s:77:2:77:Infinity":37,"s:77:27:77:Infinity":38,"b:78:2:78:Infinity:undefined:undefined:undefined:undefined":7,"s:78:2:78:Infinity":39,"s:78:26:78:Infinity":40,"b:79:2:79:Infinity:undefined:undefined:undefined:undefined":8,"s:79:2:79:Infinity":41,"s:79:27:79:Infinity":42,"b:80:2:80:Infinity:undefined:undefined:undefined:undefined":9,"s:80:2:80:Infinity":43,"s:80:28:80:Infinity":44,"b:81:2:81:Infinity:undefined:undefined:undefined:undefined":10,"s:81:2:81:Infinity":45,"s:81:31:81:Infinity":46,"b:82:2:82:Infinity:undefined:undefined:undefined:undefined":11,"s:82:2:82:Infinity":47,"s:82:28:82:Infinity":48,"b:83:2:83:Infinity:undefined:undefined:undefined:undefined":12,"s:83:2:83:Infinity":49,"s:83:27:83:Infinity":50,"b:84:2:84:Infinity:undefined:undefined:undefined:undefined":13,"s:84:2:84:Infinity":51,"s:84:26:84:Infinity":52,"s:85:2:85:Infinity":53,"f:88:16:88:32":4,"b:89:2:89:Infinity:undefined:undefined:undefined:undefined":14,"s:89:2:89:Infinity":54,"s:89:28:89:Infinity":55,"b:90:2:92:Infinity:undefined:undefined:undefined:undefined":15,"s:90:2:92:Infinity":56,"b:90:6:90:40:90:40:90:69":16,"s:91:4:91:Infinity":57,"s:93:2:93:Infinity":58,"f:96:16:96:31":5,"s:97:2:97:Infinity":59,"b:97:9:97:33:97:33:97:Infinity":17,"s:100:48:115:Infinity":60,"s:117:58:155:Infinity":61,"s:157:56:170:Infinity":62,"s:172:61:209:Infinity":63,"s:211:57:249:Infinity":64,"s:251:50:288:Infinity":65,"s:290:60:293:Infinity":66},"fnNames":{}}} +,"/workspace/src/components/ui/AndroidBottomNav.tsx": {"path":"/workspace/src/components/ui/AndroidBottomNav.tsx","statementMap":{"0":{"start":{"line":18,"column":28},"end":{"line":24,"column":null}},"1":{"start":{"line":27,"column":10},"end":{"line":27,"column":null}},"2":{"start":{"line":28,"column":8},"end":{"line":28,"column":null}},"3":{"start":{"line":28,"column":45},"end":{"line":28,"column":63}},"4":{"start":{"line":29,"column":8},"end":{"line":29,"column":null}},"5":{"start":{"line":29,"column":42},"end":{"line":29,"column":57}},"6":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"7":{"start":{"line":31,"column":18},"end":{"line":31,"column":null}},"8":{"start":{"line":33,"column":2},"end":{"line":57,"column":null}},"9":{"start":{"line":36,"column":25},"end":{"line":36,"column":null}},"10":{"start":{"line":37,"column":8},"end":{"line":54,"column":null}},"11":{"start":{"line":45,"column":14},"end":{"line":49,"column":null}},"12":{"start":{"line":46,"column":16},"end":{"line":46,"column":null}},"13":{"start":{"line":48,"column":16},"end":{"line":48,"column":null}}},"fnMap":{"0":{"name":"AndroidBottomNav","decl":{"start":{"line":26,"column":24},"end":{"line":26,"column":41}},"loc":{"start":{"line":26,"column":79},"end":{"line":59,"column":null}},"line":26},"1":{"name":"(anonymous_1)","decl":{"start":{"line":28,"column":27},"end":{"line":28,"column":39}},"loc":{"start":{"line":28,"column":45},"end":{"line":28,"column":63}},"line":28},"2":{"name":"(anonymous_2)","decl":{"start":{"line":29,"column":24},"end":{"line":29,"column":36}},"loc":{"start":{"line":29,"column":42},"end":{"line":29,"column":57}},"line":29},"3":{"name":"(anonymous_3)","decl":{"start":{"line":35,"column":16},"end":{"line":35,"column":21}},"loc":{"start":{"line":35,"column":57},"end":{"line":56,"column":7}},"line":35},"4":{"name":"(anonymous_4)","decl":{"start":{"line":44,"column":12},"end":{"line":44,"column":27}},"loc":{"start":{"line":44,"column":27},"end":{"line":50,"column":null}},"line":44}},"branchMap":{"0":{"loc":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"type":"if","locations":[{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},{"start":{},"end":{}}],"line":31},"1":{"loc":{"start":{"line":36,"column":25},"end":{"line":36,"column":null}},"type":"cond-expr","locations":[{"start":{"line":36,"column":33},"end":{"line":36,"column":62}},{"start":{"line":36,"column":62},"end":{"line":36,"column":null}}],"line":36},"2":{"loc":{"start":{"line":42,"column":14},"end":{"line":42,"column":null}},"type":"cond-expr","locations":[{"start":{"line":42,"column":25},"end":{"line":42,"column":41}},{"start":{"line":42,"column":41},"end":{"line":42,"column":null}}],"line":42},"3":{"loc":{"start":{"line":45,"column":14},"end":{"line":49,"column":null}},"type":"if","locations":[{"start":{"line":45,"column":14},"end":{"line":49,"column":null}},{"start":{"line":47,"column":21},"end":{"line":49,"column":null}}],"line":45},"4":{"loc":{"start":{"line":48,"column":30},"end":{"line":48,"column":71}},"type":"cond-expr","locations":[{"start":{"line":48,"column":59},"end":{"line":48,"column":66}},{"start":{"line":48,"column":66},"end":{"line":48,"column":71}}],"line":48}},"s":{"0":1,"1":3,"2":3,"3":2,"4":3,"5":2,"6":3,"7":1,"8":2,"9":10,"10":10,"11":1,"12":0,"13":1},"f":{"0":3,"1":2,"2":2,"3":10,"4":1},"b":{"0":[1,2],"1":[8,2],"2":[2,8],"3":[0,1],"4":[0,1]},"meta":{"lastBranch":5,"lastFunction":5,"lastStatement":14,"seen":{"s:18:28:24:Infinity":0,"f:26:24:26:41":0,"s:27:10:27:Infinity":1,"s:28:8:28:Infinity":2,"f:28:27:28:39":1,"s:28:45:28:63":3,"s:29:8:29:Infinity":4,"f:29:24:29:36":2,"s:29:42:29:57":5,"b:31:2:31:Infinity:undefined:undefined:undefined:undefined":0,"s:31:2:31:Infinity":6,"s:31:18:31:Infinity":7,"s:33:2:57:Infinity":8,"f:35:16:35:21":3,"s:36:25:36:Infinity":9,"b:36:33:36:62:36:62:36:Infinity":1,"s:37:8:54:Infinity":10,"b:42:25:42:41:42:41:42:Infinity":2,"f:44:12:44:27":4,"b:45:14:49:Infinity:47:21:49:Infinity":3,"s:45:14:49:Infinity":11,"s:46:16:46:Infinity":12,"s:48:16:48:Infinity":13,"b:48:59:48:66:48:66:48:71":4},"fnNames":{}}} +,"/workspace/src/components/ui/AndroidShareSheet.tsx": {"path":"/workspace/src/components/ui/AndroidShareSheet.tsx","statementMap":{"0":{"start":{"line":22,"column":37},"end":{"line":27,"column":null}},"1":{"start":{"line":35,"column":10},"end":{"line":35,"column":null}},"2":{"start":{"line":36,"column":18},"end":{"line":36,"column":null}},"3":{"start":{"line":38,"column":8},"end":{"line":56,"column":null}},"4":{"start":{"line":40,"column":6},"end":{"line":40,"column":null}},"5":{"start":{"line":40,"column":19},"end":{"line":40,"column":null}},"6":{"start":{"line":41,"column":6},"end":{"line":41,"column":null}},"7":{"start":{"line":42,"column":6},"end":{"line":53,"column":null}},"8":{"start":{"line":43,"column":8},"end":{"line":47,"column":null}},"9":{"start":{"line":49,"column":8},"end":{"line":49,"column":null}},"10":{"start":{"line":51,"column":8},"end":{"line":51,"column":null}},"11":{"start":{"line":52,"column":8},"end":{"line":52,"column":null}},"12":{"start":{"line":58,"column":2},"end":{"line":115,"column":null}},"13":{"start":{"line":88,"column":18},"end":{"line":100,"column":null}},"14":{"start":{"line":90,"column":35},"end":{"line":90,"column":null}}},"fnMap":{"0":{"name":"AndroidShareSheet","decl":{"start":{"line":29,"column":24},"end":{"line":29,"column":42}},"loc":{"start":{"line":34,"column":27},"end":{"line":117,"column":null}},"line":34},"1":{"name":"(anonymous_1)","decl":{"start":{"line":39,"column":4},"end":{"line":39,"column":11}},"loc":{"start":{"line":39,"column":32},"end":{"line":54,"column":null}},"line":39},"2":{"name":"(anonymous_2)","decl":{"start":{"line":87,"column":31},"end":{"line":87,"column":36}},"loc":{"start":{"line":88,"column":18},"end":{"line":100,"column":null}},"line":88},"3":{"name":"(anonymous_3)","decl":{"start":{"line":90,"column":20},"end":{"line":90,"column":35}},"loc":{"start":{"line":90,"column":35},"end":{"line":90,"column":null}},"line":90}},"branchMap":{"0":{"loc":{"start":{"line":40,"column":6},"end":{"line":40,"column":null}},"type":"if","locations":[{"start":{"line":40,"column":6},"end":{"line":40,"column":null}},{"start":{},"end":{}}],"line":40},"1":{"loc":{"start":{"line":60,"column":7},"end":{"line":115,"column":null}},"type":"binary-expr","locations":[{"start":{"line":60,"column":7},"end":{"line":60,"column":null}},{"start":{"line":61,"column":8},"end":{"line":115,"column":null}}],"line":60}},"s":{"0":1,"1":6,"2":6,"3":6,"4":1,"5":0,"6":1,"7":1,"8":1,"9":0,"10":1,"11":1,"12":6,"13":20,"14":1},"f":{"0":6,"1":1,"2":20,"3":1},"b":{"0":[0,1],"1":[6,5]},"meta":{"lastBranch":2,"lastFunction":4,"lastStatement":15,"seen":{"s:22:37:27:Infinity":0,"f:29:24:29:42":0,"s:35:10:35:Infinity":1,"s:36:18:36:Infinity":2,"s:38:8:56:Infinity":3,"f:39:4:39:11":1,"b:40:6:40:Infinity:undefined:undefined:undefined:undefined":0,"s:40:6:40:Infinity":4,"s:40:19:40:Infinity":5,"s:41:6:41:Infinity":6,"s:42:6:53:Infinity":7,"s:43:8:47:Infinity":8,"s:49:8:49:Infinity":9,"s:51:8:51:Infinity":10,"s:52:8:52:Infinity":11,"s:58:2:115:Infinity":12,"b:60:7:60:Infinity:61:8:115:Infinity":1,"f:87:31:87:36":2,"s:88:18:100:Infinity":13,"f:90:20:90:35":3,"s:90:35:90:Infinity":14},"fnNames":{}}} +,"/workspace/src/components/ui/AppProperties.tsx": {"path":"/workspace/src/components/ui/AppProperties.tsx","statementMap":{"0":{"start":{"line":5,"column":27},"end":{"line":29,"column":null}},"1":{"start":{"line":30,"column":32},"end":{"line":30,"column":null}},"2":{"start":{"line":32,"column":7},"end":{"line":113,"column":null}},"3":{"start":{"line":33,"column":2},"end":{"line":33,"column":null}},"4":{"start":{"line":34,"column":2},"end":{"line":34,"column":null}},"5":{"start":{"line":35,"column":2},"end":{"line":35,"column":null}},"6":{"start":{"line":36,"column":2},"end":{"line":36,"column":null}},"7":{"start":{"line":37,"column":2},"end":{"line":37,"column":null}},"8":{"start":{"line":38,"column":2},"end":{"line":38,"column":null}},"9":{"start":{"line":39,"column":2},"end":{"line":39,"column":null}},"10":{"start":{"line":40,"column":2},"end":{"line":40,"column":null}},"11":{"start":{"line":41,"column":2},"end":{"line":41,"column":null}},"12":{"start":{"line":42,"column":2},"end":{"line":42,"column":null}},"13":{"start":{"line":43,"column":2},"end":{"line":43,"column":null}},"14":{"start":{"line":44,"column":2},"end":{"line":44,"column":null}},"15":{"start":{"line":45,"column":2},"end":{"line":45,"column":null}},"16":{"start":{"line":46,"column":2},"end":{"line":46,"column":null}},"17":{"start":{"line":47,"column":2},"end":{"line":47,"column":null}},"18":{"start":{"line":48,"column":2},"end":{"line":48,"column":null}},"19":{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},"20":{"start":{"line":50,"column":2},"end":{"line":50,"column":null}},"21":{"start":{"line":51,"column":2},"end":{"line":51,"column":null}},"22":{"start":{"line":52,"column":2},"end":{"line":52,"column":null}},"23":{"start":{"line":53,"column":2},"end":{"line":53,"column":null}},"24":{"start":{"line":54,"column":2},"end":{"line":54,"column":null}},"25":{"start":{"line":55,"column":2},"end":{"line":55,"column":null}},"26":{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},"27":{"start":{"line":57,"column":2},"end":{"line":57,"column":null}},"28":{"start":{"line":58,"column":2},"end":{"line":58,"column":null}},"29":{"start":{"line":59,"column":2},"end":{"line":59,"column":null}},"30":{"start":{"line":60,"column":2},"end":{"line":60,"column":null}},"31":{"start":{"line":61,"column":2},"end":{"line":61,"column":null}},"32":{"start":{"line":62,"column":2},"end":{"line":62,"column":null}},"33":{"start":{"line":63,"column":2},"end":{"line":63,"column":null}},"34":{"start":{"line":64,"column":2},"end":{"line":64,"column":null}},"35":{"start":{"line":65,"column":2},"end":{"line":65,"column":null}},"36":{"start":{"line":66,"column":2},"end":{"line":66,"column":null}},"37":{"start":{"line":67,"column":2},"end":{"line":67,"column":null}},"38":{"start":{"line":68,"column":2},"end":{"line":68,"column":null}},"39":{"start":{"line":69,"column":2},"end":{"line":69,"column":null}},"40":{"start":{"line":70,"column":2},"end":{"line":70,"column":null}},"41":{"start":{"line":71,"column":2},"end":{"line":71,"column":null}},"42":{"start":{"line":72,"column":2},"end":{"line":72,"column":null}},"43":{"start":{"line":73,"column":2},"end":{"line":73,"column":null}},"44":{"start":{"line":74,"column":2},"end":{"line":74,"column":null}},"45":{"start":{"line":75,"column":2},"end":{"line":75,"column":null}},"46":{"start":{"line":76,"column":2},"end":{"line":76,"column":null}},"47":{"start":{"line":77,"column":2},"end":{"line":77,"column":null}},"48":{"start":{"line":78,"column":2},"end":{"line":78,"column":null}},"49":{"start":{"line":79,"column":2},"end":{"line":79,"column":null}},"50":{"start":{"line":80,"column":2},"end":{"line":80,"column":null}},"51":{"start":{"line":81,"column":2},"end":{"line":81,"column":null}},"52":{"start":{"line":82,"column":2},"end":{"line":82,"column":null}},"53":{"start":{"line":83,"column":2},"end":{"line":83,"column":null}},"54":{"start":{"line":84,"column":2},"end":{"line":84,"column":null}},"55":{"start":{"line":85,"column":2},"end":{"line":85,"column":null}},"56":{"start":{"line":86,"column":2},"end":{"line":86,"column":null}},"57":{"start":{"line":87,"column":2},"end":{"line":87,"column":null}},"58":{"start":{"line":88,"column":2},"end":{"line":88,"column":null}},"59":{"start":{"line":89,"column":2},"end":{"line":89,"column":null}},"60":{"start":{"line":90,"column":2},"end":{"line":90,"column":null}},"61":{"start":{"line":91,"column":2},"end":{"line":91,"column":null}},"62":{"start":{"line":92,"column":2},"end":{"line":92,"column":null}},"63":{"start":{"line":93,"column":2},"end":{"line":93,"column":null}},"64":{"start":{"line":94,"column":2},"end":{"line":94,"column":null}},"65":{"start":{"line":95,"column":2},"end":{"line":95,"column":null}},"66":{"start":{"line":96,"column":2},"end":{"line":96,"column":null}},"67":{"start":{"line":97,"column":2},"end":{"line":97,"column":null}},"68":{"start":{"line":98,"column":2},"end":{"line":98,"column":null}},"69":{"start":{"line":99,"column":2},"end":{"line":99,"column":null}},"70":{"start":{"line":100,"column":2},"end":{"line":100,"column":null}},"71":{"start":{"line":101,"column":2},"end":{"line":101,"column":null}},"72":{"start":{"line":102,"column":2},"end":{"line":102,"column":null}},"73":{"start":{"line":103,"column":2},"end":{"line":103,"column":null}},"74":{"start":{"line":104,"column":2},"end":{"line":104,"column":null}},"75":{"start":{"line":105,"column":2},"end":{"line":105,"column":null}},"76":{"start":{"line":106,"column":2},"end":{"line":106,"column":null}},"77":{"start":{"line":107,"column":2},"end":{"line":107,"column":null}},"78":{"start":{"line":108,"column":2},"end":{"line":108,"column":null}},"79":{"start":{"line":109,"column":2},"end":{"line":109,"column":null}},"80":{"start":{"line":110,"column":2},"end":{"line":110,"column":null}},"81":{"start":{"line":111,"column":2},"end":{"line":111,"column":null}},"82":{"start":{"line":112,"column":2},"end":{"line":112,"column":null}},"83":{"start":{"line":115,"column":7},"end":{"line":119,"column":null}},"84":{"start":{"line":116,"column":2},"end":{"line":116,"column":null}},"85":{"start":{"line":117,"column":2},"end":{"line":117,"column":null}},"86":{"start":{"line":118,"column":2},"end":{"line":118,"column":null}},"87":{"start":{"line":121,"column":7},"end":{"line":131,"column":null}},"88":{"start":{"line":122,"column":2},"end":{"line":122,"column":null}},"89":{"start":{"line":123,"column":2},"end":{"line":123,"column":null}},"90":{"start":{"line":124,"column":2},"end":{"line":124,"column":null}},"91":{"start":{"line":125,"column":2},"end":{"line":125,"column":null}},"92":{"start":{"line":126,"column":2},"end":{"line":126,"column":null}},"93":{"start":{"line":127,"column":2},"end":{"line":127,"column":null}},"94":{"start":{"line":128,"column":2},"end":{"line":128,"column":null}},"95":{"start":{"line":129,"column":2},"end":{"line":129,"column":null}},"96":{"start":{"line":130,"column":2},"end":{"line":130,"column":null}},"97":{"start":{"line":133,"column":7},"end":{"line":138,"column":null}},"98":{"start":{"line":134,"column":2},"end":{"line":134,"column":null}},"99":{"start":{"line":135,"column":2},"end":{"line":135,"column":null}},"100":{"start":{"line":136,"column":2},"end":{"line":136,"column":null}},"101":{"start":{"line":137,"column":2},"end":{"line":137,"column":null}},"102":{"start":{"line":140,"column":7},"end":{"line":143,"column":null}},"103":{"start":{"line":141,"column":2},"end":{"line":141,"column":null}},"104":{"start":{"line":142,"column":2},"end":{"line":142,"column":null}},"105":{"start":{"line":152,"column":7},"end":{"line":161,"column":null}},"106":{"start":{"line":153,"column":2},"end":{"line":153,"column":null}},"107":{"start":{"line":154,"column":2},"end":{"line":154,"column":null}},"108":{"start":{"line":155,"column":2},"end":{"line":155,"column":null}},"109":{"start":{"line":156,"column":2},"end":{"line":156,"column":null}},"110":{"start":{"line":157,"column":2},"end":{"line":157,"column":null}},"111":{"start":{"line":158,"column":2},"end":{"line":158,"column":null}},"112":{"start":{"line":159,"column":2},"end":{"line":159,"column":null}},"113":{"start":{"line":160,"column":2},"end":{"line":160,"column":null}},"114":{"start":{"line":163,"column":7},"end":{"line":166,"column":null}},"115":{"start":{"line":164,"column":2},"end":{"line":164,"column":null}},"116":{"start":{"line":165,"column":2},"end":{"line":165,"column":null}},"117":{"start":{"line":227,"column":7},"end":{"line":230,"column":null}},"118":{"start":{"line":228,"column":2},"end":{"line":228,"column":null}},"119":{"start":{"line":229,"column":2},"end":{"line":229,"column":null}},"120":{"start":{"line":232,"column":28},"end":{"line":236,"column":null}},"121":{"start":{"line":277,"column":7},"end":{"line":280,"column":null}},"122":{"start":{"line":278,"column":2},"end":{"line":278,"column":null}},"123":{"start":{"line":279,"column":2},"end":{"line":279,"column":null}},"124":{"start":{"line":322,"column":7},"end":{"line":327,"column":null}},"125":{"start":{"line":323,"column":2},"end":{"line":323,"column":null}},"126":{"start":{"line":324,"column":2},"end":{"line":324,"column":null}},"127":{"start":{"line":325,"column":2},"end":{"line":325,"column":null}},"128":{"start":{"line":326,"column":2},"end":{"line":326,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":32,"column":7},"end":{"line":32,"column":12}},"loc":{"start":{"line":32,"column":7},"end":{"line":113,"column":null}},"line":32},"1":{"name":"(anonymous_1)","decl":{"start":{"line":115,"column":7},"end":{"line":115,"column":12}},"loc":{"start":{"line":115,"column":7},"end":{"line":119,"column":null}},"line":115},"2":{"name":"(anonymous_2)","decl":{"start":{"line":121,"column":7},"end":{"line":121,"column":12}},"loc":{"start":{"line":121,"column":7},"end":{"line":131,"column":null}},"line":121},"3":{"name":"(anonymous_3)","decl":{"start":{"line":133,"column":7},"end":{"line":133,"column":12}},"loc":{"start":{"line":133,"column":7},"end":{"line":138,"column":null}},"line":133},"4":{"name":"(anonymous_4)","decl":{"start":{"line":140,"column":7},"end":{"line":140,"column":12}},"loc":{"start":{"line":140,"column":7},"end":{"line":143,"column":null}},"line":140},"5":{"name":"(anonymous_5)","decl":{"start":{"line":152,"column":7},"end":{"line":152,"column":12}},"loc":{"start":{"line":152,"column":7},"end":{"line":161,"column":null}},"line":152},"6":{"name":"(anonymous_6)","decl":{"start":{"line":163,"column":7},"end":{"line":163,"column":12}},"loc":{"start":{"line":163,"column":7},"end":{"line":166,"column":null}},"line":163},"7":{"name":"(anonymous_7)","decl":{"start":{"line":227,"column":7},"end":{"line":227,"column":12}},"loc":{"start":{"line":227,"column":7},"end":{"line":230,"column":null}},"line":227},"8":{"name":"(anonymous_8)","decl":{"start":{"line":277,"column":7},"end":{"line":277,"column":12}},"loc":{"start":{"line":277,"column":7},"end":{"line":280,"column":null}},"line":277},"9":{"name":"(anonymous_9)","decl":{"start":{"line":322,"column":7},"end":{"line":322,"column":12}},"loc":{"start":{"line":322,"column":7},"end":{"line":327,"column":null}},"line":322}},"branchMap":{},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"31":1,"32":1,"33":1,"34":1,"35":1,"36":1,"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"51":1,"52":1,"53":1,"54":1,"55":1,"56":1,"57":1,"58":1,"59":1,"60":1,"61":1,"62":1,"63":1,"64":1,"65":1,"66":1,"67":1,"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":1,"83":1,"84":1,"85":1,"86":1,"87":1,"88":1,"89":1,"90":1,"91":1,"92":1,"93":1,"94":1,"95":1,"96":1,"97":1,"98":1,"99":1,"100":1,"101":1,"102":1,"103":1,"104":1,"105":1,"106":1,"107":1,"108":1,"109":1,"110":1,"111":1,"112":1,"113":1,"114":1,"115":1,"116":1,"117":1,"118":1,"119":1,"120":1,"121":1,"122":1,"123":1,"124":1,"125":1,"126":1,"127":1,"128":1},"f":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1},"b":{},"meta":{"lastBranch":0,"lastFunction":10,"lastStatement":129,"seen":{"s:5:27:29:Infinity":0,"s:30:32:30:Infinity":1,"s:32:7:113:Infinity":2,"f:32:7:32:12":0,"s:33:2:33:Infinity":3,"s:34:2:34:Infinity":4,"s:35:2:35:Infinity":5,"s:36:2:36:Infinity":6,"s:37:2:37:Infinity":7,"s:38:2:38:Infinity":8,"s:39:2:39:Infinity":9,"s:40:2:40:Infinity":10,"s:41:2:41:Infinity":11,"s:42:2:42:Infinity":12,"s:43:2:43:Infinity":13,"s:44:2:44:Infinity":14,"s:45:2:45:Infinity":15,"s:46:2:46:Infinity":16,"s:47:2:47:Infinity":17,"s:48:2:48:Infinity":18,"s:49:2:49:Infinity":19,"s:50:2:50:Infinity":20,"s:51:2:51:Infinity":21,"s:52:2:52:Infinity":22,"s:53:2:53:Infinity":23,"s:54:2:54:Infinity":24,"s:55:2:55:Infinity":25,"s:56:2:56:Infinity":26,"s:57:2:57:Infinity":27,"s:58:2:58:Infinity":28,"s:59:2:59:Infinity":29,"s:60:2:60:Infinity":30,"s:61:2:61:Infinity":31,"s:62:2:62:Infinity":32,"s:63:2:63:Infinity":33,"s:64:2:64:Infinity":34,"s:65:2:65:Infinity":35,"s:66:2:66:Infinity":36,"s:67:2:67:Infinity":37,"s:68:2:68:Infinity":38,"s:69:2:69:Infinity":39,"s:70:2:70:Infinity":40,"s:71:2:71:Infinity":41,"s:72:2:72:Infinity":42,"s:73:2:73:Infinity":43,"s:74:2:74:Infinity":44,"s:75:2:75:Infinity":45,"s:76:2:76:Infinity":46,"s:77:2:77:Infinity":47,"s:78:2:78:Infinity":48,"s:79:2:79:Infinity":49,"s:80:2:80:Infinity":50,"s:81:2:81:Infinity":51,"s:82:2:82:Infinity":52,"s:83:2:83:Infinity":53,"s:84:2:84:Infinity":54,"s:85:2:85:Infinity":55,"s:86:2:86:Infinity":56,"s:87:2:87:Infinity":57,"s:88:2:88:Infinity":58,"s:89:2:89:Infinity":59,"s:90:2:90:Infinity":60,"s:91:2:91:Infinity":61,"s:92:2:92:Infinity":62,"s:93:2:93:Infinity":63,"s:94:2:94:Infinity":64,"s:95:2:95:Infinity":65,"s:96:2:96:Infinity":66,"s:97:2:97:Infinity":67,"s:98:2:98:Infinity":68,"s:99:2:99:Infinity":69,"s:100:2:100:Infinity":70,"s:101:2:101:Infinity":71,"s:102:2:102:Infinity":72,"s:103:2:103:Infinity":73,"s:104:2:104:Infinity":74,"s:105:2:105:Infinity":75,"s:106:2:106:Infinity":76,"s:107:2:107:Infinity":77,"s:108:2:108:Infinity":78,"s:109:2:109:Infinity":79,"s:110:2:110:Infinity":80,"s:111:2:111:Infinity":81,"s:112:2:112:Infinity":82,"s:115:7:119:Infinity":83,"f:115:7:115:12":1,"s:116:2:116:Infinity":84,"s:117:2:117:Infinity":85,"s:118:2:118:Infinity":86,"s:121:7:131:Infinity":87,"f:121:7:121:12":2,"s:122:2:122:Infinity":88,"s:123:2:123:Infinity":89,"s:124:2:124:Infinity":90,"s:125:2:125:Infinity":91,"s:126:2:126:Infinity":92,"s:127:2:127:Infinity":93,"s:128:2:128:Infinity":94,"s:129:2:129:Infinity":95,"s:130:2:130:Infinity":96,"s:133:7:138:Infinity":97,"f:133:7:133:12":3,"s:134:2:134:Infinity":98,"s:135:2:135:Infinity":99,"s:136:2:136:Infinity":100,"s:137:2:137:Infinity":101,"s:140:7:143:Infinity":102,"f:140:7:140:12":4,"s:141:2:141:Infinity":103,"s:142:2:142:Infinity":104,"s:152:7:161:Infinity":105,"f:152:7:152:12":5,"s:153:2:153:Infinity":106,"s:154:2:154:Infinity":107,"s:155:2:155:Infinity":108,"s:156:2:156:Infinity":109,"s:157:2:157:Infinity":110,"s:158:2:158:Infinity":111,"s:159:2:159:Infinity":112,"s:160:2:160:Infinity":113,"s:163:7:166:Infinity":114,"f:163:7:163:12":6,"s:164:2:164:Infinity":115,"s:165:2:165:Infinity":116,"s:227:7:230:Infinity":117,"f:227:7:227:12":7,"s:228:2:228:Infinity":118,"s:229:2:229:Infinity":119,"s:232:28:236:Infinity":120,"s:277:7:280:Infinity":121,"f:277:7:277:12":8,"s:278:2:278:Infinity":122,"s:279:2:279:Infinity":123,"s:322:7:327:Infinity":124,"f:322:7:322:12":9,"s:323:2:323:Infinity":125,"s:324:2:324:Infinity":126,"s:325:2:325:Infinity":127,"s:326:2:326:Infinity":128},"fnNames":{}}} +,"/workspace/src/components/ui/Text.tsx": {"path":"/workspace/src/components/ui/Text.tsx","statementMap":{"0":{"start":{"line":20,"column":13},"end":{"line":40,"column":null}},"1":{"start":{"line":22,"column":22},"end":{"line":22,"column":null}},"2":{"start":{"line":24,"column":4},"end":{"line":37,"column":null}},"3":{"start":{"line":42,"column":0},"end":{"line":42,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":20,"column":20},"end":{"line":20,"column":null}},"loc":{"start":{"line":21,"column":94},"end":{"line":39,"column":null}},"line":21}},"branchMap":{"0":{"loc":{"start":{"line":21,"column":5},"end":{"line":21,"column":34}},"type":"default-arg","locations":[{"start":{"line":21,"column":15},"end":{"line":21,"column":34}}],"line":21},"1":{"loc":{"start":{"line":22,"column":22},"end":{"line":22,"column":null}},"type":"binary-expr","locations":[{"start":{"line":22,"column":22},"end":{"line":22,"column":28}},{"start":{"line":22,"column":28},"end":{"line":22,"column":null}}],"line":22},"2":{"loc":{"start":{"line":29,"column":27},"end":{"line":29,"column":null}},"type":"binary-expr","locations":[{"start":{"line":29,"column":27},"end":{"line":29,"column":37}},{"start":{"line":29,"column":37},"end":{"line":29,"column":null}}],"line":29},"3":{"loc":{"start":{"line":30,"column":26},"end":{"line":30,"column":null}},"type":"binary-expr","locations":[{"start":{"line":30,"column":26},"end":{"line":30,"column":35}},{"start":{"line":30,"column":35},"end":{"line":30,"column":null}}],"line":30}},"s":{"0":1,"1":25,"2":25,"3":1},"f":{"0":25},"b":{"0":[25],"1":[25,25],"2":[25,25],"3":[25,25]},"meta":{"lastBranch":4,"lastFunction":1,"lastStatement":4,"seen":{"s:20:13:40:Infinity":0,"f:20:20:20:Infinity":0,"b:21:15:21:34":0,"s:22:22:22:Infinity":1,"b:22:22:22:28:22:28:22:Infinity":1,"s:24:4:37:Infinity":2,"b:29:27:29:37:29:37:29:Infinity":2,"b:30:26:30:35:30:35:30:Infinity":3,"s:42:0:42:Infinity":3},"fnNames":{}}} +,"/workspace/src/hooks/useAndroidBackHandler.ts": {"path":"/workspace/src/hooks/useAndroidBackHandler.ts","statementMap":{"0":{"start":{"line":6,"column":2},"end":{"line":100,"column":null}},"1":{"start":{"line":7,"column":23},"end":{"line":7,"column":null}},"2":{"start":{"line":8,"column":4},"end":{"line":8,"column":null}},"3":{"start":{"line":8,"column":34},"end":{"line":8,"column":null}},"4":{"start":{"line":10,"column":4},"end":{"line":95,"column":null}},"5":{"start":{"line":11,"column":17},"end":{"line":11,"column":null}},"6":{"start":{"line":13,"column":6},"end":{"line":16,"column":null}},"7":{"start":{"line":14,"column":8},"end":{"line":14,"column":null}},"8":{"start":{"line":14,"column":34},"end":{"line":14,"column":103}},"9":{"start":{"line":15,"column":8},"end":{"line":15,"column":null}},"10":{"start":{"line":17,"column":6},"end":{"line":20,"column":null}},"11":{"start":{"line":18,"column":8},"end":{"line":18,"column":null}},"12":{"start":{"line":19,"column":8},"end":{"line":19,"column":null}},"13":{"start":{"line":21,"column":6},"end":{"line":24,"column":null}},"14":{"start":{"line":22,"column":8},"end":{"line":22,"column":null}},"15":{"start":{"line":23,"column":8},"end":{"line":23,"column":null}},"16":{"start":{"line":25,"column":6},"end":{"line":28,"column":null}},"17":{"start":{"line":26,"column":8},"end":{"line":26,"column":null}},"18":{"start":{"line":27,"column":8},"end":{"line":27,"column":null}},"19":{"start":{"line":29,"column":6},"end":{"line":32,"column":null}},"20":{"start":{"line":30,"column":8},"end":{"line":30,"column":null}},"21":{"start":{"line":31,"column":8},"end":{"line":31,"column":null}},"22":{"start":{"line":33,"column":6},"end":{"line":36,"column":null}},"23":{"start":{"line":34,"column":8},"end":{"line":34,"column":null}},"24":{"start":{"line":35,"column":8},"end":{"line":35,"column":null}},"25":{"start":{"line":37,"column":6},"end":{"line":40,"column":null}},"26":{"start":{"line":38,"column":8},"end":{"line":38,"column":null}},"27":{"start":{"line":39,"column":8},"end":{"line":39,"column":null}},"28":{"start":{"line":41,"column":6},"end":{"line":44,"column":null}},"29":{"start":{"line":42,"column":8},"end":{"line":42,"column":null}},"30":{"start":{"line":43,"column":8},"end":{"line":43,"column":null}},"31":{"start":{"line":45,"column":6},"end":{"line":48,"column":null}},"32":{"start":{"line":46,"column":8},"end":{"line":46,"column":null}},"33":{"start":{"line":47,"column":8},"end":{"line":47,"column":null}},"34":{"start":{"line":49,"column":6},"end":{"line":61,"column":null}},"35":{"start":{"line":50,"column":8},"end":{"line":59,"column":null}},"36":{"start":{"line":60,"column":8},"end":{"line":60,"column":null}},"37":{"start":{"line":62,"column":6},"end":{"line":74,"column":null}},"38":{"start":{"line":63,"column":8},"end":{"line":72,"column":null}},"39":{"start":{"line":73,"column":8},"end":{"line":73,"column":null}},"40":{"start":{"line":75,"column":6},"end":{"line":78,"column":null}},"41":{"start":{"line":76,"column":8},"end":{"line":76,"column":null}},"42":{"start":{"line":76,"column":34},"end":{"line":76,"column":105}},"43":{"start":{"line":77,"column":8},"end":{"line":77,"column":null}},"44":{"start":{"line":79,"column":6},"end":{"line":82,"column":null}},"45":{"start":{"line":80,"column":8},"end":{"line":80,"column":null}},"46":{"start":{"line":80,"column":34},"end":{"line":80,"column":103}},"47":{"start":{"line":81,"column":8},"end":{"line":81,"column":null}},"48":{"start":{"line":83,"column":6},"end":{"line":88,"column":null}},"49":{"start":{"line":84,"column":8},"end":{"line":86,"column":null}},"50":{"start":{"line":87,"column":8},"end":{"line":87,"column":null}},"51":{"start":{"line":89,"column":6},"end":{"line":92,"column":null}},"52":{"start":{"line":90,"column":8},"end":{"line":90,"column":null}},"53":{"start":{"line":91,"column":8},"end":{"line":91,"column":null}},"54":{"start":{"line":94,"column":6},"end":{"line":94,"column":null}},"55":{"start":{"line":97,"column":4},"end":{"line":99,"column":null}},"56":{"start":{"line":98,"column":6},"end":{"line":98,"column":null}}},"fnMap":{"0":{"name":"useAndroidBackHandler","decl":{"start":{"line":5,"column":16},"end":{"line":5,"column":40}},"loc":{"start":{"line":5,"column":40},"end":{"line":101,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":6,"column":2},"end":{"line":6,"column":18}},"loc":{"start":{"line":6,"column":18},"end":{"line":100,"column":5}},"line":6},"2":{"name":"(anonymous_2)","decl":{"start":{"line":10,"column":20},"end":{"line":10,"column":48}},"loc":{"start":{"line":10,"column":48},"end":{"line":95,"column":null}},"line":10},"3":{"name":"(anonymous_3)","decl":{"start":{"line":14,"column":11},"end":{"line":14,"column":18}},"loc":{"start":{"line":14,"column":34},"end":{"line":14,"column":103}},"line":14},"4":{"name":"(anonymous_4)","decl":{"start":{"line":76,"column":11},"end":{"line":76,"column":18}},"loc":{"start":{"line":76,"column":34},"end":{"line":76,"column":105}},"line":76},"5":{"name":"(anonymous_5)","decl":{"start":{"line":80,"column":11},"end":{"line":80,"column":18}},"loc":{"start":{"line":80,"column":34},"end":{"line":80,"column":103}},"line":80},"6":{"name":"(anonymous_6)","decl":{"start":{"line":97,"column":4},"end":{"line":97,"column":17}},"loc":{"start":{"line":97,"column":17},"end":{"line":99,"column":null}},"line":97}},"branchMap":{"0":{"loc":{"start":{"line":8,"column":4},"end":{"line":8,"column":null}},"type":"if","locations":[{"start":{"line":8,"column":4},"end":{"line":8,"column":null}},{"start":{},"end":{}}],"line":8},"1":{"loc":{"start":{"line":13,"column":6},"end":{"line":16,"column":null}},"type":"if","locations":[{"start":{"line":13,"column":6},"end":{"line":16,"column":null}},{"start":{},"end":{}}],"line":13},"2":{"loc":{"start":{"line":17,"column":6},"end":{"line":20,"column":null}},"type":"if","locations":[{"start":{"line":17,"column":6},"end":{"line":20,"column":null}},{"start":{},"end":{}}],"line":17},"3":{"loc":{"start":{"line":21,"column":6},"end":{"line":24,"column":null}},"type":"if","locations":[{"start":{"line":21,"column":6},"end":{"line":24,"column":null}},{"start":{},"end":{}}],"line":21},"4":{"loc":{"start":{"line":25,"column":6},"end":{"line":28,"column":null}},"type":"if","locations":[{"start":{"line":25,"column":6},"end":{"line":28,"column":null}},{"start":{},"end":{}}],"line":25},"5":{"loc":{"start":{"line":29,"column":6},"end":{"line":32,"column":null}},"type":"if","locations":[{"start":{"line":29,"column":6},"end":{"line":32,"column":null}},{"start":{},"end":{}}],"line":29},"6":{"loc":{"start":{"line":33,"column":6},"end":{"line":36,"column":null}},"type":"if","locations":[{"start":{"line":33,"column":6},"end":{"line":36,"column":null}},{"start":{},"end":{}}],"line":33},"7":{"loc":{"start":{"line":37,"column":6},"end":{"line":40,"column":null}},"type":"if","locations":[{"start":{"line":37,"column":6},"end":{"line":40,"column":null}},{"start":{},"end":{}}],"line":37},"8":{"loc":{"start":{"line":41,"column":6},"end":{"line":44,"column":null}},"type":"if","locations":[{"start":{"line":41,"column":6},"end":{"line":44,"column":null}},{"start":{},"end":{}}],"line":41},"9":{"loc":{"start":{"line":45,"column":6},"end":{"line":48,"column":null}},"type":"if","locations":[{"start":{"line":45,"column":6},"end":{"line":48,"column":null}},{"start":{},"end":{}}],"line":45},"10":{"loc":{"start":{"line":49,"column":6},"end":{"line":61,"column":null}},"type":"if","locations":[{"start":{"line":49,"column":6},"end":{"line":61,"column":null}},{"start":{},"end":{}}],"line":49},"11":{"loc":{"start":{"line":62,"column":6},"end":{"line":74,"column":null}},"type":"if","locations":[{"start":{"line":62,"column":6},"end":{"line":74,"column":null}},{"start":{},"end":{}}],"line":62},"12":{"loc":{"start":{"line":75,"column":6},"end":{"line":78,"column":null}},"type":"if","locations":[{"start":{"line":75,"column":6},"end":{"line":78,"column":null}},{"start":{},"end":{}}],"line":75},"13":{"loc":{"start":{"line":79,"column":6},"end":{"line":82,"column":null}},"type":"if","locations":[{"start":{"line":79,"column":6},"end":{"line":82,"column":null}},{"start":{},"end":{}}],"line":79},"14":{"loc":{"start":{"line":83,"column":6},"end":{"line":88,"column":null}},"type":"if","locations":[{"start":{"line":83,"column":6},"end":{"line":88,"column":null}},{"start":{},"end":{}}],"line":83},"15":{"loc":{"start":{"line":89,"column":6},"end":{"line":92,"column":null}},"type":"if","locations":[{"start":{"line":89,"column":6},"end":{"line":92,"column":null}},{"start":{},"end":{}}],"line":89}},"s":{"0":4,"1":4,"2":4,"3":1,"4":3,"5":2,"6":2,"7":1,"8":0,"9":1,"10":1,"11":0,"12":0,"13":1,"14":0,"15":0,"16":1,"17":0,"18":0,"19":1,"20":0,"21":0,"22":1,"23":0,"24":0,"25":1,"26":0,"27":0,"28":1,"29":0,"30":0,"31":1,"32":0,"33":0,"34":1,"35":0,"36":0,"37":1,"38":0,"39":0,"40":1,"41":0,"42":0,"43":0,"44":1,"45":0,"46":0,"47":0,"48":1,"49":0,"50":0,"51":1,"52":0,"53":0,"54":1,"55":3,"56":3},"f":{"0":4,"1":4,"2":2,"3":0,"4":0,"5":0,"6":3},"b":{"0":[1,3],"1":[1,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1]},"meta":{"lastBranch":16,"lastFunction":7,"lastStatement":57,"seen":{"f:5:16:5:40":0,"s:6:2:100:Infinity":0,"f:6:2:6:18":1,"s:7:23:7:Infinity":1,"b:8:4:8:Infinity:undefined:undefined:undefined:undefined":0,"s:8:4:8:Infinity":2,"s:8:34:8:Infinity":3,"s:10:4:95:Infinity":4,"f:10:20:10:48":2,"s:11:17:11:Infinity":5,"b:13:6:16:Infinity:undefined:undefined:undefined:undefined":1,"s:13:6:16:Infinity":6,"s:14:8:14:Infinity":7,"f:14:11:14:18":3,"s:14:34:14:103":8,"s:15:8:15:Infinity":9,"b:17:6:20:Infinity:undefined:undefined:undefined:undefined":2,"s:17:6:20:Infinity":10,"s:18:8:18:Infinity":11,"s:19:8:19:Infinity":12,"b:21:6:24:Infinity:undefined:undefined:undefined:undefined":3,"s:21:6:24:Infinity":13,"s:22:8:22:Infinity":14,"s:23:8:23:Infinity":15,"b:25:6:28:Infinity:undefined:undefined:undefined:undefined":4,"s:25:6:28:Infinity":16,"s:26:8:26:Infinity":17,"s:27:8:27:Infinity":18,"b:29:6:32:Infinity:undefined:undefined:undefined:undefined":5,"s:29:6:32:Infinity":19,"s:30:8:30:Infinity":20,"s:31:8:31:Infinity":21,"b:33:6:36:Infinity:undefined:undefined:undefined:undefined":6,"s:33:6:36:Infinity":22,"s:34:8:34:Infinity":23,"s:35:8:35:Infinity":24,"b:37:6:40:Infinity:undefined:undefined:undefined:undefined":7,"s:37:6:40:Infinity":25,"s:38:8:38:Infinity":26,"s:39:8:39:Infinity":27,"b:41:6:44:Infinity:undefined:undefined:undefined:undefined":8,"s:41:6:44:Infinity":28,"s:42:8:42:Infinity":29,"s:43:8:43:Infinity":30,"b:45:6:48:Infinity:undefined:undefined:undefined:undefined":9,"s:45:6:48:Infinity":31,"s:46:8:46:Infinity":32,"s:47:8:47:Infinity":33,"b:49:6:61:Infinity:undefined:undefined:undefined:undefined":10,"s:49:6:61:Infinity":34,"s:50:8:59:Infinity":35,"s:60:8:60:Infinity":36,"b:62:6:74:Infinity:undefined:undefined:undefined:undefined":11,"s:62:6:74:Infinity":37,"s:63:8:72:Infinity":38,"s:73:8:73:Infinity":39,"b:75:6:78:Infinity:undefined:undefined:undefined:undefined":12,"s:75:6:78:Infinity":40,"s:76:8:76:Infinity":41,"f:76:11:76:18":4,"s:76:34:76:105":42,"s:77:8:77:Infinity":43,"b:79:6:82:Infinity:undefined:undefined:undefined:undefined":13,"s:79:6:82:Infinity":44,"s:80:8:80:Infinity":45,"f:80:11:80:18":5,"s:80:34:80:103":46,"s:81:8:81:Infinity":47,"b:83:6:88:Infinity:undefined:undefined:undefined:undefined":14,"s:83:6:88:Infinity":48,"s:84:8:86:Infinity":49,"s:87:8:87:Infinity":50,"b:89:6:92:Infinity:undefined:undefined:undefined:undefined":15,"s:89:6:92:Infinity":51,"s:90:8:90:Infinity":52,"s:91:8:91:Infinity":53,"s:94:6:94:Infinity":54,"s:97:4:99:Infinity":55,"f:97:4:97:17":6,"s:98:6:98:Infinity":56},"fnNames":{}}} +,"/workspace/src/hooks/useTouchGestures.ts": {"path":"/workspace/src/hooks/useTouchGestures.ts","statementMap":{"0":{"start":{"line":9,"column":2},"end":{"line":9,"column":null}},"1":{"start":{"line":13,"column":13},"end":{"line":13,"column":null}},"2":{"start":{"line":14,"column":13},"end":{"line":14,"column":null}},"3":{"start":{"line":15,"column":2},"end":{"line":15,"column":null}},"4":{"start":{"line":19,"column":2},"end":{"line":19,"column":null}},"5":{"start":{"line":23,"column":2},"end":{"line":23,"column":null}},"6":{"start":{"line":38,"column":19},"end":{"line":38,"column":null}},"7":{"start":{"line":39,"column":19},"end":{"line":39,"column":null}},"8":{"start":{"line":40,"column":24},"end":{"line":40,"column":null}},"9":{"start":{"line":42,"column":8},"end":{"line":42,"column":null}},"10":{"start":{"line":43,"column":8},"end":{"line":43,"column":null}},"11":{"start":{"line":45,"column":8},"end":{"line":51,"column":null}},"12":{"start":{"line":46,"column":4},"end":{"line":46,"column":null}},"13":{"start":{"line":46,"column":32},"end":{"line":46,"column":null}},"14":{"start":{"line":47,"column":4},"end":{"line":47,"column":null}},"15":{"start":{"line":48,"column":15},"end":{"line":48,"column":null}},"16":{"start":{"line":49,"column":15},"end":{"line":49,"column":null}},"17":{"start":{"line":50,"column":4},"end":{"line":50,"column":null}},"18":{"start":{"line":53,"column":8},"end":{"line":69,"column":null}},"19":{"start":{"line":55,"column":6},"end":{"line":55,"column":null}},"20":{"start":{"line":55,"column":34},"end":{"line":55,"column":null}},"21":{"start":{"line":56,"column":6},"end":{"line":56,"column":null}},"22":{"start":{"line":57,"column":17},"end":{"line":57,"column":null}},"23":{"start":{"line":58,"column":17},"end":{"line":58,"column":null}},"24":{"start":{"line":59,"column":30},"end":{"line":59,"column":null}},"25":{"start":{"line":60,"column":6},"end":{"line":63,"column":null}},"26":{"start":{"line":61,"column":8},"end":{"line":61,"column":null}},"27":{"start":{"line":62,"column":8},"end":{"line":62,"column":null}},"28":{"start":{"line":64,"column":20},"end":{"line":64,"column":null}},"29":{"start":{"line":65,"column":23},"end":{"line":65,"column":null}},"30":{"start":{"line":66,"column":6},"end":{"line":66,"column":null}},"31":{"start":{"line":71,"column":8},"end":{"line":79,"column":null}},"32":{"start":{"line":72,"column":4},"end":{"line":78,"column":null}},"33":{"start":{"line":75,"column":6},"end":{"line":75,"column":null}},"34":{"start":{"line":81,"column":2},"end":{"line":94,"column":null}},"35":{"start":{"line":82,"column":15},"end":{"line":82,"column":null}},"36":{"start":{"line":83,"column":4},"end":{"line":83,"column":null}},"37":{"start":{"line":83,"column":13},"end":{"line":83,"column":null}},"38":{"start":{"line":85,"column":4},"end":{"line":85,"column":null}},"39":{"start":{"line":86,"column":4},"end":{"line":86,"column":null}},"40":{"start":{"line":87,"column":4},"end":{"line":87,"column":null}},"41":{"start":{"line":89,"column":4},"end":{"line":93,"column":null}},"42":{"start":{"line":90,"column":6},"end":{"line":90,"column":null}},"43":{"start":{"line":91,"column":6},"end":{"line":91,"column":null}},"44":{"start":{"line":92,"column":6},"end":{"line":92,"column":null}},"45":{"start":{"line":96,"column":2},"end":{"line":100,"column":null}},"46":{"start":{"line":98,"column":6},"end":{"line":98,"column":null}},"47":{"start":{"line":113,"column":27},"end":{"line":113,"column":null}},"48":{"start":{"line":114,"column":8},"end":{"line":114,"column":null}},"49":{"start":{"line":115,"column":8},"end":{"line":115,"column":null}},"50":{"start":{"line":117,"column":8},"end":{"line":123,"column":null}},"51":{"start":{"line":118,"column":4},"end":{"line":118,"column":null}},"52":{"start":{"line":118,"column":32},"end":{"line":118,"column":null}},"53":{"start":{"line":119,"column":4},"end":{"line":119,"column":null}},"54":{"start":{"line":120,"column":15},"end":{"line":120,"column":null}},"55":{"start":{"line":121,"column":15},"end":{"line":121,"column":null}},"56":{"start":{"line":122,"column":4},"end":{"line":122,"column":null}},"57":{"start":{"line":125,"column":8},"end":{"line":137,"column":null}},"58":{"start":{"line":127,"column":6},"end":{"line":127,"column":null}},"59":{"start":{"line":127,"column":34},"end":{"line":127,"column":null}},"60":{"start":{"line":128,"column":6},"end":{"line":128,"column":null}},"61":{"start":{"line":129,"column":17},"end":{"line":129,"column":null}},"62":{"start":{"line":130,"column":17},"end":{"line":130,"column":null}},"63":{"start":{"line":131,"column":27},"end":{"line":131,"column":null}},"64":{"start":{"line":132,"column":20},"end":{"line":132,"column":null}},"65":{"start":{"line":133,"column":26},"end":{"line":133,"column":null}},"66":{"start":{"line":134,"column":6},"end":{"line":134,"column":null}},"67":{"start":{"line":139,"column":8},"end":{"line":143,"column":null}},"68":{"start":{"line":140,"column":4},"end":{"line":142,"column":null}},"69":{"start":{"line":141,"column":6},"end":{"line":141,"column":null}},"70":{"start":{"line":145,"column":2},"end":{"line":158,"column":null}},"71":{"start":{"line":146,"column":15},"end":{"line":146,"column":null}},"72":{"start":{"line":147,"column":4},"end":{"line":147,"column":null}},"73":{"start":{"line":147,"column":13},"end":{"line":147,"column":null}},"74":{"start":{"line":149,"column":4},"end":{"line":149,"column":null}},"75":{"start":{"line":150,"column":4},"end":{"line":150,"column":null}},"76":{"start":{"line":151,"column":4},"end":{"line":151,"column":null}},"77":{"start":{"line":153,"column":4},"end":{"line":157,"column":null}},"78":{"start":{"line":154,"column":6},"end":{"line":154,"column":null}},"79":{"start":{"line":155,"column":6},"end":{"line":155,"column":null}},"80":{"start":{"line":156,"column":6},"end":{"line":156,"column":null}},"81":{"start":{"line":160,"column":2},"end":{"line":164,"column":null}},"82":{"start":{"line":162,"column":6},"end":{"line":162,"column":null}},"83":{"start":{"line":179,"column":22},"end":{"line":179,"column":null}},"84":{"start":{"line":180,"column":21},"end":{"line":180,"column":null}},"85":{"start":{"line":181,"column":19},"end":{"line":181,"column":null}},"86":{"start":{"line":183,"column":8},"end":{"line":183,"column":null}},"87":{"start":{"line":184,"column":8},"end":{"line":184,"column":null}},"88":{"start":{"line":185,"column":8},"end":{"line":185,"column":null}},"89":{"start":{"line":187,"column":8},"end":{"line":196,"column":null}},"90":{"start":{"line":189,"column":6},"end":{"line":189,"column":null}},"91":{"start":{"line":189,"column":34},"end":{"line":189,"column":null}},"92":{"start":{"line":190,"column":20},"end":{"line":190,"column":null}},"93":{"start":{"line":191,"column":6},"end":{"line":191,"column":null}},"94":{"start":{"line":192,"column":6},"end":{"line":192,"column":null}},"95":{"start":{"line":193,"column":6},"end":{"line":193,"column":null}},"96":{"start":{"line":198,"column":8},"end":{"line":208,"column":null}},"97":{"start":{"line":200,"column":6},"end":{"line":200,"column":null}},"98":{"start":{"line":200,"column":85},"end":{"line":200,"column":null}},"99":{"start":{"line":201,"column":6},"end":{"line":201,"column":null}},"100":{"start":{"line":202,"column":27},"end":{"line":202,"column":null}},"101":{"start":{"line":203,"column":17},"end":{"line":203,"column":null}},"102":{"start":{"line":204,"column":17},"end":{"line":204,"column":null}},"103":{"start":{"line":205,"column":6},"end":{"line":205,"column":null}},"104":{"start":{"line":210,"column":8},"end":{"line":228,"column":null}},"105":{"start":{"line":212,"column":6},"end":{"line":212,"column":null}},"106":{"start":{"line":212,"column":59},"end":{"line":212,"column":null}},"107":{"start":{"line":214,"column":6},"end":{"line":222,"column":null}},"108":{"start":{"line":215,"column":25},"end":{"line":215,"column":null}},"109":{"start":{"line":216,"column":19},"end":{"line":216,"column":null}},"110":{"start":{"line":217,"column":19},"end":{"line":217,"column":null}},"111":{"start":{"line":218,"column":8},"end":{"line":221,"column":null}},"112":{"start":{"line":223,"column":6},"end":{"line":223,"column":null}},"113":{"start":{"line":224,"column":6},"end":{"line":224,"column":null}},"114":{"start":{"line":225,"column":6},"end":{"line":225,"column":null}},"115":{"start":{"line":230,"column":2},"end":{"line":243,"column":null}},"116":{"start":{"line":231,"column":15},"end":{"line":231,"column":null}},"117":{"start":{"line":232,"column":4},"end":{"line":232,"column":null}},"118":{"start":{"line":232,"column":13},"end":{"line":232,"column":null}},"119":{"start":{"line":234,"column":4},"end":{"line":234,"column":null}},"120":{"start":{"line":235,"column":4},"end":{"line":235,"column":null}},"121":{"start":{"line":236,"column":4},"end":{"line":236,"column":null}},"122":{"start":{"line":238,"column":4},"end":{"line":242,"column":null}},"123":{"start":{"line":239,"column":6},"end":{"line":239,"column":null}},"124":{"start":{"line":240,"column":6},"end":{"line":240,"column":null}},"125":{"start":{"line":241,"column":6},"end":{"line":241,"column":null}},"126":{"start":{"line":245,"column":2},"end":{"line":253,"column":null}},"127":{"start":{"line":247,"column":6},"end":{"line":247,"column":null}},"128":{"start":{"line":248,"column":6},"end":{"line":248,"column":null}},"129":{"start":{"line":251,"column":6},"end":{"line":251,"column":null}},"130":{"start":{"line":267,"column":20},"end":{"line":267,"column":null}},"131":{"start":{"line":268,"column":22},"end":{"line":268,"column":null}},"132":{"start":{"line":269,"column":23},"end":{"line":269,"column":null}},"133":{"start":{"line":271,"column":8},"end":{"line":271,"column":null}},"134":{"start":{"line":272,"column":8},"end":{"line":272,"column":null}},"135":{"start":{"line":274,"column":8},"end":{"line":278,"column":null}},"136":{"start":{"line":275,"column":4},"end":{"line":275,"column":null}},"137":{"start":{"line":275,"column":32},"end":{"line":275,"column":null}},"138":{"start":{"line":276,"column":4},"end":{"line":276,"column":null}},"139":{"start":{"line":277,"column":4},"end":{"line":277,"column":null}},"140":{"start":{"line":280,"column":8},"end":{"line":285,"column":null}},"141":{"start":{"line":287,"column":8},"end":{"line":311,"column":null}},"142":{"start":{"line":289,"column":6},"end":{"line":289,"column":null}},"143":{"start":{"line":289,"column":67},"end":{"line":289,"column":null}},"144":{"start":{"line":290,"column":23},"end":{"line":290,"column":null}},"145":{"start":{"line":291,"column":17},"end":{"line":291,"column":null}},"146":{"start":{"line":292,"column":17},"end":{"line":292,"column":null}},"147":{"start":{"line":293,"column":17},"end":{"line":293,"column":null}},"148":{"start":{"line":296,"column":32},"end":{"line":296,"column":null}},"149":{"start":{"line":297,"column":27},"end":{"line":297,"column":null}},"150":{"start":{"line":298,"column":31},"end":{"line":298,"column":null}},"151":{"start":{"line":300,"column":6},"end":{"line":306,"column":null}},"152":{"start":{"line":301,"column":8},"end":{"line":305,"column":null}},"153":{"start":{"line":302,"column":10},"end":{"line":302,"column":null}},"154":{"start":{"line":304,"column":10},"end":{"line":304,"column":null}},"155":{"start":{"line":308,"column":6},"end":{"line":308,"column":null}},"156":{"start":{"line":313,"column":2},"end":{"line":330,"column":null}},"157":{"start":{"line":314,"column":20},"end":{"line":318,"column":null}},"158":{"start":{"line":321,"column":4},"end":{"line":321,"column":null}},"159":{"start":{"line":322,"column":4},"end":{"line":322,"column":null}},"160":{"start":{"line":323,"column":4},"end":{"line":323,"column":null}},"161":{"start":{"line":325,"column":4},"end":{"line":329,"column":null}},"162":{"start":{"line":326,"column":6},"end":{"line":326,"column":null}},"163":{"start":{"line":327,"column":6},"end":{"line":327,"column":null}},"164":{"start":{"line":328,"column":6},"end":{"line":328,"column":null}}},"fnMap":{"0":{"name":"getTouchPoint","decl":{"start":{"line":8,"column":9},"end":{"line":8,"column":23}},"loc":{"start":{"line":8,"column":44},"end":{"line":10,"column":null}},"line":8},"1":{"name":"getDistance","decl":{"start":{"line":12,"column":9},"end":{"line":12,"column":21}},"loc":{"start":{"line":12,"column":51},"end":{"line":16,"column":null}},"line":12},"2":{"name":"getAngle","decl":{"start":{"line":18,"column":9},"end":{"line":18,"column":18}},"loc":{"start":{"line":18,"column":48},"end":{"line":20,"column":null}},"line":18},"3":{"name":"getMidpoint","decl":{"start":{"line":22,"column":9},"end":{"line":22,"column":21}},"loc":{"start":{"line":22,"column":50},"end":{"line":24,"column":null}},"line":22},"4":{"name":"usePinchZoom","decl":{"start":{"line":30,"column":16},"end":{"line":30,"column":null}},"loc":{"start":{"line":37,"column":2},"end":{"line":101,"column":null}},"line":37},"5":{"name":"(anonymous_5)","decl":{"start":{"line":45,"column":27},"end":{"line":45,"column":40}},"loc":{"start":{"line":45,"column":58},"end":{"line":51,"column":5}},"line":45},"6":{"name":"(anonymous_6)","decl":{"start":{"line":53,"column":26},"end":{"line":53,"column":null}},"loc":{"start":{"line":54,"column":23},"end":{"line":67,"column":null}},"line":54},"7":{"name":"(anonymous_7)","decl":{"start":{"line":71,"column":25},"end":{"line":71,"column":38}},"loc":{"start":{"line":71,"column":56},"end":{"line":79,"column":5}},"line":71},"8":{"name":"(anonymous_8)","decl":{"start":{"line":81,"column":2},"end":{"line":81,"column":18}},"loc":{"start":{"line":81,"column":18},"end":{"line":94,"column":5}},"line":81},"9":{"name":"(anonymous_9)","decl":{"start":{"line":89,"column":4},"end":{"line":89,"column":17}},"loc":{"start":{"line":89,"column":17},"end":{"line":93,"column":null}},"line":89},"10":{"name":"(anonymous_10)","decl":{"start":{"line":97,"column":4},"end":{"line":97,"column":22}},"loc":{"start":{"line":97,"column":40},"end":{"line":99,"column":null}},"line":97},"11":{"name":"useTwoFingerRotate","decl":{"start":{"line":107,"column":16},"end":{"line":107,"column":null}},"loc":{"start":{"line":112,"column":2},"end":{"line":165,"column":null}},"line":112},"12":{"name":"(anonymous_12)","decl":{"start":{"line":117,"column":27},"end":{"line":117,"column":40}},"loc":{"start":{"line":117,"column":58},"end":{"line":123,"column":5}},"line":117},"13":{"name":"(anonymous_13)","decl":{"start":{"line":125,"column":26},"end":{"line":125,"column":null}},"loc":{"start":{"line":126,"column":23},"end":{"line":135,"column":null}},"line":126},"14":{"name":"(anonymous_14)","decl":{"start":{"line":139,"column":25},"end":{"line":139,"column":43}},"loc":{"start":{"line":139,"column":43},"end":{"line":143,"column":5}},"line":139},"15":{"name":"(anonymous_15)","decl":{"start":{"line":145,"column":2},"end":{"line":145,"column":18}},"loc":{"start":{"line":145,"column":18},"end":{"line":158,"column":5}},"line":145},"16":{"name":"(anonymous_16)","decl":{"start":{"line":153,"column":4},"end":{"line":153,"column":17}},"loc":{"start":{"line":153,"column":17},"end":{"line":157,"column":null}},"line":153},"17":{"name":"(anonymous_17)","decl":{"start":{"line":161,"column":4},"end":{"line":161,"column":25}},"loc":{"start":{"line":161,"column":46},"end":{"line":163,"column":null}},"line":161},"18":{"name":"useCanvasPan","decl":{"start":{"line":171,"column":16},"end":{"line":171,"column":null}},"loc":{"start":{"line":178,"column":2},"end":{"line":254,"column":null}},"line":178},"19":{"name":"(anonymous_19)","decl":{"start":{"line":187,"column":27},"end":{"line":187,"column":null}},"loc":{"start":{"line":188,"column":23},"end":{"line":194,"column":null}},"line":188},"20":{"name":"(anonymous_20)","decl":{"start":{"line":198,"column":26},"end":{"line":198,"column":null}},"loc":{"start":{"line":199,"column":23},"end":{"line":206,"column":null}},"line":199},"21":{"name":"(anonymous_21)","decl":{"start":{"line":210,"column":25},"end":{"line":210,"column":null}},"loc":{"start":{"line":211,"column":23},"end":{"line":226,"column":null}},"line":211},"22":{"name":"(anonymous_22)","decl":{"start":{"line":230,"column":2},"end":{"line":230,"column":18}},"loc":{"start":{"line":230,"column":18},"end":{"line":243,"column":5}},"line":230},"23":{"name":"(anonymous_23)","decl":{"start":{"line":238,"column":4},"end":{"line":238,"column":17}},"loc":{"start":{"line":238,"column":17},"end":{"line":242,"column":null}},"line":238},"24":{"name":"(anonymous_24)","decl":{"start":{"line":246,"column":4},"end":{"line":246,"column":23}},"loc":{"start":{"line":246,"column":23},"end":{"line":249,"column":null}},"line":246},"25":{"name":"(anonymous_25)","decl":{"start":{"line":250,"column":4},"end":{"line":250,"column":16}},"loc":{"start":{"line":250,"column":43},"end":{"line":252,"column":null}},"line":250},"26":{"name":"useSwipeNavigation","decl":{"start":{"line":260,"column":16},"end":{"line":260,"column":null}},"loc":{"start":{"line":266,"column":2},"end":{"line":331,"column":null}},"line":266},"27":{"name":"(anonymous_27)","decl":{"start":{"line":274,"column":27},"end":{"line":274,"column":40}},"loc":{"start":{"line":274,"column":58},"end":{"line":278,"column":5}},"line":274},"28":{"name":"(anonymous_28)","decl":{"start":{"line":280,"column":26},"end":{"line":280,"column":null}},"loc":{"start":{"line":281,"column":23},"end":{"line":283,"column":null}},"line":281},"29":{"name":"(anonymous_29)","decl":{"start":{"line":287,"column":25},"end":{"line":287,"column":null}},"loc":{"start":{"line":288,"column":23},"end":{"line":309,"column":null}},"line":288},"30":{"name":"(anonymous_30)","decl":{"start":{"line":313,"column":2},"end":{"line":313,"column":18}},"loc":{"start":{"line":313,"column":18},"end":{"line":330,"column":5}},"line":313},"31":{"name":"(anonymous_31)","decl":{"start":{"line":325,"column":4},"end":{"line":325,"column":17}},"loc":{"start":{"line":325,"column":17},"end":{"line":329,"column":null}},"line":325}},"branchMap":{"0":{"loc":{"start":{"line":38,"column":19},"end":{"line":38,"column":null}},"type":"binary-expr","locations":[{"start":{"line":38,"column":19},"end":{"line":38,"column":40}},{"start":{"line":38,"column":40},"end":{"line":38,"column":null}}],"line":38},"1":{"loc":{"start":{"line":39,"column":19},"end":{"line":39,"column":null}},"type":"binary-expr","locations":[{"start":{"line":39,"column":19},"end":{"line":39,"column":40}},{"start":{"line":39,"column":40},"end":{"line":39,"column":null}}],"line":39},"2":{"loc":{"start":{"line":46,"column":4},"end":{"line":46,"column":null}},"type":"if","locations":[{"start":{"line":46,"column":4},"end":{"line":46,"column":null}},{"start":{},"end":{}}],"line":46},"3":{"loc":{"start":{"line":55,"column":6},"end":{"line":55,"column":null}},"type":"if","locations":[{"start":{"line":55,"column":6},"end":{"line":55,"column":null}},{"start":{},"end":{}}],"line":55},"4":{"loc":{"start":{"line":60,"column":6},"end":{"line":63,"column":null}},"type":"if","locations":[{"start":{"line":60,"column":6},"end":{"line":63,"column":null}},{"start":{},"end":{}}],"line":60},"5":{"loc":{"start":{"line":72,"column":4},"end":{"line":78,"column":null}},"type":"if","locations":[{"start":{"line":72,"column":4},"end":{"line":78,"column":null}},{"start":{},"end":{}}],"line":72},"6":{"loc":{"start":{"line":83,"column":4},"end":{"line":83,"column":null}},"type":"if","locations":[{"start":{"line":83,"column":4},"end":{"line":83,"column":null}},{"start":{},"end":{}}],"line":83},"7":{"loc":{"start":{"line":118,"column":4},"end":{"line":118,"column":null}},"type":"if","locations":[{"start":{"line":118,"column":4},"end":{"line":118,"column":null}},{"start":{},"end":{}}],"line":118},"8":{"loc":{"start":{"line":127,"column":6},"end":{"line":127,"column":null}},"type":"if","locations":[{"start":{"line":127,"column":6},"end":{"line":127,"column":null}},{"start":{},"end":{}}],"line":127},"9":{"loc":{"start":{"line":140,"column":4},"end":{"line":142,"column":null}},"type":"if","locations":[{"start":{"line":140,"column":4},"end":{"line":142,"column":null}},{"start":{},"end":{}}],"line":140},"10":{"loc":{"start":{"line":147,"column":4},"end":{"line":147,"column":null}},"type":"if","locations":[{"start":{"line":147,"column":4},"end":{"line":147,"column":null}},{"start":{},"end":{}}],"line":147},"11":{"loc":{"start":{"line":189,"column":6},"end":{"line":189,"column":null}},"type":"if","locations":[{"start":{"line":189,"column":6},"end":{"line":189,"column":null}},{"start":{},"end":{}}],"line":189},"12":{"loc":{"start":{"line":200,"column":6},"end":{"line":200,"column":null}},"type":"if","locations":[{"start":{"line":200,"column":6},"end":{"line":200,"column":null}},{"start":{},"end":{}}],"line":200},"13":{"loc":{"start":{"line":200,"column":10},"end":{"line":200,"column":85}},"type":"binary-expr","locations":[{"start":{"line":200,"column":10},"end":{"line":200,"column":35}},{"start":{"line":200,"column":35},"end":{"line":200,"column":61}},{"start":{"line":200,"column":61},"end":{"line":200,"column":85}}],"line":200},"14":{"loc":{"start":{"line":212,"column":6},"end":{"line":212,"column":null}},"type":"if","locations":[{"start":{"line":212,"column":6},"end":{"line":212,"column":null}},{"start":{},"end":{}}],"line":212},"15":{"loc":{"start":{"line":212,"column":10},"end":{"line":212,"column":59}},"type":"binary-expr","locations":[{"start":{"line":212,"column":10},"end":{"line":212,"column":35}},{"start":{"line":212,"column":35},"end":{"line":212,"column":59}}],"line":212},"16":{"loc":{"start":{"line":214,"column":6},"end":{"line":222,"column":null}},"type":"if","locations":[{"start":{"line":214,"column":6},"end":{"line":222,"column":null}},{"start":{},"end":{}}],"line":214},"17":{"loc":{"start":{"line":232,"column":4},"end":{"line":232,"column":null}},"type":"if","locations":[{"start":{"line":232,"column":4},"end":{"line":232,"column":null}},{"start":{},"end":{}}],"line":232},"18":{"loc":{"start":{"line":267,"column":20},"end":{"line":267,"column":null}},"type":"binary-expr","locations":[{"start":{"line":267,"column":20},"end":{"line":267,"column":42}},{"start":{"line":267,"column":42},"end":{"line":267,"column":null}}],"line":267},"19":{"loc":{"start":{"line":275,"column":4},"end":{"line":275,"column":null}},"type":"if","locations":[{"start":{"line":275,"column":4},"end":{"line":275,"column":null}},{"start":{},"end":{}}],"line":275},"20":{"loc":{"start":{"line":289,"column":6},"end":{"line":289,"column":null}},"type":"if","locations":[{"start":{"line":289,"column":6},"end":{"line":289,"column":null}},{"start":{},"end":{}}],"line":289},"21":{"loc":{"start":{"line":289,"column":10},"end":{"line":289,"column":67}},"type":"binary-expr","locations":[{"start":{"line":289,"column":10},"end":{"line":289,"column":36}},{"start":{"line":289,"column":36},"end":{"line":289,"column":67}}],"line":289},"22":{"loc":{"start":{"line":300,"column":6},"end":{"line":306,"column":null}},"type":"if","locations":[{"start":{"line":300,"column":6},"end":{"line":306,"column":null}},{"start":{},"end":{}}],"line":300},"23":{"loc":{"start":{"line":300,"column":10},"end":{"line":300,"column":65}},"type":"binary-expr","locations":[{"start":{"line":300,"column":10},"end":{"line":300,"column":31}},{"start":{"line":300,"column":31},"end":{"line":300,"column":47}},{"start":{"line":300,"column":47},"end":{"line":300,"column":65}}],"line":300},"24":{"loc":{"start":{"line":301,"column":8},"end":{"line":305,"column":null}},"type":"if","locations":[{"start":{"line":301,"column":8},"end":{"line":305,"column":null}},{"start":{"line":303,"column":15},"end":{"line":305,"column":null}}],"line":301}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":1,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":1,"32":0,"33":0,"34":1,"35":1,"36":1,"37":0,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":0,"47":1,"48":1,"49":1,"50":1,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":1,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0,"64":0,"65":0,"66":0,"67":1,"68":0,"69":0,"70":1,"71":1,"72":1,"73":0,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":0,"83":1,"84":1,"85":1,"86":1,"87":1,"88":1,"89":1,"90":0,"91":0,"92":0,"93":0,"94":0,"95":0,"96":1,"97":0,"98":0,"99":0,"100":0,"101":0,"102":0,"103":0,"104":1,"105":0,"106":0,"107":0,"108":0,"109":0,"110":0,"111":0,"112":0,"113":0,"114":0,"115":1,"116":1,"117":1,"118":0,"119":1,"120":1,"121":1,"122":1,"123":1,"124":1,"125":1,"126":1,"127":0,"128":0,"129":0,"130":1,"131":1,"132":1,"133":1,"134":1,"135":1,"136":0,"137":0,"138":0,"139":0,"140":1,"141":1,"142":0,"143":0,"144":0,"145":0,"146":0,"147":0,"148":0,"149":0,"150":0,"151":0,"152":0,"153":0,"154":0,"155":0,"156":1,"157":1,"158":1,"159":1,"160":1,"161":1,"162":1,"163":1,"164":1},"f":{"0":0,"1":0,"2":0,"3":0,"4":1,"5":0,"6":0,"7":0,"8":1,"9":1,"10":0,"11":1,"12":0,"13":0,"14":0,"15":1,"16":1,"17":0,"18":1,"19":0,"20":0,"21":0,"22":1,"23":1,"24":0,"25":0,"26":1,"27":0,"28":0,"29":0,"30":1,"31":1},"b":{"0":[1,1],"1":[1,1],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,1],"7":[0,0],"8":[0,0],"9":[0,0],"10":[0,1],"11":[0,0],"12":[0,0],"13":[0,0,0],"14":[0,0],"15":[0,0],"16":[0,0],"17":[0,1],"18":[1,1],"19":[0,0],"20":[0,0],"21":[0,0],"22":[0,0],"23":[0,0,0],"24":[0,0]},"meta":{"lastBranch":25,"lastFunction":32,"lastStatement":165,"seen":{"f:8:9:8:23":0,"s:9:2:9:Infinity":0,"f:12:9:12:21":1,"s:13:13:13:Infinity":1,"s:14:13:14:Infinity":2,"s:15:2:15:Infinity":3,"f:18:9:18:18":2,"s:19:2:19:Infinity":4,"f:22:9:22:21":3,"s:23:2:23:Infinity":5,"f:30:16:30:Infinity":4,"s:38:19:38:Infinity":6,"b:38:19:38:40:38:40:38:Infinity":0,"s:39:19:39:Infinity":7,"b:39:19:39:40:39:40:39:Infinity":1,"s:40:24:40:Infinity":8,"s:42:8:42:Infinity":9,"s:43:8:43:Infinity":10,"s:45:8:51:Infinity":11,"f:45:27:45:40":5,"b:46:4:46:Infinity:undefined:undefined:undefined:undefined":2,"s:46:4:46:Infinity":12,"s:46:32:46:Infinity":13,"s:47:4:47:Infinity":14,"s:48:15:48:Infinity":15,"s:49:15:49:Infinity":16,"s:50:4:50:Infinity":17,"s:53:8:69:Infinity":18,"f:53:26:53:Infinity":6,"b:55:6:55:Infinity:undefined:undefined:undefined:undefined":3,"s:55:6:55:Infinity":19,"s:55:34:55:Infinity":20,"s:56:6:56:Infinity":21,"s:57:17:57:Infinity":22,"s:58:17:58:Infinity":23,"s:59:30:59:Infinity":24,"b:60:6:63:Infinity:undefined:undefined:undefined:undefined":4,"s:60:6:63:Infinity":25,"s:61:8:61:Infinity":26,"s:62:8:62:Infinity":27,"s:64:20:64:Infinity":28,"s:65:23:65:Infinity":29,"s:66:6:66:Infinity":30,"s:71:8:79:Infinity":31,"f:71:25:71:38":7,"b:72:4:78:Infinity:undefined:undefined:undefined:undefined":5,"s:72:4:78:Infinity":32,"s:75:6:75:Infinity":33,"s:81:2:94:Infinity":34,"f:81:2:81:18":8,"s:82:15:82:Infinity":35,"b:83:4:83:Infinity:undefined:undefined:undefined:undefined":6,"s:83:4:83:Infinity":36,"s:83:13:83:Infinity":37,"s:85:4:85:Infinity":38,"s:86:4:86:Infinity":39,"s:87:4:87:Infinity":40,"s:89:4:93:Infinity":41,"f:89:4:89:17":9,"s:90:6:90:Infinity":42,"s:91:6:91:Infinity":43,"s:92:6:92:Infinity":44,"s:96:2:100:Infinity":45,"f:97:4:97:22":10,"s:98:6:98:Infinity":46,"f:107:16:107:Infinity":11,"s:113:27:113:Infinity":47,"s:114:8:114:Infinity":48,"s:115:8:115:Infinity":49,"s:117:8:123:Infinity":50,"f:117:27:117:40":12,"b:118:4:118:Infinity:undefined:undefined:undefined:undefined":7,"s:118:4:118:Infinity":51,"s:118:32:118:Infinity":52,"s:119:4:119:Infinity":53,"s:120:15:120:Infinity":54,"s:121:15:121:Infinity":55,"s:122:4:122:Infinity":56,"s:125:8:137:Infinity":57,"f:125:26:125:Infinity":13,"b:127:6:127:Infinity:undefined:undefined:undefined:undefined":8,"s:127:6:127:Infinity":58,"s:127:34:127:Infinity":59,"s:128:6:128:Infinity":60,"s:129:17:129:Infinity":61,"s:130:17:130:Infinity":62,"s:131:27:131:Infinity":63,"s:132:20:132:Infinity":64,"s:133:26:133:Infinity":65,"s:134:6:134:Infinity":66,"s:139:8:143:Infinity":67,"f:139:25:139:43":14,"b:140:4:142:Infinity:undefined:undefined:undefined:undefined":9,"s:140:4:142:Infinity":68,"s:141:6:141:Infinity":69,"s:145:2:158:Infinity":70,"f:145:2:145:18":15,"s:146:15:146:Infinity":71,"b:147:4:147:Infinity:undefined:undefined:undefined:undefined":10,"s:147:4:147:Infinity":72,"s:147:13:147:Infinity":73,"s:149:4:149:Infinity":74,"s:150:4:150:Infinity":75,"s:151:4:151:Infinity":76,"s:153:4:157:Infinity":77,"f:153:4:153:17":16,"s:154:6:154:Infinity":78,"s:155:6:155:Infinity":79,"s:156:6:156:Infinity":80,"s:160:2:164:Infinity":81,"f:161:4:161:25":17,"s:162:6:162:Infinity":82,"f:171:16:171:Infinity":18,"s:179:22:179:Infinity":83,"s:180:21:180:Infinity":84,"s:181:19:181:Infinity":85,"s:183:8:183:Infinity":86,"s:184:8:184:Infinity":87,"s:185:8:185:Infinity":88,"s:187:8:196:Infinity":89,"f:187:27:187:Infinity":19,"b:189:6:189:Infinity:undefined:undefined:undefined:undefined":11,"s:189:6:189:Infinity":90,"s:189:34:189:Infinity":91,"s:190:20:190:Infinity":92,"s:191:6:191:Infinity":93,"s:192:6:192:Infinity":94,"s:193:6:193:Infinity":95,"s:198:8:208:Infinity":96,"f:198:26:198:Infinity":20,"b:200:6:200:Infinity:undefined:undefined:undefined:undefined":12,"s:200:6:200:Infinity":97,"b:200:10:200:35:200:35:200:61:200:61:200:85":13,"s:200:85:200:Infinity":98,"s:201:6:201:Infinity":99,"s:202:27:202:Infinity":100,"s:203:17:203:Infinity":101,"s:204:17:204:Infinity":102,"s:205:6:205:Infinity":103,"s:210:8:228:Infinity":104,"f:210:25:210:Infinity":21,"b:212:6:212:Infinity:undefined:undefined:undefined:undefined":14,"s:212:6:212:Infinity":105,"b:212:10:212:35:212:35:212:59":15,"s:212:59:212:Infinity":106,"b:214:6:222:Infinity:undefined:undefined:undefined:undefined":16,"s:214:6:222:Infinity":107,"s:215:25:215:Infinity":108,"s:216:19:216:Infinity":109,"s:217:19:217:Infinity":110,"s:218:8:221:Infinity":111,"s:223:6:223:Infinity":112,"s:224:6:224:Infinity":113,"s:225:6:225:Infinity":114,"s:230:2:243:Infinity":115,"f:230:2:230:18":22,"s:231:15:231:Infinity":116,"b:232:4:232:Infinity:undefined:undefined:undefined:undefined":17,"s:232:4:232:Infinity":117,"s:232:13:232:Infinity":118,"s:234:4:234:Infinity":119,"s:235:4:235:Infinity":120,"s:236:4:236:Infinity":121,"s:238:4:242:Infinity":122,"f:238:4:238:17":23,"s:239:6:239:Infinity":123,"s:240:6:240:Infinity":124,"s:241:6:241:Infinity":125,"s:245:2:253:Infinity":126,"f:246:4:246:23":24,"s:247:6:247:Infinity":127,"s:248:6:248:Infinity":128,"f:250:4:250:16":25,"s:251:6:251:Infinity":129,"f:260:16:260:Infinity":26,"s:267:20:267:Infinity":130,"b:267:20:267:42:267:42:267:Infinity":18,"s:268:22:268:Infinity":131,"s:269:23:269:Infinity":132,"s:271:8:271:Infinity":133,"s:272:8:272:Infinity":134,"s:274:8:278:Infinity":135,"f:274:27:274:40":27,"b:275:4:275:Infinity:undefined:undefined:undefined:undefined":19,"s:275:4:275:Infinity":136,"s:275:32:275:Infinity":137,"s:276:4:276:Infinity":138,"s:277:4:277:Infinity":139,"s:280:8:285:Infinity":140,"f:280:26:280:Infinity":28,"s:287:8:311:Infinity":141,"f:287:25:287:Infinity":29,"b:289:6:289:Infinity:undefined:undefined:undefined:undefined":20,"s:289:6:289:Infinity":142,"b:289:10:289:36:289:36:289:67":21,"s:289:67:289:Infinity":143,"s:290:23:290:Infinity":144,"s:291:17:291:Infinity":145,"s:292:17:292:Infinity":146,"s:293:17:293:Infinity":147,"s:296:32:296:Infinity":148,"s:297:27:297:Infinity":149,"s:298:31:298:Infinity":150,"b:300:6:306:Infinity:undefined:undefined:undefined:undefined":22,"s:300:6:306:Infinity":151,"b:300:10:300:31:300:31:300:47:300:47:300:65":23,"b:301:8:305:Infinity:303:15:305:Infinity":24,"s:301:8:305:Infinity":152,"s:302:10:302:Infinity":153,"s:304:10:304:Infinity":154,"s:308:6:308:Infinity":155,"s:313:2:330:Infinity":156,"f:313:2:313:18":30,"s:314:20:318:Infinity":157,"s:321:4:321:Infinity":158,"s:322:4:322:Infinity":159,"s:323:4:323:Infinity":160,"s:325:4:329:Infinity":161,"f:325:4:325:17":31,"s:326:6:326:Infinity":162,"s:327:6:327:Infinity":163,"s:328:6:328:Infinity":164},"fnNames":{}}} +,"/workspace/src/store/useUIStore.ts": {"path":"/workspace/src/store/useUIStore.ts","statementMap":{"0":{"start":{"line":4,"column":26},"end":{"line":14,"column":null}},"1":{"start":{"line":136,"column":13},"end":{"line":220,"column":null}},"2":{"start":{"line":136,"column":57},"end":{"line":220,"column":2}},"3":{"start":{"line":201,"column":22},"end":{"line":201,"column":null}},"4":{"start":{"line":201,"column":38},"end":{"line":201,"column":95}},"5":{"start":{"line":204,"column":20},"end":{"line":204,"column":null}},"6":{"start":{"line":205,"column":4},"end":{"line":215,"column":null}},"7":{"start":{"line":206,"column":6},"end":{"line":206,"column":null}},"8":{"start":{"line":208,"column":27},"end":{"line":208,"column":null}},"9":{"start":{"line":209,"column":23},"end":{"line":209,"column":null}},"10":{"start":{"line":210,"column":6},"end":{"line":214,"column":null}},"11":{"start":{"line":219,"column":39},"end":{"line":219,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":136,"column":26},"end":{"line":136,"column":43}},"loc":{"start":{"line":136,"column":57},"end":{"line":220,"column":2}},"line":136},"1":{"name":"(anonymous_1)","decl":{"start":{"line":201,"column":2},"end":{"line":201,"column":10}},"loc":{"start":{"line":201,"column":22},"end":{"line":201,"column":null}},"line":201},"2":{"name":"(anonymous_2)","decl":{"start":{"line":201,"column":22},"end":{"line":201,"column":27}},"loc":{"start":{"line":201,"column":38},"end":{"line":201,"column":95}},"line":201},"3":{"name":"(anonymous_3)","decl":{"start":{"line":203,"column":2},"end":{"line":203,"column":18}},"loc":{"start":{"line":203,"column":30},"end":{"line":216,"column":null}},"line":203},"4":{"name":"(anonymous_4)","decl":{"start":{"line":219,"column":2},"end":{"line":219,"column":27}},"loc":{"start":{"line":219,"column":39},"end":{"line":219,"column":null}},"line":219}},"branchMap":{"0":{"loc":{"start":{"line":201,"column":38},"end":{"line":201,"column":95}},"type":"cond-expr","locations":[{"start":{"line":201,"column":70},"end":{"line":201,"column":87}},{"start":{"line":201,"column":87},"end":{"line":201,"column":95}}],"line":201},"1":{"loc":{"start":{"line":205,"column":4},"end":{"line":215,"column":null}},"type":"if","locations":[{"start":{"line":205,"column":4},"end":{"line":215,"column":null}},{"start":{"line":207,"column":11},"end":{"line":215,"column":null}}],"line":205},"2":{"loc":{"start":{"line":208,"column":27},"end":{"line":208,"column":null}},"type":"cond-expr","locations":[{"start":{"line":208,"column":37},"end":{"line":208,"column":74}},{"start":{"line":208,"column":74},"end":{"line":208,"column":null}}],"line":208},"3":{"loc":{"start":{"line":209,"column":23},"end":{"line":209,"column":null}},"type":"cond-expr","locations":[{"start":{"line":209,"column":33},"end":{"line":209,"column":70}},{"start":{"line":209,"column":70},"end":{"line":209,"column":null}}],"line":209},"4":{"loc":{"start":{"line":211,"column":24},"end":{"line":211,"column":null}},"type":"cond-expr","locations":[{"start":{"line":211,"column":50},"end":{"line":211,"column":54}},{"start":{"line":211,"column":54},"end":{"line":211,"column":null}}],"line":211}},"s":{"0":1,"1":1,"2":1,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0},"f":{"0":1,"1":0,"2":0,"3":0,"4":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0]},"meta":{"lastBranch":5,"lastFunction":5,"lastStatement":12,"seen":{"s:4:26:14:Infinity":0,"s:136:13:220:Infinity":1,"f:136:26:136:43":0,"s:136:57:220:2":2,"f:201:2:201:10":1,"s:201:22:201:Infinity":3,"f:201:22:201:27":2,"s:201:38:201:95":4,"b:201:70:201:87:201:87:201:95":0,"f:203:2:203:18":3,"s:204:20:204:Infinity":5,"b:205:4:215:Infinity:207:11:215:Infinity":1,"s:205:4:215:Infinity":6,"s:206:6:206:Infinity":7,"s:208:27:208:Infinity":8,"b:208:37:208:74:208:74:208:Infinity":2,"s:209:23:209:Infinity":9,"b:209:33:209:70:209:70:209:Infinity":3,"s:210:6:214:Infinity":10,"b:211:50:211:54:211:54:211:Infinity":4,"f:219:2:219:27":4,"s:219:39:219:Infinity":11},"fnNames":{}}} +,"/workspace/src/types/typography.ts": {"path":"/workspace/src/types/typography.ts","statementMap":{"0":{"start":{"line":5,"column":59},"end":{"line":10,"column":null}},"1":{"start":{"line":11,"column":56},"end":{"line":20,"column":null}},"2":{"start":{"line":23,"column":60},"end":{"line":28,"column":null}},"3":{"start":{"line":29,"column":58},"end":{"line":38,"column":null}},"4":{"start":{"line":48,"column":64},"end":{"line":101,"column":null}}},"fnMap":{},"branchMap":{},"s":{"0":1,"1":1,"2":1,"3":1,"4":1},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":5,"seen":{"s:5:59:10:Infinity":0,"s:11:56:20:Infinity":1,"s:23:60:28:Infinity":2,"s:29:58:38:Infinity":3,"s:48:64:101:Infinity":4},"fnNames":{}}} +,"/workspace/src/utils/adjustments.ts": {"path":"/workspace/src/utils/adjustments.ts","statementMap":{"0":{"start":{"line":5,"column":7},"end":{"line":10,"column":null}},"1":{"start":{"line":6,"column":2},"end":{"line":6,"column":null}},"2":{"start":{"line":7,"column":2},"end":{"line":7,"column":null}},"3":{"start":{"line":8,"column":2},"end":{"line":8,"column":null}},"4":{"start":{"line":9,"column":2},"end":{"line":9,"column":null}},"5":{"start":{"line":12,"column":7},"end":{"line":18,"column":null}},"6":{"start":{"line":13,"column":2},"end":{"line":13,"column":null}},"7":{"start":{"line":14,"column":2},"end":{"line":14,"column":null}},"8":{"start":{"line":15,"column":2},"end":{"line":15,"column":null}},"9":{"start":{"line":16,"column":2},"end":{"line":16,"column":null}},"10":{"start":{"line":17,"column":2},"end":{"line":17,"column":null}},"11":{"start":{"line":20,"column":7},"end":{"line":23,"column":null}},"12":{"start":{"line":21,"column":2},"end":{"line":21,"column":null}},"13":{"start":{"line":22,"column":2},"end":{"line":22,"column":null}},"14":{"start":{"line":31,"column":7},"end":{"line":39,"column":null}},"15":{"start":{"line":32,"column":2},"end":{"line":32,"column":null}},"16":{"start":{"line":33,"column":2},"end":{"line":33,"column":null}},"17":{"start":{"line":34,"column":2},"end":{"line":34,"column":null}},"18":{"start":{"line":35,"column":2},"end":{"line":35,"column":null}},"19":{"start":{"line":36,"column":2},"end":{"line":36,"column":null}},"20":{"start":{"line":37,"column":2},"end":{"line":37,"column":null}},"21":{"start":{"line":38,"column":2},"end":{"line":38,"column":null}},"22":{"start":{"line":41,"column":7},"end":{"line":50,"column":null}},"23":{"start":{"line":42,"column":2},"end":{"line":42,"column":null}},"24":{"start":{"line":43,"column":2},"end":{"line":43,"column":null}},"25":{"start":{"line":44,"column":2},"end":{"line":44,"column":null}},"26":{"start":{"line":45,"column":2},"end":{"line":45,"column":null}},"27":{"start":{"line":46,"column":2},"end":{"line":46,"column":null}},"28":{"start":{"line":47,"column":2},"end":{"line":47,"column":null}},"29":{"start":{"line":48,"column":2},"end":{"line":48,"column":null}},"30":{"start":{"line":49,"column":2},"end":{"line":49,"column":null}},"31":{"start":{"line":52,"column":7},"end":{"line":59,"column":null}},"32":{"start":{"line":53,"column":2},"end":{"line":53,"column":null}},"33":{"start":{"line":54,"column":2},"end":{"line":54,"column":null}},"34":{"start":{"line":55,"column":2},"end":{"line":55,"column":null}},"35":{"start":{"line":56,"column":2},"end":{"line":56,"column":null}},"36":{"start":{"line":57,"column":2},"end":{"line":57,"column":null}},"37":{"start":{"line":58,"column":2},"end":{"line":58,"column":null}},"38":{"start":{"line":61,"column":7},"end":{"line":72,"column":null}},"39":{"start":{"line":62,"column":2},"end":{"line":62,"column":null}},"40":{"start":{"line":63,"column":2},"end":{"line":63,"column":null}},"41":{"start":{"line":64,"column":2},"end":{"line":64,"column":null}},"42":{"start":{"line":65,"column":2},"end":{"line":65,"column":null}},"43":{"start":{"line":66,"column":2},"end":{"line":66,"column":null}},"44":{"start":{"line":67,"column":2},"end":{"line":67,"column":null}},"45":{"start":{"line":68,"column":2},"end":{"line":68,"column":null}},"46":{"start":{"line":69,"column":2},"end":{"line":69,"column":null}},"47":{"start":{"line":70,"column":2},"end":{"line":70,"column":null}},"48":{"start":{"line":71,"column":2},"end":{"line":71,"column":null}},"49":{"start":{"line":74,"column":7},"end":{"line":87,"column":null}},"50":{"start":{"line":75,"column":2},"end":{"line":75,"column":null}},"51":{"start":{"line":76,"column":2},"end":{"line":76,"column":null}},"52":{"start":{"line":77,"column":2},"end":{"line":77,"column":null}},"53":{"start":{"line":78,"column":2},"end":{"line":78,"column":null}},"54":{"start":{"line":79,"column":2},"end":{"line":79,"column":null}},"55":{"start":{"line":80,"column":2},"end":{"line":80,"column":null}},"56":{"start":{"line":81,"column":2},"end":{"line":81,"column":null}},"57":{"start":{"line":82,"column":2},"end":{"line":82,"column":null}},"58":{"start":{"line":83,"column":2},"end":{"line":83,"column":null}},"59":{"start":{"line":84,"column":2},"end":{"line":84,"column":null}},"60":{"start":{"line":85,"column":2},"end":{"line":85,"column":null}},"61":{"start":{"line":86,"column":2},"end":{"line":86,"column":null}},"62":{"start":{"line":89,"column":7},"end":{"line":93,"column":null}},"63":{"start":{"line":90,"column":2},"end":{"line":90,"column":null}},"64":{"start":{"line":91,"column":2},"end":{"line":91,"column":null}},"65":{"start":{"line":92,"column":2},"end":{"line":92,"column":null}},"66":{"start":{"line":95,"column":7},"end":{"line":104,"column":null}},"67":{"start":{"line":96,"column":2},"end":{"line":96,"column":null}},"68":{"start":{"line":97,"column":2},"end":{"line":97,"column":null}},"69":{"start":{"line":98,"column":2},"end":{"line":98,"column":null}},"70":{"start":{"line":99,"column":2},"end":{"line":99,"column":null}},"71":{"start":{"line":100,"column":2},"end":{"line":100,"column":null}},"72":{"start":{"line":101,"column":2},"end":{"line":101,"column":null}},"73":{"start":{"line":102,"column":2},"end":{"line":102,"column":null}},"74":{"start":{"line":103,"column":2},"end":{"line":103,"column":null}},"75":{"start":{"line":106,"column":7},"end":{"line":117,"column":null}},"76":{"start":{"line":107,"column":2},"end":{"line":107,"column":null}},"77":{"start":{"line":108,"column":2},"end":{"line":108,"column":null}},"78":{"start":{"line":109,"column":2},"end":{"line":109,"column":null}},"79":{"start":{"line":110,"column":2},"end":{"line":110,"column":null}},"80":{"start":{"line":111,"column":2},"end":{"line":111,"column":null}},"81":{"start":{"line":112,"column":2},"end":{"line":112,"column":null}},"82":{"start":{"line":113,"column":2},"end":{"line":113,"column":null}},"83":{"start":{"line":114,"column":2},"end":{"line":114,"column":null}},"84":{"start":{"line":115,"column":2},"end":{"line":115,"column":null}},"85":{"start":{"line":116,"column":2},"end":{"line":116,"column":null}},"86":{"start":{"line":173,"column":65},"end":{"line":195,"column":null}},"87":{"start":{"line":404,"column":42},"end":{"line":410,"column":null}},"88":{"start":{"line":412,"column":49},"end":{"line":419,"column":null}},"89":{"start":{"line":421,"column":52},"end":{"line":429,"column":null}},"90":{"start":{"line":431,"column":74},"end":{"line":441,"column":null}},"91":{"start":{"line":443,"column":13},"end":{"line":448,"column":null}},"92":{"start":{"line":443,"column":65},"end":{"line":448,"column":null}},"93":{"start":{"line":450,"column":13},"end":{"line":467,"column":null}},"94":{"start":{"line":450,"column":47},"end":{"line":467,"column":null}},"95":{"start":{"line":469,"column":40},"end":{"line":469,"column":null}},"96":{"start":{"line":471,"column":57},"end":{"line":516,"column":null}},"97":{"start":{"line":518,"column":53},"end":{"line":525,"column":null}},"98":{"start":{"line":527,"column":48},"end":{"line":617,"column":null}},"99":{"start":{"line":619,"column":6},"end":{"line":636,"column":null}},"100":{"start":{"line":619,"column":50},"end":{"line":636,"column":null}},"101":{"start":{"line":620,"column":41},"end":{"line":620,"column":50}},"102":{"start":{"line":624,"column":43},"end":{"line":624,"column":52}},"103":{"start":{"line":628,"column":41},"end":{"line":628,"column":50}},"104":{"start":{"line":632,"column":39},"end":{"line":632,"column":48}},"105":{"start":{"line":638,"column":6},"end":{"line":643,"column":null}},"106":{"start":{"line":638,"column":63},"end":{"line":643,"column":null}},"107":{"start":{"line":645,"column":13},"end":{"line":749,"column":null}},"108":{"start":{"line":646,"column":2},"end":{"line":648,"column":null}},"109":{"start":{"line":647,"column":4},"end":{"line":647,"column":null}},"110":{"start":{"line":650,"column":8},"end":{"line":658,"column":null}},"111":{"start":{"line":651,"column":4},"end":{"line":657,"column":null}},"112":{"start":{"line":651,"column":64},"end":{"line":657,"column":6}},"113":{"start":{"line":660,"column":8},"end":{"line":693,"column":null}},"114":{"start":{"line":661,"column":33},"end":{"line":661,"column":null}},"115":{"start":{"line":662,"column":31},"end":{"line":662,"column":null}},"116":{"start":{"line":664,"column":4},"end":{"line":692,"column":null}},"117":{"start":{"line":695,"column":8},"end":{"line":699,"column":null}},"118":{"start":{"line":695,"column":87},"end":{"line":699,"column":4}},"119":{"start":{"line":701,"column":2},"end":{"line":748,"column":null}},"120":{"start":{"line":756,"column":68},"end":{"line":862,"column":null}},"121":{"start":{"line":864,"column":50},"end":{"line":866,"column":null}},"122":{"start":{"line":866,"column":22},"end":{"line":866,"column":32}},"123":{"start":{"line":868,"column":45},"end":{"line":918,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":5,"column":7},"end":{"line":5,"column":12}},"loc":{"start":{"line":5,"column":7},"end":{"line":10,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":12,"column":7},"end":{"line":12,"column":12}},"loc":{"start":{"line":12,"column":7},"end":{"line":18,"column":null}},"line":12},"2":{"name":"(anonymous_2)","decl":{"start":{"line":20,"column":7},"end":{"line":20,"column":12}},"loc":{"start":{"line":20,"column":7},"end":{"line":23,"column":null}},"line":20},"3":{"name":"(anonymous_3)","decl":{"start":{"line":31,"column":7},"end":{"line":31,"column":12}},"loc":{"start":{"line":31,"column":7},"end":{"line":39,"column":null}},"line":31},"4":{"name":"(anonymous_4)","decl":{"start":{"line":41,"column":7},"end":{"line":41,"column":12}},"loc":{"start":{"line":41,"column":7},"end":{"line":50,"column":null}},"line":41},"5":{"name":"(anonymous_5)","decl":{"start":{"line":52,"column":7},"end":{"line":52,"column":12}},"loc":{"start":{"line":52,"column":7},"end":{"line":59,"column":null}},"line":52},"6":{"name":"(anonymous_6)","decl":{"start":{"line":61,"column":7},"end":{"line":61,"column":12}},"loc":{"start":{"line":61,"column":7},"end":{"line":72,"column":null}},"line":61},"7":{"name":"(anonymous_7)","decl":{"start":{"line":74,"column":7},"end":{"line":74,"column":12}},"loc":{"start":{"line":74,"column":7},"end":{"line":87,"column":null}},"line":74},"8":{"name":"(anonymous_8)","decl":{"start":{"line":89,"column":7},"end":{"line":89,"column":12}},"loc":{"start":{"line":89,"column":7},"end":{"line":93,"column":null}},"line":89},"9":{"name":"(anonymous_9)","decl":{"start":{"line":95,"column":7},"end":{"line":95,"column":12}},"loc":{"start":{"line":95,"column":7},"end":{"line":104,"column":null}},"line":95},"10":{"name":"(anonymous_10)","decl":{"start":{"line":106,"column":7},"end":{"line":106,"column":12}},"loc":{"start":{"line":106,"column":7},"end":{"line":117,"column":null}},"line":106},"11":{"name":"(anonymous_11)","decl":{"start":{"line":443,"column":13},"end":{"line":443,"column":65}},"loc":{"start":{"line":443,"column":65},"end":{"line":448,"column":null}},"line":443},"12":{"name":"(anonymous_12)","decl":{"start":{"line":450,"column":13},"end":{"line":450,"column":47}},"loc":{"start":{"line":450,"column":47},"end":{"line":467,"column":null}},"line":450},"13":{"name":"(anonymous_13)","decl":{"start":{"line":619,"column":6},"end":{"line":619,"column":25}},"loc":{"start":{"line":619,"column":50},"end":{"line":636,"column":null}},"line":619},"14":{"name":"(anonymous_14)","decl":{"start":{"line":620,"column":22},"end":{"line":620,"column":27}},"loc":{"start":{"line":620,"column":41},"end":{"line":620,"column":50}},"line":620},"15":{"name":"(anonymous_15)","decl":{"start":{"line":624,"column":24},"end":{"line":624,"column":29}},"loc":{"start":{"line":624,"column":43},"end":{"line":624,"column":52}},"line":624},"16":{"name":"(anonymous_16)","decl":{"start":{"line":628,"column":22},"end":{"line":628,"column":27}},"loc":{"start":{"line":628,"column":41},"end":{"line":628,"column":50}},"line":628},"17":{"name":"(anonymous_17)","decl":{"start":{"line":632,"column":20},"end":{"line":632,"column":25}},"loc":{"start":{"line":632,"column":39},"end":{"line":632,"column":48}},"line":632},"18":{"name":"(anonymous_18)","decl":{"start":{"line":638,"column":6},"end":{"line":638,"column":29}},"loc":{"start":{"line":638,"column":63},"end":{"line":643,"column":null}},"line":638},"19":{"name":"(anonymous_19)","decl":{"start":{"line":645,"column":13},"end":{"line":645,"column":43}},"loc":{"start":{"line":645,"column":83},"end":{"line":749,"column":null}},"line":645},"20":{"name":"(anonymous_20)","decl":{"start":{"line":650,"column":8},"end":{"line":650,"column":29}},"loc":{"start":{"line":650,"column":49},"end":{"line":658,"column":null}},"line":650},"21":{"name":"(anonymous_21)","decl":{"start":{"line":651,"column":28},"end":{"line":651,"column":33}},"loc":{"start":{"line":651,"column":64},"end":{"line":657,"column":6}},"line":651},"22":{"name":"(anonymous_22)","decl":{"start":{"line":660,"column":58},"end":{"line":660,"column":63}},"loc":{"start":{"line":660,"column":96},"end":{"line":693,"column":3}},"line":660},"23":{"name":"(anonymous_23)","decl":{"start":{"line":695,"column":66},"end":{"line":695,"column":71}},"loc":{"start":{"line":695,"column":87},"end":{"line":699,"column":4}},"line":695},"24":{"name":"(anonymous_24)","decl":{"start":{"line":866,"column":3},"end":{"line":866,"column":12}},"loc":{"start":{"line":866,"column":22},"end":{"line":866,"column":32}},"line":866}},"branchMap":{"0":{"loc":{"start":{"line":620,"column":8},"end":{"line":623,"column":null}},"type":"binary-expr","locations":[{"start":{"line":620,"column":8},"end":{"line":620,"column":55}},{"start":{"line":620,"column":55},"end":{"line":623,"column":null}}],"line":620},"1":{"loc":{"start":{"line":624,"column":9},"end":{"line":627,"column":null}},"type":"binary-expr","locations":[{"start":{"line":624,"column":9},"end":{"line":624,"column":57}},{"start":{"line":624,"column":57},"end":{"line":627,"column":null}}],"line":624},"2":{"loc":{"start":{"line":628,"column":8},"end":{"line":631,"column":null}},"type":"binary-expr","locations":[{"start":{"line":628,"column":8},"end":{"line":628,"column":55}},{"start":{"line":628,"column":55},"end":{"line":631,"column":null}}],"line":628},"3":{"loc":{"start":{"line":632,"column":7},"end":{"line":635,"column":null}},"type":"binary-expr","locations":[{"start":{"line":632,"column":7},"end":{"line":632,"column":53}},{"start":{"line":632,"column":53},"end":{"line":635,"column":null}}],"line":632},"4":{"loc":{"start":{"line":639,"column":52},"end":{"line":639,"column":72}},"type":"binary-expr","locations":[{"start":{"line":639,"column":52},"end":{"line":639,"column":68}},{"start":{"line":639,"column":68},"end":{"line":639,"column":72}}],"line":639},"5":{"loc":{"start":{"line":640,"column":51},"end":{"line":640,"column":70}},"type":"binary-expr","locations":[{"start":{"line":640,"column":51},"end":{"line":640,"column":66}},{"start":{"line":640,"column":66},"end":{"line":640,"column":70}}],"line":640},"6":{"loc":{"start":{"line":641,"column":53},"end":{"line":641,"column":74}},"type":"binary-expr","locations":[{"start":{"line":641,"column":53},"end":{"line":641,"column":70}},{"start":{"line":641,"column":70},"end":{"line":641,"column":74}}],"line":641},"7":{"loc":{"start":{"line":642,"column":52},"end":{"line":642,"column":72}},"type":"binary-expr","locations":[{"start":{"line":642,"column":52},"end":{"line":642,"column":68}},{"start":{"line":642,"column":68},"end":{"line":642,"column":72}}],"line":642},"8":{"loc":{"start":{"line":646,"column":2},"end":{"line":648,"column":null}},"type":"if","locations":[{"start":{"line":646,"column":2},"end":{"line":648,"column":null}},{"start":{},"end":{}}],"line":646},"9":{"loc":{"start":{"line":651,"column":12},"end":{"line":651,"column":26}},"type":"binary-expr","locations":[{"start":{"line":651,"column":12},"end":{"line":651,"column":24}},{"start":{"line":651,"column":24},"end":{"line":651,"column":26}}],"line":651},"10":{"loc":{"start":{"line":660,"column":27},"end":{"line":660,"column":56}},"type":"binary-expr","locations":[{"start":{"line":660,"column":27},"end":{"line":660,"column":54}},{"start":{"line":660,"column":54},"end":{"line":660,"column":56}}],"line":660},"11":{"loc":{"start":{"line":661,"column":33},"end":{"line":661,"column":null}},"type":"binary-expr","locations":[{"start":{"line":661,"column":33},"end":{"line":661,"column":62}},{"start":{"line":661,"column":62},"end":{"line":661,"column":null}}],"line":661},"12":{"loc":{"start":{"line":666,"column":10},"end":{"line":666,"column":null}},"type":"binary-expr","locations":[{"start":{"line":666,"column":10},"end":{"line":666,"column":30}},{"start":{"line":666,"column":24},"end":{"line":666,"column":null}}],"line":666},"13":{"loc":{"start":{"line":671,"column":21},"end":{"line":671,"column":null}},"type":"binary-expr","locations":[{"start":{"line":671,"column":21},"end":{"line":671,"column":57}},{"start":{"line":671,"column":57},"end":{"line":671,"column":null}}],"line":671},"14":{"loc":{"start":{"line":672,"column":20},"end":{"line":672,"column":null}},"type":"binary-expr","locations":[{"start":{"line":672,"column":20},"end":{"line":672,"column":55}},{"start":{"line":672,"column":55},"end":{"line":672,"column":null}}],"line":672},"15":{"loc":{"start":{"line":673,"column":24},"end":{"line":673,"column":null}},"type":"binary-expr","locations":[{"start":{"line":673,"column":24},"end":{"line":673,"column":63}},{"start":{"line":673,"column":63},"end":{"line":673,"column":null}}],"line":673},"16":{"loc":{"start":{"line":674,"column":13},"end":{"line":674,"column":null}},"type":"binary-expr","locations":[{"start":{"line":674,"column":13},"end":{"line":674,"column":41}},{"start":{"line":674,"column":41},"end":{"line":674,"column":null}}],"line":674},"17":{"loc":{"start":{"line":675,"column":70},"end":{"line":675,"column":111}},"type":"binary-expr","locations":[{"start":{"line":675,"column":70},"end":{"line":675,"column":107}},{"start":{"line":675,"column":107},"end":{"line":675,"column":111}}],"line":675},"18":{"loc":{"start":{"line":676,"column":52},"end":{"line":676,"column":84}},"type":"binary-expr","locations":[{"start":{"line":676,"column":52},"end":{"line":676,"column":80}},{"start":{"line":676,"column":80},"end":{"line":676,"column":84}}],"line":676},"19":{"loc":{"start":{"line":677,"column":16},"end":{"line":677,"column":null}},"type":"cond-expr","locations":[{"start":{"line":677,"column":46},"end":{"line":677,"column":93}},{"start":{"line":677,"column":93},"end":{"line":677,"column":null}}],"line":677},"20":{"loc":{"start":{"line":678,"column":21},"end":{"line":680,"column":null}},"type":"cond-expr","locations":[{"start":{"line":679,"column":12},"end":{"line":679,"column":null}},{"start":{"line":680,"column":12},"end":{"line":680,"column":null}}],"line":678},"21":{"loc":{"start":{"line":681,"column":25},"end":{"line":683,"column":null}},"type":"cond-expr","locations":[{"start":{"line":682,"column":12},"end":{"line":682,"column":null}},{"start":{"line":683,"column":12},"end":{"line":683,"column":null}}],"line":681},"22":{"loc":{"start":{"line":684,"column":19},"end":{"line":684,"column":null}},"type":"binary-expr","locations":[{"start":{"line":684,"column":19},"end":{"line":684,"column":53}},{"start":{"line":684,"column":53},"end":{"line":684,"column":null}}],"line":684},"23":{"loc":{"start":{"line":687,"column":14},"end":{"line":687,"column":null}},"type":"binary-expr","locations":[{"start":{"line":687,"column":14},"end":{"line":687,"column":56}},{"start":{"line":687,"column":56},"end":{"line":687,"column":null}}],"line":687},"24":{"loc":{"start":{"line":689,"column":28},"end":{"line":689,"column":null}},"type":"binary-expr","locations":[{"start":{"line":689,"column":28},"end":{"line":689,"column":71}},{"start":{"line":689,"column":71},"end":{"line":689,"column":null}}],"line":689},"25":{"loc":{"start":{"line":695,"column":31},"end":{"line":695,"column":64}},"type":"binary-expr","locations":[{"start":{"line":695,"column":31},"end":{"line":695,"column":62}},{"start":{"line":695,"column":62},"end":{"line":695,"column":64}}],"line":695},"26":{"loc":{"start":{"line":704,"column":17},"end":{"line":704,"column":null}},"type":"binary-expr","locations":[{"start":{"line":704,"column":17},"end":{"line":704,"column":50}},{"start":{"line":704,"column":50},"end":{"line":704,"column":null}}],"line":704},"27":{"loc":{"start":{"line":705,"column":16},"end":{"line":705,"column":null}},"type":"binary-expr","locations":[{"start":{"line":705,"column":16},"end":{"line":705,"column":48}},{"start":{"line":705,"column":48},"end":{"line":705,"column":null}}],"line":705},"28":{"loc":{"start":{"line":706,"column":20},"end":{"line":706,"column":null}},"type":"binary-expr","locations":[{"start":{"line":706,"column":20},"end":{"line":706,"column":56}},{"start":{"line":706,"column":56},"end":{"line":706,"column":null}}],"line":706},"29":{"loc":{"start":{"line":707,"column":24},"end":{"line":707,"column":null}},"type":"binary-expr","locations":[{"start":{"line":707,"column":24},"end":{"line":707,"column":64}},{"start":{"line":707,"column":64},"end":{"line":707,"column":null}}],"line":707},"30":{"loc":{"start":{"line":708,"column":15},"end":{"line":708,"column":null}},"type":"binary-expr","locations":[{"start":{"line":708,"column":15},"end":{"line":708,"column":46}},{"start":{"line":708,"column":46},"end":{"line":708,"column":null}}],"line":708},"31":{"loc":{"start":{"line":709,"column":15},"end":{"line":709,"column":null}},"type":"binary-expr","locations":[{"start":{"line":709,"column":15},"end":{"line":709,"column":46}},{"start":{"line":709,"column":46},"end":{"line":709,"column":null}}],"line":709},"32":{"loc":{"start":{"line":710,"column":26},"end":{"line":710,"column":null}},"type":"binary-expr","locations":[{"start":{"line":710,"column":26},"end":{"line":710,"column":68}},{"start":{"line":710,"column":68},"end":{"line":710,"column":null}}],"line":710},"33":{"loc":{"start":{"line":711,"column":24},"end":{"line":711,"column":null}},"type":"binary-expr","locations":[{"start":{"line":711,"column":24},"end":{"line":711,"column":64}},{"start":{"line":711,"column":64},"end":{"line":711,"column":null}}],"line":711},"34":{"loc":{"start":{"line":712,"column":19},"end":{"line":712,"column":null}},"type":"binary-expr","locations":[{"start":{"line":712,"column":19},"end":{"line":712,"column":54}},{"start":{"line":712,"column":54},"end":{"line":712,"column":null}}],"line":712},"35":{"loc":{"start":{"line":713,"column":27},"end":{"line":713,"column":null}},"type":"binary-expr","locations":[{"start":{"line":713,"column":27},"end":{"line":713,"column":70}},{"start":{"line":713,"column":70},"end":{"line":713,"column":null}}],"line":713},"36":{"loc":{"start":{"line":714,"column":20},"end":{"line":714,"column":null}},"type":"binary-expr","locations":[{"start":{"line":714,"column":20},"end":{"line":714,"column":56}},{"start":{"line":714,"column":56},"end":{"line":714,"column":null}}],"line":714},"37":{"loc":{"start":{"line":715,"column":25},"end":{"line":715,"column":null}},"type":"binary-expr","locations":[{"start":{"line":715,"column":25},"end":{"line":715,"column":66}},{"start":{"line":715,"column":66},"end":{"line":715,"column":null}}],"line":715},"38":{"loc":{"start":{"line":716,"column":26},"end":{"line":718,"column":null}},"type":"cond-expr","locations":[{"start":{"line":717,"column":8},"end":{"line":717,"column":null}},{"start":{"line":718,"column":8},"end":{"line":718,"column":null}}],"line":716},"39":{"loc":{"start":{"line":719,"column":25},"end":{"line":719,"column":null}},"type":"binary-expr","locations":[{"start":{"line":719,"column":25},"end":{"line":719,"column":66}},{"start":{"line":719,"column":66},"end":{"line":719,"column":null}}],"line":719},"40":{"loc":{"start":{"line":720,"column":23},"end":{"line":720,"column":null}},"type":"binary-expr","locations":[{"start":{"line":720,"column":23},"end":{"line":720,"column":62}},{"start":{"line":720,"column":62},"end":{"line":720,"column":null}}],"line":720},"41":{"loc":{"start":{"line":721,"column":25},"end":{"line":721,"column":null}},"type":"binary-expr","locations":[{"start":{"line":721,"column":25},"end":{"line":721,"column":66}},{"start":{"line":721,"column":66},"end":{"line":721,"column":null}}],"line":721},"42":{"loc":{"start":{"line":722,"column":21},"end":{"line":722,"column":null}},"type":"binary-expr","locations":[{"start":{"line":722,"column":21},"end":{"line":722,"column":58}},{"start":{"line":722,"column":58},"end":{"line":722,"column":null}}],"line":722},"43":{"loc":{"start":{"line":723,"column":21},"end":{"line":723,"column":null}},"type":"binary-expr","locations":[{"start":{"line":723,"column":21},"end":{"line":723,"column":58}},{"start":{"line":723,"column":58},"end":{"line":723,"column":null}}],"line":723},"44":{"loc":{"start":{"line":724,"column":20},"end":{"line":724,"column":null}},"type":"binary-expr","locations":[{"start":{"line":724,"column":20},"end":{"line":724,"column":56}},{"start":{"line":724,"column":56},"end":{"line":724,"column":null}}],"line":724},"45":{"loc":{"start":{"line":725,"column":22},"end":{"line":725,"column":null}},"type":"binary-expr","locations":[{"start":{"line":725,"column":22},"end":{"line":725,"column":60}},{"start":{"line":725,"column":60},"end":{"line":725,"column":null}}],"line":725},"46":{"loc":{"start":{"line":726,"column":22},"end":{"line":726,"column":null}},"type":"binary-expr","locations":[{"start":{"line":726,"column":22},"end":{"line":726,"column":60}},{"start":{"line":726,"column":60},"end":{"line":726,"column":null}}],"line":726},"47":{"loc":{"start":{"line":727,"column":69},"end":{"line":727,"column":111}},"type":"binary-expr","locations":[{"start":{"line":727,"column":69},"end":{"line":727,"column":107}},{"start":{"line":727,"column":107},"end":{"line":727,"column":111}}],"line":727},"48":{"loc":{"start":{"line":728,"column":61},"end":{"line":728,"column":99}},"type":"binary-expr","locations":[{"start":{"line":728,"column":61},"end":{"line":728,"column":95}},{"start":{"line":728,"column":95},"end":{"line":728,"column":99}}],"line":728},"49":{"loc":{"start":{"line":729,"column":43},"end":{"line":729,"column":72}},"type":"binary-expr","locations":[{"start":{"line":729,"column":43},"end":{"line":729,"column":68}},{"start":{"line":729,"column":68},"end":{"line":729,"column":72}}],"line":729},"50":{"loc":{"start":{"line":730,"column":12},"end":{"line":730,"column":null}},"type":"cond-expr","locations":[{"start":{"line":730,"column":39},"end":{"line":730,"column":83}},{"start":{"line":730,"column":83},"end":{"line":730,"column":null}}],"line":730},"51":{"loc":{"start":{"line":731,"column":17},"end":{"line":731,"column":null}},"type":"cond-expr","locations":[{"start":{"line":731,"column":49},"end":{"line":731,"column":98}},{"start":{"line":731,"column":98},"end":{"line":731,"column":null}}],"line":731},"52":{"loc":{"start":{"line":732,"column":21},"end":{"line":734,"column":null}},"type":"cond-expr","locations":[{"start":{"line":733,"column":8},"end":{"line":733,"column":null}},{"start":{"line":734,"column":8},"end":{"line":734,"column":null}}],"line":732},"53":{"loc":{"start":{"line":735,"column":15},"end":{"line":735,"column":null}},"type":"binary-expr","locations":[{"start":{"line":735,"column":15},"end":{"line":735,"column":46}},{"start":{"line":735,"column":46},"end":{"line":735,"column":null}}],"line":735},"54":{"loc":{"start":{"line":740,"column":10},"end":{"line":740,"column":null}},"type":"binary-expr","locations":[{"start":{"line":740,"column":10},"end":{"line":740,"column":40}},{"start":{"line":740,"column":40},"end":{"line":740,"column":null}}],"line":740},"55":{"loc":{"start":{"line":741,"column":20},"end":{"line":741,"column":null}},"type":"binary-expr","locations":[{"start":{"line":741,"column":20},"end":{"line":741,"column":64}},{"start":{"line":741,"column":64},"end":{"line":741,"column":null}}],"line":741},"56":{"loc":{"start":{"line":745,"column":10},"end":{"line":745,"column":null}},"type":"binary-expr","locations":[{"start":{"line":745,"column":10},"end":{"line":745,"column":49}},{"start":{"line":745,"column":49},"end":{"line":745,"column":null}}],"line":745},"57":{"loc":{"start":{"line":747,"column":24},"end":{"line":747,"column":null}},"type":"binary-expr","locations":[{"start":{"line":747,"column":24},"end":{"line":747,"column":64}},{"start":{"line":747,"column":64},"end":{"line":747,"column":null}}],"line":747}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"31":1,"32":1,"33":1,"34":1,"35":1,"36":1,"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1,"47":1,"48":1,"49":1,"50":1,"51":1,"52":1,"53":1,"54":1,"55":1,"56":1,"57":1,"58":1,"59":1,"60":1,"61":1,"62":1,"63":1,"64":1,"65":1,"66":1,"67":1,"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":1,"83":1,"84":1,"85":1,"86":1,"87":1,"88":1,"89":1,"90":1,"91":1,"92":3,"93":1,"94":4,"95":1,"96":1,"97":1,"98":1,"99":1,"100":2,"101":4,"102":4,"103":4,"104":4,"105":1,"106":1,"107":1,"108":2,"109":1,"110":1,"111":0,"112":0,"113":1,"114":0,"115":0,"116":0,"117":2,"118":0,"119":2,"120":1,"121":1,"122":21,"123":1},"f":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":3,"12":4,"13":2,"14":4,"15":4,"16":4,"17":4,"18":1,"19":2,"20":0,"21":0,"22":0,"23":0,"24":21},"b":{"0":[2,0],"1":[2,0],"2":[2,0],"3":[2,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,1],"9":[0,0],"10":[1,0],"11":[0,0],"12":[0,0],"13":[0,0],"14":[0,0],"15":[0,0],"16":[0,0],"17":[0,0],"18":[0,0],"19":[0,0],"20":[0,0],"21":[0,0],"22":[0,0],"23":[0,0],"24":[0,0],"25":[2,0],"26":[2,0],"27":[2,0],"28":[2,0],"29":[2,0],"30":[2,1],"31":[2,1],"32":[2,0],"33":[2,0],"34":[2,0],"35":[2,0],"36":[2,0],"37":[2,0],"38":[0,1],"39":[2,0],"40":[2,0],"41":[2,0],"42":[2,0],"43":[2,0],"44":[2,0],"45":[2,0],"46":[2,0],"47":[2,0],"48":[2,0],"49":[2,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[2,0],"54":[2,0],"55":[2,0],"56":[2,0],"57":[2,0]},"meta":{"lastBranch":58,"lastFunction":25,"lastStatement":124,"seen":{"s:5:7:10:Infinity":0,"f:5:7:5:12":0,"s:6:2:6:Infinity":1,"s:7:2:7:Infinity":2,"s:8:2:8:Infinity":3,"s:9:2:9:Infinity":4,"s:12:7:18:Infinity":5,"f:12:7:12:12":1,"s:13:2:13:Infinity":6,"s:14:2:14:Infinity":7,"s:15:2:15:Infinity":8,"s:16:2:16:Infinity":9,"s:17:2:17:Infinity":10,"s:20:7:23:Infinity":11,"f:20:7:20:12":2,"s:21:2:21:Infinity":12,"s:22:2:22:Infinity":13,"s:31:7:39:Infinity":14,"f:31:7:31:12":3,"s:32:2:32:Infinity":15,"s:33:2:33:Infinity":16,"s:34:2:34:Infinity":17,"s:35:2:35:Infinity":18,"s:36:2:36:Infinity":19,"s:37:2:37:Infinity":20,"s:38:2:38:Infinity":21,"s:41:7:50:Infinity":22,"f:41:7:41:12":4,"s:42:2:42:Infinity":23,"s:43:2:43:Infinity":24,"s:44:2:44:Infinity":25,"s:45:2:45:Infinity":26,"s:46:2:46:Infinity":27,"s:47:2:47:Infinity":28,"s:48:2:48:Infinity":29,"s:49:2:49:Infinity":30,"s:52:7:59:Infinity":31,"f:52:7:52:12":5,"s:53:2:53:Infinity":32,"s:54:2:54:Infinity":33,"s:55:2:55:Infinity":34,"s:56:2:56:Infinity":35,"s:57:2:57:Infinity":36,"s:58:2:58:Infinity":37,"s:61:7:72:Infinity":38,"f:61:7:61:12":6,"s:62:2:62:Infinity":39,"s:63:2:63:Infinity":40,"s:64:2:64:Infinity":41,"s:65:2:65:Infinity":42,"s:66:2:66:Infinity":43,"s:67:2:67:Infinity":44,"s:68:2:68:Infinity":45,"s:69:2:69:Infinity":46,"s:70:2:70:Infinity":47,"s:71:2:71:Infinity":48,"s:74:7:87:Infinity":49,"f:74:7:74:12":7,"s:75:2:75:Infinity":50,"s:76:2:76:Infinity":51,"s:77:2:77:Infinity":52,"s:78:2:78:Infinity":53,"s:79:2:79:Infinity":54,"s:80:2:80:Infinity":55,"s:81:2:81:Infinity":56,"s:82:2:82:Infinity":57,"s:83:2:83:Infinity":58,"s:84:2:84:Infinity":59,"s:85:2:85:Infinity":60,"s:86:2:86:Infinity":61,"s:89:7:93:Infinity":62,"f:89:7:89:12":8,"s:90:2:90:Infinity":63,"s:91:2:91:Infinity":64,"s:92:2:92:Infinity":65,"s:95:7:104:Infinity":66,"f:95:7:95:12":9,"s:96:2:96:Infinity":67,"s:97:2:97:Infinity":68,"s:98:2:98:Infinity":69,"s:99:2:99:Infinity":70,"s:100:2:100:Infinity":71,"s:101:2:101:Infinity":72,"s:102:2:102:Infinity":73,"s:103:2:103:Infinity":74,"s:106:7:117:Infinity":75,"f:106:7:106:12":10,"s:107:2:107:Infinity":76,"s:108:2:108:Infinity":77,"s:109:2:109:Infinity":78,"s:110:2:110:Infinity":79,"s:111:2:111:Infinity":80,"s:112:2:112:Infinity":81,"s:113:2:113:Infinity":82,"s:114:2:114:Infinity":83,"s:115:2:115:Infinity":84,"s:116:2:116:Infinity":85,"s:173:65:195:Infinity":86,"s:404:42:410:Infinity":87,"s:412:49:419:Infinity":88,"s:421:52:429:Infinity":89,"s:431:74:441:Infinity":90,"s:443:13:448:Infinity":91,"f:443:13:443:65":11,"s:443:65:448:Infinity":92,"s:450:13:467:Infinity":93,"f:450:13:450:47":12,"s:450:47:467:Infinity":94,"s:469:40:469:Infinity":95,"s:471:57:516:Infinity":96,"s:518:53:525:Infinity":97,"s:527:48:617:Infinity":98,"s:619:6:636:Infinity":99,"f:619:6:619:25":13,"s:619:50:636:Infinity":100,"b:620:8:620:55:620:55:623:Infinity":0,"f:620:22:620:27":14,"s:620:41:620:50":101,"b:624:9:624:57:624:57:627:Infinity":1,"f:624:24:624:29":15,"s:624:43:624:52":102,"b:628:8:628:55:628:55:631:Infinity":2,"f:628:22:628:27":16,"s:628:41:628:50":103,"b:632:7:632:53:632:53:635:Infinity":3,"f:632:20:632:25":17,"s:632:39:632:48":104,"s:638:6:643:Infinity":105,"f:638:6:638:29":18,"s:638:63:643:Infinity":106,"b:639:52:639:68:639:68:639:72":4,"b:640:51:640:66:640:66:640:70":5,"b:641:53:641:70:641:70:641:74":6,"b:642:52:642:68:642:68:642:72":7,"s:645:13:749:Infinity":107,"f:645:13:645:43":19,"b:646:2:648:Infinity:undefined:undefined:undefined:undefined":8,"s:646:2:648:Infinity":108,"s:647:4:647:Infinity":109,"s:650:8:658:Infinity":110,"f:650:8:650:29":20,"s:651:4:657:Infinity":111,"b:651:12:651:24:651:24:651:26":9,"f:651:28:651:33":21,"s:651:64:657:6":112,"s:660:8:693:Infinity":113,"b:660:27:660:54:660:54:660:56":10,"f:660:58:660:63":22,"s:661:33:661:Infinity":114,"b:661:33:661:62:661:62:661:Infinity":11,"s:662:31:662:Infinity":115,"s:664:4:692:Infinity":116,"b:666:10:666:30:666:24:666:Infinity":12,"b:671:21:671:57:671:57:671:Infinity":13,"b:672:20:672:55:672:55:672:Infinity":14,"b:673:24:673:63:673:63:673:Infinity":15,"b:674:13:674:41:674:41:674:Infinity":16,"b:675:70:675:107:675:107:675:111":17,"b:676:52:676:80:676:80:676:84":18,"b:677:46:677:93:677:93:677:Infinity":19,"b:679:12:679:Infinity:680:12:680:Infinity":20,"b:682:12:682:Infinity:683:12:683:Infinity":21,"b:684:19:684:53:684:53:684:Infinity":22,"b:687:14:687:56:687:56:687:Infinity":23,"b:689:28:689:71:689:71:689:Infinity":24,"s:695:8:699:Infinity":117,"b:695:31:695:62:695:62:695:64":25,"f:695:66:695:71":23,"s:695:87:699:4":118,"s:701:2:748:Infinity":119,"b:704:17:704:50:704:50:704:Infinity":26,"b:705:16:705:48:705:48:705:Infinity":27,"b:706:20:706:56:706:56:706:Infinity":28,"b:707:24:707:64:707:64:707:Infinity":29,"b:708:15:708:46:708:46:708:Infinity":30,"b:709:15:709:46:709:46:709:Infinity":31,"b:710:26:710:68:710:68:710:Infinity":32,"b:711:24:711:64:711:64:711:Infinity":33,"b:712:19:712:54:712:54:712:Infinity":34,"b:713:27:713:70:713:70:713:Infinity":35,"b:714:20:714:56:714:56:714:Infinity":36,"b:715:25:715:66:715:66:715:Infinity":37,"b:717:8:717:Infinity:718:8:718:Infinity":38,"b:719:25:719:66:719:66:719:Infinity":39,"b:720:23:720:62:720:62:720:Infinity":40,"b:721:25:721:66:721:66:721:Infinity":41,"b:722:21:722:58:722:58:722:Infinity":42,"b:723:21:723:58:723:58:723:Infinity":43,"b:724:20:724:56:724:56:724:Infinity":44,"b:725:22:725:60:725:60:725:Infinity":45,"b:726:22:726:60:726:60:726:Infinity":46,"b:727:69:727:107:727:107:727:111":47,"b:728:61:728:95:728:95:728:99":48,"b:729:43:729:68:729:68:729:72":49,"b:730:39:730:83:730:83:730:Infinity":50,"b:731:49:731:98:731:98:731:Infinity":51,"b:733:8:733:Infinity:734:8:734:Infinity":52,"b:735:15:735:46:735:46:735:Infinity":53,"b:740:10:740:40:740:40:740:Infinity":54,"b:741:20:741:64:741:64:741:Infinity":55,"b:745:10:745:49:745:49:745:Infinity":56,"b:747:24:747:64:747:64:747:Infinity":57,"s:756:68:862:Infinity":120,"s:864:50:866:Infinity":121,"f:866:3:866:12":24,"s:866:22:866:32":122,"s:868:45:918:Infinity":123},"fnNames":{}}} +} diff --git a/coverage/favicon.png b/coverage/favicon.png new file mode 100644 index 0000000000..c1525b811a Binary files /dev/null and b/coverage/favicon.png differ diff --git a/coverage/hooks/index.html b/coverage/hooks/index.html new file mode 100644 index 0000000000..9f8b725b42 --- /dev/null +++ b/coverage/hooks/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for hooks + + + + + + + + + +
+
+

All files hooks

+
+ +
+ 44.59% + Statements + 99/222 +
+ + +
+ 32.14% + Branches + 27/84 +
+ + +
+ 41.02% + Functions + 16/39 +
+ + +
+ 47.57% + Lines + 98/206 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
useAndroidBackHandler.ts +
+
45.61%26/5756.25%18/3257.14%4/747.16%25/53
useTouchGestures.ts +
+
44.24%73/16517.3%9/5237.5%12/3247.71%73/153
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/hooks/useAndroidBackHandler.ts.html b/coverage/hooks/useAndroidBackHandler.ts.html new file mode 100644 index 0000000000..53b6a2cb5b --- /dev/null +++ b/coverage/hooks/useAndroidBackHandler.ts.html @@ -0,0 +1,388 @@ + + + + + + Code coverage report for hooks/useAndroidBackHandler.ts + + + + + + + + + +
+
+

All files / hooks useAndroidBackHandler.ts

+
+ +
+ 45.61% + Statements + 26/57 +
+ + +
+ 56.25% + Branches + 18/32 +
+ + +
+ 57.14% + Functions + 4/7 +
+ + +
+ 47.16% + Lines + 25/53 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102  +  +  +  +  +4x +4x +4x +  +3x +2x +  +2x +1x +1x +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +1x +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +1x +  +  +3x +3x +  +  +  + 
import { useEffect } from 'react';
+import { useUIStore } from '../store/useUIStore';
+import { useSettingsStore } from '../store/useSettingsStore';
+ 
+export function useAndroidBackHandler() {
+  useEffect(() => {
+    const osPlatform = useSettingsStore.getState().osPlatform;
+    if (osPlatform !== 'android') return;
+ 
+    (window as any).__handleAndroidBack = () => {
+      const ui = useUIStore.getState();
+ 
+      if (ui.confirmModalState.isOpen) {
+        ui.setUI((state: any) => ({ confirmModalState: { ...state.confirmModalState, isOpen: false } }));
+        return;
+      }
+      Iif (ui.isCreateFolderModalOpen) {
+        ui.setUI({ isCreateFolderModalOpen: false });
+        return;
+      }
+      Iif (ui.isRenameFolderModalOpen) {
+        ui.setUI({ isRenameFolderModalOpen: false });
+        return;
+      }
+      Iif (ui.isRenameFileModalOpen) {
+        ui.setUI({ isRenameFileModalOpen: false });
+        return;
+      }
+      Iif (ui.isImportModalOpen) {
+        ui.setUI({ isImportModalOpen: false });
+        return;
+      }
+      Iif (ui.isCopyPasteSettingsModalOpen) {
+        ui.setUI({ isCopyPasteSettingsModalOpen: false });
+        return;
+      }
+      Iif (ui.isCreateAlbumModalOpen) {
+        ui.setUI({ isCreateAlbumModalOpen: false });
+        return;
+      }
+      Iif (ui.isCreateAlbumGroupModalOpen) {
+        ui.setUI({ isCreateAlbumGroupModalOpen: false });
+        return;
+      }
+      Iif (ui.isRenameAlbumModalOpen) {
+        ui.setUI({ isRenameAlbumModalOpen: false });
+        return;
+      }
+      Iif (ui.panoramaModalState.isOpen) {
+        ui.setUI({
+          panoramaModalState: {
+            isOpen: false,
+            isProcessing: false,
+            progressMessage: '',
+            finalImageBase64: null,
+            error: null,
+            stitchingSourcePaths: [],
+          },
+        });
+        return;
+      }
+      Iif (ui.hdrModalState.isOpen) {
+        ui.setUI({
+          hdrModalState: {
+            isOpen: false,
+            isProcessing: false,
+            progressMessage: '',
+            finalImageBase64: null,
+            error: null,
+            stitchingSourcePaths: [],
+          },
+        });
+        return;
+      }
+      Iif (ui.negativeModalState.isOpen) {
+        ui.setUI((state: any) => ({ negativeModalState: { ...state.negativeModalState, isOpen: false } }));
+        return;
+      }
+      Iif (ui.denoiseModalState.isOpen) {
+        ui.setUI((state: any) => ({ denoiseModalState: { ...state.denoiseModalState, isOpen: false } }));
+        return;
+      }
+      Iif (ui.cullingModalState.isOpen) {
+        ui.setUI({
+          cullingModalState: { isOpen: false, progress: null, suggestions: null, error: null, pathsToCull: [] },
+        });
+        return;
+      }
+      Iif (ui.collageModalState.isOpen) {
+        ui.setUI({ collageModalState: { isOpen: false, sourceImages: [] } });
+        return;
+      }
+ 
+      window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, cancelable: true }));
+    };
+ 
+    return () => {
+      delete (window as any).__handleAndroidBack;
+    };
+  }, []);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/hooks/useTouchGestures.ts.html b/coverage/hooks/useTouchGestures.ts.html new file mode 100644 index 0000000000..1497e5fa41 --- /dev/null +++ b/coverage/hooks/useTouchGestures.ts.html @@ -0,0 +1,1078 @@ + + + + + + Code coverage report for hooks/useTouchGestures.ts + + + + + + + + + +
+
+

All files / hooks useTouchGestures.ts

+
+ +
+ 44.24% + Statements + 73/165 +
+ + +
+ 17.3% + Branches + 9/52 +
+ + +
+ 37.5% + Functions + 12/32 +
+ + +
+ 47.71% + Lines + 73/153 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +1x +1x +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +1x +1x +1x +  +1x +1x +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +1x +1x +1x +  +1x +1x +1x +  +1x +1x +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +1x +1x +1x +  +1x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +1x +1x +1x +  +1x +1x +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +1x +1x +  +1x +  +  +  +  +  +1x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +1x +1x +1x +  +1x +1x +1x +1x +  +  +  + 
import { useEffect, useCallback, useRef, RefObject } from 'react';
+ 
+interface Point {
+  x: number;
+  y: number;
+}
+ 
+function getTouchPoint(touch: Touch): Point {
+  return { x: touch.clientX, y: touch.clientY };
+}
+ 
+function getDistance(p1: Point, p2: Point): number {
+  const dx = p1.x - p2.x;
+  const dy = p1.y - p2.y;
+  return Math.sqrt(dx * dx + dy * dy);
+}
+ 
+function getAngle(p1: Point, p2: Point): number {
+  return Math.atan2(p2.y - p1.y, p2.x - p1.x);
+}
+ 
+function getMidpoint(p1: Point, p2: Point): Point {
+  return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };
+}
+ 
+/**
+ * 双指缩放手势 hook
+ * 监听元素上的双指捏合/张开手势,回调当前缩放比例
+ */
+export function usePinchZoom(
+  ref: RefObject<HTMLElement>,
+  options?: {
+    minScale?: number;
+    maxScale?: number;
+    onScaleChange?: (scale: number) => void;
+  },
+) {
+  const minScale = options?.minScale ?? 0.1;
+  const maxScale = options?.maxScale ?? 10;
+  const onScaleChange = options?.onScaleChange;
+ 
+  const initialDistanceRef = useRef(0);
+  const currentScaleRef = useRef(1);
+ 
+  const handleTouchStart = useCallback((e: TouchEvent) => {
+    if (e.touches.length !== 2) return;
+    e.preventDefault();
+    const p1 = getTouchPoint(e.touches[0]);
+    const p2 = getTouchPoint(e.touches[1]);
+    initialDistanceRef.current = getDistance(p1, p2);
+  }, []);
+ 
+  const handleTouchMove = useCallback(
+    (e: TouchEvent) => {
+      if (e.touches.length !== 2) return;
+      e.preventDefault();
+      const p1 = getTouchPoint(e.touches[0]);
+      const p2 = getTouchPoint(e.touches[1]);
+      const currentDistance = getDistance(p1, p2);
+      if (initialDistanceRef.current === 0) {
+        initialDistanceRef.current = currentDistance;
+        return;
+      }
+      const ratio = currentDistance / initialDistanceRef.current;
+      const newScale = Math.min(maxScale, Math.max(minScale, currentScaleRef.current * ratio));
+      onScaleChange?.(newScale);
+    },
+    [minScale, maxScale, onScaleChange],
+  );
+ 
+  const handleTouchEnd = useCallback((e: TouchEvent) => {
+    if (e.touches.length < 2) {
+      // Finalize the scale
+      // The last scale value is already applied via onScaleChange
+      initialDistanceRef.current = 0;
+      // Update the ref to the last emitted scale so next gesture is relative
+      // Consumer should call a setter to keep currentScaleRef in sync
+    }
+  }, []);
+ 
+  useEffect(() => {
+    const el = ref.current;
+    Iif (!el) return;
+ 
+    el.addEventListener('touchstart', handleTouchStart, { passive: false });
+    el.addEventListener('touchmove', handleTouchMove, { passive: false });
+    el.addEventListener('touchend', handleTouchEnd);
+ 
+    return () => {
+      el.removeEventListener('touchstart', handleTouchStart);
+      el.removeEventListener('touchmove', handleTouchMove);
+      el.removeEventListener('touchend', handleTouchEnd);
+    };
+  }, [ref, handleTouchStart, handleTouchMove, handleTouchEnd]);
+ 
+  return {
+    setCurrentScale: (scale: number) => {
+      currentScaleRef.current = scale;
+    },
+  };
+}
+ 
+/**
+ * 双指旋转手势 hook
+ * 监听元素上的双指旋转手势,回调旋转角度(弧度)
+ */
+export function useTwoFingerRotate(
+  ref: RefObject<HTMLElement>,
+  options?: {
+    onRotationChange?: (rotation: number) => void;
+  },
+) {
+  const onRotationChange = options?.onRotationChange;
+  const initialAngleRef = useRef(0);
+  const currentRotationRef = useRef(0);
+ 
+  const handleTouchStart = useCallback((e: TouchEvent) => {
+    if (e.touches.length !== 2) return;
+    e.preventDefault();
+    const p1 = getTouchPoint(e.touches[0]);
+    const p2 = getTouchPoint(e.touches[1]);
+    initialAngleRef.current = getAngle(p1, p2);
+  }, []);
+ 
+  const handleTouchMove = useCallback(
+    (e: TouchEvent) => {
+      if (e.touches.length !== 2) return;
+      e.preventDefault();
+      const p1 = getTouchPoint(e.touches[0]);
+      const p2 = getTouchPoint(e.touches[1]);
+      const currentAngle = getAngle(p1, p2);
+      const delta = currentAngle - initialAngleRef.current;
+      const newRotation = currentRotationRef.current + delta;
+      onRotationChange?.(newRotation);
+    },
+    [onRotationChange],
+  );
+ 
+  const handleTouchEnd = useCallback(() => {
+    if (initialAngleRef.current !== 0) {
+      initialAngleRef.current = 0;
+    }
+  }, []);
+ 
+  useEffect(() => {
+    const el = ref.current;
+    Iif (!el) return;
+ 
+    el.addEventListener('touchstart', handleTouchStart, { passive: false });
+    el.addEventListener('touchmove', handleTouchMove, { passive: false });
+    el.addEventListener('touchend', handleTouchEnd);
+ 
+    return () => {
+      el.removeEventListener('touchstart', handleTouchStart);
+      el.removeEventListener('touchmove', handleTouchMove);
+      el.removeEventListener('touchend', handleTouchEnd);
+    };
+  }, [ref, handleTouchStart, handleTouchMove, handleTouchEnd]);
+ 
+  return {
+    setCurrentRotation: (rotation: number) => {
+      currentRotationRef.current = rotation;
+    },
+  };
+}
+ 
+/**
+ * 单指拖拽画布手势 hook
+ * 监听元素上的单指拖拽,回调偏移量 {dx, dy}
+ */
+export function useCanvasPan(
+  ref: RefObject<HTMLElement>,
+  options?: {
+    onPanChange?: (offset: { dx: number; dy: number }) => void;
+    onPanStart?: () => void;
+    onPanEnd?: () => void;
+  },
+) {
+  const onPanChange = options?.onPanChange;
+  const onPanStart = options?.onPanStart;
+  const onPanEnd = options?.onPanEnd;
+ 
+  const startPointRef = useRef<Point | null>(null);
+  const accumulatedRef = useRef({ dx: 0, dy: 0 });
+  const isPanningRef = useRef(false);
+ 
+  const handleTouchStart = useCallback(
+    (e: TouchEvent) => {
+      if (e.touches.length !== 1) return;
+      const point = getTouchPoint(e.touches[0]);
+      startPointRef.current = point;
+      isPanningRef.current = true;
+      onPanStart?.();
+    },
+    [onPanStart],
+  );
+ 
+  const handleTouchMove = useCallback(
+    (e: TouchEvent) => {
+      if (!isPanningRef.current || e.touches.length !== 1 || !startPointRef.current) return;
+      e.preventDefault();
+      const currentPoint = getTouchPoint(e.touches[0]);
+      const dx = currentPoint.x - startPointRef.current.x;
+      const dy = currentPoint.y - startPointRef.current.y;
+      onPanChange?.({ dx: accumulatedRef.current.dx + dx, dy: accumulatedRef.current.dy + dy });
+    },
+    [onPanChange],
+  );
+ 
+  const handleTouchEnd = useCallback(
+    (e: TouchEvent) => {
+      if (!isPanningRef.current || !startPointRef.current) return;
+      // Finalize: accumulate the delta
+      if (e.changedTouches.length > 0) {
+        const endPoint = getTouchPoint(e.changedTouches[0]);
+        const dx = endPoint.x - startPointRef.current.x;
+        const dy = endPoint.y - startPointRef.current.y;
+        accumulatedRef.current = {
+          dx: accumulatedRef.current.dx + dx,
+          dy: accumulatedRef.current.dy + dy,
+        };
+      }
+      isPanningRef.current = false;
+      startPointRef.current = null;
+      onPanEnd?.();
+    },
+    [onPanEnd],
+  );
+ 
+  useEffect(() => {
+    const el = ref.current;
+    Iif (!el) return;
+ 
+    el.addEventListener('touchstart', handleTouchStart, { passive: false });
+    el.addEventListener('touchmove', handleTouchMove, { passive: false });
+    el.addEventListener('touchend', handleTouchEnd);
+ 
+    return () => {
+      el.removeEventListener('touchstart', handleTouchStart);
+      el.removeEventListener('touchmove', handleTouchMove);
+      el.removeEventListener('touchend', handleTouchEnd);
+    };
+  }, [ref, handleTouchStart, handleTouchMove, handleTouchEnd]);
+ 
+  return {
+    resetOffset: () => {
+      accumulatedRef.current = { dx: 0, dy: 0 };
+      onPanChange?.({ dx: 0, dy: 0 });
+    },
+    setOffset: (dx: number, dy: number) => {
+      accumulatedRef.current = { dx, dy };
+    },
+  };
+}
+ 
+/**
+ * 滑动翻页手势 hook(胶片条使用)
+ * 检测左滑/右滑手势并回调方向
+ */
+export function useSwipeNavigation(
+  options?: {
+    threshold?: number;
+    onSwipeLeft?: () => void;
+    onSwipeRight?: () => void;
+  },
+) {
+  const threshold = options?.threshold ?? 50;
+  const onSwipeLeft = options?.onSwipeLeft;
+  const onSwipeRight = options?.onSwipeRight;
+ 
+  const startPointRef = useRef<Point | null>(null);
+  const startTimeRef = useRef(0);
+ 
+  const handleTouchStart = useCallback((e: TouchEvent) => {
+    if (e.touches.length !== 1) return;
+    startPointRef.current = getTouchPoint(e.touches[0]);
+    startTimeRef.current = Date.now();
+  }, []);
+ 
+  const handleTouchMove = useCallback(
+    (e: TouchEvent) => {
+      // Optional: could add real-time visual feedback here
+    },
+    [],
+  );
+ 
+  const handleTouchEnd = useCallback(
+    (e: TouchEvent) => {
+      if (!startPointRef.current || e.changedTouches.length === 0) return;
+      const endPoint = getTouchPoint(e.changedTouches[0]);
+      const dx = endPoint.x - startPointRef.current.x;
+      const dy = endPoint.y - startPointRef.current.y;
+      const dt = Date.now() - startTimeRef.current;
+ 
+      // Only count as swipe if horizontal movement is dominant and exceeds threshold
+      const isHorizontalSwipe = Math.abs(dx) > Math.abs(dy) * 1.5;
+      const isFastEnough = dt < 500;
+      const exceedsThreshold = Math.abs(dx) > threshold;
+ 
+      if (isHorizontalSwipe && isFastEnough && exceedsThreshold) {
+        if (dx < 0) {
+          onSwipeLeft?.();
+        } else {
+          onSwipeRight?.();
+        }
+      }
+ 
+      startPointRef.current = null;
+    },
+    [threshold, onSwipeLeft, onSwipeRight],
+  );
+ 
+  useEffect(() => {
+    const handler = {
+      start: handleTouchStart,
+      move: handleTouchMove,
+      end: handleTouchEnd,
+    };
+ 
+    // Attach to window for global swipe detection (e.g. filmstrip)
+    window.addEventListener('touchstart', handler.start, { passive: true });
+    window.addEventListener('touchmove', handler.move, { passive: true });
+    window.addEventListener('touchend', handler.end);
+ 
+    return () => {
+      window.removeEventListener('touchstart', handler.start);
+      window.removeEventListener('touchmove', handler.move);
+      window.removeEventListener('touchend', handler.end);
+    };
+  }, [handleTouchStart, handleTouchMove, handleTouchEnd]);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/index.html b/coverage/index.html new file mode 100644 index 0000000000..6d866e2998 --- /dev/null +++ b/coverage/index.html @@ -0,0 +1,191 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 70.27% + Statements + 416/592 +
+ + +
+ 34.08% + Branches + 91/267 +
+ + +
+ 64.21% + Functions + 61/95 +
+ + +
+ 74.13% + Lines + 407/549 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
components/panel/right +
+
47.76%32/670%0/3650%3/661.53%32/52
components/ui +
+
98.14%159/16285.71%18/21100%20/2098.73%156/158
hooks +
+
44.59%99/22232.14%27/8441.02%16/3947.57%98/206
store +
+
25%3/120%0/1020%1/520%2/10
types +
+
100%5/5100%0/0100%0/0100%5/5
utils +
+
95.16%118/12439.65%46/11684%21/2596.61%114/118
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/prettify.css b/coverage/prettify.css new file mode 100644 index 0000000000..b317a7cda3 --- /dev/null +++ b/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/prettify.js b/coverage/prettify.js new file mode 100644 index 0000000000..b3225238f2 --- /dev/null +++ b/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/sort-arrow-sprite.png b/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000000..6ed68316eb Binary files /dev/null and b/coverage/sort-arrow-sprite.png differ diff --git a/coverage/sorter.js b/coverage/sorter.js new file mode 100644 index 0000000000..4ed70ae5ac --- /dev/null +++ b/coverage/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/store/index.html b/coverage/store/index.html new file mode 100644 index 0000000000..89e23ebace --- /dev/null +++ b/coverage/store/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for store + + + + + + + + + +
+
+

All files store

+
+ +
+ 25% + Statements + 3/12 +
+ + +
+ 0% + Branches + 0/10 +
+ + +
+ 20% + Functions + 1/5 +
+ + +
+ 20% + Lines + 2/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
useUIStore.ts +
+
25%3/120%0/1020%1/520%2/10
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/store/useUIStore.ts.html b/coverage/store/useUIStore.ts.html new file mode 100644 index 0000000000..f344690fa5 --- /dev/null +++ b/coverage/store/useUIStore.ts.html @@ -0,0 +1,745 @@ + + + + + + Code coverage report for store/useUIStore.ts + + + + + + + + + +
+
+

All files / store useUIStore.ts

+
+ +
+ 25% + Statements + 3/12 +
+ + +
+ 0% + Branches + 0/10 +
+ + +
+ 20% + Functions + 1/5 +
+ + +
+ 20% + Lines + 2/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { create } from 'zustand';
+import { ImageFile, LibraryViewMode, Panel, UiVisibility, CullingSuggestions } from '../components/ui/AppProperties';
+ 
+const RIGHT_PANEL_ORDER = [
+  Panel.Metadata,
+  Panel.Adjustments,
+  Panel.Color,
+  Panel.Portrait,
+  Panel.Crop,
+  Panel.Masks,
+  Panel.Ai,
+  Panel.Presets,
+  Panel.Export,
+];
+ 
+export interface CollapsibleSectionsState {
+  basic: boolean;
+  color: boolean;
+  curves: boolean;
+  details: boolean;
+  effects: boolean;
+}
+ 
+export interface ConfirmModalState {
+  confirmText?: string;
+  confirmVariant?: string;
+  isOpen: boolean;
+  message?: string;
+  onConfirm?(): void;
+  title?: string;
+}
+ 
+export interface CollageModalState {
+  isOpen: boolean;
+  sourceImages: ImageFile[];
+}
+ 
+export interface PanoramaModalState {
+  error: string | null;
+  finalImageBase64: string | null;
+  isOpen: boolean;
+  isProcessing: boolean;
+  progressMessage: string | null;
+  stitchingSourcePaths: Array<string>;
+}
+ 
+export interface HdrModalState {
+  error: string | null;
+  finalImageBase64: string | null;
+  isOpen: boolean;
+  isProcessing: boolean;
+  progressMessage: string | null;
+  stitchingSourcePaths: Array<string>;
+}
+ 
+export interface DenoiseModalState {
+  isOpen: boolean;
+  isProcessing: boolean;
+  previewBase64: string | null;
+  originalBase64?: string | null;
+  error: string | null;
+  targetPaths: string[];
+  progressMessage: string | null;
+  isRaw: boolean;
+}
+ 
+export interface NegativeConversionModalState {
+  isOpen: boolean;
+  targetPaths: Array<string>;
+}
+ 
+export interface CullingModalState {
+  isOpen: boolean;
+  suggestions: CullingSuggestions | null;
+  progress: { current: number; total: number; stage: string } | null;
+  error: string | null;
+  pathsToCull: Array<string>;
+}
+ 
+interface UIState {
+  // View & Layout
+  activeView: string;
+  isFullScreen: boolean;
+  isWindowFullScreen: boolean;
+  isInstantTransition: boolean;
+  isLayoutReady: boolean;
+  uiVisibility: UiVisibility;
+  isLibraryExportPanelVisible: boolean;
+ 
+  // Dimensions
+  leftPanelWidth: number;
+  rightPanelWidth: number;
+  bottomPanelHeight: number;
+  compactEditorPanelHeightOverride: number | null;
+ 
+  // Right Panel
+  activeRightPanel: Panel | null;
+  renderedRightPanel: Panel | null;
+  slideDirection: number;
+  collapsibleSectionsState: CollapsibleSectionsState;
+ 
+  // Modals & Dialogs
+  isCreateFolderModalOpen: boolean;
+  isRenameFolderModalOpen: boolean;
+  isRenameFileModalOpen: boolean;
+  renameTargetPaths: Array<string>;
+  isImportModalOpen: boolean;
+  isCopyPasteSettingsModalOpen: boolean;
+  importTargetFolder: string | null;
+  importSourcePaths: Array<string>;
+  folderActionTarget: string | null;
+ 
+  // Album Modals
+  isCreateAlbumModalOpen: boolean;
+  isCreateAlbumGroupModalOpen: boolean;
+  isRenameAlbumModalOpen: boolean;
+  isSmartAlbumModalOpen: boolean;
+  albumActionTarget: string | null;
+ 
+  // Complex Modal States
+  confirmModalState: ConfirmModalState;
+  panoramaModalState: PanoramaModalState;
+  hdrModalState: HdrModalState;
+  negativeModalState: NegativeConversionModalState;
+  denoiseModalState: DenoiseModalState;
+  cullingModalState: CullingModalState;
+  collageModalState: CollageModalState;
+ 
+  // Actions
+  setUI: (updater: Partial<UIState> | ((state: UIState) => Partial<UIState>)) => void;
+  setRightPanel: (panel: Panel | null) => void;
+  customEscapeHandler: (() => void) | null;
+  setCustomEscapeHandler: (handler: (() => void) | null) => void;
+}
+ 
+export const useUIStore = create<UIState>((set, get) => ({
+  activeView: 'library',
+  isFullScreen: false,
+  isWindowFullScreen: false,
+  isInstantTransition: false,
+  isLayoutReady: false,
+  uiVisibility: { folderTree: true, filmstrip: true },
+  isLibraryExportPanelVisible: false,
+ 
+  leftPanelWidth: 256,
+  rightPanelWidth: 320,
+  bottomPanelHeight: 144,
+  compactEditorPanelHeightOverride: null,
+ 
+  activeRightPanel: Panel.Adjustments,
+  renderedRightPanel: Panel.Adjustments,
+  slideDirection: 1,
+  collapsibleSectionsState: { basic: true, color: false, curves: true, details: false, effects: false },
+ 
+  isCreateFolderModalOpen: false,
+  isRenameFolderModalOpen: false,
+  isRenameFileModalOpen: false,
+  renameTargetPaths: [],
+  isImportModalOpen: false,
+  isCopyPasteSettingsModalOpen: false,
+  importTargetFolder: null,
+  importSourcePaths: [],
+  folderActionTarget: null,
+ 
+  isCreateAlbumModalOpen: false,
+  isCreateAlbumGroupModalOpen: false,
+  isRenameAlbumModalOpen: false,
+  isSmartAlbumModalOpen: false,
+  albumActionTarget: null,
+ 
+  confirmModalState: { isOpen: false },
+  panoramaModalState: {
+    error: null,
+    finalImageBase64: null,
+    isOpen: false,
+    isProcessing: false,
+    progressMessage: '',
+    stitchingSourcePaths: [],
+  },
+  hdrModalState: {
+    error: null,
+    finalImageBase64: null,
+    isOpen: false,
+    isProcessing: false,
+    progressMessage: '',
+    stitchingSourcePaths: [],
+  },
+  negativeModalState: { isOpen: false, targetPaths: [] },
+  denoiseModalState: {
+    isOpen: false,
+    isProcessing: false,
+    previewBase64: null,
+    error: null,
+    targetPaths: [],
+    progressMessage: null,
+    isRaw: false,
+  },
+  cullingModalState: { isOpen: false, suggestions: null, progress: null, error: null, pathsToCull: [] },
+  collageModalState: { isOpen: false, sourceImages: [] },
+ 
+  setUI: (updater) => set((state) => (typeof updater === 'function' ? updater(state) : updater)),
+ 
+  setRightPanel: (panelId) => {
+    const current = get().activeRightPanel;
+    if (panelId === current) {
+      set({ activeRightPanel: null });
+    } else {
+      const currentIndex = current ? RIGHT_PANEL_ORDER.indexOf(current) : -1;
+      const newIndex = panelId ? RIGHT_PANEL_ORDER.indexOf(panelId) : -1;
+      set({
+        slideDirection: newIndex > currentIndex ? 1 : -1,
+        activeRightPanel: panelId,
+        renderedRightPanel: panelId,
+      });
+    }
+  },
+ 
+  customEscapeHandler: null,
+  setCustomEscapeHandler: (handler) => set({ customEscapeHandler: handler }),
+}));
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/types/index.html b/coverage/types/index.html new file mode 100644 index 0000000000..964944e7b9 --- /dev/null +++ b/coverage/types/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for types + + + + + + + + + +
+
+

All files types

+
+ +
+ 100% + Statements + 5/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 5/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
typography.ts +
+
100%5/5100%0/0100%0/0100%5/5
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/types/typography.ts.html b/coverage/types/typography.ts.html new file mode 100644 index 0000000000..32c53fff4d --- /dev/null +++ b/coverage/types/typography.ts.html @@ -0,0 +1,388 @@ + + + + + + Code coverage report for types/typography.ts + + + + + + + + + +
+
+

All files / types typography.ts

+
+ +
+ 100% + Statements + 5/5 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 5/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
export type TextVariant = 'displayLarge' | 'display' | 'headline' | 'title' | 'heading' | 'body' | 'label' | 'small';
+export type TextWeight = 'bold' | 'semibold' | 'medium' | 'normal';
+export type TextColor = 'primary' | 'secondary' | 'accent' | 'button' | 'info' | 'success' | 'error' | 'white';
+ 
+export const TextWeights: Record<TextWeight, TextWeight> = {
+  bold: 'bold',
+  semibold: 'semibold',
+  medium: 'medium',
+  normal: 'normal',
+};
+export const TextColors: Record<TextColor, TextColor> = {
+  primary: 'primary',
+  secondary: 'secondary',
+  accent: 'accent',
+  button: 'button',
+  info: 'info',
+  success: 'success',
+  error: 'error',
+  white: 'white',
+};
+ 
+// Map keys to classes
+export const TEXT_WEIGHT_KEYS: Record<TextWeight, string> = {
+  bold: 'font-bold',
+  semibold: 'font-semibold',
+  medium: 'font-medium',
+  normal: 'font-normal',
+};
+export const TEXT_COLOR_KEYS: Record<TextColor, string> = {
+  primary: 'text-text-primary',
+  secondary: 'text-text-secondary',
+  accent: 'text-accent',
+  button: 'text-button-text',
+  info: 'text-blue-400',
+  success: 'text-green-400',
+  error: 'text-red-400',
+  white: 'text-white',
+};
+ 
+export interface VariantConfig {
+  size: string;
+  defaultWeight: TextWeight;
+  defaultColor: TextColor;
+  defaultElement: React.ElementType;
+  extraClasses?: string;
+}
+ 
+export const TextVariants: Record<TextVariant, VariantConfig> = {
+  displayLarge: {
+    size: 'text-5xl',
+    defaultWeight: 'bold',
+    defaultColor: 'primary',
+    defaultElement: 'h1',
+    extraClasses: 'text-shadow-shiny mb-4',
+  },
+  display: {
+    size: 'text-3xl',
+    defaultWeight: 'bold',
+    defaultColor: 'primary',
+    defaultElement: 'h1',
+    extraClasses: 'text-shadow-shiny',
+  },
+  headline: {
+    size: 'text-2xl',
+    defaultWeight: 'bold',
+    defaultColor: 'primary',
+    defaultElement: 'h1',
+    extraClasses: 'text-shadow-shiny',
+  },
+  title: {
+    size: 'text-xl',
+    defaultWeight: 'bold',
+    defaultColor: 'primary',
+    defaultElement: 'h2',
+    extraClasses: 'text-shadow-shiny',
+  },
+  heading: {
+    size: 'text-base',
+    defaultWeight: 'semibold',
+    defaultColor: 'primary',
+    defaultElement: 'h3',
+  },
+  body: {
+    size: 'text-sm',
+    defaultWeight: 'normal',
+    defaultColor: 'secondary',
+    defaultElement: 'p',
+  },
+  label: {
+    size: 'text-sm',
+    defaultWeight: 'medium',
+    defaultColor: 'secondary',
+    defaultElement: 'span',
+  },
+  small: {
+    size: 'text-xs',
+    defaultWeight: 'normal',
+    defaultColor: 'secondary',
+    defaultElement: 'p',
+  },
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/utils/adjustments.ts.html b/coverage/utils/adjustments.ts.html new file mode 100644 index 0000000000..26ef4b2ad6 --- /dev/null +++ b/coverage/utils/adjustments.ts.html @@ -0,0 +1,2839 @@ + + + + + + Code coverage report for utils/adjustments.ts + + + + + + + + + +
+
+

All files / utils adjustments.ts

+
+ +
+ 95.16% + Statements + 118/124 +
+ + +
+ 39.65% + Branches + 46/116 +
+ + +
+ 84% + Functions + 21/25 +
+ + +
+ 96.61% + Lines + 114/118 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919  +  +  +  +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +4x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +4x +  +  +  +4x +  +  +  +4x +  +  +  +4x +  +  +  +  +  +1x +  +  +  +  +  +  +1x +2x +1x +  +  +1x +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +  +  +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +21x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { Crop } from 'react-image-crop';
+import { v4 as uuidv4 } from 'uuid';
+import { SubMask, SubMaskMode } from '../components/panel/right/Masks';
+ 
+export enum ActiveChannel {
+  Blue = 'blue',
+  Green = 'green',
+  Luma = 'luma',
+  Red = 'red',
+}
+ 
+export enum DisplayMode {
+  Luma = 'luma',
+  Rgb = 'rgb',
+  Parade = 'parade',
+  Vectorscope = 'vectorscope',
+  Histogram = 'histogram',
+}
+ 
+export enum PasteMode {
+  Merge = 'merge',
+  Replace = 'replace',
+}
+ 
+export interface CopyPasteSettings {
+  mode: PasteMode;
+  includedAdjustments: Array<string>;
+  knownAdjustments: Array<string>;
+}
+ 
+export enum BasicAdjustment {
+  Blacks = 'blacks',
+  Brightness = 'brightness',
+  Contrast = 'contrast',
+  Exposure = 'exposure',
+  Highlights = 'highlights',
+  Shadows = 'shadows',
+  Whites = 'whites',
+}
+ 
+export enum ColorAdjustment {
+  ColorGrading = 'colorGrading',
+  Hsl = 'hsl',
+  Hue = 'hue',
+  Luminance = 'luminance',
+  Saturation = 'saturation',
+  Temperature = 'temperature',
+  Tint = 'tint',
+  Vibrance = 'vibrance',
+}
+ 
+export enum ColorGrading {
+  Balance = 'balance',
+  Blending = 'blending',
+  Global = 'global',
+  Highlights = 'highlights',
+  Midtones = 'midtones',
+  Shadows = 'shadows',
+}
+ 
+export enum DetailsAdjustment {
+  Clarity = 'clarity',
+  Dehaze = 'dehaze',
+  Structure = 'structure',
+  Centré = 'centré',
+  ColorNoiseReduction = 'colorNoiseReduction',
+  LumaNoiseReduction = 'lumaNoiseReduction',
+  Sharpness = 'sharpness',
+  SharpnessThreshold = 'sharpnessThreshold',
+  ChromaticAberrationRedCyan = 'chromaticAberrationRedCyan',
+  ChromaticAberrationBlueYellow = 'chromaticAberrationBlueYellow',
+}
+ 
+export enum Effect {
+  GrainAmount = 'grainAmount',
+  GrainRoughness = 'grainRoughness',
+  GrainSize = 'grainSize',
+  LutData = 'lutData',
+  LutIntensity = 'lutIntensity',
+  LutName = 'lutName',
+  LutPath = 'lutPath',
+  LutSize = 'lutSize',
+  VignetteAmount = 'vignetteAmount',
+  VignetteFeather = 'vignetteFeather',
+  VignetteMidpoint = 'vignetteMidpoint',
+  VignetteRoundness = 'vignetteRoundness',
+}
+ 
+export enum CreativeAdjustment {
+  GlowAmount = 'glowAmount',
+  HalationAmount = 'halationAmount',
+  FlareAmount = 'flareAmount',
+}
+ 
+export enum TransformAdjustment {
+  TransformDistortion = 'transformDistortion',
+  TransformVertical = 'transformVertical',
+  TransformHorizontal = 'transformHorizontal',
+  TransformRotate = 'transformRotate',
+  TransformAspect = 'transformAspect',
+  TransformScale = 'transformScale',
+  TransformXOffset = 'transformXOffset',
+  TransformYOffset = 'transformYOffset',
+}
+ 
+export enum LensAdjustment {
+  LensCorrectionMode = 'lensCorrectionMode',
+  LensMaker = 'lensMaker',
+  LensModel = 'lensModel',
+  LensDistortionAmount = 'lensDistortionAmount',
+  LensVignetteAmount = 'lensVignetteAmount',
+  LensTcaAmount = 'lensTcaAmount',
+  LensDistortionParams = 'lensDistortionParams',
+  LensDistortionEnabled = 'lensDistortionEnabled',
+  LensTcaEnabled = 'lensTcaEnabled',
+  LensVignetteEnabled = 'lensVignetteEnabled',
+}
+ 
+export interface ColorCalibration {
+  shadowsTint: number;
+  redHue: number;
+  redSaturation: number;
+  greenHue: number;
+  greenSaturation: number;
+  blueHue: number;
+  blueSaturation: number;
+}
+ 
+export interface ParametricCurveSettings {
+  darks: number;
+  shadows: number;
+  highlights: number;
+  lights: number;
+  whiteLevel: number;
+  blackLevel: number;
+  split1: number;
+  split2: number;
+  split3: number;
+}
+ 
+export interface ParametricCurve {
+  [index: string]: ParametricCurveSettings;
+  blue: ParametricCurveSettings;
+  green: ParametricCurveSettings;
+  luma: ParametricCurveSettings;
+  red: ParametricCurveSettings;
+}
+ 
+export interface PortraitAdjustments {
+  skinSmoothingStrength: number;
+  skinSmoothingDetailPreserve: number;
+  faceSlimAmount: number;
+  jawAmount: number;
+  foreheadAmount: number;
+  eyeEnlargeAmount: number;
+  eyeBrightenAmount: number;
+  teethWhitenBrightness: number;
+  teethWhitenDesaturate: number;
+  lipstickColor: string;
+  lipstickOpacity: number;
+  blushColor: string;
+  blushOpacity: number;
+  eyebrowColor: string;
+  eyebrowOpacity: number;
+  hairHueShift: number;
+  hairBrightness: number;
+  bodySlimAmount: number;
+  bodyHeightAmount: number;
+  legLengthAmount: number;
+  blemishSpots: Array<{ x: number; y: number; radius: number }>;
+}
+ 
+export const INITIAL_PORTRAIT_ADJUSTMENTS: PortraitAdjustments = {
+  skinSmoothingStrength: 0,
+  skinSmoothingDetailPreserve: 0,
+  faceSlimAmount: 0,
+  jawAmount: 0,
+  foreheadAmount: 0,
+  eyeEnlargeAmount: 0,
+  eyeBrightenAmount: 0,
+  teethWhitenBrightness: 0,
+  teethWhitenDesaturate: 0,
+  lipstickColor: '#cc2244',
+  lipstickOpacity: 0,
+  blushColor: '#dd6688',
+  blushOpacity: 0,
+  eyebrowColor: '#443322',
+  eyebrowOpacity: 0,
+  hairHueShift: 0,
+  hairBrightness: 0,
+  bodySlimAmount: 0,
+  bodyHeightAmount: 0,
+  legLengthAmount: 0,
+  blemishSpots: [],
+};
+ 
+export interface Adjustments {
+  [index: string]: any;
+  aiPatches: Array<AiPatch>;
+  aspectRatio: number | null;
+  blacks: number;
+  brightness: number;
+  centré: number;
+  clarity: number;
+  chromaticAberrationBlueYellow: number;
+  chromaticAberrationRedCyan: number;
+  colorCalibration: ColorCalibration;
+  colorGrading: ColorGradingProps;
+  colorNoiseReduction: number;
+  contrast: number;
+  curves: Curves;
+  pointCurves?: Curves;
+  parametricCurve?: ParametricCurve;
+  curveMode?: 'point' | 'parametric';
+  crop: Crop | null;
+  dehaze: number;
+  exposure: number;
+  flipHorizontal: boolean;
+  flipVertical: boolean;
+  flareAmount: number;
+  glowAmount: number;
+  grainAmount: number;
+  grainRoughness: number;
+  grainSize: number;
+  halationAmount: number;
+  highlights: number;
+  hsl: Hsl;
+  hue: number;
+  lensCorrectionMode: 'auto' | 'manual';
+  lensDistortionAmount: number;
+  lensVignetteAmount: number;
+  lensTcaAmount: number;
+  lensDistortionEnabled: boolean;
+  lensTcaEnabled: boolean;
+  lensVignetteEnabled: boolean;
+  lensDistortionParams: {
+    k1: number;
+    k2: number;
+    k3: number;
+    model: number;
+    tca_vr: number;
+    tca_vb: number;
+    vig_k1: number;
+    vig_k2: number;
+    vig_k3: number;
+  } | null;
+  lensMaker: string | null;
+  lensModel: string | null;
+  lumaNoiseReduction: number;
+  lutData?: string | null;
+  lutIntensity?: number;
+  lutName?: string | null;
+  lutPath?: string | null;
+  lutSize?: number;
+  masks: Array<MaskContainer>;
+  orientationSteps: number;
+  portrait: PortraitAdjustments;
+  rotation: number;
+  saturation: number;
+  sectionVisibility: SectionVisibility;
+  shadows: number;
+  sharpness: number;
+  sharpnessThreshold: number;
+  showClipping: boolean;
+  structure: number;
+  temperature: number;
+  tint: number;
+  toneMapper: 'agx' | 'basic';
+  transformDistortion: number;
+  transformVertical: number;
+  transformHorizontal: number;
+  transformRotate: number;
+  transformAspect: number;
+  transformScale: number;
+  transformXOffset: number;
+  transformYOffset: number;
+  vibrance: number;
+  vignetteAmount: number;
+  vignetteFeather: number;
+  vignetteMidpoint: number;
+  vignetteRoundness: number;
+  whites: number;
+}
+ 
+export interface AiPatch {
+  id: string;
+  isLoading: boolean;
+  invert: boolean;
+  name: string;
+  patchData: any | null;
+  prompt: string;
+  subMasks: Array<SubMask>;
+  visible: boolean;
+}
+ 
+export interface Color {
+  color: string;
+  name: string;
+}
+ 
+interface ColorGradingProps {
+  [index: string]: number | HueSatLum;
+  balance: number;
+  blending: number;
+  global: HueSatLum;
+  highlights: HueSatLum;
+  midtones: HueSatLum;
+  shadows: HueSatLum;
+}
+ 
+export interface Coord {
+  x: number;
+  y: number;
+}
+ 
+export interface Curves {
+  [index: string]: Array<Coord>;
+  blue: Array<Coord>;
+  green: Array<Coord>;
+  luma: Array<Coord>;
+  red: Array<Coord>;
+}
+ 
+export interface HueSatLum {
+  hue: number;
+  saturation: number;
+  luminance: number;
+}
+ 
+interface Hsl {
+  [index: string]: HueSatLum;
+  aquas: HueSatLum;
+  blues: HueSatLum;
+  greens: HueSatLum;
+  magentas: HueSatLum;
+  oranges: HueSatLum;
+  purples: HueSatLum;
+  reds: HueSatLum;
+  yellows: HueSatLum;
+}
+ 
+export interface MaskAdjustments {
+  [index: string]: any;
+  blacks: number;
+  brightness: number;
+  clarity: number;
+  colorGrading: ColorGradingProps;
+  colorNoiseReduction: number;
+  contrast: number;
+  curves: Curves;
+  pointCurves?: Curves;
+  parametricCurve?: ParametricCurve;
+  curveMode?: 'point' | 'parametric';
+  dehaze: number;
+  exposure: number;
+  flareAmount: number;
+  glowAmount: number;
+  halationAmount: number;
+  highlights: number;
+  hsl: Hsl;
+  hue: number;
+  id?: string;
+  lumaNoiseReduction: number;
+  saturation: number;
+  sectionVisibility: SectionVisibility;
+  shadows: number;
+  sharpness: number;
+  sharpnessThreshold: number;
+  structure: number;
+  temperature: number;
+  tint: number;
+  vibrance: number;
+  whites: number;
+}
+ 
+export interface MaskContainer {
+  adjustments: MaskAdjustments;
+  id?: any;
+  invert: boolean;
+  name: string;
+  opacity: number;
+  subMasks: Array<SubMask>;
+  visible: boolean;
+}
+ 
+export interface Sections {
+  [index: string]: Array<string>;
+  basic: Array<string>;
+  curves: Array<string>;
+  color: Array<string>;
+  details: Array<string>;
+  effects: Array<string>;
+}
+ 
+export interface SectionVisibility {
+  [index: string]: boolean;
+  basic: boolean;
+  curves: boolean;
+  color: boolean;
+  details: boolean;
+  effects: boolean;
+}
+ 
+export const COLOR_LABELS: Array<Color> = [
+  { name: 'red', color: '#ef4444' },
+  { name: 'yellow', color: '#facc15' },
+  { name: 'green', color: '#4ade80' },
+  { name: 'blue', color: '#60a5fa' },
+  { name: 'purple', color: '#a78bfa' },
+];
+ 
+const INITIAL_COLOR_GRADING: ColorGradingProps = {
+  balance: 0,
+  blending: 50,
+  global: { hue: 0, saturation: 0, luminance: 0 },
+  highlights: { hue: 0, saturation: 0, luminance: 0 },
+  midtones: { hue: 0, saturation: 0, luminance: 0 },
+  shadows: { hue: 0, saturation: 0, luminance: 0 },
+};
+ 
+const INITIAL_COLOR_CALIBRATION: ColorCalibration = {
+  shadowsTint: 0,
+  redHue: 0,
+  redSaturation: 0,
+  greenHue: 0,
+  greenSaturation: 0,
+  blueHue: 0,
+  blueSaturation: 0,
+};
+ 
+export const DEFAULT_PARAMETRIC_CURVE_SETTINGS: ParametricCurveSettings = {
+  darks: 0,
+  shadows: 0,
+  highlights: 0,
+  lights: 0,
+  whiteLevel: 0,
+  blackLevel: 0,
+  split1: 25,
+  split2: 50,
+  split3: 75,
+};
+ 
+export const getDefaultParametricCurve = (): ParametricCurve => ({
+  luma: { ...DEFAULT_PARAMETRIC_CURVE_SETTINGS },
+  red: { ...DEFAULT_PARAMETRIC_CURVE_SETTINGS },
+  green: { ...DEFAULT_PARAMETRIC_CURVE_SETTINGS },
+  blue: { ...DEFAULT_PARAMETRIC_CURVE_SETTINGS },
+});
+ 
+export const getDefaultCurves = (): Curves => ({
+  blue: [
+    { x: 0, y: 0 },
+    { x: 255, y: 255 },
+  ],
+  green: [
+    { x: 0, y: 0 },
+    { x: 255, y: 255 },
+  ],
+  luma: [
+    { x: 0, y: 0 },
+    { x: 255, y: 255 },
+  ],
+  red: [
+    { x: 0, y: 0 },
+    { x: 255, y: 255 },
+  ],
+});
+ 
+export const DEFAULT_PARAMETRIC_CURVE = getDefaultParametricCurve();
+ 
+export const INITIAL_MASK_ADJUSTMENTS: MaskAdjustments = {
+  blacks: 0,
+  brightness: 0,
+  clarity: 0,
+  colorGrading: { ...INITIAL_COLOR_GRADING },
+  colorNoiseReduction: 0,
+  contrast: 0,
+  curves: getDefaultCurves(),
+  pointCurves: getDefaultCurves(),
+  parametricCurve: getDefaultParametricCurve(),
+  curveMode: 'point',
+  dehaze: 0,
+  exposure: 0,
+  flareAmount: 0,
+  glowAmount: 0,
+  halationAmount: 0,
+  highlights: 0,
+  hsl: {
+    aquas: { hue: 0, saturation: 0, luminance: 0 },
+    blues: { hue: 0, saturation: 0, luminance: 0 },
+    greens: { hue: 0, saturation: 0, luminance: 0 },
+    magentas: { hue: 0, saturation: 0, luminance: 0 },
+    oranges: { hue: 0, saturation: 0, luminance: 0 },
+    purples: { hue: 0, saturation: 0, luminance: 0 },
+    reds: { hue: 0, saturation: 0, luminance: 0 },
+    yellows: { hue: 0, saturation: 0, luminance: 0 },
+  },
+  hue: 0,
+  lumaNoiseReduction: 0,
+  saturation: 0,
+  sectionVisibility: {
+    basic: true,
+    curves: true,
+    color: true,
+    details: true,
+    effects: true,
+  },
+  shadows: 0,
+  sharpness: 0,
+  sharpnessThreshold: 15,
+  structure: 0,
+  temperature: 0,
+  tint: 0,
+  vibrance: 0,
+  whites: 0,
+};
+ 
+export const INITIAL_MASK_CONTAINER: MaskContainer = {
+  adjustments: INITIAL_MASK_ADJUSTMENTS,
+  invert: false,
+  name: 'New Mask',
+  opacity: 100,
+  subMasks: [],
+  visible: true,
+};
+ 
+export const INITIAL_ADJUSTMENTS: Adjustments = {
+  aiPatches: [],
+  aspectRatio: null,
+  blacks: 0,
+  brightness: 0,
+  centré: 0,
+  clarity: 0,
+  chromaticAberrationBlueYellow: 0,
+  chromaticAberrationRedCyan: 0,
+  colorCalibration: { ...INITIAL_COLOR_CALIBRATION },
+  colorGrading: { ...INITIAL_COLOR_GRADING },
+  colorNoiseReduction: 0,
+  contrast: 0,
+  crop: null,
+  curves: getDefaultCurves(),
+  pointCurves: getDefaultCurves(),
+  parametricCurve: getDefaultParametricCurve(),
+  curveMode: 'point',
+  dehaze: 0,
+  exposure: 0,
+  flipHorizontal: false,
+  flipVertical: false,
+  flareAmount: 0,
+  glowAmount: 0,
+  grainAmount: 0,
+  grainRoughness: 50,
+  grainSize: 25,
+  halationAmount: 0,
+  highlights: 0,
+  hsl: {
+    aquas: { hue: 0, saturation: 0, luminance: 0 },
+    blues: { hue: 0, saturation: 0, luminance: 0 },
+    greens: { hue: 0, saturation: 0, luminance: 0 },
+    magentas: { hue: 0, saturation: 0, luminance: 0 },
+    oranges: { hue: 0, saturation: 0, luminance: 0 },
+    purples: { hue: 0, saturation: 0, luminance: 0 },
+    reds: { hue: 0, saturation: 0, luminance: 0 },
+    yellows: { hue: 0, saturation: 0, luminance: 0 },
+  },
+  hue: 0,
+  lensCorrectionMode: 'manual',
+  lensDistortionAmount: 100,
+  lensVignetteAmount: 100,
+  lensTcaAmount: 100,
+  lensDistortionEnabled: true,
+  lensTcaEnabled: true,
+  lensVignetteEnabled: true,
+  lensDistortionParams: null,
+  lensMaker: null,
+  lensModel: null,
+  lumaNoiseReduction: 0,
+  lutData: null,
+  lutIntensity: 100,
+  lutName: null,
+  lutPath: null,
+  lutSize: 0,
+  masks: [],
+  orientationSteps: 0,
+  portrait: { ...INITIAL_PORTRAIT_ADJUSTMENTS },
+  rotation: 0,
+  saturation: 0,
+  sectionVisibility: {
+    basic: true,
+    curves: true,
+    color: true,
+    details: true,
+    effects: true,
+  },
+  shadows: 0,
+  sharpness: 0,
+  sharpnessThreshold: 15,
+  showClipping: false,
+  structure: 0,
+  temperature: 0,
+  tint: 0,
+  toneMapper: 'basic',
+  transformDistortion: 0,
+  transformVertical: 0,
+  transformHorizontal: 0,
+  transformRotate: 0,
+  transformAspect: 0,
+  transformScale: 100,
+  transformXOffset: 0,
+  transformYOffset: 0,
+  vibrance: 0,
+  vignetteAmount: 0,
+  vignetteFeather: 50,
+  vignetteMidpoint: 50,
+  vignetteRoundness: 0,
+  whites: 0,
+};
+ 
+const deepCloneCurves = (curves: any): Curves => ({
+  blue: curves?.blue?.map((p: Coord) => ({ ...p })) || [
+    { x: 0, y: 0 },
+    { x: 255, y: 255 },
+  ],
+  green: curves?.green?.map((p: Coord) => ({ ...p })) || [
+    { x: 0, y: 0 },
+    { x: 255, y: 255 },
+  ],
+  luma: curves?.luma?.map((p: Coord) => ({ ...p })) || [
+    { x: 0, y: 0 },
+    { x: 255, y: 255 },
+  ],
+  red: curves?.red?.map((p: Coord) => ({ ...p })) || [
+    { x: 0, y: 0 },
+    { x: 255, y: 255 },
+  ],
+});
+ 
+const deepCloneParametric = (pCurve: any): ParametricCurve => ({
+  luma: { ...DEFAULT_PARAMETRIC_CURVE_SETTINGS, ...(pCurve?.luma || {}) },
+  red: { ...DEFAULT_PARAMETRIC_CURVE_SETTINGS, ...(pCurve?.red || {}) },
+  green: { ...DEFAULT_PARAMETRIC_CURVE_SETTINGS, ...(pCurve?.green || {}) },
+  blue: { ...DEFAULT_PARAMETRIC_CURVE_SETTINGS, ...(pCurve?.blue || {}) },
+});
+ 
+export const normalizeLoadedAdjustments = (loadedAdjustments: Adjustments): any => {
+  if (!loadedAdjustments) {
+    return INITIAL_ADJUSTMENTS;
+  }
+ 
+  const normalizeSubMasks = (subMasks: any[]) => {
+    return (subMasks || []).map((subMask: Partial<SubMask>) => ({
+      visible: true,
+      mode: SubMaskMode.Additive,
+      invert: false,
+      opacity: 100,
+      ...subMask,
+    }));
+  };
+ 
+  const normalizedMasks = (loadedAdjustments.masks || []).map((maskContainer: MaskContainer) => {
+    const containerAdjustments = maskContainer.adjustments || {};
+    const normalizedSubMasks = normalizeSubMasks(maskContainer.subMasks);
+ 
+    return {
+      ...INITIAL_MASK_CONTAINER,
+      id: maskContainer.id || uuidv4(),
+      ...maskContainer,
+      adjustments: {
+        ...INITIAL_MASK_ADJUSTMENTS,
+        ...containerAdjustments,
+        flareAmount: containerAdjustments.flareAmount ?? INITIAL_MASK_ADJUSTMENTS.flareAmount,
+        glowAmount: containerAdjustments.glowAmount ?? INITIAL_MASK_ADJUSTMENTS.glowAmount,
+        halationAmount: containerAdjustments.halationAmount ?? INITIAL_MASK_ADJUSTMENTS.halationAmount,
+        hue: containerAdjustments.hue ?? INITIAL_MASK_ADJUSTMENTS.hue,
+        colorGrading: { ...INITIAL_MASK_ADJUSTMENTS.colorGrading, ...(containerAdjustments.colorGrading || {}) },
+        hsl: { ...INITIAL_MASK_ADJUSTMENTS.hsl, ...(containerAdjustments.hsl || {}) },
+        curves: containerAdjustments.curves ? deepCloneCurves(containerAdjustments.curves) : getDefaultCurves(),
+        pointCurves: containerAdjustments.pointCurves
+          ? deepCloneCurves(containerAdjustments.pointCurves)
+          : getDefaultCurves(),
+        parametricCurve: containerAdjustments.parametricCurve
+          ? deepCloneParametric(containerAdjustments.parametricCurve)
+          : getDefaultParametricCurve(),
+        curveMode: containerAdjustments.curveMode || INITIAL_MASK_ADJUSTMENTS.curveMode,
+        sectionVisibility: {
+          ...INITIAL_MASK_ADJUSTMENTS.sectionVisibility,
+          ...(containerAdjustments.sectionVisibility || {}),
+        },
+        sharpnessThreshold: containerAdjustments.sharpnessThreshold ?? INITIAL_MASK_ADJUSTMENTS.sharpnessThreshold,
+      },
+      subMasks: normalizedSubMasks,
+    };
+  });
+ 
+  const normalizedAiPatches = (loadedAdjustments.aiPatches || []).map((patch: any) => ({
+    visible: true,
+    ...patch,
+    subMasks: normalizeSubMasks(patch.subMasks),
+  }));
+ 
+  return {
+    ...INITIAL_ADJUSTMENTS,
+    ...loadedAdjustments,
+    flareAmount: loadedAdjustments.flareAmount ?? INITIAL_ADJUSTMENTS.flareAmount,
+    glowAmount: loadedAdjustments.glowAmount ?? INITIAL_ADJUSTMENTS.glowAmount,
+    halationAmount: loadedAdjustments.halationAmount ?? INITIAL_ADJUSTMENTS.halationAmount,
+    lensCorrectionMode: loadedAdjustments.lensCorrectionMode || 'manual',
+    lensMaker: loadedAdjustments.lensMaker ?? INITIAL_ADJUSTMENTS.lensMaker,
+    lensModel: loadedAdjustments.lensModel ?? INITIAL_ADJUSTMENTS.lensModel,
+    lensDistortionAmount: loadedAdjustments.lensDistortionAmount ?? INITIAL_ADJUSTMENTS.lensDistortionAmount,
+    lensVignetteAmount: loadedAdjustments.lensVignetteAmount ?? INITIAL_ADJUSTMENTS.lensVignetteAmount,
+    lensTcaAmount: loadedAdjustments.lensTcaAmount ?? INITIAL_ADJUSTMENTS.lensTcaAmount,
+    lensDistortionEnabled: loadedAdjustments.lensDistortionEnabled ?? INITIAL_ADJUSTMENTS.lensDistortionEnabled,
+    lensTcaEnabled: loadedAdjustments.lensTcaEnabled ?? INITIAL_ADJUSTMENTS.lensTcaEnabled,
+    lensVignetteEnabled: loadedAdjustments.lensVignetteEnabled ?? INITIAL_ADJUSTMENTS.lensVignetteEnabled,
+    lensDistortionParams: loadedAdjustments.lensDistortionParams
+      ? { ...INITIAL_ADJUSTMENTS.lensDistortionParams, ...loadedAdjustments.lensDistortionParams }
+      : INITIAL_ADJUSTMENTS.lensDistortionParams,
+    transformDistortion: loadedAdjustments.transformDistortion ?? INITIAL_ADJUSTMENTS.transformDistortion,
+    transformVertical: loadedAdjustments.transformVertical ?? INITIAL_ADJUSTMENTS.transformVertical,
+    transformHorizontal: loadedAdjustments.transformHorizontal ?? INITIAL_ADJUSTMENTS.transformHorizontal,
+    transformRotate: loadedAdjustments.transformRotate ?? INITIAL_ADJUSTMENTS.transformRotate,
+    transformAspect: loadedAdjustments.transformAspect ?? INITIAL_ADJUSTMENTS.transformAspect,
+    transformScale: loadedAdjustments.transformScale ?? INITIAL_ADJUSTMENTS.transformScale,
+    transformXOffset: loadedAdjustments.transformXOffset ?? INITIAL_ADJUSTMENTS.transformXOffset,
+    transformYOffset: loadedAdjustments.transformYOffset ?? INITIAL_ADJUSTMENTS.transformYOffset,
+    colorCalibration: { ...INITIAL_ADJUSTMENTS.colorCalibration, ...(loadedAdjustments.colorCalibration || {}) },
+    colorGrading: { ...INITIAL_ADJUSTMENTS.colorGrading, ...(loadedAdjustments.colorGrading || {}) },
+    hsl: { ...INITIAL_ADJUSTMENTS.hsl, ...(loadedAdjustments.hsl || {}) },
+    curves: loadedAdjustments.curves ? deepCloneCurves(loadedAdjustments.curves) : getDefaultCurves(),
+    pointCurves: loadedAdjustments.pointCurves ? deepCloneCurves(loadedAdjustments.pointCurves) : getDefaultCurves(),
+    parametricCurve: loadedAdjustments.parametricCurve
+      ? deepCloneParametric(loadedAdjustments.parametricCurve)
+      : getDefaultParametricCurve(),
+    curveMode: loadedAdjustments.curveMode || INITIAL_ADJUSTMENTS.curveMode,
+    masks: normalizedMasks,
+    aiPatches: normalizedAiPatches,
+    portrait: {
+      ...INITIAL_PORTRAIT_ADJUSTMENTS,
+      ...(loadedAdjustments.portrait || {}),
+      blemishSpots: loadedAdjustments.portrait?.blemishSpots || [],
+    },
+    sectionVisibility: {
+      ...INITIAL_ADJUSTMENTS.sectionVisibility,
+      ...(loadedAdjustments.sectionVisibility || {}),
+    },
+    sharpnessThreshold: loadedAdjustments.sharpnessThreshold ?? INITIAL_ADJUSTMENTS.sharpnessThreshold,
+  };
+};
+ 
+export interface AdjustmentGroup {
+  label: string;
+  keys: string[];
+}
+ 
+export const ADJUSTMENT_GROUPS: Record<string, AdjustmentGroup[]> = {
+  basic: [
+    {
+      label: 'modals.copyPaste.groups.exposureToneMapper',
+      keys: [BasicAdjustment.Exposure, 'toneMapper'],
+    },
+    {
+      label: 'modals.copyPaste.groups.tone',
+      keys: [
+        BasicAdjustment.Brightness,
+        BasicAdjustment.Contrast,
+        BasicAdjustment.Highlights,
+        BasicAdjustment.Shadows,
+        BasicAdjustment.Whites,
+        BasicAdjustment.Blacks,
+      ],
+    },
+    {
+      label: 'modals.copyPaste.groups.curves',
+      keys: ['curves', 'pointCurves', 'parametricCurve', 'curveMode'],
+    },
+  ],
+  color: [
+    { label: 'modals.copyPaste.groups.whiteBalance', keys: [ColorAdjustment.Temperature, ColorAdjustment.Tint] },
+    { label: 'modals.copyPaste.groups.presence', keys: [ColorAdjustment.Saturation, ColorAdjustment.Vibrance] },
+    {
+      label: 'modals.copyPaste.groups.hueShift',
+      keys: [ColorAdjustment.Hue],
+    },
+    { label: 'modals.copyPaste.groups.colorGrading', keys: [ColorAdjustment.ColorGrading] },
+    { label: 'modals.copyPaste.groups.colorMixer', keys: [ColorAdjustment.Hsl] },
+    { label: 'modals.copyPaste.groups.colorCalibration', keys: ['colorCalibration'] },
+  ],
+  details: [
+    {
+      label: 'modals.copyPaste.groups.clarityDehaze',
+      keys: [
+        DetailsAdjustment.Clarity,
+        DetailsAdjustment.Structure,
+        DetailsAdjustment.Dehaze,
+        DetailsAdjustment.Centré,
+      ],
+    },
+    {
+      label: 'modals.copyPaste.groups.sharpness',
+      keys: [DetailsAdjustment.Sharpness, DetailsAdjustment.SharpnessThreshold],
+    },
+    {
+      label: 'modals.copyPaste.groups.noiseReduction',
+      keys: [DetailsAdjustment.LumaNoiseReduction, DetailsAdjustment.ColorNoiseReduction],
+    },
+    {
+      label: 'modals.copyPaste.groups.chromaticAberration',
+      keys: [DetailsAdjustment.ChromaticAberrationRedCyan, DetailsAdjustment.ChromaticAberrationBlueYellow],
+    },
+  ],
+  effects: [
+    {
+      label: 'modals.copyPaste.groups.vignette',
+      keys: [Effect.VignetteAmount, Effect.VignetteFeather, Effect.VignetteMidpoint, Effect.VignetteRoundness],
+    },
+    { label: 'modals.copyPaste.groups.grain', keys: [Effect.GrainAmount, Effect.GrainRoughness, Effect.GrainSize] },
+    {
+      label: 'modals.copyPaste.groups.halationGlow',
+      keys: [CreativeAdjustment.GlowAmount, CreativeAdjustment.HalationAmount, CreativeAdjustment.FlareAmount],
+    },
+    {
+      label: 'modals.copyPaste.groups.lut',
+      keys: [Effect.LutIntensity, Effect.LutName, Effect.LutPath, Effect.LutSize, Effect.LutData],
+    },
+  ],
+  geometry: [
+    { label: 'modals.copyPaste.groups.cropAspectRatio', keys: ['crop', 'aspectRatio'] },
+    {
+      label: 'modals.copyPaste.groups.transformRotation',
+      keys: [
+        'rotation',
+        'flipHorizontal',
+        'flipVertical',
+        'orientationSteps',
+        TransformAdjustment.TransformDistortion,
+        TransformAdjustment.TransformVertical,
+        TransformAdjustment.TransformHorizontal,
+        TransformAdjustment.TransformRotate,
+        TransformAdjustment.TransformAspect,
+        TransformAdjustment.TransformScale,
+        TransformAdjustment.TransformXOffset,
+        TransformAdjustment.TransformYOffset,
+      ],
+    },
+    {
+      label: 'modals.copyPaste.groups.lensCorrection',
+      keys: [
+        LensAdjustment.LensCorrectionMode,
+        LensAdjustment.LensMaker,
+        LensAdjustment.LensModel,
+        LensAdjustment.LensDistortionAmount,
+        LensAdjustment.LensVignetteAmount,
+        LensAdjustment.LensTcaAmount,
+        LensAdjustment.LensDistortionEnabled,
+        LensAdjustment.LensTcaEnabled,
+        LensAdjustment.LensVignetteEnabled,
+      ],
+    },
+  ],
+  masks: [{ label: 'modals.copyPaste.groups.masks', keys: ['masks'] }],
+};
+ 
+export const COPYABLE_ADJUSTMENT_KEYS: string[] = Object.values(ADJUSTMENT_GROUPS)
+  .flat()
+  .flatMap((group) => group.keys);
+ 
+export const ADJUSTMENT_SECTIONS: Sections = {
+  basic: [
+    BasicAdjustment.Brightness,
+    BasicAdjustment.Contrast,
+    BasicAdjustment.Highlights,
+    BasicAdjustment.Shadows,
+    BasicAdjustment.Whites,
+    BasicAdjustment.Blacks,
+    BasicAdjustment.Exposure,
+    'toneMapper',
+  ],
+  curves: ['curves', 'pointCurves', 'parametricCurve', 'curveMode'],
+  color: [
+    ColorAdjustment.Saturation,
+    ColorAdjustment.Temperature,
+    ColorAdjustment.Tint,
+    ColorAdjustment.Vibrance,
+    ColorAdjustment.Hsl,
+    ColorAdjustment.ColorGrading,
+    'colorCalibration',
+    ColorAdjustment.Hue,
+  ],
+  details: [
+    DetailsAdjustment.Clarity,
+    DetailsAdjustment.Dehaze,
+    DetailsAdjustment.Structure,
+    DetailsAdjustment.Centré,
+    DetailsAdjustment.Sharpness,
+    DetailsAdjustment.SharpnessThreshold,
+    DetailsAdjustment.LumaNoiseReduction,
+    DetailsAdjustment.ColorNoiseReduction,
+    DetailsAdjustment.ChromaticAberrationRedCyan,
+    DetailsAdjustment.ChromaticAberrationBlueYellow,
+  ],
+  effects: [
+    CreativeAdjustment.GlowAmount,
+    CreativeAdjustment.HalationAmount,
+    CreativeAdjustment.FlareAmount,
+    Effect.GrainAmount,
+    Effect.GrainRoughness,
+    Effect.GrainSize,
+    Effect.LutIntensity,
+    Effect.LutName,
+    Effect.LutPath,
+    Effect.LutSize,
+    Effect.VignetteAmount,
+    Effect.VignetteFeather,
+    Effect.VignetteMidpoint,
+    Effect.VignetteRoundness,
+  ],
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/utils/index.html b/coverage/utils/index.html new file mode 100644 index 0000000000..eb25640931 --- /dev/null +++ b/coverage/utils/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for utils + + + + + + + + + +
+
+

All files utils

+
+ +
+ 95.16% + Statements + 118/124 +
+ + +
+ 39.65% + Branches + 46/116 +
+ + +
+ 84% + Functions + 21/25 +
+ + +
+ 96.61% + Lines + 114/118 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
adjustments.ts +
+
95.16%118/12439.65%46/11684%21/2596.61%114/118
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/data/io.github.CyberTimon.RapidRAW.metainfo.xml b/data/io.github.CyberTimon.RapidRAW.metainfo.xml index 6554b0967c..ed44447155 100644 --- a/data/io.github.CyberTimon.RapidRAW.metainfo.xml +++ b/data/io.github.CyberTimon.RapidRAW.metainfo.xml @@ -2,10 +2,10 @@ io.github.CyberTimon.RapidRAW - RapidRAW - A non-destructive, and GPU-accelerated RAW image editor built with performance in mind - - Timon Käch + RAW工坊 + 热爱摄影的开发者,为摄影爱好者打造专业的RAW图像处理工具 + + 带娃的小陈工 FSFAP @@ -35,9 +35,55 @@ - https://github.com/CyberTimon/RapidRAW + https://github.com/Tri250/RapidRAW + + https://github.com/Tri250/RapidRAW/releases/tag/v1.8.5 + +

RAW工坊 v1.8.5 - Bug修复与工作流优化

+
    +
  • 修复AI蒙版预计算在切换蒙版时永远不触发的问题(useEffect依赖非响应式)
  • +
  • 修复lint工作流Node.js版本不一致问题(20→22)
  • +
  • 修复lint工作流clippy缺少build-essential系统依赖
  • +
  • 修复几何变换预览线条坐标可能越界的问题
  • +
  • 修复release工作流:添加tag推送触发支持
  • +
  • 修复build工作流:添加显式permissions确保上传权限
  • +
  • 修复build工作流:npm install→npm ci提高CI可靠性
  • +
+
+
+ + https://github.com/Tri250/RapidRAW/releases/tag/v1.7.0 + +

RAW工坊 v1.7.0 - Android端全面增强·AI智能评分·语义搜索·智能相册

+
    +
  • Android 双击对比原图/左右滑动切图/长按快捷菜单
  • +
  • Android 底部导航栏,快速切换基础/色彩/人像/导出面板
  • +
  • AI 智能评分与描述,一键评分写入EXIF
  • +
  • AI 批量评分,右键多图批量处理
  • +
  • 搜索自动补全,80+摄影关键词即时建议
  • +
  • Android 高级筛选面板(日期/相机/焦距/AI标签)
  • +
  • 智能相册,基于条件自动归集照片
  • +
  • 收藏夹,跨文件夹快速收藏照片
  • +
  • Android 批量操作栏(评分/导出/删除/加入相册)
  • +
+
+
+ + https://github.com/Tri250/RapidRAW/releases/tag/v1.6.1 + +

RAW工坊 v1.6.1 - 深空黑主题·中文本地化·面板交互重构

+
    +
  • 深空黑 + 青墨绿默认主题色,深邃专业
  • +
  • 中文首页品牌标题改为"RAW工坊"
  • +
  • 面板交互重构,更符合国内摄影师工作流:基础 → 色彩 → 人像 → 构图 → 蒙版 → AI → 预设 → 导出
  • +
  • 新增独立"色彩"面板(曲线+颜色)和"人像"面板(细节+效果)
  • +
  • 特别鸣谢更改为「带娃的小陈工」个人简介
  • +
  • 首页去除图像来源、版本号、捐赠链接
  • +
+
+
https://github.com/CyberTimon/RapidRAW/releases/tag/v1.5.9 diff --git a/eslint.config.js b/eslint.config.js index 32acc076bd..092408a9f0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,83 +1,28 @@ -const js = require('@eslint/js'); -const tseslint = require('typescript-eslint'); -const react = require('eslint-plugin-react'); -const i18next = require('eslint-plugin-i18next'); +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' -const tsFiles = ['**/*.{ts,tsx}']; - -const jsRecommendedForTs = { - ...js.configs.recommended, - files: tsFiles, -}; - -const tsRecommended = tseslint.configs.recommended.map((config) => - config.files ? config : { ...config, files: tsFiles }, -); - -module.exports = [ - { - ignores: [ - 'dist/**', - 'node_modules/**', - 'src-tauri/target/**', - 'src-tauri/gen/**', - 'src-tauri/rawler/**', - 'data/**', - ], - }, - jsRecommendedForTs, - ...tsRecommended, +export default tseslint.config( + { ignores: ['dist'] }, { - files: tsFiles, - plugins: { - react, - i18next, - }, + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], languageOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, + ecmaVersion: 2020, + globals: globals.browser, }, - settings: { - react: { - version: 'detect', - }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, }, rules: { - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }, - ], - 'i18next/no-literal-string': [ + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ 'warn', - { - markupOnly: true, - ignoreAttribute: [ - 'className', - 'style', - 'data-tooltip', - 'variant', - 'size', - 'color', - 'weight', - 'fillOrigin', - 'id', - 'name', - 'type', - 'value', - 'label', - 'placeholder', - 'stroke', - 'fill', - 'viewBox', - ], - }, + { allowConstantExport: true }, ], }, }, -]; +) diff --git a/index.html b/index.html index 43449e4278..7650e180a4 100644 --- a/index.html +++ b/index.html @@ -1,17 +1,24 @@ - + - - - - - - RapidRAW + + + My Trae Project + -
- + diff --git a/package-lock.json b/package-lock.json index a79f719b50..94f16d4b26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,204 +1,350 @@ { - "name": "rapidraw", + "name": "workspace", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "rapidraw", + "name": "workspace", + "version": "0.0.0", "dependencies": { - "@clerk/react": "^6.12.0", - "@dnd-kit/core": "^6.3.1", - "@tauri-apps/api": "^2.11.1", - "@tauri-apps/plugin-dialog": "^2.7.1", - "@tauri-apps/plugin-os": "^2.3.2", - "@tauri-apps/plugin-process": "^2.3.1", - "@tauri-apps/plugin-shell": "^2.3.5", - "@uiw/color-convert": "^2.10.3", - "@uiw/react-color-wheel": "^2.10.3", "clsx": "^2.1.1", - "framer-motion": "^12.42.2", - "i18next": "^26.3.4", - "konva": "^10.3.0", - "lodash.debounce": "^4.0.8", - "lodash.throttle": "^4.1.1", - "lucide-react": "^1.23.0", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-i18next": "^17.0.8", - "react-image-crop": "^11.1.2", - "react-konva": "^19.2.5", - "react-toastify": "^11.1.0", - "react-window": "^2.2.7", - "simple-icons": "^16.25.0", - "uuid": "^14.0.1", - "zustand": "^5.0.14" + "lucide-react": "^0.511.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.3.0", + "tailwind-merge": "^3.0.2", + "zustand": "^5.0.3" }, "devDependencies": { - "@eslint/js": "^9.39.2", - "@tailwindcss/vite": "^4.3.2", - "@tauri-apps/cli": "^2.11.4", - "@types/lodash.debounce": "^4.0.9", - "@types/lodash.throttle": "^4.1.9", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", - "esbuild": "^0.28.1", - "eslint": "^9.39.2", - "eslint-plugin-i18next": "^6.1.5", - "eslint-plugin-react": "^7.37.5", - "i18next-cli": "^1.65.0", - "prettier": "^3.9.4", - "tailwindcss": "^4.3.2", - "typescript": "^6.0.0", - "typescript-eslint": "^8.63.0", - "vite": "^8.1.3" - } - }, - "node_modules/@babel/runtime": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.0.tgz", - "integrity": "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==", + "@eslint/js": "^9.25.0", + "@types/node": "^22.15.30", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "babel-plugin-react-dev-locator": "^1.0.6", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5", + "vite-tsconfig-paths": "^5.1.4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", - "peer": true + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@clerk/react": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@clerk/react/-/react-6.12.0.tgz", - "integrity": "sha512-QswCaOkrh56Mh1FnFPsaAF2n6zgA9sdUtvsf6tTVTe4XLp4FnD+rIqIQOKtZ6LhdvvKtZyyzo8KFcvOOtmFuyQ==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "@clerk/shared": "^4.25.0", - "tslib": "2.8.1" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">=20.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", - "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@clerk/shared": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.25.0.tgz", - "integrity": "sha512-49WX0F0wqQvY2vf2ehNRZcKby0zkuaMinxF8O9GONOkaDktWISVghVHyCl+aN2hYk0r80UAXosD1T3rLSUlWog==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "@tanstack/query-core": "^5.100.6", - "dequal": "2.0.3", - "glob-to-regexp": "0.4.1", - "js-cookie": "3.0.7" + "@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": ">=20.9.0" + "node": ">=6.9.0" }, - "peerDependencies": { - "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", - "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@croct/json": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@croct/json/-/json-2.1.0.tgz", - "integrity": "sha512-UrWfjNQVlBxN+OVcFwHmkjARMW55MBN04E9KfGac8ac8z1QnFVuiOOFtMWXCk3UwsyRqhsNaFoYLZC+xxqsVjQ==", + "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==", "dev": true, - "license": "MIT" + "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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@croct/json5-parser": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@croct/json5-parser/-/json5-parser-0.2.2.tgz", - "integrity": "sha512-0NJMLrbeLbQ0eCVj3UoH/kG2QckUgOASfwmfDTjyW1xAYPyTNJXcWVT/dssJdTJd0pRchW+qF0VFWQHcxs1OVw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@croct/json": "^2.1.0" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "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==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.0.0" + "@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": { - "react": ">=16.8.0" + "@babel/core": "^7.0.0" } }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, "license": "MIT", "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.0.0" + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "react": ">=16.8.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "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==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -213,9 +359,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -230,9 +376,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -247,9 +393,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -264,9 +410,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -281,9 +427,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -298,9 +444,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -315,9 +461,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -332,9 +478,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -349,9 +495,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -366,9 +512,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -383,9 +529,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -400,9 +546,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -417,9 +563,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -434,9 +580,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -451,9 +597,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -468,9 +614,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -485,9 +631,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -502,9 +648,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -519,9 +665,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -536,9 +682,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -553,9 +699,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -570,9 +716,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -587,9 +733,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -604,9 +750,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -621,9 +767,9 @@ } }, "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==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -744,6 +890,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/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/@eslint/js": { "version": "9.39.4", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", @@ -847,433 +1006,119 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inquirer/ansi": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", - "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "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==", "dev": true, "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@inquirer/checkbox": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", - "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@inquirer/confirm": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", - "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", + "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==", "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">=6.0.0" } }, - "node_modules/@inquirer/core": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", - "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "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==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7", - "cli-width": "^4.1.0", - "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@inquirer/editor": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", - "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "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": { - "@inquirer/core": "^11.2.1", - "@inquirer/external-editor": "^3.0.3", - "@inquirer/type": "^4.0.7" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">= 8" } }, - "node_modules/@inquirer/expand": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", - "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "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", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">= 8" } }, - "node_modules/@inquirer/external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "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": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.2" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">= 8" } }, - "node_modules/@inquirer/figures": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", - "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - } + "license": "MIT" }, - "node_modules/@inquirer/input": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", - "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", - "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", - "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", - "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^5.2.1", - "@inquirer/confirm": "^6.1.1", - "@inquirer/editor": "^5.2.2", - "@inquirer/expand": "^5.1.1", - "@inquirer/input": "^5.1.2", - "@inquirer/number": "^4.1.1", - "@inquirer/password": "^5.1.1", - "@inquirer/rawlist": "^5.3.1", - "@inquirer/search": "^4.2.1", - "@inquirer/select": "^5.2.1" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", - "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", - "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", - "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/figures": "^2.0.7", - "@inquirer/type": "^4.0.7" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", - "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } + "os": [ + "android" + ] }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -1282,15 +1127,12 @@ "optional": true, "os": [ "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -1299,15 +1141,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -1316,15 +1155,26 @@ "optional": true, "os": [ "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -1333,15 +1183,12 @@ "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], @@ -1350,3768 +1197,1590 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ - "arm64" + "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ - "s390x" + "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ - "x64" + "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ - "x64" + "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "linux" + ] }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ - "wasm32" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "linux" + ] }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ - "x64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@swc/core": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", - "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.27" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.43", - "@swc/core-darwin-x64": "1.15.43", - "@swc/core-linux-arm-gnueabihf": "1.15.43", - "@swc/core-linux-arm64-gnu": "1.15.43", - "@swc/core-linux-arm64-musl": "1.15.43", - "@swc/core-linux-ppc64-gnu": "1.15.43", - "@swc/core-linux-s390x-gnu": "1.15.43", - "@swc/core-linux-x64-gnu": "1.15.43", - "@swc/core-linux-x64-musl": "1.15.43", - "@swc/core-win32-arm64-msvc": "1.15.43", - "@swc/core-win32-ia32-msvc": "1.15.43", - "@swc/core-win32-x64-msvc": "1.15.43" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } + "linux" + ] }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", - "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "license": "Apache-2.0 AND MIT", + "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } + "linux" + ] }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", - "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], "dev": true, - "license": "Apache-2.0 AND MIT", + "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } + "linux" + ] }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", - "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ - "arm" + "x64" ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } + "openbsd" + ] }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", - "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } + "openharmony" + ] }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", - "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 AND MIT", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } + "win32" + ] }, - "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", - "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ - "ppc64" + "ia32" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } + "win32" + ] }, - "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", - "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ - "s390x" + "x64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } + "win32" + ] }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", - "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } + "win32" + ] }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", - "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", - "cpu": [ - "x64" - ], + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", - "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", - "cpu": [ - "arm64" - ], + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" } }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", - "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", - "cpu": [ - "ia32" - ], + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", - "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", - "cpu": [ - "x64" - ], + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" } }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "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==", "dev": true, - "license": "Apache-2.0" + "license": "MIT" }, - "node_modules/@swc/types": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", - "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } + "license": "MIT" }, - "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", - "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" + "undici-types": "~6.21.0" } }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", - "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" } }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", - "cpu": [ - "arm64" - ], + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" + "peerDependencies": { + "@types/react": "^18.0.0" } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">= 20" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">= 20" + "node": ">= 4" } }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", - "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "tailwindcss": "4.3.2" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", - "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", - "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.4", - "@tauri-apps/cli-darwin-x64": "2.11.4", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", - "@tauri-apps/cli-linux-arm64-musl": "2.11.4", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-musl": "2.11.4", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", - "@tauri-apps/cli-win32-x64-msvc": "2.11.4" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", - "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", - "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", - "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", - "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", - "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", - "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", - "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", - "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", - "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", - "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", - "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", - "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.11.0" - } - }, - "node_modules/@tauri-apps/plugin-os": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.3.2.tgz", - "integrity": "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-process": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz", - "integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@tauri-apps/plugin-shell": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", - "integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.10.1" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash.debounce": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", - "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/lodash.throttle": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", - "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/react-reconciler": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.33.0.tgz", - "integrity": "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", - "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/type-utils": "8.63.0", - "@typescript-eslint/utils": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.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.63.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", - "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", - "debug": "^4.4.3" - }, - "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 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", - "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.63.0", - "@typescript-eslint/types": "^8.63.0", - "debug": "^4.4.3" - }, - "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 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", - "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.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/tsconfig-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", - "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", - "dev": true, - "license": "MIT", - "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 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", - "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.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 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", - "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", - "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/typescript-estree": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", - "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.63.0", - "@typescript-eslint/tsconfig-utils": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.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 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", - "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.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 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", - "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.63.0", - "eslint-visitor-keys": "^5.0.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/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@uiw/color-convert": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.10.3.tgz", - "integrity": "sha512-5tIjb4CZzGR7K3Sshswsuuc6FOAFNFwjtF0hkhKH3f+CMauC4Akv7LPq6o9v68S7dIAeKvfj8qWg5Tc2I1TVSA==", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0" - } - }, - "node_modules/@uiw/react-color-wheel": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@uiw/react-color-wheel/-/react-color-wheel-2.10.3.tgz", - "integrity": "sha512-eNFWioQt8Fr3UgHpuCxN4/gniZReOit3FzYxVFVXzQD5Hd6ch6jJDDZFFTl2NGVuTfsXLIepLYe9B5FHajC7AA==", - "license": "MIT", - "dependencies": { - "@uiw/color-convert": "2.10.3", - "@uiw/react-drag-event-interactive": "2.10.3" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@uiw/react-drag-event-interactive": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.10.3.tgz", - "integrity": "sha512-veLm9HF1cairiYbGxcALYVu51T4XAzDz8fmtQ0SiLVFHBX74+iUGo2jArS/XALFBnapsLYTAWTnHXxe38mC/Vw==", - "license": "MIT", - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.19.0", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.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/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "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==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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/chardet": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", - "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", - "dev": true, - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "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/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "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-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-abstract": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", - "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract-get": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", - "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.2", - "is-callable": "^1.2.7", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.3.tgz", - "integrity": "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", - "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-abstract-get": "^1.0.0", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "is-callable": "^1.2.7", - "is-date-object": "^1.1.0", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "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/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.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", "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.5", - "@eslint/js": "9.39.4", - "@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" + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://eslint.org/donate" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-i18next": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-i18next/-/eslint-plugin-i18next-6.1.5.tgz", - "integrity": "sha512-xCTfstbK9ZpQ6UFT5S1s6zbZMhKn2o2jRSQYiU2UkV7wt8y1m3WjkUYGkGlipgRdFInYI9+LAqE+JQU/L73HmA==", - "dev": true, - "license": "ISC", - "dependencies": { - "requireindex": "~1.1.0" - }, - "engines": { - "node": ">=18.10.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "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" + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "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==", - "dev": true, - "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==", - "dev": true, - "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/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", - "figures": "^6.1.0", - "get-stream": "^9.0.0", - "human-signals": "^8.0.1", - "is-plain-obj": "^4.1.0", - "is-stream": "^4.0.1", - "npm-run-path": "^6.0.0", - "pretty-ms": "^9.2.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": "^18.19.0 || >=20.5.0" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "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-string-truncated-width": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", - "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-string-width": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "node_modules/fast-wrap-ansi": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", - "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-string-width": "^3.0.2" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", "dev": true, "license": "MIT", "dependencies": { - "is-unicode-supported": "^2.0.0" + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, "engines": { - "node": ">=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "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==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "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==", + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", "dev": true, "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, "engines": { - "node": ">=16" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.2.7" + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/framer-motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", - "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.42.2", - "motion-utils": "^12.39.0", - "tslib": "^2.4.0" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/function.prototype.name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", - "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2", - "hasown": "^2.0.4", - "is-callable": "^1.2.7", - "is-document.all": "^1.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">= 0.4" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "node_modules/@typescript-eslint/typescript-estree/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": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", "dev": true, "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0" + }, "engines": { - "node": ">= 0.4" + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "eslint-visitor-keys": "^5.0.0" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.4.0" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "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": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" + "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": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "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": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "color-convert": "^2.0.1" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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==", + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.3" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=10.13.0" + "node": ">= 8" } }, - "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/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/anymatch/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": "18 || 20 || >=22" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" }, "engines": { - "node": "18 || 20 || >=22" + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "node_modules/babel-plugin-react-dev-locator": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/babel-plugin-react-dev-locator/-/babel-plugin-react-dev-locator-1.0.6.tgz", + "integrity": "sha512-XWi+6x6e4NvVwvOqwitci/BZU1xbNfNuL94kfT4kWfP/d9p3RYRVOrogs1Z1otlYfQUO07cy/20z8eY9blFSpw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.5" + "@babel/core": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12.0.0" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "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", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "node_modules/baseline-browser-mapping": { + "version": "2.10.44", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.44.tgz", + "integrity": "sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==", "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6.0.0" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "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==", + "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/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0" + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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": { - "has-symbols": "^1.0.3" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=6" } }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "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": { - "void-elements": "3.1.0" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/human-signals": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "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/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": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } + "license": "MIT" }, - "node_modules/i18next": { - "version": "26.3.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz", - "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==", - "funding": [ - { - "type": "individual", - "url": "https://www.locize.com/i18next" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - }, - { - "type": "individual", - "url": "https://www.locize.com" - } - ], + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", - "peerDependencies": { - "typescript": "^5 || ^6" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/i18next-cli": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/i18next-cli/-/i18next-cli-1.65.0.tgz", - "integrity": "sha512-sak+2Ry4P7wtl7xMAZg2sWG2vup1lRHFBKA7h5IeEqFUog51QEgeYUhHxd2x85+MvS4BhVOZZs833clnd1WgYA==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "@croct/json5-parser": "^0.2.2", - "@swc/core": "^1.15.41", - "chokidar": "^5.0.0", - "commander": "^14.0.3", - "execa": "^9.6.1", - "glob": "^13.0.6", - "i18next": "^26.3.1", - "i18next-resources-for-ts": "^2.1.0", - "inquirer": "^14.0.2", - "jiti": "^2.7.0", - "jsonc-parser": "^3.3.1", - "magic-string": "^0.30.21", - "minimatch": "^10.2.5", - "ora": "^9.4.0", - "react": "^19.2.7", - "react-i18next": "^17.0.8", - "yaml": "^2.9.0" - }, - "bin": { - "i18next-cli": "dist/esm/cli.js" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=22" + "node": ">= 8" } }, - "node_modules/i18next-cli/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=4" } }, - "node_modules/i18next-cli/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "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==", + "devOptional": 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==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "ms": "^2.1.3" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/i18next-cli/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "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": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "node_modules/i18next-resources-for-ts": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-2.1.0.tgz", - "integrity": "sha512-n5UexwEVt0OoIAhG2MWpSnAVJW1U8mQrQTmXyxc5DMAx+NLhcLZhSMJo/FnUsA5JQ3obTYqTgB7YIuZKWpDgow==", + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.6", - "@swc/core": "^1.15.18", - "chokidar": "^5.0.0", - "yaml": "^2.8.2" - }, - "bin": { - "i18next-resources-for-ts": "bin/i18next-resources-for-ts.js" - } + "license": "Apache-2.0" }, - "node_modules/i18next-resources-for-ts/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.394", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.394.tgz", + "integrity": "sha512-Wmt2Gm0o8JWBuGgmc4XZ0u9s1RaCRqhxP47phplmfg04+qypTUurpeJGP45A7Fhv7jdrrVH44PLlR9qXo37cVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">= 0.4" } }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">=6" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, "engines": { - "node": ">=6" + "node": ">=10" }, "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/inquirer": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-14.0.2.tgz", - "integrity": "sha512-VsSx1JneSNp3ld1veMTLe+UDcUD8Tw2/jjOthhkX3/IX2q+xHhVELifeb/hsb1fBw31pabEPNUf/xUOyb+KZjA==", + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^11.2.1", - "@inquirer/prompts": "^8.5.2", - "@inquirer/type": "^4.0.7", - "mute-stream": "^3.0.0", - "run-async": "^4.0.6" + "@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.5", + "@eslint/js": "9.39.4", + "@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": ">=23.5.0 || ^22.13.0 || ^20.17.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" }, "peerDependencies": { - "@types/node": ">=18" + "jiti": "*" }, "peerDependenciesMeta": { - "@types/node": { + "jiti": { "optional": true } } }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "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": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "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": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "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": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "has-bigints": "^1.0.2" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "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": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=4.0" } }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "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": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "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==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "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": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" + "@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": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8.6.0" } }, - "node_modules/is-document.all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", - "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "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": "MIT", + "license": "ISC", "dependencies": { - "call-bound": "^1.0.4" + "is-glob": "^4.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "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", - "engines": { - "node": ">=0.10.0" + "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/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, "engines": { - "node": ">= 0.4" + "node": ">=12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "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": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=16.0.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==", + "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": { - "is-extglob": "^2.1.1" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "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": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "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", - "engines": { - "node": ">= 0.4" + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=16" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "ISC" }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, "engines": { - "node": ">= 0.4" + "node": "*" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6.9.0" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "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": "MIT", + "license": "ISC", "dependencies": { - "call-bound": "^1.0.3" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10.13.0" } }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -5121,108 +2790,94 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "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==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "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", - "dependencies": { - "which-typed-array": "^1.1.16" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 4" } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "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.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.8.19" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -5231,83 +2886,62 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "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", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/its-fine": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", - "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "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": { - "@types/react-reconciler": "^0.28.9" + "is-extglob": "^2.1.1" }, - "peerDependencies": { - "react": "^19.0.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/its-fine/node_modules/@types/react-reconciler": { - "version": "0.28.9", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", - "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "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", - "peerDependencies": { - "@types/react": "*" + "engines": { + "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-cookie": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz", - "integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5333,6 +2967,19 @@ "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==", + "dev": true, + "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", @@ -5354,27 +3001,17 @@ "dev": true, "license": "MIT" }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": ">=4.0" + "node": ">=6" } }, "node_modules/keyv": { @@ -5387,26 +3024,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/konva": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/konva/-/konva-10.3.0.tgz", - "integrity": "sha512-gt19K2gzY4lHbnkvsku7eSmB+A9PTS2jG4F9coBMsdjM1UKfJNxJbDbXVpeCW1wjEGRwBD3nBamcHnqJhAeKlg==", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/lavrton" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/konva" - }, - { - "type": "github", - "url": "https://github.com/sponsors/lavrton" - } - ], - "license": "MIT" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5427,6 +3044,8 @@ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -5464,6 +3083,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5485,6 +3105,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5506,6 +3127,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5527,6 +3149,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5548,6 +3171,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5564,14 +3188,12 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5588,14 +3210,12 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5612,14 +3232,12 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5636,14 +3254,12 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5665,6 +3281,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5686,6 +3303,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5694,6 +3312,26 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5710,12 +3348,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5723,34 +3355,10 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -5759,56 +3367,50 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/lucide-react": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", - "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "version": "0.511.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz", + "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "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", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": ">= 8" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "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": ">= 0.4" + "node": ">=8.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "node_modules/micromatch/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": ">=18" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/minimatch": { @@ -5824,31 +3426,6 @@ "node": "*" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/motion-dom": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", - "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.39.0" - } - }, - "node_modules/motion-utils": { - "version": "12.39.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", - "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5856,213 +3433,82 @@ "dev": true, "license": "MIT" }, - "node_modules/mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, - "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/node-exports-info": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", - "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "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/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 6" } }, "node_modules/optionator": { @@ -6083,60 +3529,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", - "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.2", - "string-width": "^8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6182,19 +3574,6 @@ "node": ">=6" } }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "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", @@ -6222,23 +3601,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6259,14 +3621,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" } }, "node_modules/postcss": { @@ -6298,302 +3670,293 @@ "node": "^10 || ^12 || >=14" } }, - "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.9.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", - "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "peerDependencies": { + "postcss": "^8.0.0" } }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { - "parse-ms": "^4.0.0" + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "license": "MIT", + "camelcase-css": "^2.0.1" + }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" + "node": "^12 || ^14 || >= 16" }, "peerDependencies": { - "react": "^19.2.7" + "postcss": "^8.4.21" } }, - "node_modules/react-i18next": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", - "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.29.2", - "html-parse-stringify": "^3.0.1", - "use-sync-external-store": "^1.6.0" + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" }, "peerDependencies": { - "i18next": ">= 26.2.0", - "react": ">= 16.8.0", - "typescript": "^5 || ^6" + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "react-dom": { + "jiti": { "optional": true }, - "react-native": { + "postcss": { "optional": true }, - "typescript": { + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } }, - "node_modules/react-i18next/node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, "engines": { - "node": ">=6.9.0" + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" } }, - "node_modules/react-image-crop": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-11.1.2.tgz", - "integrity": "sha512-+0Pc2fxpwKL4u4oLmdKBw8XSwUceFbXbKEHvFOlsl/MGB1OVNic4uBlAPmEHGXYgoJIq+b63xHbc/aJMG0AVkA==", - "license": "ISC", - "peerDependencies": { - "react": ">=16.13.1" + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, "license": "MIT" }, - "node_modules/react-konva": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-19.2.5.tgz", - "integrity": "sha512-AdsuDB59GdB86QdenTAqRzr6Tfw3z9x9Opxo4BoqDjdNLsjekF9a1VhN4FlMPvg0qtrMl3d4dNmagaUTGH7fSA==", + "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/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": "patreon", - "url": "https://www.patreon.com/lavrton" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "opencollective", - "url": "https://opencollective.com/konva" + "type": "patreon", + "url": "https://www.patreon.com/feross" }, { - "type": "github", - "url": "https://github.com/sponsors/lavrton" - } - ], - "license": "MIT", - "dependencies": { - "@types/react-reconciler": "^0.33.0", - "its-fine": "^2.0.0", - "react-reconciler": "0.33.0", - "scheduler": "0.27.0" - }, - "peerDependencies": { - "konva": "^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0", - "react": "^19.2.0", - "react-dom": "^19.2.0" - } + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "node_modules/react-reconciler": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", - "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^19.2.0" } }, - "node_modules/react-toastify": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.1.0.tgz", - "integrity": "sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==", + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { - "clsx": "^2.1.1" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18 || ^19", - "react-dom": "^18 || ^19" + "react": "^18.3.1" } }, - "node_modules/react-window": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz", - "integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==", + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "dependencies": { + "loose-envify": "^1.1.0" } }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "node": ">=0.10.0" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">= 0.4" + "node": ">=20.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" + "react-router": "7.18.1" }, "engines": { - "node": ">= 0.4" + "node": ">=20.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/requireindex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", - "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.5" + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/resolve": { - "version": "2.0.0-next.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", - "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.2", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "pify": "^2.3.0" } }, "node_modules/resolve-from": { @@ -6606,135 +3969,86 @@ "node": ">=4" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "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", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" + "@types/estree": "1.0.9" }, "bin": { - "rolldown": "bin/cli.mjs" + "rollup": "dist/bin/rollup" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, - "node_modules/run-async": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", - "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", - "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "get-intrinsic": "^1.3.0", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "queue-microtask": "^1.2.2" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "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/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -6745,54 +4059,11 @@ "semver": "bin/semver.js" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -6817,114 +4088,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-icons": { - "version": "16.25.0", - "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.25.0.tgz", - "integrity": "sha512-ReYPfbqjkTBovxjerHw2tf6E1rQdaCqNJKaJbd78Mui8w+78xQlE1aDO/2ekID++n8qa+1t+oaueYLeiSoQsIA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/simple-icons" - }, - { - "type": "github", - "url": "https://github.com/sponsors/simple-icons" - } - ], - "license": "CC0-1.0", - "engines": { - "node": ">=0.12.18" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6935,105 +4098,71 @@ "node": ">=0.10.0" } }, - "node_modules/stdin-discarder": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", - "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "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": ">=18" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">= 0.4" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 6" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "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": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/string.prototype.repeat": { + "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", - "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-object-atoms": "^1.1.2", - "has-property-descriptors": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, "engines": { "node": ">= 0.4" }, @@ -7041,104 +4170,143 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", - "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.2" + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, - "engines": { - "node": ">= 0.4" + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 8.10.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "node_modules/tailwindcss/node_modules/chokidar/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": "MIT", + "license": "ISC", "dependencies": { - "ansi-regex": "^6.2.2" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">= 6" } }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "jiti": "bin/jiti.js" } }, - "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==", + "node_modules/tailwindcss/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" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "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==", + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=8" + "node": ">=8.10.0" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, "engines": { "node": ">= 0.4" }, @@ -7146,25 +4314,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } }, - "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==", + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "thenify": ">= 3.1.0 < 4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "engines": { + "node": ">=0.8" } }, "node_modules/tinyglobby": { @@ -7184,6 +4354,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "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", @@ -7197,108 +4380,53 @@ "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/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } + "license": "Apache-2.0" }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "deprecated": "unmaintained", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" + "bin": { + "tsconfck": "bin/tsconfck.js" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" + "node": "^18 || >=20" }, - "engines": { - "node": ">= 0.4" + "peerDependencies": { + "typescript": "^5.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/typed-array-length": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", - "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "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": { - "call-bind": "^1.0.9", - "for-each": "^0.3.5", - "gopd": "^1.2.0", - "is-typed-array": "^1.1.15", - "possible-typed-array-names": "^1.1.0", - "reflect.getprototypeof": "^1.0.10" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8.0" } }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7332,36 +4460,42 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, "node_modules/uri-js": { @@ -7379,41 +4513,38 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", + "optional": true, + "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/uuid": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", - "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", - "tinyglobby": "^0.2.17" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -7422,15 +4553,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -7439,18 +4569,15 @@ "@types/node": { "optional": true }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, "jiti": { "optional": true }, "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -7474,13 +4601,24 @@ } } }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } } }, "node_modules/which": { @@ -7499,95 +4637,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7598,12 +4647,21 @@ "node": ">=0.10.0" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -7627,19 +4685,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zustand": { "version": "5.0.14", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", diff --git a/package.json b/package.json index 75435431bc..89693bea11 100644 --- a/package.json +++ b/package.json @@ -1,71 +1,41 @@ { - "name": "rapidraw", - "author": "Timon Käch", + "name": "workspace", "private": true, + "version": "0.0.0", + "type": "module", "scripts": { "dev": "vite", - "build": "vite build", - "tauri": "tauri", - "start": "tauri dev", - "typecheck": "tsc --noEmit", + "build": "tsc -b && vite build", "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", - "i18n:extract": "i18next-cli extract", - "i18n:check": "i18next-cli extract --ci --dry-run", - "i18n:lint": "i18next-cli lint" + "preview": "vite preview", + "check": "tsc -b --noEmit" }, "dependencies": { - "@clerk/react": "^6.12.0", - "@dnd-kit/core": "^6.3.1", - "@tauri-apps/api": "^2.11.1", - "@tauri-apps/plugin-dialog": "^2.7.1", - "@tauri-apps/plugin-os": "^2.3.2", - "@tauri-apps/plugin-process": "^2.3.1", - "@tauri-apps/plugin-shell": "^2.3.5", - "@uiw/color-convert": "^2.10.3", - "@uiw/react-color-wheel": "^2.10.3", "clsx": "^2.1.1", - "framer-motion": "^12.42.2", - "i18next": "^26.3.4", - "konva": "^10.3.0", - "lodash.debounce": "^4.0.8", - "lodash.throttle": "^4.1.1", - "lucide-react": "^1.23.0", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-i18next": "^17.0.8", - "react-image-crop": "^11.1.2", - "react-konva": "^19.2.5", - "react-toastify": "^11.1.0", - "react-window": "^2.2.7", - "simple-icons": "^16.25.0", - "uuid": "^14.0.1", - "zustand": "^5.0.14" + "lucide-react": "^0.511.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.3.0", + "tailwind-merge": "^3.0.2", + "zustand": "^5.0.3" }, "devDependencies": { - "@eslint/js": "^9.39.2", - "@tailwindcss/vite": "^4.3.2", - "@tauri-apps/cli": "^2.11.4", - "@types/lodash.debounce": "^4.0.9", - "@types/lodash.throttle": "^4.1.9", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", - "esbuild": "^0.28.1", - "eslint": "^9.39.2", - "eslint-plugin-i18next": "^6.1.5", - "eslint-plugin-react": "^7.37.5", - "i18next-cli": "^1.65.0", - "prettier": "^3.9.4", - "tailwindcss": "^4.3.2", - "typescript": "^6.0.0", - "typescript-eslint": "^8.63.0", - "vite": "^8.1.3" - }, - "allowScripts": { - "esbuild@0.28.1": true, - "@swc/core@1.15.43": true + "@eslint/js": "^9.25.0", + "@types/node": "^22.15.30", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "babel-plugin-react-dev-locator": "^1.0.6", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5", + "vite-tsconfig-paths": "^5.1.4" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000..1d8a859e69 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,10 @@ +/** WARNING: DON'T EDIT THIS FILE */ +/** WARNING: DON'T EDIT THIS FILE */ +/** WARNING: DON'T EDIT THIS FILE */ + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000000..c04c3c1794 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0da3327dc8..ebd60b6ec6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6085,9 +6085,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.39.5" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ "libc", "memchr", @@ -6529,7 +6529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 4e5de94882..be1e132c9c 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -74,45 +74,56 @@ fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - let (download_filename, lib_name, expected_hash) = - match (target_os.as_str(), target_arch.as_str()) { - ("windows", "x86_64") => ( - "onnxruntime-windows-x86_64.dll", - "onnxruntime.dll", - "579b636403983254346a5c1d80bd28f1519cd1e284cd204f8d4ff41f8d711559", - ), - ("windows", "aarch64") => ( - "onnxruntime-windows-aarch64.dll", - "onnxruntime.dll", - "79281671a386ed1baab9dbdbb09fe55f99577011472e9526cf9d0b468bb6bcc7", - ), - ("linux", "x86_64") => ( - "libonnxruntime-linux-x86_64.so", - "libonnxruntime.so", - "3da6146e14e7b8aaec625dde11d6114c7457c87a5f93d744897da8781e35c673", - ), - ("linux", "aarch64") => ( - "libonnxruntime-linux-aarch64.so", - "libonnxruntime.so", - "0afd69a0ae38c5099fd0e8604dda398ac43dee67cd9c6394b5142b19e82528de", - ), - ("macos", "x86_64") => ( - "libonnxruntime-macos-x86_64.dylib", - "libonnxruntime.dylib", - "283e595e61cf65df7a6b1d59a1616cbd35c8b6399dd90d799d99b71a3ff83160", - ), - ("macos", "aarch64") => ( - "libonnxruntime-macos-aarch64.dylib", - "libonnxruntime.dylib", - "2b885992d3d6fa4130d39ec84a80d7504ff52750027c547bb22c86165f19406a", - ), - ("android", "aarch64") => ( - "libonnxruntime-android-arm64-v8a.so", - "libonnxruntime.so", - "999ecfdb5b5a13e4097487773b6d71ce8a075408a237daab072e8f5e817bd78e", - ), - _ => panic!("Unsupported target: {}-{}", target_os, target_arch), - }; + let (download_filename, lib_name, expected_hash) = match ( + target_os.as_str(), + target_arch.as_str(), + ) { + ("windows", "x86_64") => ( + "onnxruntime-windows-x86_64.dll", + "onnxruntime.dll", + "579b636403983254346a5c1d80bd28f1519cd1e284cd204f8d4ff41f8d711559", + ), + ("windows", "aarch64") => ( + "onnxruntime-windows-aarch64.dll", + "onnxruntime.dll", + "79281671a386ed1baab9dbdbb09fe55f99577011472e9526cf9d0b468bb6bcc7", + ), + ("linux", "x86_64") => ( + "libonnxruntime-linux-x86_64.so", + "libonnxruntime.so", + "3da6146e14e7b8aaec625dde11d6114c7457c87a5f93d744897da8781e35c673", + ), + ("linux", "aarch64") => ( + "libonnxruntime-linux-aarch64.so", + "libonnxruntime.so", + "0afd69a0ae38c5099fd0e8604dda398ac43dee67cd9c6394b5142b19e82528de", + ), + ("macos", "x86_64") => ( + "libonnxruntime-macos-x86_64.dylib", + "libonnxruntime.dylib", + "283e595e61cf65df7a6b1d59a1616cbd35c8b6399dd90d799d99b71a3ff83160", + ), + ("macos", "aarch64") => ( + "libonnxruntime-macos-aarch64.dylib", + "libonnxruntime.dylib", + "2b885992d3d6fa4130d39ec84a80d7504ff52750027c547bb22c86165f19406a", + ), + ("android", "aarch64") => ( + "libonnxruntime-android-arm64-v8a.so", + "libonnxruntime.so", + "999ecfdb5b5a13e4097487773b6d71ce8a075408a237daab072e8f5e817bd78e", + ), + ("android", _) => { + // ONNX Runtime is only available for arm64-v8a; skip for other Android ABIs + println!( + "cargo:warning=ONNX Runtime not available for android-{}. Skipping AI model download.", + target_arch + ); + tauri_build::build(); + return; + } + _ => panic!("Unsupported target: {}-{}", target_os, target_arch), + }; let dest_dir = if target_os == "android" { manifest_dir.join("libs").join("arm64-v8a") @@ -124,7 +135,20 @@ fn main() { let dest_path = dest_dir.join(lib_name); let mut is_valid = false; - if dest_path.exists() { + let skip_download = env::var("ORT_SKIP_DOWNLOAD").unwrap_or_default() == "1"; + + if skip_download && dest_path.exists() { + println!( + "cargo:warning=ORT_SKIP_DOWNLOAD=1 and library exists at {:?}. Skipping download.", + dest_path + ); + is_valid = true; + } else if skip_download && !dest_path.exists() { + println!( + "cargo:warning=ORT_SKIP_DOWNLOAD=1 but library not found at {:?}. Attempting download.", + dest_path + ); + } else if dest_path.exists() { match verify_sha256(&dest_path, expected_hash) { Ok(true) => { println!( diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts index 9f971c0bbe..cac04080ae 100644 --- a/src-tauri/gen/android/app/build.gradle.kts +++ b/src-tauri/gen/android/app/build.gradle.kts @@ -23,6 +23,11 @@ android { targetSdk = 36 versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + + // Only build for arm64-v8a since ONNX Runtime is only available for this ABI + ndk { + abiFilters += listOf("arm64-v8a") + } } signingConfigs { @@ -55,13 +60,17 @@ android { } getByName("release") { signingConfig = signingConfigs.getByName("release") - + isMinifyEnabled = true proguardFiles( *fileTree(".") { include("**/*.pro") } .plus(getDefaultProguardFile("proguard-android-optimize.txt")) .toList().toTypedArray() ) + // Keep .so files uncompressed in APK for direct mmap loading (matches extractNativeLibs="false") + packaging { + jniLibs.useLegacyPackaging = false + } } } kotlinOptions { diff --git a/src-tauri/gen/android/app/proguard-rules.pro b/src-tauri/gen/android/app/proguard-rules.pro index 998f7885bc..bd90b10133 100644 --- a/src-tauri/gen/android/app/proguard-rules.pro +++ b/src-tauri/gen/android/app/proguard-rules.pro @@ -1,24 +1,79 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile +# RapidRAW ProGuard Rules for Android Release Builds +# Keep line numbers for debugging +-keepattributes SourceFile,LineNumberTable +-renamesourcefileattribute SourceFile +# === Tauri / JNI Rules === # Keep rustls-platform-verifier JNI classes --keep, includedescriptorclasses class org.rustls.platformverifier.** { *; } \ No newline at end of file +-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; } + +# Keep all JNI native methods +-keepclasseswithmembernames class * { + native ; +} + +# Keep Tauri generated classes +-keep class org.tauri.** { *; } +-keep class com.tauri.** { *; } + +# === ONNX Runtime Rules === +-keep class ai.onnxruntime.** { *; } +-keep class org.bytedeco.** { *; } + +# === AndroidX Rules === +-keep class androidx.core.content.FileProvider { *; } +-keep class androidx.webkit.** { *; } + +# === WebView Rules === +-keep class android.webkit.** { *; } +-keepclassmembers class * { + @android.webkit.JavascriptInterface ; +} + +# === Kotlin Serialization === +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.AnnotationsKt +-keepclassmembers class kotlinx.serialization.json.** { + *** Companion; +} +-keepclasseswithmembers class kotlinx.serialization.json.** { + kotlinx.serialization.KSerializer serializer(...); +} +-keep,includedescriptorclasses class **$$serializer { *; } +-keepclassmembers class * { + *** Companion; +} +-keepclasseswithmembers class * { + kotlinx.serialization.KSerializer serializer(...); +} + +# === General Optimization === +# Keep data classes used with JSON +-keepclassmembers class * { + @com.google.gson.annotations.SerializedName ; +} + +# Remove logging in release +-assumenosideeffects class android.util.Log { + public static *** d(...); + public static *** v(...); + public static *** i(...); +} + +# Keep the application class +-keep class io.github.CyberTimon.RapidRAW.** { *; } + +# Keep Parcelable implementations +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +# Keep Serializable +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 2b043bf870..30178ef83b 100644 --- a/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,24 @@ - + + + + + + + + + + + + + + + @@ -9,19 +27,46 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.rapid_raw" - android:usesCleartextTraffic="${usesCleartextTraffic}"> + android:usesCleartextTraffic="${usesCleartextTraffic}" + android:requestLegacyExternalStorage="true" + android:allowBackup="true" + android:fullBackupContent="@xml/backup_rules" + android:supportsRtl="true" + android:extractNativeLibs="false" + android:pageSizeCompat="enabled" + android:resizeableActivity="true" + android:localeConfig="@xml/locales_config"> + + android:exported="true" + android:windowSoftInputMode="adjustResize" + android:resizeableActivity="true"> + + + + + + + + + + + + + + + + - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - val ime = insets.getInsets(WindowInsetsCompat.Type.ime()) - val bottomPadding = if (insets.isVisible(WindowInsetsCompat.Type.ime())) { - ime.bottom - } else { - systemBars.bottom - } - - view.setPadding( - systemBars.left, - systemBars.top, - systemBars.right, - bottomPadding - ) - - insets - } - - ViewCompat.requestApplyInsets(rootView) - } - - override fun onWebViewCreate(webView: WebView) { - super.onWebViewCreate(webView) - this.webView = webView - - webView.setBackgroundColor(safeMarginBackgroundColor) - webView.fitsSystemWindows = true - - onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - this@MainActivity.webView?.evaluateJavascript("window.__handleAndroidBack()", null) - } - }) - } -} + private val safeMarginBackgroundColor = Color.rgb(24, 24, 24) + private var webView: WebView? = null + private var stateRestored = false + + companion object { + private const val STATE_KEY_RESTORED = "rapidraw_state_restored" + private val REQUIRED_PERMISSIONS = buildList { + // Storage permissions + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(Manifest.permission.READ_MEDIA_IMAGES) + } else { + add(Manifest.permission.READ_EXTERNAL_STORAGE) + } + // Notification permission for Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(Manifest.permission.POST_NOTIFICATIONS) + } + } + } + + // Permission result launcher + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val allGranted = permissions.values.all { it } + if (!allGranted) { + Toast.makeText( + this, + getString(R.string.permission_denied), + Toast.LENGTH_LONG + ).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + // Android 12+ handles splash screen natively via the theme + + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + stateRestored = savedInstanceState?.getBoolean(STATE_KEY_RESTORED, false) ?: false + + val rootView: View = findViewById(android.R.id.content) + rootView.setBackgroundColor(safeMarginBackgroundColor) + + ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val ime = insets.getInsets(WindowInsetsCompat.Type.ime()) + val isKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) + + val bottomPadding = if (isKeyboardVisible) { + ime.bottom + } else { + systemBars.bottom + } + + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + bottomPadding + ) + + // Notify WebView of keyboard state change + if (isKeyboardVisible) { + webView?.evaluateJavascript( + "window.__handleKeyboardChange && window.__handleKeyboardChange(true, ${ime.bottom})", + null + ) + } else { + webView?.evaluateJavascript( + "window.__handleKeyboardChange && window.__handleKeyboardChange(false, 0)", + null + ) + } + + insets + } + + ViewCompat.requestApplyInsets(rootView) + + // Request required permissions on first launch + if (!stateRestored || !allPermissionsGranted()) { + requestRequiredPermissions() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(STATE_KEY_RESTORED, true) + } + + override fun onWebViewCreate(webView: WebView) { + super.onWebViewCreate(webView) + this.webView = webView + + webView.setBackgroundColor(safeMarginBackgroundColor) + webView.fitsSystemWindows = true + + // Enable multi-process WebView for crash isolation (Android 7+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + try { + WebView.setDataDirectorySuffix(applicationContext.packageName) + } catch (_: Exception) { + // Ignore if already set + } + } + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + this@MainActivity.webView?.evaluateJavascript( + "window.__handleAndroidBack()", + null + ) + } + }) + } + + override fun onTrimMemory(level: Int) { + super.onTrimMemory(level) + when (level) { + ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE, + ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW, + ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> { + // Notify WebView to release cached images/models + webView?.evaluateJavascript( + "window.__handleLowMemory && window.__handleLowMemory($level)", + null + ) + } + ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> { + // App is in background, release non-essential WebView resources + webView?.evaluateJavascript( + "window.__handleAppBackground && window.__handleAppBackground()", + null + ) + } + } + } + + override fun onLowMemory() { + super.onLowMemory() + // Critical memory pressure - request immediate cleanup + webView?.evaluateJavascript( + "window.__handleLowMemory && window.__handleLowMemory(-1)", + null + ) + } + + private fun allPermissionsGranted(): Boolean { + return REQUIRED_PERMISSIONS.all { permission -> + ContextCompat.checkSelfPermission(this, permission) == + PackageManager.PERMISSION_GRANTED + } + } + + private fun requestRequiredPermissions() { + val permissionsToRequest = REQUIRED_PERMISSIONS.filter { permission -> + ContextCompat.checkSelfPermission(this, permission) != + PackageManager.PERMISSION_GRANTED + }.toTypedArray() + + if (permissionsToRequest.isNotEmpty()) { + permissionLauncher.launch(permissionsToRequest) + } + } +} \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index 2b068d1146..7a7750b4d9 100644 --- a/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -22,7 +22,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml index 4fc244418b..a310776f58 100644 --- a/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml +++ b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml @@ -4,15 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/app_background" tools:context=".MainActivity"> - - - + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..03f0660733 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src-tauri/gen/android/app/src/main/res/values-night/themes.xml b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml index 719a95fea6..901c2ab6b5 100644 --- a/src-tauri/gen/android/app/src/main/res/values-night/themes.xml +++ b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml @@ -1,6 +1,11 @@ - + diff --git a/src-tauri/gen/android/app/src/main/res/values-zh/strings.xml b/src-tauri/gen/android/app/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000000..a7e06cd5af --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,13 @@ + + + RapidRAW + RapidRAW + 需要存储权限 + RapidRAW 需要访问您的照片和文件以导入和编辑图像。 + 需要通知权限 + 允许通知后,导出完成时将及时提醒您。 + 权限被拒绝,部分功能可能无法正常工作。 + 导出完成 + %s 已保存到您的相册中。 + 来自 RapidRAW 的分享 + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/colors.xml b/src-tauri/gen/android/app/src/main/res/values/colors.xml index f8c6127d32..51c99ed34e 100644 --- a/src-tauri/gen/android/app/src/main/res/values/colors.xml +++ b/src-tauri/gen/android/app/src/main/res/values/colors.xml @@ -1,5 +1,12 @@ + #FF181818 + #FF181818 + #FF1A1A1A + #FF4A90D9 + #FF242424 + #FFFFFFFF + #FF999999 #FFBB86FC #FF6200EE #FF3700B3 diff --git a/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml b/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml index ea9c223a6c..b36fa42a10 100644 --- a/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml +++ b/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #fff + #1A1A1A \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/strings.xml b/src-tauri/gen/android/app/src/main/res/values/strings.xml index 031222fd52..dd753f4e17 100644 --- a/src-tauri/gen/android/app/src/main/res/values/strings.xml +++ b/src-tauri/gen/android/app/src/main/res/values/strings.xml @@ -1,4 +1,12 @@ RapidRAW RapidRAW + Storage Access Required + RapidRAW needs access to your photos and files to import and edit images. + Notification Permission + Allow notifications to be alerted when exports are complete. + Permission denied. Some features may not work properly. + Export Complete + %s has been saved to your gallery. + Shared from RapidRAW \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/themes.xml b/src-tauri/gen/android/app/src/main/res/values/themes.xml index 719a95fea6..dea3c8f611 100644 --- a/src-tauri/gen/android/app/src/main/res/values/themes.xml +++ b/src-tauri/gen/android/app/src/main/res/values/themes.xml @@ -1,6 +1,15 @@ + + diff --git a/src-tauri/gen/android/app/src/main/res/xml/backup_rules.xml b/src-tauri/gen/android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..d38f6d35d8 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml index 782d63b993..8fa5a84336 100644 --- a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml +++ b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -1,5 +1,8 @@ - - + + + + + diff --git a/src-tauri/gen/android/app/src/main/res/xml/locales_config.xml b/src-tauri/gen/android/app/src/main/res/xml/locales_config.xml new file mode 100644 index 0000000000..ba0d7c3dbd --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src-tauri/icons/256x256.png b/src-tauri/icons/256x256.png new file mode 100644 index 0000000000..e90700694c Binary files /dev/null and b/src-tauri/icons/256x256.png differ diff --git a/src-tauri/icons/512x512.png b/src-tauri/icons/512x512.png new file mode 100644 index 0000000000..45caec9046 Binary files /dev/null and b/src-tauri/icons/512x512.png differ diff --git a/src-tauri/src/ai_commands.rs b/src-tauri/src/ai_commands.rs index 7ad5793633..400ef2897d 100644 --- a/src-tauri/src/ai_commands.rs +++ b/src-tauri/src/ai_commands.rs @@ -1,20 +1,25 @@ +#![allow(clippy::too_many_arguments)] + use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::io::Cursor; use base64::{Engine as _, engine::general_purpose}; -use image::{GrayImage, ImageFormat}; +use image::{GenericImageView, GrayImage, ImageFormat, Rgba}; use crate::ai_connector; use crate::ai_processing::{ AiDepthMaskParameters, AiForegroundMaskParameters, AiSkyMaskParameters, AiSubjectMaskParameters, CachedDepthMap, generate_image_embeddings, get_or_init_ai_models, - run_depth_anything_model, run_sam_decoder, run_sky_seg_model, run_u2netp_model, + get_or_init_clip_models, run_depth_anything_model, run_sam_decoder, run_sky_seg_model, + run_u2netp_model, }; use crate::app_settings::load_settings; use crate::app_state::AppState; use crate::cache_utils::GEOMETRY_KEYS; +use crate::file_management::parse_virtual_path; use crate::get_cached_full_warped_image; +use crate::tagging::{extract_color_tags, generate_tags_with_clip}; fn encode_to_base64_png(image: &GrayImage) -> Result { let mut buf = Cursor::new(Vec::new()); @@ -395,3 +400,628 @@ pub async fn test_ai_connector_connection(address: String) -> Result<(), String> Err(e) => Err(e.to_string()), } } + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AiRatingResult { + pub rating: u8, + pub description: String, + pub tags: Vec, +} + +fn compute_rating_from_features(image: &image::DynamicImage) -> (u8, String) { + let width = image.width(); + let height = image.height(); + let rgb_image = image.to_rgb8(); + + // Downsample for analysis speed + let analysis_size = 200u32; + let small = image::imageops::resize( + &rgb_image, + analysis_size, + analysis_size, + image::imageops::FilterType::Triangle, + ); + + let mut luminances: Vec = Vec::with_capacity((analysis_size * analysis_size) as usize); + let mut reds: Vec = Vec::new(); + let mut greens: Vec = Vec::new(); + let mut blues: Vec = Vec::new(); + + for pixel in small.pixels() { + let r = pixel[0] as f32 / 255.0; + let g = pixel[1] as f32 / 255.0; + let b = pixel[2] as f32 / 255.0; + let lum = 0.299 * r + 0.587 * g + 0.114 * b; + luminances.push(lum); + reds.push(r); + greens.push(g); + blues.push(b); + } + + let n = luminances.len() as f32; + + // Mean and variance of luminance + let mean_lum: f32 = luminances.iter().sum::() / n; + let var_lum: f32 = luminances + .iter() + .map(|x| (x - mean_lum).powi(2)) + .sum::() + / n; + + // Dynamic range (contrast) + let min_lum = luminances.iter().cloned().fold(f32::INFINITY, f32::min); + let max_lum = luminances.iter().cloned().fold(f32::NEG_INFINITY, f32::max); + let dynamic_range = max_lum - min_lum; + + // Color saturation (average across pixels) + let avg_sat: f32 = { + let mut sat_sum = 0.0f32; + for i in 0..luminances.len() { + let max_c = reds[i].max(greens[i]).max(blues[i]); + let min_c = reds[i].min(greens[i]).min(blues[i]); + sat_sum += if max_c > 0.0 { + (max_c - min_c) / max_c + } else { + 0.0 + }; + } + sat_sum / n + }; + + // Rule of thirds - center weight analysis + let center_score: f32 = { + let third_w = analysis_size as usize / 3; + let third_h = analysis_size as usize / 3; + let mut center_lum_sum = 0.0f32; + let mut center_count = 0usize; + let mut edge_lum_sum = 0.0f32; + let mut edge_count = 0usize; + for y in 0..analysis_size as usize { + for x in 0..analysis_size as usize { + let lum = luminances[y * analysis_size as usize + x]; + let is_center = x >= third_w && x < 2 * third_w && y >= third_h && y < 2 * third_h; + if is_center { + center_lum_sum += lum; + center_count += 1; + } else { + edge_lum_sum += lum; + edge_count += 1; + } + } + } + let center_mean = if center_count > 0 { + center_lum_sum / center_count as f32 + } else { + 0.0 + }; + let edge_mean = if edge_count > 0 { + edge_lum_sum / edge_count as f32 + } else { + 0.0 + }; + // Moderate center-edge contrast is good (subject separation), but too much is harsh + let contrast = (center_mean - edge_mean).abs(); + if contrast > 0.3 { + 0.7 + } else if contrast > 0.1 { + 1.0 + } else { + 0.6 + } + }; + + // Exposure quality: penalize too dark or too bright (clipped) + let clipped_shadows = luminances.iter().filter(|&&l| l < 0.02).count() as f32 / n; + let clipped_highlights = luminances.iter().filter(|&&l| l > 0.98).count() as f32 / n; + let exposure_score: f32 = 1.0 - (clipped_shadows + clipped_highlights).min(1.0); + + // Aspect ratio: standard ratios (3:2, 4:3, 16:9) get a slight bonus + let aspect = if height > 0 { + width as f32 / height as f32 + } else { + 1.0 + }; + let aspect_score: f32 = { + let near_standard = (aspect - 1.5).abs() < 0.1 // ~3:2 + || (aspect - 1.333).abs() < 0.1 // ~4:3 + || (aspect - 1.778).abs() < 0.1; // ~16:9 + if near_standard { 1.0 } else { 0.8 } + }; + + // Variance score: moderate variance indicates good tonal distribution + let variance_score: f32 = { + // Optimal variance around 0.06-0.10 for well-exposed photos + let optimal_var = 0.08; + let diff = (var_lum - optimal_var).abs(); + if diff < 0.02 { + 1.0 + } else if diff < 0.05 { + 0.8 + } else { + 0.5 + } + }; + + // Combine scores (weighted) + let raw_score = variance_score * 0.25 + + dynamic_range.min(1.0) * 0.2 + + avg_sat * 0.15 + + center_score * 0.15 + + exposure_score * 0.15 + + aspect_score * 0.1; + + // Map to 1-5 rating + let rating = if raw_score > 0.75 { + 5 + } else if raw_score > 0.6 { + 4 + } else if raw_score > 0.45 { + 3 + } else if raw_score > 0.3 { + 2 + } else { + 1 + }; + + // Generate description + let desc = if rating >= 4 { + if avg_sat > 0.4 { + "Rich colors, good composition".to_string() + } else if dynamic_range > 0.7 { + "Excellent dynamic range, balanced exposure".to_string() + } else { + "Overall good quality".to_string() + } + } else if rating == 3 { + if clipped_shadows > 0.1 { + "Shadow detail loss".to_string() + } else if clipped_highlights > 0.1 { + "Highlights clipped".to_string() + } else { + "Average quality".to_string() + } + } else { + if var_lum < 0.02 { + "Low contrast".to_string() + } else if avg_sat < 0.1 { + "Flat colors".to_string() + } else { + "Consider adjustments".to_string() + } + }; + + (rating, desc) +} + +#[tauri::command] +pub async fn generate_ai_rating( + path: String, + state: tauri::State<'_, AppState>, + app_handle: tauri::AppHandle, +) -> Result { + let (source_path, _) = parse_virtual_path(&path); + let source_path_str = source_path.to_string_lossy().to_string(); + let settings = load_settings(app_handle.clone()).unwrap_or_default(); + + // Load image + let image_bytes = + std::fs::read(&source_path).map_err(|e| format!("Failed to read image: {}", e))?; + let image = crate::image_loader::load_base_image_from_bytes( + &image_bytes, + &source_path_str, + true, + &settings, + None, + ) + .map_err(|e| format!("Failed to load image: {}", e))?; + + // Compute heuristic rating + let (rating, description) = compute_rating_from_features(&image); + + // Get tags using CLIP if available + let tags = + match get_or_init_clip_models(&app_handle, &state.ai_state, &state.ai_init_lock).await { + Ok(clip_models) => { + generate_tags_with_clip(&image, &clip_models.model, &clip_models.tokenizer, None, 8) + .unwrap_or_else(|_| extract_color_tags(&image)) + } + Err(_) => extract_color_tags(&image), + }; + + Ok(AiRatingResult { + rating, + description, + tags, + }) +} + +#[tauri::command] +pub async fn generate_ai_ratings_batch( + paths: Vec, + app_handle: tauri::AppHandle, + state: tauri::State<'_, AppState>, +) -> Result, String> { + let settings = load_settings(app_handle.clone()).unwrap_or_default(); + + // Try to init CLIP models once for the batch + let clip_models = get_or_init_clip_models(&app_handle, &state.ai_state, &state.ai_init_lock) + .await + .ok(); + + let mut results = Vec::with_capacity(paths.len()); + + for path in &paths { + let (source_path, _) = parse_virtual_path(path); + let source_path_str = source_path.to_string_lossy().to_string(); + + let image_bytes = match std::fs::read(&source_path) { + Ok(b) => b, + Err(e) => { + log::warn!("Failed to read image {}: {}", source_path_str, e); + results.push(AiRatingResult { + rating: 0, + description: format!("读取失败: {}", e), + tags: Vec::new(), + }); + continue; + } + }; + + let image = match crate::image_loader::load_base_image_from_bytes( + &image_bytes, + &source_path_str, + true, + &settings, + None, + ) { + Ok(img) => img, + Err(e) => { + log::warn!("Failed to load image {}: {}", source_path_str, e); + results.push(AiRatingResult { + rating: 0, + description: format!("加载失败: {}", e), + tags: Vec::new(), + }); + continue; + } + }; + + let (rating, description) = compute_rating_from_features(&image); + + let tags = match &clip_models { + Some(cm) => generate_tags_with_clip(&image, &cm.model, &cm.tokenizer, None, 8) + .unwrap_or_else(|_| extract_color_tags(&image)), + None => extract_color_tags(&image), + }; + + results.push(AiRatingResult { + rating, + description, + tags, + }); + } + + Ok(results) +} + +// --------------------------------------------------------------------------- +// Sky Replacement +// --------------------------------------------------------------------------- + +/// Replace the sky region using an existing sky mask with Poisson-like blending +/// at the edges. `sky_mask` is a grayscale mask (0=keep original, 255=use sky). +/// `sky_image_data` is the new sky image as raw RGBA bytes. +/// `blend_amount` controls edge blending (0..1). +#[tauri::command] +pub fn generate_ai_sky_replace( + state: tauri::State, + sky_mask: Vec, + sky_image_data: Vec, + blend_amount: f32, +) -> Result, String> { + let loaded_image = state + .original_image + .lock() + .unwrap() + .clone() + .ok_or("No original image loaded")?; + + let (w, h) = loaded_image.image.as_ref().dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + let mut base_rgba = loaded_image.image.to_rgba8(); + + // Decode the sky image + let sky_img = image::load_from_memory(&sky_image_data) + .map_err(|e| format!("Failed to decode sky image: {}", e))?; + let sky_rgba = sky_img + .resize_exact(w, h, image::imageops::FilterType::Lanczos3) + .to_rgba8(); + + // Validate mask dimensions + if sky_mask.len() != (w as usize * h as usize) { + return Err(format!( + "Sky mask size {} does not match image {}x{}={}", + sky_mask.len(), + w, + h, + w as usize * h as usize + )); + } + + let blend = blend_amount.clamp(0.0, 1.0); + let blend_radius = (blend * 10.0).round() as i32; // pixel radius for edge blending + + // Build a feathered mask from the binary sky_mask + // Apply a simple box blur for feathering + let mut feathered = vec![0.0f32; (w * h) as usize]; + for y in 0..h { + for x in 0..w { + let idx = (y * w + x) as usize; + feathered[idx] = sky_mask[idx] as f32 / 255.0; + } + } + + // Simple box blur for feathering + if blend_radius > 0 { + let mut blurred = vec![0.0f32; feathered.len()]; + let w_usize = w as usize; + let h_usize = h as usize; + + // Horizontal pass + for y in 0..h_usize { + for x in 0..w_usize { + let mut sum = 0.0f32; + let mut count = 0; + for kx in -blend_radius..=blend_radius { + let nx = (x as i32 + kx).clamp(0, w_usize as i32 - 1) as usize; + sum += feathered[y * w_usize + nx]; + count += 1; + } + blurred[y * w_usize + x] = sum / count as f32; + } + } + + // Vertical pass + for y in 0..h_usize { + for x in 0..w_usize { + let mut sum = 0.0f32; + let mut count = 0; + for ky in -blend_radius..=blend_radius { + let ny = (y as i32 + ky).clamp(0, h_usize as i32 - 1) as usize; + sum += blurred[ny * w_usize + x]; + count += 1; + } + feathered[y * w_usize + x] = sum / count as f32; + } + } + } + + // Composite: blend original and sky using the feathered mask + for y in 0..h { + for x in 0..w { + let idx = (y * w + x) as usize; + let alpha = feathered[idx]; + + let base = base_rgba.get_pixel(x, y); + let sky = sky_rgba.get_pixel(x, y); + + let r = (base[0] as f32 * (1.0 - alpha) + sky[0] as f32 * alpha).round() as u8; + let g = (base[1] as f32 * (1.0 - alpha) + sky[1] as f32 * alpha).round() as u8; + let b = (base[2] as f32 * (1.0 - alpha) + sky[2] as f32 * alpha).round() as u8; + + base_rgba.put_pixel(x, y, Rgba([r, g, b, base[3]])); + } + } + + // Encode result as PNG + let mut buf = Cursor::new(Vec::new()); + base_rgba + .write_to(&mut buf, ImageFormat::Png) + .map_err(|e| format!("Failed to encode result: {}", e))?; + + Ok(buf.into_inner()) +} + +// --------------------------------------------------------------------------- +// Background Removal +// --------------------------------------------------------------------------- + +/// Remove background using the existing foreground mask from AI state. +/// Produces an RGBA PNG with alpha channel from the mask. +#[tauri::command] +pub fn generate_ai_background_remove(state: tauri::State) -> Result, String> { + let loaded_image = state + .original_image + .lock() + .unwrap() + .clone() + .ok_or("No original image loaded")?; + + let (w, h) = loaded_image.image.as_ref().dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + // Get the foreground mask from AI state via depth map + let foreground_mask = { + let ai_state_lock = state.ai_state.lock().unwrap(); + let ai_state = ai_state_lock + .as_ref() + .ok_or("AI state not initialized. Please generate a depth mask first.")?; + + if let Some(depth) = &ai_state.depth_map { + // Use depth map: treat closer objects (higher depth value) as foreground + let mut mask = depth.depth_image.clone(); + // Threshold: pixels above 128 are foreground + for pixel in mask.pixels_mut() { + pixel[0] = if pixel[0] > 128 { 255 } else { 0 }; + } + // Resize to match + image::imageops::resize(&mask, w, h, image::imageops::FilterType::Triangle) + } else { + return Err("No depth map available. Please generate a depth mask first.".to_string()); + } + }; + + // Resize mask to match image dimensions + let mask_resized = if foreground_mask.width() != w || foreground_mask.height() != h { + image::imageops::resize( + &foreground_mask, + w, + h, + image::imageops::FilterType::Triangle, + ) + } else { + foreground_mask + }; + + // Apply mask as alpha channel + let mut rgba = loaded_image.image.to_rgba8(); + for y in 0..h { + for x in 0..w { + let alpha = mask_resized.get_pixel(x, y)[0]; + let pixel = rgba.get_pixel_mut(x, y); + pixel[3] = alpha; + } + } + + // Encode as PNG + let mut buf = Cursor::new(Vec::new()); + rgba.write_to(&mut buf, ImageFormat::Png) + .map_err(|e| format!("Failed to encode result: {}", e))?; + + Ok(buf.into_inner()) +} + +// --------------------------------------------------------------------------- +// Super Resolution +// --------------------------------------------------------------------------- + +/// Apply 2x super-resolution using Lanczos3 upsampling followed by +/// sharpening enhancement. No deep learning model is used. +#[tauri::command] +pub async fn apply_super_resolution( + state: tauri::State<'_, AppState>, + scale: f32, +) -> Result, String> { + let loaded_image = state + .original_image + .lock() + .unwrap() + .clone() + .ok_or("No original image loaded")?; + + let (w, h) = loaded_image.image.as_ref().dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + tokio::task::spawn_blocking(move || { + let effective_scale = scale.clamp(1.0, 4.0); + let new_w = (w as f32 * effective_scale).round() as u32; + let new_h = (h as f32 * effective_scale).round() as u32; + + // Step 1: Lanczos3 upsampling + let mut upscaled = loaded_image + .image + .resize_exact(new_w, new_h, image::imageops::FilterType::Lanczos3) + .to_rgba8(); + + // Step 2: Multi-scale detail enhancement + // Create blurred versions at different scales for frequency separation + let blurred_light = image::imageops::blur(&upscaled, 2.0); + let blurred_detail = image::imageops::blur(&upscaled, 0.8); + + let unsharp_amount = 0.6_f32; + let edge_amount = 0.35_f32; + let detail_boost = 0.25_f32; + + for y in 0..new_h { + for x in 0..new_w { + let orig = upscaled.get_pixel(x, y); + let blur_l = blurred_light.get_pixel(x, y); + let blur_d = blurred_detail.get_pixel(x, y); + + // Unsharp mask: sharpened = original + amount * (original - blurred) + let us_r = orig[0] as f32 + unsharp_amount * (orig[0] as f32 - blur_l[0] as f32); + let us_g = orig[1] as f32 + unsharp_amount * (orig[1] as f32 - blur_l[1] as f32); + let us_b = orig[2] as f32 + unsharp_amount * (orig[2] as f32 - blur_l[2] as f32); + + // Detail recovery: boost high-frequency details lost in upsampling + let detail_r = (orig[0] as f32 - blur_d[0] as f32) * detail_boost; + let detail_g = (orig[1] as f32 - blur_d[1] as f32) * detail_boost; + let detail_b = (orig[2] as f32 - blur_d[2] as f32) * detail_boost; + + // Local edge enhancement using Laplacian-like operator on neighbors + let mut lap_r = 0.0_f32; + let mut lap_g = 0.0_f32; + let mut lap_b = 0.0_f32; + let mut lap_count = 0_u32; + + for dy in -1i32..=1 { + for dx in -1i32..=1 { + if dx == 0 && dy == 0 { + continue; + } + let nx = (x as i32 + dx).clamp(0, new_w as i32 - 1) as u32; + let ny = (y as i32 + dy).clamp(0, new_h as i32 - 1) as u32; + let np = upscaled.get_pixel(nx, ny); + lap_r += np[0] as f32; + lap_g += np[1] as f32; + lap_b += np[2] as f32; + lap_count += 1; + } + } + + if lap_count > 0 { + let inv_count = 1.0 / lap_count as f32; + let avg_r = lap_r * inv_count; + let avg_g = lap_g * inv_count; + let avg_b = lap_b * inv_count; + lap_r = (orig[0] as f32 - avg_r) * edge_amount; + lap_g = (orig[1] as f32 - avg_g) * edge_amount; + lap_b = (orig[2] as f32 - avg_b) * edge_amount; + } + + let r = (us_r + detail_r + lap_r).round().clamp(0.0, 255.0) as u8; + let g = (us_g + detail_g + lap_g).round().clamp(0.0, 255.0) as u8; + let b = (us_b + detail_b + lap_b).round().clamp(0.0, 255.0) as u8; + + upscaled.put_pixel(x, y, Rgba([r, g, b, orig[3]])); + } + } + + // Step 3: Subtle chroma noise reduction on upscaled image + // (prevents color artifacts from the enhancement) + let chroma_blurred = image::imageops::blur(&upscaled, 0.5); + let chroma_blend = 0.15_f32; + for y in 0..new_h { + for x in 0..new_w { + let orig = upscaled.get_pixel(x, y); + let blur = chroma_blurred.get_pixel(x, y); + let r = (orig[0] as f32 * (1.0 - chroma_blend) + blur[0] as f32 * chroma_blend) + .round() + .clamp(0.0, 255.0) as u8; + let g = (orig[1] as f32 * (1.0 - chroma_blend) + blur[1] as f32 * chroma_blend) + .round() + .clamp(0.0, 255.0) as u8; + let b = (orig[2] as f32 * (1.0 - chroma_blend) + blur[2] as f32 * chroma_blend) + .round() + .clamp(0.0, 255.0) as u8; + upscaled.put_pixel(x, y, Rgba([r, g, b, orig[3]])); + } + } + + // Encode as PNG + let mut buf = Cursor::new(Vec::new()); + upscaled + .write_to(&mut buf, ImageFormat::Png) + .map_err(|e| format!("Failed to encode result: {}", e))?; + + Ok(buf.into_inner()) + }).await.map_err(|e| format!("Super resolution task failed: {}", e))? +} diff --git a/src-tauri/src/ai_connector.rs b/src-tauri/src/ai_connector.rs index 78769b9b69..73509c9d4d 100644 --- a/src-tauri/src/ai_connector.rs +++ b/src-tauri/src/ai_connector.rs @@ -31,7 +31,7 @@ pub fn generate_source_id(path_str: &str) -> Result { let metadata = fs::metadata(path)?; let mod_time = metadata .modified() - .unwrap_or(SystemTime::UNIX_EPOCH) + .map_err(|_| anyhow!("Cannot determine modification time for: {}", path_str))? .duration_since(SystemTime::UNIX_EPOCH)? .as_secs(); @@ -112,7 +112,10 @@ pub async fn check_status(address: &str) -> Result { .get(format!("http://{}/health", address)) .send() .await; - Ok(res.is_ok()) + match res { + Ok(response) => Ok(response.status().is_success()), + Err(_) => Ok(false), + } } pub async fn process_inpainting( @@ -128,12 +131,17 @@ pub async fn process_inpainting( let mask_b64 = image_to_base64(mask_image)?; let (w, h) = full_source_image.dimensions(); + let seed: u64 = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let payload = InpaintRequest { source_id: source_id.clone(), prompt, negative_prompt: "blur, low quality, distortion, watermark".to_string(), mask_image_base64: mask_b64, - seed: 0, + seed: seed as i64, }; let url = format!("{}/inpaint", base_url); diff --git a/src-tauri/src/ai_processing.rs b/src-tauri/src/ai_processing.rs index 0af07ba649..9f176fbfd6 100644 --- a/src-tauri/src/ai_processing.rs +++ b/src-tauri/src/ai_processing.rs @@ -3,7 +3,7 @@ use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use image::imageops::{self, FilterType}; use image::{ DynamicImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgb, Rgb32FImage, Rgba, RgbaImage, @@ -11,6 +11,31 @@ use image::{ use ndarray::{Array, Array4, IxDyn}; use ort::session::Session; use ort::value::Tensor; + +fn get_execution_providers() -> Vec { + use ort::execution_providers::*; + let mut eps = Vec::new(); + + #[cfg(target_os = "windows")] + { + eps.push(DirectMLExecutionProvider::default().build()); + eps.push(CUDAExecutionProvider::default().build()); + } + + #[cfg(target_os = "macos")] + { + eps.push(CoreMLExecutionProvider::default().build()); + } + + #[cfg(target_os = "linux")] + { + eps.push(CUDAExecutionProvider::default().build()); + eps.push(ROCmExecutionProvider::default().build()); + } + + eps.push(CPUExecutionProvider::default().build()); + eps +} use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tauri::Emitter; @@ -18,6 +43,31 @@ use tauri::Manager; use tokenizers::Tokenizer; use tokio::sync::Mutex as TokioMutex; +/// Default mirror base URL for HuggingFace model downloads. +/// For Chinese users, set to "https://hf-mirror.com" or other domestic CDN. +/// Leave empty to use the default HuggingFace URLs directly. +const DEFAULT_HF_MIRROR_BASE: &str = ""; + +/// Environment variable to override the HuggingFace mirror base URL at runtime. +const HF_MIRROR_ENV_VAR: &str = "RAPIDRAW_HF_MIRROR"; + +fn resolve_model_url(original_url: &str) -> String { + let mirror_base = std::env::var(HF_MIRROR_ENV_VAR) + .ok() + .filter(|v| !v.is_empty()) + .unwrap_or_else(|| DEFAULT_HF_MIRROR_BASE.to_string()); + + if mirror_base.is_empty() { + return original_url.to_string(); + } + + // Replace huggingface.co with the mirror domain + original_url.replace( + "https://huggingface.co/", + &format!("{}/", mirror_base.trim_end_matches('/')), + ) +} + const ENCODER_URL: &str = "https://huggingface.co/CyberTimon/RapidRAW-Models/resolve/main/sam_vit_b_01ec64_encoder.onnx?download=true"; const DECODER_URL: &str = "https://huggingface.co/CyberTimon/RapidRAW-Models/resolve/main/sam_vit_b_01ec64_decoder.onnx?download=true"; const ENCODER_FILENAME: &str = "sam_vit_b_01ec64_encoder.onnx"; @@ -59,6 +109,70 @@ const DEPTH_FILENAME: &str = "depth_anything_v2_vits.onnx"; const DEPTH_INPUT_SIZE: u32 = 518; const DEPTH_SHA256: &str = "d2b11a11c1d4a12b47608fa65a17ee9a4c605b55ee1730c8e3b526304f2562be"; +const SCRFD_FILENAME: &str = "scrfd_10g_bnkps.onnx"; +const SCRFD_URL: &str = + "https://huggingface.co/datasets/Alltitude/insightface/resolve/main/scrfd_10g_bnkps.onnx"; + +const FACE_LANDMARK_106_FILENAME: &str = "2d106det.onnx"; +const FACE_LANDMARK_106_URL: &str = + "https://huggingface.co/datasets/Alltitude/insightface/resolve/main/2d106det.onnx"; + +/// Check if there is sufficient available memory for AI model loading. +/// `required_mb` is the minimum required memory in MB. +fn check_available_memory(required_mb: u64) -> anyhow::Result<()> { + #[cfg(target_os = "android")] + { + // On Android, read /proc/meminfo for available memory + if let Ok(meminfo) = std::fs::read_to_string("/proc/meminfo") { + for line in meminfo.lines() { + if line.starts_with("MemAvailable:") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + if let Ok(kb) = parts[1].parse::() { + let available_mb = kb / 1024; + if available_mb < required_mb { + return Err(anyhow::anyhow!( + "设备内存不足(可用 {}MB,需要 {}MB)。建议关闭后台应用后重试。", + available_mb, + required_mb + )); + } + } + } + break; + } + // Also check MemAvailable's predecessor: MemFree + Cached + if line.starts_with("MemFree:") { + // Will be used as fallback if MemAvailable not found + } + } + } + } + #[cfg(not(target_os = "android"))] + { + let _ = required_mb; // No memory check on non-Android platforms + } + Ok(()) +} + +/// Format an OOM-related error with a user-friendly message. +fn format_oom_error(model_name: &str, error: &dyn std::fmt::Display) -> String { + let error_str = error.to_string(); + let is_oom = error_str.to_lowercase().contains("out of memory") + || error_str.to_lowercase().contains("oom") + || error_str.to_lowercase().contains("alloc") + || error_str.to_lowercase().contains("memory"); + + if is_oom { + format!( + "加载{}失败:设备内存不足。建议关闭后台应用后重试。(详情:{})", + model_name, error_str + ) + } else { + format!("加载{}失败:{}", model_name, error_str) + } +} + pub struct AiModels { pub sam_encoder: Mutex, pub sam_decoder: Mutex, @@ -93,6 +207,7 @@ pub struct AiState { pub lama_model: Option>>, pub embeddings: Option, pub depth_map: Option, + pub face_landmark_detector: Option>>, } fn edt_1d(f: &mut [f32], v: &mut [usize], z: &mut [f32], d: &mut [f32]) { @@ -212,7 +327,8 @@ fn persist_downloaded_asset(dest: &Path, bytes: &[u8]) -> Result<()> { } async fn download_model(url: &str, dest: &Path) -> Result<()> { - let response = reqwest::get(url).await?.error_for_status()?; + let resolved_url = resolve_model_url(url); + let response = reqwest::get(&resolved_url).await?.error_for_status()?; let bytes = response.bytes().await?; persist_downloaded_asset(dest, &bytes) } @@ -376,17 +492,45 @@ pub async fn get_or_init_ai_models( let _ = ort::init().with_name("AI").commit(); + // Memory threshold check: ensure sufficient memory before loading AI models on Android + check_available_memory(500)?; // Require at least 500MB available + let encoder_path = models_dir.join(ENCODER_FILENAME); let decoder_path = models_dir.join(DECODER_FILENAME); let u2netp_path = models_dir.join(U2NETP_FILENAME); let sky_seg_path = models_dir.join(SKYSEG_FILENAME); let depth_path = models_dir.join(DEPTH_FILENAME); - let sam_encoder = Session::builder()?.commit_from_file(encoder_path)?; - let sam_decoder = Session::builder()?.commit_from_file(decoder_path)?; - let u2netp = Session::builder()?.commit_from_file(u2netp_path)?; - let sky_seg = Session::builder()?.commit_from_file(sky_seg_path)?; - let depth_anything = Session::builder()?.commit_from_file(depth_path)?; + let sam_encoder = Session::builder() + .map_err(|e| anyhow::Error::msg(format_oom_error("SAM Encoder", &e)))? + .with_execution_providers(get_execution_providers()) + .map_err(|e| anyhow::Error::msg(format_oom_error("SAM Encoder", &e)))? + .commit_from_file(encoder_path) + .map_err(|e| anyhow::Error::msg(format_oom_error("SAM Encoder", &e)))?; + let sam_decoder = Session::builder() + .map_err(|e| anyhow::Error::msg(format_oom_error("SAM Decoder", &e)))? + .with_execution_providers(get_execution_providers()) + .map_err(|e| anyhow::Error::msg(format_oom_error("SAM Decoder", &e)))? + .commit_from_file(decoder_path) + .map_err(|e| anyhow::Error::msg(format_oom_error("SAM Decoder", &e)))?; + let u2netp = Session::builder() + .map_err(|e| anyhow::Error::msg(format_oom_error("Foreground Model", &e)))? + .with_execution_providers(get_execution_providers()) + .map_err(|e| anyhow::Error::msg(format_oom_error("Foreground Model", &e)))? + .commit_from_file(u2netp_path) + .map_err(|e| anyhow::Error::msg(format_oom_error("Foreground Model", &e)))?; + let sky_seg = Session::builder() + .map_err(|e| anyhow::Error::msg(format_oom_error("Sky Model", &e)))? + .with_execution_providers(get_execution_providers()) + .map_err(|e| anyhow::Error::msg(format_oom_error("Sky Model", &e)))? + .commit_from_file(sky_seg_path) + .map_err(|e| anyhow::Error::msg(format_oom_error("Sky Model", &e)))?; + let depth_anything = Session::builder() + .map_err(|e| anyhow::Error::msg(format_oom_error("Depth Model", &e)))? + .with_execution_providers(get_execution_providers()) + .map_err(|e| anyhow::Error::msg(format_oom_error("Depth Model", &e)))? + .commit_from_file(depth_path) + .map_err(|e| anyhow::Error::msg(format_oom_error("Depth Model", &e)))?; crate::register_exit_handler(); @@ -409,6 +553,7 @@ pub async fn get_or_init_ai_models( lama_model: None, embeddings: None, depth_map: None, + face_landmark_detector: None, }); } @@ -452,8 +597,16 @@ pub async fn get_or_init_denoise_model( .await?; let _ = ort::init().with_name("AI-Denoise").commit(); + + check_available_memory(200)?; + let model_path = models_dir.join(DENOISE_FILENAME); - let session = Session::builder()?.commit_from_file(model_path)?; + let session = Session::builder() + .map_err(|e| anyhow::Error::msg(format_oom_error("Denoise Model", &e)))? + .with_execution_providers(get_execution_providers()) + .map_err(|e| anyhow::Error::msg(format_oom_error("Denoise Model", &e)))? + .commit_from_file(model_path) + .map_err(|e| anyhow::Error::msg(format_oom_error("Denoise Model", &e)))?; let denoise_model = Arc::new(Mutex::new(session)); crate::register_exit_handler(); @@ -469,6 +622,7 @@ pub async fn get_or_init_denoise_model( lama_model: None, embeddings: None, depth_map: None, + face_landmark_detector: None, }); } @@ -521,8 +675,18 @@ pub async fn get_or_init_clip_models( } let _ = ort::init().with_name("AI-Tagging").commit(); + + check_available_memory(200)?; + let clip_model_path = models_dir.join(CLIP_MODEL_FILENAME); - let model = Mutex::new(Session::builder()?.commit_from_file(clip_model_path)?); + let model = Mutex::new( + Session::builder() + .map_err(|e| anyhow::Error::msg(format_oom_error("CLIP Model", &e)))? + .with_execution_providers(get_execution_providers()) + .map_err(|e| anyhow::Error::msg(format_oom_error("CLIP Model", &e)))? + .commit_from_file(clip_model_path) + .map_err(|e| anyhow::Error::msg(format_oom_error("CLIP Model", &e)))?, + ); let tokenizer = Tokenizer::from_file(clip_tokenizer_path).map_err(|e| anyhow::anyhow!(e.to_string()))?; @@ -541,6 +705,7 @@ pub async fn get_or_init_clip_models( lama_model: None, embeddings: None, depth_map: None, + face_landmark_detector: None, }); } @@ -584,8 +749,17 @@ pub async fn get_or_init_lama_model( .await?; let _ = ort::init().with_name("AI-Inpainting").commit(); + + // Memory threshold check before loading LaMa inpainting model + check_available_memory(200)?; // Require at least 200MB available + let model_path = models_dir.join(LAMA_FILENAME); - let session = Session::builder()?.commit_from_file(model_path)?; + let session = Session::builder() + .map_err(|e| anyhow::Error::msg(format_oom_error("Inpainting Model", &e)))? + .with_execution_providers(get_execution_providers()) + .map_err(|e| anyhow::Error::msg(format_oom_error("Inpainting Model", &e)))? + .commit_from_file(model_path) + .map_err(|e| anyhow::Error::msg(format_oom_error("Inpainting Model", &e)))?; let lama_model = Arc::new(Mutex::new(session)); crate::register_exit_handler(); @@ -601,12 +775,108 @@ pub async fn get_or_init_lama_model( lama_model: Some(lama_model.clone()), embeddings: None, depth_map: None, + face_landmark_detector: None, }); } Ok(lama_model) } +async fn download_model_if_missing( + app_handle: &tauri::AppHandle, + models_dir: &Path, + filename: &str, + url: &str, + model_name: &str, +) -> Result<(), String> { + let dest_path = models_dir.join(filename); + if dest_path.exists() { + return Ok(()); + } + let _ = app_handle.emit("ai-model-download-start", model_name); + let result = download_model(url, &dest_path) + .await + .map_err(|e| e.to_string()); + let _ = app_handle.emit("ai-model-download-finish", model_name); + result +} + +pub async fn get_or_init_face_landmark_detector( + app_handle: &tauri::AppHandle, + ai_state_mutex: &Mutex>, + ai_init_lock: &TokioMutex<()>, +) -> Result>, String> { + if let Some(detector) = ai_state_mutex + .lock() + .unwrap() + .as_ref() + .and_then(|state| state.face_landmark_detector.clone()) + { + return Ok(detector); + } + + let _guard = ai_init_lock.lock().await; + + if let Some(detector) = ai_state_mutex + .lock() + .unwrap() + .as_ref() + .and_then(|state| state.face_landmark_detector.clone()) + { + return Ok(detector); + } + + let models_dir = get_models_dir(app_handle).map_err(|e| e.to_string())?; + + download_model_if_missing( + app_handle, + &models_dir, + SCRFD_FILENAME, + SCRFD_URL, + "Face Detection Model", + ) + .await?; + + download_model_if_missing( + app_handle, + &models_dir, + FACE_LANDMARK_106_FILENAME, + FACE_LANDMARK_106_URL, + "Face Landmark Model", + ) + .await?; + + let _ = ort::init().with_name("AI-FaceLandmark").commit(); + + check_available_memory(300).map_err(|e| e.to_string())?; + + let scrfd_path = models_dir.join(SCRFD_FILENAME); + let landmark_path = models_dir.join(FACE_LANDMARK_106_FILENAME); + + let detector = crate::face_landmark::FaceLandmarkDetector::new(&scrfd_path, &landmark_path) + .map_err(|e| format_oom_error("Face Detection", &e))?; + let detector = Arc::new(Mutex::new(detector)); + + crate::register_exit_handler(); + + let mut ai_state_lock = ai_state_mutex.lock().unwrap(); + if let Some(state) = ai_state_lock.as_mut() { + state.face_landmark_detector = Some(detector.clone()); + } else { + *ai_state_lock = Some(AiState { + models: None, + denoise_model: None, + clip_models: None, + lama_model: None, + embeddings: None, + depth_map: None, + face_landmark_detector: Some(detector.clone()), + }); + } + + Ok(detector) +} + #[derive(Clone, Copy)] struct TileParams { cs: usize, @@ -860,6 +1130,10 @@ pub fn run_lama_inpainting( ) -> Result { let (w, h) = image.dimensions(); + if w == 0 || h == 0 { + return Ok(RgbaImage::new(0, 0)); + } + let (mut min_x, mut min_y) = (w, h); let (mut max_x, mut max_y) = (0u32, 0u32); let mut has_mask = false; @@ -892,6 +1166,10 @@ pub fn run_lama_inpainting( let crop_w = x1 - x0 + 1; let crop_h = y1 - y0 + 1; + if crop_w == 0 || crop_h == 0 { + return Ok(image.to_rgba8()); + } + let rgba = image.to_rgba8(); let cropped_img = imageops::crop_imm(&rgba, x0, y0, crop_w, crop_h).to_image(); @@ -1041,6 +1319,22 @@ pub fn run_sam_decoder( end_point: (f64, f64), ) -> Result { let (orig_width, orig_height) = embeddings.original_size; + + // Guard: if point coordinates are invalid (NaN/inf) or result in empty + // point arrays, return an empty (all-black) mask instead of panicking + // when constructing zero-length ndarray shapes. + if start_point.0.is_nan() + || start_point.1.is_nan() + || end_point.0.is_nan() + || end_point.1.is_nan() + || start_point.0.is_infinite() + || start_point.1.is_infinite() + || end_point.0.is_infinite() + || end_point.1.is_infinite() + { + return Ok(GrayImage::new(orig_width, orig_height)); + } + let long_side = orig_width.max(orig_height) as f64; let scale = SAM_INPUT_SIZE as f64 / long_side; @@ -1068,6 +1362,11 @@ pub fn run_sam_decoder( point_labels.push(3.0f32); } + // Guard: if point arrays are somehow empty, return an empty mask. + if point_coords.is_empty() { + return Ok(GrayImage::new(orig_width, orig_height)); + } + let mut mask_input = Array::zeros((1, 1, 256, 256)).into_dyn(); let mut has_mask_input = 0.0f32; @@ -1124,7 +1423,7 @@ pub fn run_sam_decoder( let w = mask_dims[3]; let area = h * w; - let mask_slice = mask_tensor.as_slice().unwrap(); + let mask_slice = mask_tensor.as_slice().ok_or_else(|| anyhow!("Failed to extract mask tensor data - tensor may not be contiguous"))?; let first_mask_slice = &mask_slice[0..area]; if i == iters - 1 { @@ -1231,9 +1530,9 @@ pub fn run_sam_decoder( .collect(); let img_mask_f32 = - ImageBuffer::, Vec>::from_raw(w as u32, h as u32, mask_f32_vec).unwrap(); + ImageBuffer::, Vec>::from_raw(w as u32, h as u32, mask_f32_vec).ok_or_else(|| anyhow!("Failed to create mask image buffer - dimension mismatch"))?; let img_gaus_f32 = - ImageBuffer::, Vec>::from_raw(w as u32, h as u32, gaus_dt).unwrap(); + ImageBuffer::, Vec>::from_raw(w as u32, h as u32, gaus_dt).ok_or_else(|| anyhow!("Failed to create gaussian image buffer - dimension mismatch"))?; let resized_mask = imageops::resize(&img_mask_f32, 256, 256, FilterType::Triangle); let resized_gaus = imageops::resize(&img_gaus_f32, 256, 256, FilterType::Triangle); @@ -1309,7 +1608,7 @@ pub fn run_sky_seg_model( let mut session = sky_seg_session.lock().unwrap(); let outputs = session.run(ort::inputs![t_input])?; let output_tensor = outputs[0].try_extract_array::()?.to_owned(); - let out_slice = output_tensor.as_slice().unwrap(); + let out_slice = output_tensor.as_slice().ok_or_else(|| anyhow!("Failed to extract output tensor data - tensor may not be contiguous"))?; let mut min_val = f32::MAX; let mut max_val = f32::MIN; @@ -1390,7 +1689,7 @@ pub fn run_u2netp_model( let mut session = u2netp_session.lock().unwrap(); let outputs = session.run(ort::inputs![t_input])?; let output_tensor = outputs[0].try_extract_array::()?.to_owned(); - let out_slice = output_tensor.as_slice().unwrap(); + let out_slice = output_tensor.as_slice().ok_or_else(|| anyhow!("Failed to extract output tensor data - tensor may not be contiguous"))?; let mut min_val = f32::MAX; let mut max_val = f32::MIN; @@ -1431,6 +1730,11 @@ pub fn run_depth_anything_model( image: &DynamicImage, depth_session: &Mutex, ) -> Result { + let (orig_width, orig_height) = image.dimensions(); + if orig_width == 0 || orig_height == 0 { + anyhow::bail!("Input image has zero dimensions for depth estimation"); + } + let resized_image = image.resize(DEPTH_INPUT_SIZE, DEPTH_INPUT_SIZE, FilterType::Triangle); let (resized_w, resized_h) = resized_image.dimensions(); let resized_rgb = resized_image.into_rgb8(); @@ -1469,7 +1773,7 @@ pub fn run_depth_anything_model( let mut session = depth_session.lock().unwrap(); let outputs = session.run(ort::inputs![t_input])?; let output_tensor = outputs[0].try_extract_array::()?.to_owned(); - let out_slice = output_tensor.as_slice().unwrap(); + let out_slice = output_tensor.as_slice().ok_or_else(|| anyhow!("Failed to extract output tensor data - tensor may not be contiguous"))?; let usize_size = DEPTH_INPUT_SIZE as usize; @@ -1507,7 +1811,9 @@ pub fn run_depth_anything_model( let depth_map = GrayImage::from_raw(resized_w, resized_h, cropped_depth_data) .ok_or_else(|| anyhow::anyhow!("Failed to create mask from Depth output"))?; - Ok(depth_map) + let final_depth = imageops::resize(&depth_map, orig_width, orig_height, FilterType::Triangle); + + Ok(final_depth) } #[derive(Serialize, Deserialize, Debug, Clone, Default)] diff --git a/src-tauri/src/android_integration.rs b/src-tauri/src/android_integration.rs index f67da917e7..ae65a4b8d9 100644 --- a/src-tauri/src/android_integration.rs +++ b/src-tauri/src/android_integration.rs @@ -1,5 +1,5 @@ #[cfg(target_os = "android")] -use jni::objects::{JObject, JString, JValue}; +use jni::objects::{JObject, JString, JValue, JValueGen}; #[cfg(target_os = "android")] use jni::{JNIEnv, JavaVM}; #[cfg(target_os = "android")] @@ -117,10 +117,22 @@ pub fn get_android_cached_lut_path(uri: &str, extension: &str) -> anyhow::Result } let dirs_array: jni::objects::JObjectArray = dirs_array_obj.into(); + let array_length = env + .get_array_length(&dirs_array) + .map_err(|e| anyhow::anyhow!(map_android_jni_error(&mut env, e)))?; + + if array_length == 0 { + return Err(anyhow::anyhow!("No external media directories available on this device")); + } + let dir_file = env .get_object_array_element(&dirs_array, 0) .map_err(|e| anyhow::anyhow!(map_android_jni_error(&mut env, e)))?; + if dir_file.is_null() { + return Err(anyhow::anyhow!("Primary external media directory is null")); + } + let path_jstring = env .call_method(&dir_file, "getAbsolutePath", "()Ljava/lang/String;", &[]) .and_then(|v| v.l()) @@ -342,7 +354,7 @@ pub fn read_android_content_uri(uri_str: &str) -> Result, String> { loop { let read_count = env - .call_method(&input_stream, "read", "([B)I", &[(&java_buffer).into()]) + .call_method(&input_stream, "read", "([B)I", &[JValueGen::from(&*java_buffer as &JObject)]) .and_then(|value| value.i()) .map_err(|e| map_android_jni_error(&mut env, e))?; @@ -351,7 +363,7 @@ pub fn read_android_content_uri(uri_str: &str) -> Result, String> { } if read_count == 0 { - continue; + break; } let read_len = read_count as usize; @@ -506,11 +518,21 @@ pub fn save_bytes_to_android_media_store( } let write_result = (|| -> Result<(), String> { - let byte_array = env - .byte_array_from_slice(bytes) - .map_err(|e| map_android_jni_error(&mut env, e))?; - env.call_method(&output_stream, "write", "([B)V", &[(&byte_array).into()]) - .map_err(|e| map_android_jni_error(&mut env, e))?; + // Write in chunks to avoid OOM on large files (e.g. RAW images) + const CHUNK_SIZE: usize = 1024 * 1024; // 1 MB + let mut offset = 0; + while offset < bytes.len() { + let end = std::cmp::min(offset + CHUNK_SIZE, bytes.len()); + let chunk = &bytes[offset..end]; + let byte_array = env + .byte_array_from_slice(chunk) + .map_err(|e| map_android_jni_error(&mut env, e))?; + env.call_method(&output_stream, "write", "([B)V", &[JValueGen::from(&*byte_array as &JObject)]) + .map_err(|e| map_android_jni_error(&mut env, e))?; + // Explicitly delete local reference to prevent accumulation in large file writes + let _ = env.delete_local_ref(JObject::from(byte_array)); + offset = end; + } env.call_method(&output_stream, "flush", "()V", &[]) .map_err(|e| map_android_jni_error(&mut env, e))?; Ok(()) @@ -599,6 +621,14 @@ pub fn get_android_internal_library_root() -> Result { let dirs_array: jni::objects::JObjectArray = dirs_array_obj.into(); + let array_length = env + .get_array_length(&dirs_array) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + if array_length == 0 { + return Err("No external media directories available on this device".to_string()); + } + let dir_file = env .get_object_array_element(&dirs_array, 0) .map_err(|e| map_android_jni_error(&mut env, e))?; @@ -625,3 +655,186 @@ pub fn get_android_internal_library_root() -> Result { } Ok(library_dir) } + +#[tauri::command] +pub fn save_to_android_gallery(file_path: String, mime_type: String) -> Result<(), String> { + #[cfg(target_os = "android")] + { + let bytes = fs::read(&file_path).map_err(|e| format!("Failed to read file: {}", e))?; + let file_name = PathBuf::from(&file_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("image.jpg") + .to_string(); + save_image_bytes_to_android_gallery(&file_name, &mime_type, &bytes) + } + #[cfg(not(target_os = "android"))] + { + let _ = (file_path, mime_type); + Err("save_to_android_gallery is only available on Android".to_string()) + } +} + +#[tauri::command] +pub fn share_image(file_path: String, mime_type: String, title: String, target_package: Option) -> Result<(), String> { + #[cfg(target_os = "android")] + { + let vm = unsafe { JavaVM::from_raw(android_context().vm().cast()) } + .map_err(|e| format!("Failed to access Android JVM: {}", e))?; + let mut env = vm + .attach_current_thread() + .map_err(|e| format!("Failed to attach current thread: {}", e))?; + + let context = env + .new_local_ref(unsafe { JObject::from_raw(android_context().context().cast()) }) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + // Create Intent with ACTION_SEND + let intent_class = env + .find_class("android/content/Intent") + .map_err(|e| map_android_jni_error(&mut env, e))?; + let action_send = env + .new_string("android.intent.action.SEND") + .map_err(|e| map_android_jni_error(&mut env, e))?; + let intent = env + .new_object( + &intent_class, + "(Ljava/lang/String;)V", + &[(&action_send).into()], + ) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + // Set type + let mime_jstring = env + .new_string(&mime_type) + .map_err(|e| map_android_jni_error(&mut env, e))?; + env.call_method( + &intent, + "setType", + "(Ljava/lang/String;)Landroid/content/Intent;", + &[(&mime_jstring).into()], + ) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + // Parse file URI and set as EXTRA_STREAM + let file_obj = env + .new_string(&file_path) + .map_err(|e| map_android_jni_error(&mut env, e))?; + let file_class = env + .find_class("java/io/File") + .map_err(|e| map_android_jni_error(&mut env, e))?; + let file_instance = env + .new_object(file_class, "(Ljava/lang/String;)V", &[(&file_obj).into()]) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + // Get the application ID dynamically from the context to match Manifest's ${applicationId}.fileprovider + let package_name = env + .call_method(&context, "getPackageName", "()Ljava/lang/String;", &[]) + .and_then(|v| v.l()) + .map_err(|e| map_android_jni_error(&mut env, e))?; + let package_name_str: String = env + .get_string(&JString::from(package_name)) + .map_err(|e| map_android_jni_error(&mut env, e))? + .into(); + let authority_str = format!("{}.fileprovider", package_name_str); + let authority = env + .new_string(&authority_str) + .map_err(|e| map_android_jni_error(&mut env, e))?; + let file_provider_class = env + .find_class("androidx/core/content/FileProvider") + .map_err(|e| { + clear_pending_android_exception(&mut env); + map_android_jni_error(&mut env, e) + })?; + + let uri = env + .call_static_method( + file_provider_class, + "getUriForFile", + "(Landroid/content/Context;Ljava/lang/String;Ljava/io/File;)Landroid/net/Uri;", + &[ + (&context).into(), + (&authority).into(), + (&file_instance).into(), + ], + ) + .and_then(|v| v.l()) + .map_err(|e| { + clear_pending_android_exception(&mut env); + map_android_jni_error(&mut env, e) + })?; + + if uri.is_null() { + clear_pending_android_exception(&mut env); + return Err("FileProvider.getUriForFile returned null URI. Check file_paths.xml configuration.".to_string()); + } + + let stream_key = env + .new_string("android.intent.extra.STREAM") + .map_err(|e| map_android_jni_error(&mut env, e))?; + env.call_method( + &intent, + "putExtra", + "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;", + &[(&stream_key).into(), (&uri).into()], + ) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + // Add FLAG_GRANT_READ_URI_PERMISSION + let flag_value: i32 = 1; // FLAG_GRANT_READ_URI_PERMISSION + env.call_method( + &intent, + "addFlags", + "(I)Landroid/content/Intent;", + &[JValue::from(flag_value)], + ) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + // If a specific target package is provided, set it on the intent + if let Some(pkg) = target_package { + let pkg_jstring = env + .new_string(&pkg) + .map_err(|e| map_android_jni_error(&mut env, e))?; + let set_pkg_result = env.call_method( + &intent, + "setPackage", + "(Ljava/lang/String;)Landroid/content/Intent;", + &[(&pkg_jstring).into()], + ); + if set_pkg_result.is_err() { + clear_pending_android_exception(&mut env); + log::warn!("Failed to set package '{}' on share intent, falling back to chooser", pkg); + } + } + + // Create chooser intent + let title_jstring = env + .new_string(&title) + .map_err(|e| map_android_jni_error(&mut env, e))?; + let chooser = env + .call_static_method( + &intent_class, + "createChooser", + "(Landroid/content/Intent;Ljava/lang/CharSequence;)Landroid/content/Intent;", + &[(&intent).into(), (&title_jstring).into()], + ) + .and_then(|v| v.l()) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + // Start activity + env.call_method( + &context, + "startActivity", + "(Landroid/content/Intent;)V", + &[(&chooser).into()], + ) + .map_err(|e| map_android_jni_error(&mut env, e))?; + + Ok(()) + } + #[cfg(not(target_os = "android"))] + { + let _ = (file_path, mime_type, title, target_package); + Err("share_image is only available on Android".to_string()) + } +} diff --git a/src-tauri/src/app_settings.rs b/src-tauri/src/app_settings.rs index cf7dedf022..a89c309228 100644 --- a/src-tauri/src/app_settings.rs +++ b/src-tauri/src/app_settings.rs @@ -444,10 +444,13 @@ impl Default for AppSettings { pinned_folders: Vec::new(), thumbnail_resolution: Some(720), #[cfg(target_os = "android")] - editor_preview_resolution: Some(1280), + editor_preview_resolution: Some(1440), #[cfg(not(target_os = "android"))] editor_preview_resolution: Some(1920), enable_zoom_hifi: Some(true), + #[cfg(target_os = "macos")] + use_full_dpi_rendering: Some(true), + #[cfg(not(target_os = "macos"))] use_full_dpi_rendering: Some(false), enable_live_previews: Some(true), live_preview_quality: Some("high".to_string()), @@ -465,11 +468,11 @@ impl Default for AppSettings { custom_ai_tags: Some(Vec::new()), ai_tag_count: Some(10), #[cfg(target_os = "android")] - thumbnail_size: Some("small".to_string()), + thumbnail_size: Some("medium".to_string()), #[cfg(not(target_os = "android"))] thumbnail_size: Some("medium".to_string()), thumbnail_aspect_ratio: Some("cover".to_string()), - ai_provider: Some("cpu".to_string()), + ai_provider: Some("auto".to_string()), adjustment_visibility: default_adjustment_visibility(), open_tree_sections: default_open_tree_sections(), copy_paste_settings: CopyPasteSettings::default(), @@ -483,7 +486,7 @@ impl Default for AppSettings { export_presets: default_export_presets(), my_lenses: Some(Vec::new()), #[cfg(target_os = "android")] - high_res_zoom_multiplier: Some(0.75), + high_res_zoom_multiplier: Some(1.0), #[cfg(not(target_os = "android"))] high_res_zoom_multiplier: Some(1.0), enable_folder_image_counts: Some(false), @@ -502,11 +505,11 @@ impl Default for AppSettings { zoom_speed_multiplier: Some(1.0), keybinds: HashMap::new(), #[cfg(target_os = "android")] - thumbnail_worker_threads: Some(2), + thumbnail_worker_threads: Some(3), #[cfg(not(target_os = "android"))] thumbnail_worker_threads: Some(4), #[cfg(target_os = "android")] - image_cache_size: Some(2), + image_cache_size: Some(4), #[cfg(not(target_os = "android"))] image_cache_size: Some(5), tonemapper_override_enabled: Some(false), @@ -518,7 +521,7 @@ impl Default for AppSettings { raw_preprocessing_sharpening: Some(0.35), apply_preprocessing_to_non_raws: Some(false), exif_overlay: Some("off".to_string()), - language: Some("en".to_string()), + language: Some("zh-CN".to_string()), folder_tree_sort: Some(FolderTreeSort::default()), } } diff --git a/src-tauri/src/app_state.rs b/src-tauri/src/app_state.rs index 83ebfddf0d..a50158634f 100644 --- a/src-tauri/src/app_state.rs +++ b/src-tauri/src/app_state.rs @@ -149,8 +149,10 @@ pub struct AppState { pub ai_init_lock: TokioMutex<()>, pub export_task_handle: Mutex>>, pub hdr_result: Arc>>, + pub hdr_merge_lock: TokioMutex<()>, pub panorama_result: Arc>>, pub denoise_result: Arc>>, + pub denoise_lock: TokioMutex<()>, pub indexing_task_handle: Mutex>>, pub lut_cache: Mutex>>, pub initial_file_path: Mutex>, diff --git a/src-tauri/src/culling.rs b/src-tauri/src/culling.rs index 1a6d5dd90b..2110fa2ed0 100644 --- a/src-tauri/src/culling.rs +++ b/src-tauri/src/culling.rs @@ -127,7 +127,10 @@ fn analyze_image( hasher: &image_hasher::Hasher, settings: &crate::app_settings::AppSettings, ) -> Result { - const ANALYSIS_DIM: u32 = 720; // FIXME: How should we calculate good focus if it's downscaled?!? + const ANALYSIS_DIM: u32 = 720; + // For focus assessment, we sample a center crop from the full-resolution + // image to avoid losing fine detail that the downscale would remove. + const FOCUS_CROP_DIM: u32 = 1500; if crate::file_management::is_cloud_placeholder(Path::new(path)) { return Err(format!("'{}' is stored in iCloud and not downloaded", path)); @@ -142,19 +145,31 @@ fn analyze_image( let thumbnail = img.thumbnail(ANALYSIS_DIM, ANALYSIS_DIM); let gray_thumbnail = thumbnail.to_luma8(); - let sharpness_metric = calculate_laplacian_variance(&gray_thumbnail); let exposure_metric = calculate_exposure_metric(&gray_thumbnail); - let (thumb_w, thumb_h) = gray_thumbnail.dimensions(); - let center_crop = imageops::crop_imm( - &gray_thumbnail, - thumb_w / 4, - thumb_h / 4, - thumb_w / 2, - thumb_h / 2, - ) - .to_image(); - let center_focus_metric = calculate_laplacian_variance(¢er_crop); + // Compute focus metrics from a higher-resolution center crop of the + // original image, which preserves the fine detail needed for an + // accurate Laplacian variance measurement. + let (sharpness_metric, center_focus_metric) = if width >= 3 && height >= 3 { + let crop_x = (width.saturating_sub(FOCUS_CROP_DIM) / 2).max(0); + let crop_y = (height.saturating_sub(FOCUS_CROP_DIM) / 2).max(0); + let crop_w = FOCUS_CROP_DIM.min(width); + let crop_h = FOCUS_CROP_DIM.min(height); + + let full_gray = img.to_luma8(); + let center_crop = imageops::crop_imm(&full_gray, crop_x, crop_y, crop_w, crop_h).to_image(); + + let sharpness = calculate_laplacian_variance(¢er_crop); + + let (cw, ch) = center_crop.dimensions(); + let inner_crop = + imageops::crop_imm(¢er_crop, cw / 4, ch / 4, cw / 2, ch / 2).to_image(); + let center_focus = calculate_laplacian_variance(&inner_crop); + + (sharpness, center_focus) + } else { + (0.0, 0.0) + }; let normalized_sharpness = ((sharpness_metric + 1.0).log10() / 3.5).min(1.0); let normalized_center_focus = ((center_focus_metric + 1.0).log10() / 3.5).min(1.0); @@ -280,7 +295,7 @@ pub async fn cull_images( .result .quality_score .partial_cmp(&successful_analyses[a].result.quality_score) - .unwrap_or(std::cmp::Ordering::Equal) + .unwrap_or_else(|| if successful_analyses[b].result.quality_score.is_nan() { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less }) }); let representative_idx = current_group_indices[0]; diff --git a/src-tauri/src/denoising.rs b/src-tauri/src/denoising.rs index fb6bdc849e..c29935338d 100644 --- a/src-tauri/src/denoising.rs +++ b/src-tauri/src/denoising.rs @@ -56,6 +56,8 @@ pub async fn apply_denoising( app_handle: tauri::AppHandle, state: tauri::State<'_, AppState>, ) -> Result<(), String> { + let _denoise_guard = state.denoise_lock.lock().await; + let (source_path, _) = parse_virtual_path(&path); let path_str = source_path.to_string_lossy().to_string(); @@ -73,18 +75,20 @@ pub async fn apply_denoising( let denoise_result_handle = state.denoise_result.clone(); - tokio::task::spawn_blocking(move || { - match denoise_image(path_str, intensity, method, app_handle.clone(), ai_session) { - Ok((image, _)) => { + let result = tokio::task::spawn_blocking(move || { + denoise_image(path_str, intensity, method, app_handle.clone(), ai_session) + .map(|(image, _)| { *denoise_result_handle.lock().unwrap() = Some(image); - } - Err(e) => { - let _ = app_handle.emit("denoise-error", e); - } - } + }) + .map_err(|e| { + let _ = app_handle.emit("denoise-error", e.clone()); + e + }) }) .await - .map_err(|e| format!("Denoising task failed: {}", e)) + .map_err(|e| format!("Denoising task failed: {}", e))?; + + result } #[tauri::command] @@ -95,6 +99,8 @@ pub async fn batch_denoise_images( app_handle: tauri::AppHandle, state: tauri::State<'_, AppState>, ) -> Result, String> { + let _denoise_guard = state.denoise_lock.lock().await; + let mut ai_session = None; if method == "ai" { let session = crate::ai_processing::get_or_init_denoise_model( @@ -688,7 +694,7 @@ fn block_matching_joint( } let valid_slice = &mut candidates[0..cand_count]; - valid_slice.sort_unstable_by(|a, b| a.dist.partial_cmp(&b.dist).unwrap_or(Ordering::Equal)); + valid_slice.sort_unstable_by(|a, b| a.dist.partial_cmp(&b.dist).unwrap_or_else(|| if a.dist.is_nan() { Ordering::Greater } else { Ordering::Less })); let limit = MAX_GROUP_SIZE.min(cand_count); let p2_limit = prev_power_of_two(limit); diff --git a/src-tauri/src/exif_processing.rs b/src-tauri/src/exif_processing.rs index 50a6a2ad81..134c74730c 100644 --- a/src-tauri/src/exif_processing.rs +++ b/src-tauri/src/exif_processing.rs @@ -673,8 +673,7 @@ pub fn write_image_with_metadata( keep_metadata: bool, strip_gps: bool, ) -> Result<(), String> { - // FIXME: temporary solution until I find a way to write metadata to TIFF - if !keep_metadata || output_format.to_lowercase() == "tiff" { + if !keep_metadata { return Ok(()); } @@ -683,16 +682,6 @@ pub fn write_image_with_metadata( return Ok(()); } - // Skip TIFF sources to avoid potential tag corruption issues - let original_ext = original_path - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_lowercase(); - if original_ext == "tiff" || original_ext == "tif" { - return Ok(()); - } - let file_type = match output_format.to_lowercase().as_str() { "jpg" | "jpeg" => FileExtension::JPEG, "png" => FileExtension::PNG { diff --git a/src-tauri/src/export_processing.rs b/src-tauri/src/export_processing.rs index ddbf33ff8d..bec7db8c3e 100644 --- a/src-tauri/src/export_processing.rs +++ b/src-tauri/src/export_processing.rs @@ -34,6 +34,7 @@ use crate::lut_processing::{ use crate::mask_generation::{MaskDefinition, generate_mask_bitmap}; use crate::cache_utils::{calculate_full_job_hash, calculate_transform_hash}; +use crate::portrait_processing::{apply_portrait_adjustments, detect_face_regions}; use crate::{ apply_all_transformations, generate_transformed_preview, get_cached_or_generate_mask, hydrate_adjustments, load_settings, resolve_warped_image_for_masks, @@ -274,6 +275,8 @@ fn apply_export_resize_and_watermark( } #[allow(clippy::too_many_arguments)] +#[allow(clippy::if_same_then_else)] +#[allow(clippy::collapsible_if)] fn process_image_for_export_pipeline( path: &str, base_image: &DynamicImage, @@ -317,7 +320,7 @@ fn process_image_for_export_pipeline( let unique_hash = calculate_full_job_hash(path, js_adjustments); - process_and_get_dynamic_image( + let mut result = process_and_get_dynamic_image( context, state, transformed_image.as_ref(), @@ -329,7 +332,19 @@ fn process_image_for_export_pipeline( roi: None, }, debug_tag, - ) + )?; + + // Apply portrait adjustments if present + if let Some(portrait_json) = js_adjustments.get("portrait") { + if !portrait_json.is_null() { + let face_regions = detect_face_regions(&result); + if let Err(e) = apply_portrait_adjustments(&mut result, portrait_json, &face_regions) { + log::warn!("Portrait processing failed during export: {}", e); + } + } + } + + Ok(result) } fn set_timestamps_from_exif(src: &Path, dst: &Path) { @@ -658,7 +673,31 @@ fn export_adjustments_as_lut( let identity_image = generate_identity_lut_image(lut_size); let tm_override = resolve_tonemapper_override_from_handle(app_handle, false); - let mut all_adjustments = get_all_adjustments_from_json(js_adjustments, false, tm_override); + + // Strip geometric transforms from the JSON so the LUT is only color-based. + // Geometric ops (crop, rotate, lens correction) would alter the identity + // image dimensions and cause get_pixel out-of-bounds in convert_image_to_cube_lut. + let mut clean_json = js_adjustments.clone(); + if let Some(obj) = clean_json.as_object_mut() { + for key in [ + "crop", + "rotation", + "orientationSteps", + "straighten", + "transformDistortion", + "transformVertical", + "transformHorizontal", + "transformRotate", + "transformAspect", + "transformScale", + "transformXOffset", + "transformYOffset", + ] { + obj.remove(key); + } + } + + let mut all_adjustments = get_all_adjustments_from_json(&clean_json, false, tm_override); all_adjustments.global.show_clipping = 0; all_adjustments.global.vignette_amount = 0.0; @@ -678,7 +717,7 @@ fn export_adjustments_as_lut( let lut_path = js_adjustments["lutPath"].as_str(); let lut = lut_path.and_then(|p| get_or_load_lut(state, p).ok()); - let unique_hash = calculate_full_job_hash(source_path_str, js_adjustments); + let unique_hash = calculate_full_job_hash(source_path_str, &clean_json); let processed_lut = process_and_get_dynamic_image( context, @@ -694,6 +733,18 @@ fn export_adjustments_as_lut( "export_lut", )?; + // Defensive check: ensure output dimensions match the expected LUT grid. + let expected_h = lut_size * lut_size; + if processed_lut.width() != lut_size || processed_lut.height() != expected_h { + return Err(format!( + "LUT export produced unexpected dimensions: {}x{} (expected {}x{}). A geometric transform may still have been applied.", + processed_lut.width(), + processed_lut.height(), + lut_size, + expected_h + )); + } + convert_image_to_cube_lut(&processed_lut, lut_size) } diff --git a/src-tauri/src/face_landmark.rs b/src-tauri/src/face_landmark.rs new file mode 100644 index 0000000000..c031e88d7a --- /dev/null +++ b/src-tauri/src/face_landmark.rs @@ -0,0 +1,492 @@ +#![allow(clippy::collapsible_if)] +#![allow(clippy::unnecessary_cast)] + +use std::path::Path; + +use image::{DynamicImage, GenericImageView, Rgb, RgbImage}; +use ndarray::{Array, Array4, IxDyn}; +use ort::session::Session; +use ort::value::Tensor; + +pub struct FaceDetection { + pub bbox: (f32, f32, f32, f32), // x1, y1, x2, y2 + pub confidence: f32, + pub kps5: [(f32, f32); 5], // 左眼, 右眼, 鼻尖, 左嘴角, 右嘴角 +} + +pub struct FaceLandmarks106 { + pub bbox: (f32, f32, f32, f32), + pub points: [(f32, f32); 106], + pub confidence: f32, +} + +// 106点索引定义 (InsightFace 2d106det 标准) +// 0-32: 轮廓 (33点) +// 33-42: 左眉 (10点) +// 43-52: 右眉 (10点) +// 53-72: 左眼 (20点) +// 73-92: 右眼 (20点) +// 93-96: 鼻梁 (4点) +// 97-106: 鼻尖 (10点) +// 107-116: 左鼻孔 (10点) +// 117-126: 右鼻孔 (10点) +// 127-134: 上嘴唇外轮廓 (8点) +// 135-142: 下嘴唇外轮廓 (8点) +// 143-150: 上嘴唇内轮廓 (8点) +// 151-158: 下嘴唇内轮廓 (8点) +// +// 注:以上语义分组基于 InsightFace 2d106det 的公开参考定义。 + +pub struct FaceLandmarkDetector { + scrfd_session: Session, + landmark_session: Session, +} + +impl FaceLandmarkDetector { + pub fn new(scrfd_path: &Path, landmark_path: &Path) -> Result { + let scrfd_session = Session::builder() + .map_err(|e| e.to_string())? + .commit_from_file(scrfd_path) + .map_err(|e| e.to_string())?; + let landmark_session = Session::builder() + .map_err(|e| e.to_string())? + .commit_from_file(landmark_path) + .map_err(|e| e.to_string())?; + Ok(Self { + scrfd_session, + landmark_session, + }) + } + + pub fn detect_faces(&mut self, img: &DynamicImage) -> Result, String> { + let (orig_w, orig_h) = img.dimensions(); + let input_size = 640u32; + + // Letterbox resize: keep aspect ratio, pad with black + let scale = (input_size as f32) / (orig_w.max(orig_h) as f32); + let new_w = (orig_w as f32 * scale).round() as u32; + let new_h = (orig_h as f32 * scale).round() as u32; + let dx = ((input_size - new_w) / 2) as f32; + let dy = ((input_size - new_h) / 2) as f32; + + let resized = img.resize(new_w, new_h, image::imageops::FilterType::Triangle); + let mut input_tensor = + Array4::::zeros((1, 3, input_size as usize, input_size as usize)); + + for y in 0..new_h { + for x in 0..new_w { + let p = resized.get_pixel(x, y); + let dest_x = (x as f32 + dx) as usize; + let dest_y = (y as f32 + dy) as usize; + if dest_x < input_size as usize && dest_y < input_size as usize { + input_tensor[[0, 0, dest_y, dest_x]] = p[0] as f32 / 255.0; + input_tensor[[0, 1, dest_y, dest_x]] = p[1] as f32 / 255.0; + input_tensor[[0, 2, dest_y, dest_x]] = p[2] as f32 / 255.0; + } + } + } + + let t_input = Tensor::from_array( + input_tensor + .into_shape_with_order((1, 3, input_size as usize, input_size as usize)) + .map_err(|e| format!("Tensor reshape failed: {}", e))? + .into_dyn() + .as_standard_layout() + .into_owned(), + ) + .map_err(|e| e.to_string())?; + + let outputs = self + .scrfd_session + .run(ort::inputs![t_input]) + .map_err(|e| e.to_string())?; + + // Use outputs.len() instead of self.scrfd_session.outputs.len() + // to avoid borrow conflict with the mutable borrow from run() + let output_count = outputs.len(); + let mut scores: Vec> = Vec::new(); + let mut bboxes: Vec> = Vec::new(); + let mut kpss: Vec> = Vec::new(); + + for i in 0..output_count { + let arr = outputs[i] + .try_extract_array::() + .map_err(|e| e.to_string())? + .to_owned(); + let shape = arr.shape(); + if shape.len() != 3 || shape[0] != 1 { + continue; + } + match shape[2] { + 1 => scores.push(arr), + 4 => bboxes.push(arr), + 10 => kpss.push(arr), + _ => {} + } + } + + #[derive(Default)] + struct HeadArrays { + score: Option>, + bbox: Option>, + kps: Option>, + } + + let mut by_n: std::collections::HashMap = + std::collections::HashMap::new(); + + for arr in scores { + let n = arr.shape()[1]; + by_n.entry(n).or_default().score = Some(arr); + } + for arr in bboxes { + let n = arr.shape()[1]; + by_n.entry(n).or_default().bbox = Some(arr); + } + for arr in kpss { + let n = arr.shape()[1]; + by_n.entry(n).or_default().kps = Some(arr); + } + + let mut candidates: Vec<(f32, [f32; 4], [f32; 10])> = Vec::new(); + + for (n, head) in by_n { + let score_arr = match head.score { + Some(a) => a, + None => continue, + }; + let bbox_arr = match head.bbox { + Some(a) => a, + None => continue, + }; + let kps_arr = match head.kps { + Some(a) => a, + None => continue, + }; + + let stride = ((input_size as f32) / ((n as f32).sqrt())).round() as u32; + if stride == 0 { + continue; + } + let grid_w = input_size / stride; + let grid_h = input_size / stride; + + for i in 0..n { + let score_val = score_arr[[0, i, 0]]; + if score_val < 0.5 { + continue; + } + + let grid_x = (i as u32) % grid_w; + let grid_y = (i as u32) / grid_w; + let cx = (grid_x as f32 + 0.5) * stride as f32; + let cy = (grid_y as f32 + 0.5) * stride as f32; + + let dx = bbox_arr[[0, i, 0]]; + let dy = bbox_arr[[0, i, 1]]; + let dw = bbox_arr[[0, i, 2]]; + let dh = bbox_arr[[0, i, 3]]; + let bbox_cx = cx + dx * stride as f32; + let bbox_cy = cy + dy * stride as f32; + let bw = dw.exp() * stride as f32; + let bh = dh.exp() * stride as f32; + let x1 = bbox_cx - bw * 0.5; + let y1 = bbox_cy - bh * 0.5; + let x2 = bbox_cx + bw * 0.5; + let y2 = bbox_cy + bh * 0.5; + + let mut kps5 = [0.0f32; 10]; + for j in 0..5 { + let kx = cx + kps_arr[[0, i, j * 2]] * stride as f32; + let ky = cy + kps_arr[[0, i, j * 2 + 1]] * stride as f32; + kps5[j * 2] = kx; + kps5[j * 2 + 1] = ky; + } + + candidates.push((score_val, [x1, y1, x2, y2], kps5)); + } + } + + // NMS + candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); + let mut suppressed = vec![false; candidates.len()]; + let mut result = Vec::new(); + + for i in 0..candidates.len() { + if suppressed[i] { + continue; + } + let (conf, bbox, kps) = candidates[i]; + let mut kps5 = [(0.0f32, 0.0f32); 5]; + for j in 0..5 { + let x = (kps[j * 2] - dx) / scale; + let y = (kps[j * 2 + 1] - dy) / scale; + kps5[j] = (x, y); + } + result.push(FaceDetection { + bbox: ( + (bbox[0] - dx) / scale, + (bbox[1] - dy) / scale, + (bbox[2] - dx) / scale, + (bbox[3] - dy) / scale, + ), + confidence: conf, + kps5, + }); + + for j in (i + 1)..candidates.len() { + if suppressed[j] { + continue; + } + let (_, bbox_j, _) = candidates[j]; + if iou(bbox, bbox_j) > 0.4 { + suppressed[j] = true; + } + } + } + + Ok(result) + } + + pub fn detect_landmarks_106( + &mut self, + img: &DynamicImage, + face: &FaceDetection, + ) -> Result { + let (orig_w, orig_h) = img.dimensions(); + let input_size_f = 192.0f32; + + // Standard 5-point template for 112x112 ArcFace alignment, scaled to 192x192 + let scale_factor = input_size_f / 112.0; + let template_112: [(f32, f32); 5] = [ + (38.2946, 51.6963), + (73.5318, 51.5014), + (56.0252, 71.7366), + (41.5493, 92.3655), + (70.7299, 92.2041), + ]; + let dst_pts: Vec<(f32, f32)> = template_112 + .iter() + .map(|(x, y)| (x * scale_factor, y * scale_factor)) + .collect(); + let src_pts: Vec<(f32, f32)> = face.kps5.to_vec(); + + // Compute affine transform from dst (192x192) to src (original image) + let mat = estimate_affine_transform(&dst_pts, &src_pts)?; + + // Warp source image to 192x192 using bilinear interpolation + let rgb = img.to_rgb8(); + let mut warped = RgbImage::new(192, 192); + for y in 0..192u32 { + for x in 0..192u32 { + let src_x = mat[0][0] * x as f32 + mat[0][1] * y as f32 + mat[0][2]; + let src_y = mat[1][0] * x as f32 + mat[1][1] * y as f32 + mat[1][2]; + let px = sample_bilinear_rgb(&rgb, orig_w, orig_h, src_x, src_y); + warped.put_pixel(x, y, px); + } + } + + // Normalize to -1 ~ 1 + let mut input_tensor = Array4::::zeros((1, 3, 192, 192)); + for y in 0..192u32 { + for x in 0..192u32 { + let p = warped.get_pixel(x, y); + let r = p[0] as f32 / 127.5 - 1.0; + let g = p[1] as f32 / 127.5 - 1.0; + let b = p[2] as f32 / 127.5 - 1.0; + input_tensor[[0, 0, y as usize, x as usize]] = r; + input_tensor[[0, 1, y as usize, x as usize]] = g; + input_tensor[[0, 2, y as usize, x as usize]] = b; + } + } + + let t_input = Tensor::from_array( + input_tensor + .into_shape_with_order((1, 3, 192, 192)) + .map_err(|e| format!("Landmark tensor reshape failed: {}", e))? + .into_dyn() + .as_standard_layout() + .into_owned(), + ) + .map_err(|e| e.to_string())?; + + let outputs = self + .landmark_session + .run(ort::inputs![t_input]) + .map_err(|e| e.to_string())?; + + let arr = outputs[0] + .try_extract_array::() + .map_err(|e| e.to_string())? + .to_owned(); + let shape = arr.shape().to_vec(); + + let mut points = [(0.0f32, 0.0f32); 106]; + + let extract_and_map = |slice: &[f32]| { + let mut pts = [(0.0f32, 0.0f32); 106]; + for i in 0..106 { + let x = slice[i * 2]; + let y = slice[i * 2 + 1]; + // x, y are normalized to 0-1 within the 192x192 cropped face + let dst_x = x * input_size_f; + let dst_y = y * input_size_f; + let orig_x = mat[0][0] * dst_x + mat[0][1] * dst_y + mat[0][2]; + let orig_y = mat[1][0] * dst_x + mat[1][1] * dst_y + mat[1][2]; + pts[i] = (orig_x, orig_y); + } + pts + }; + + if shape.len() == 2 && shape[1] == 212 { + let slice = arr.as_slice().ok_or("Failed to get array slice")?; + points = extract_and_map(slice); + } else if shape.len() == 3 && shape[1] == 106 && shape[2] == 2 { + let slice = arr.as_slice().ok_or("Failed to get array slice")?; + points = extract_and_map(slice); + } else { + return Err(format!("Unexpected landmark output shape: {:?}", shape)); + } + + Ok(FaceLandmarks106 { + bbox: face.bbox, + points, + confidence: face.confidence, + }) + } + + pub fn detect_all(&mut self, img: &DynamicImage) -> Result, String> { + let faces = self.detect_faces(img)?; + let mut results = Vec::new(); + for face in &faces { + results.push(self.detect_landmarks_106(img, face)?); + } + Ok(results) + } +} + +fn iou(a: [f32; 4], b: [f32; 4]) -> f32 { + let x1 = a[0].max(b[0]); + let y1 = a[1].max(b[1]); + let x2 = a[2].min(b[2]); + let y2 = a[3].min(b[3]); + let inter = (x2 - x1).max(0.0) * (y2 - y1).max(0.0); + let area_a = (a[2] - a[0]) * (a[3] - a[1]); + let area_b = (b[2] - b[0]) * (b[3] - b[1]); + let union = area_a + area_b - inter; + if union <= 0.0 { 0.0 } else { inter / union } +} + +fn sample_bilinear_rgb(img: &RgbImage, w: u32, h: u32, x: f32, y: f32) -> Rgb { + let x0 = x.floor().clamp(0.0, (w.saturating_sub(1)) as f32) as u32; + let y0 = y.floor().clamp(0.0, (h.saturating_sub(1)) as f32) as u32; + let x1 = (x0 + 1).min(w.saturating_sub(1)); + let y1 = (y0 + 1).min(h.saturating_sub(1)); + let fx = (x - x0 as f32).clamp(0.0, 1.0); + let fy = (y - y0 as f32).clamp(0.0, 1.0); + + let p00 = img.get_pixel(x0, y0); + let p10 = img.get_pixel(x1, y0); + let p01 = img.get_pixel(x0, y1); + let p11 = img.get_pixel(x1, y1); + + let mut c = [0u8; 3]; + for i in 0..3 { + let v00 = p00[i] as f32; + let v10 = p10[i] as f32; + let v01 = p01[i] as f32; + let v11 = p11[i] as f32; + let v = (v00 * (1.0 - fx) + v10 * fx) * (1.0 - fy) + (v01 * (1.0 - fx) + v11 * fx) * fy; + c[i] = v.round().clamp(0.0, 255.0) as u8; + } + Rgb(c) +} + +fn estimate_affine_transform( + src: &[(f32, f32)], + dst: &[(f32, f32)], +) -> Result<[[f32; 3]; 2], String> { + let n = src.len(); + if n < 3 || n != dst.len() { + return Err("Need at least 3 matching point pairs".to_string()); + } + + let mut a_data = Vec::with_capacity(2 * n * 6); + let mut b_data = Vec::with_capacity(2 * n); + + for i in 0..n { + let (sx, sy) = src[i]; + let (dx, dy) = dst[i]; + a_data.extend_from_slice(&[sx, sy, 1.0, 0.0, 0.0, 0.0]); + b_data.push(dx); + a_data.extend_from_slice(&[0.0, 0.0, 0.0, sx, sy, 1.0]); + b_data.push(dy); + } + + let a = nalgebra::DMatrix::from_row_slice(2 * n, 6, &a_data); + let b = nalgebra::DVector::from_row_slice(&b_data); + + let svd = nalgebra::SVD::new(a, true, true); + let x = svd + .solve(&b, 1e-6) + .map_err(|_| "Failed to solve affine transform via SVD".to_string())?; + + Ok([[x[0], x[1], x[2]], [x[3], x[4], x[5]]]) +} + +#[cfg(test)] +mod tests { + use super::*; + use image::{Rgb, RgbImage}; + + #[test] + fn test_iou_identical() { + let a = [0.0, 0.0, 10.0, 10.0]; + let b = [0.0, 0.0, 10.0, 10.0]; + assert!((iou(a, b) - 1.0).abs() < 1e-5); + } + + #[test] + fn test_iou_no_overlap() { + let a = [0.0, 0.0, 10.0, 10.0]; + let b = [20.0, 20.0, 30.0, 30.0]; + assert_eq!(iou(a, b), 0.0); + } + + #[test] + fn test_iou_partial_overlap() { + let a = [0.0, 0.0, 10.0, 10.0]; + let b = [5.0, 5.0, 15.0, 15.0]; + let inter = 5.0 * 5.0; + let union = 100.0 + 100.0 - inter; + let expected = inter / union; + assert!((iou(a, b) - expected).abs() < 1e-5); + } + + #[test] + fn test_estimate_affine_transform_minimal() { + let src = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]; + let dst = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]; + let mat = estimate_affine_transform(&src, &dst).unwrap(); + assert!((mat[0][0] - 1.0).abs() < 1e-3); + assert!((mat[1][1] - 1.0).abs() < 1e-3); + assert!((mat[0][2]).abs() < 1e-3); + assert!((mat[1][2]).abs() < 1e-3); + } + + #[test] + fn test_estimate_affine_transform_insufficient_points() { + let src = vec![(0.0, 0.0), (1.0, 0.0)]; + let dst = vec![(0.0, 0.0), (1.0, 0.0)]; + assert!(estimate_affine_transform(&src, &dst).is_err()); + } + + #[test] + fn test_sample_bilinear_rgb_clamps() { + let img = RgbImage::from_pixel(2, 2, Rgb([128, 64, 32])); + let px = sample_bilinear_rgb(&img, 2, 2, -1.0, -1.0); + assert_eq!(px[0], 128); + let px2 = sample_bilinear_rgb(&img, 2, 2, 5.0, 5.0); + assert_eq!(px2[0], 128); + } +} diff --git a/src-tauri/src/file_management.rs b/src-tauri/src/file_management.rs index 56fc585659..6ab795c01e 100644 --- a/src-tauri/src/file_management.rs +++ b/src-tauri/src/file_management.rs @@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt; use std::fs; use std::hash::{Hash, Hasher}; -use std::io::Cursor; +use std::io::{Cursor, Write}; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Arc; @@ -43,7 +43,23 @@ use crate::mask_generation::MaskDefinition; use crate::preset_converter; use crate::tagging::COLOR_TAG_PREFIX; +fn is_flatpak_env() -> bool { + std::env::var("FLATPAK_ID").is_ok() || std::path::Path::new("/.flatpak-info").exists() +} + fn resolve_thumbnail_cache_dir(app_handle: &AppHandle) -> std::result::Result { + if is_flatpak_env() { + let local_data = app_handle + .path() + .app_local_data_dir() + .map_err(|e| e.to_string())?; + let thumb_cache_dir = local_data.join("thumbnails"); + if !thumb_cache_dir.exists() { + fs::create_dir_all(&thumb_cache_dir).map_err(|e| e.to_string())?; + } + return Ok(thumb_cache_dir); + } + let cache_dir = app_handle .path() .app_cache_dir() @@ -55,6 +71,15 @@ fn resolve_thumbnail_cache_dir(app_handle: &AppHandle) -> std::result::Result std::io::Result<()> { + let dir = path.parent().unwrap_or_else(|| Path::new(".")); + let mut temp_file = tempfile::NamedTempFile::new_in(dir)?; + temp_file.write_all(content.as_bytes())?; + temp_file.flush()?; + temp_file.persist(path)?; + Ok(()) +} + fn emit_thumbnail_cache_setup_error(app_handle: &AppHandle, path: &str, reason: &str) { let _ = app_handle.emit( "thumbnail-generation-error", @@ -1359,7 +1384,11 @@ pub fn generate_thumbnail_data( let mut cache = state.thumbnail_geometry_cache.lock().unwrap(); if cache.len() > 30 { - cache.clear(); + // Remove oldest half of entries instead of clearing all + let keys: Vec<_> = cache.keys().take(cache.len() / 2).cloned().collect(); + for k in keys { + cache.remove(&k); + } } cache.insert( path_str.to_string(), @@ -2228,7 +2257,7 @@ pub fn save_metadata_and_update_thumbnail( metadata.adjustments = final_adjustments; let json_string = serde_json::to_string_pretty(&metadata).map_err(|e| e.to_string())?; - std::fs::write(&sidecar_path, json_string).map_err(|e| e.to_string())?; + atomic_write_file(&sidecar_path, &json_string).map_err(|e| e.to_string())?; if let Ok(settings) = load_settings(app_handle.clone()) && settings.enable_xmp_sync.unwrap_or(false) @@ -2347,7 +2376,7 @@ pub async fn apply_adjustments_to_paths( existing_metadata.adjustments = new_adjustments; if let Ok(json_string) = serde_json::to_string_pretty(&existing_metadata) { - let _ = std::fs::write(&sidecar_path, json_string); + let _ = atomic_write_file(&sidecar_path, &json_string); } if enable_xmp_sync { @@ -2416,7 +2445,7 @@ pub async fn reset_adjustments_for_paths( existing_metadata.adjustments = serde_json::json!({}); if let Ok(json_string) = serde_json::to_string_pretty(&existing_metadata) { - let _ = std::fs::write(&sidecar_path, json_string); + let _ = atomic_write_file(&sidecar_path, &json_string); } if enable_xmp_sync { @@ -2542,7 +2571,7 @@ pub async fn apply_auto_adjustments_to_paths( } if let Ok(json_string) = serde_json::to_string_pretty(&existing_metadata) { - let _ = std::fs::write(&sidecar_path, json_string); + let _ = atomic_write_file(&sidecar_path, &json_string); } if enable_xmp_sync { @@ -2605,7 +2634,7 @@ pub fn set_color_label_for_paths( } if let Ok(json_string) = serde_json::to_string_pretty(&metadata) { - let _ = std::fs::write(&sidecar_path, json_string); + let _ = atomic_write_file(&sidecar_path, &json_string); } if enable_xmp_sync { @@ -2635,7 +2664,7 @@ pub fn set_rating_for_paths( metadata.rating = rating; if let Ok(json_string) = serde_json::to_string_pretty(&metadata) { - let _ = std::fs::write(&sidecar_path, json_string); + let _ = atomic_write_file(&sidecar_path, &json_string); } if enable_xmp_sync { @@ -2659,7 +2688,7 @@ pub fn load_metadata(path: String, app_handle: AppHandle) -> Result Result, String> { + // Validate the file extension up front to give the user a clear error + // before reading the file. + let lower = file_path.to_lowercase(); + if !lower.ends_with(".xmp") && !lower.ends_with(".lrtemplate") { + return Err(format!( + "Unsupported legacy preset format: '{}'. Expected .xmp or .lrtemplate.", + file_path + )); + } + let content = fs::read_to_string(&file_path) - .map_err(|e| format!("Failed to read legacy preset file: {}", e))?; + .map_err(|e| format!("Failed to read legacy preset file '{}': {}", file_path, e))?; + + if content.trim().is_empty() { + return Err(format!( + "Legacy preset file '{}' is empty or unreadable.", + file_path + )); + } - let xmp_content = if file_path.to_lowercase().ends_with(".lrtemplate") { - let re = Regex::new(r#"(?s)s.xmp = "(.*)""#).unwrap(); + let xmp_content = if lower.ends_with(".lrtemplate") { + let re = Regex::new(r#"(?s)s\.xmp\s*=\s*"(.*)""#) + .map_err(|e| format!("Failed to compile lrtemplate regex: {}", e))?; if let Some(caps) = re.captures(&content) { caps.get(1) - .map(|m| m.as_str().replace(r#"\""#, r#"""#)) + .map(|m| m.as_str().replace(r#"\""#, r#"""#).replace(r#"\\"#, r#"\"#)) .unwrap_or(content) } else { - content + // Some lrtemplate files store the XMP without a "s.xmp =" wrapper. + // Try to detect XMP markers before falling back to the raw content. + if content.contains(" Result<(), String> { { let source_path_str = source_path.to_string_lossy().to_string(); Command::new("explorer") - .args(["/select,", &source_path_str]) + .arg(format!("/select,{}", source_path_str)) .spawn() .map_err(|e| e.to_string())?; } @@ -3429,6 +3493,7 @@ pub fn generate_filename_from_template( let mut result = template.to_string(); result = result.replace("{original_filename}", stem); result = result.replace("{sequence}", &sequence_str); + result = result.replace("{Date}", &local_date.format("%Y%m%d").to_string()); result = result.replace("{YYYY}", &local_date.format("%Y").to_string()); result = result.replace("{MM}", &local_date.format("%m").to_string()); result = result.replace("{DD}", &local_date.format("%d").to_string()); diff --git a/src-tauri/src/gpu_processing.rs b/src-tauri/src/gpu_processing.rs index 6ecfc78c9c..904e458528 100644 --- a/src-tauri/src/gpu_processing.rs +++ b/src-tauri/src/gpu_processing.rs @@ -42,6 +42,27 @@ pub struct DisplayTransform { pub bg_secondary: [f32; 4], } +impl DisplayTransform { + pub fn from_payload(payload: &crate::WgpuTransformPayload) -> Self { + Self { + rect: [payload.x, payload.y, payload.width, payload.height], + clip: [ + payload.clip_x, + payload.clip_y, + payload.clip_width, + payload.clip_height, + ], + window: [payload.window_width, payload.window_height], + image_size: [0.0, 0.0], + texture_size: [0.0, 0.0], + pixelated: if payload.pixelated { 1.0 } else { 0.0 }, + _pad: 0.0, + bg_primary: payload.bg_primary, + bg_secondary: payload.bg_secondary, + } + } +} + pub struct WgpuDisplay { pub surface: wgpu::Surface<'static>, pub config: wgpu::SurfaceConfiguration, @@ -64,7 +85,10 @@ impl WgpuDisplay { match self.surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(tex) | wgpu::CurrentSurfaceTexture::Suboptimal(tex) => tex, - _ => panic!("Failed to acquire surface texture"), + _ => { + eprintln!("Warning: Failed to acquire surface texture, skipping frame"); + return; + } } } _ => return, @@ -155,6 +179,11 @@ pub fn get_or_init_gpu_context( instance_desc.backends = wgpu::Backends::PRIMARY; } + #[cfg(target_os = "linux")] + { + instance_desc.backends = wgpu::Backends::VULKAN; + } + let flag_path = state.gpu_crash_flag_path.lock().unwrap().clone(); if let Some(p) = &flag_path { if let Some(parent) = p.parent() { @@ -468,10 +497,13 @@ fn read_texture_data_roi( .map_err(|e| format!("Failed receiving GPU map result: {}", e))?; map_result.map_err(|e| e.to_string())?; - let padded_data = buffer_slice - .get_mapped_range() - .map_err(|e| format!("Failed to get mapped GPU buffer range: {}", e))? - .to_vec(); + let padded_data = match buffer_slice.get_mapped_range() { + Ok(range) => range.to_vec(), + Err(e) => { + output_buffer.unmap(); + return Err(format!("Failed to get mapped GPU buffer range: {}", e)); + } + }; output_buffer.unmap(); if padded_bytes_per_row == unpadded_bytes_per_row { @@ -1914,6 +1946,7 @@ fn process_and_get_dynamic_image_inner( Ok(range) => range.to_vec(), Err(e) => { log::error!("Failed to get mapped GPU buffer range: {}", e); + output_buffer.unmap(); return; } }; diff --git a/src-tauri/src/hdr_deghosting.rs b/src-tauri/src/hdr_deghosting.rs index a778513c92..eae3ac56a7 100644 --- a/src-tauri/src/hdr_deghosting.rs +++ b/src-tauri/src/hdr_deghosting.rs @@ -11,6 +11,7 @@ use image::{DynamicImage, GenericImageView, Rgb32FImage}; use nalgebra::{Matrix2, Matrix3, Point2}; use std::fs; use std::path::Path; +use std::sync::OnceLock; use std::time::Duration; use tauri::{AppHandle, Emitter}; @@ -21,6 +22,12 @@ const DEGHOST_NON_MAXIMA_SUPPRESSION_RADIUS: f32 = 8.0; const DEGHOST_MAX_PROCESSING_DIMENSION: u32 = 3200; const DEGHOST_IDENTITY_MAX_DISPLACEMENT: f64 = 1.0; +static BRIEF_PAIRS: OnceLock, Point2)>> = OnceLock::new(); + +fn get_brief_pairs() -> &'static [(Point2, Point2)] { + BRIEF_PAIRS.get_or_init(|| processing::generate_brief_pairs().unwrap_or_else(|_| Vec::new())) +} + enum AlignmentOutcome { Warped(Rgb32FImage), AlreadyAligned, @@ -58,26 +65,30 @@ pub fn load_hdr_frames( load_base_image_from_bytes(&file_bytes, path, false, settings, None) .map_err(|e| format!("Failed to load image {}: {}", path, e))?; if !is_raw_file(path) { - dynamic_image = apply_srgb_to_linear(dynamic_image); + // Avoid re-applying sRGB→Linear if the image is already linear + // (e.g. EXR / HDR / linear TIFF). + let lower = path.to_lowercase(); + let is_already_linear = lower.ends_with(".exr") || lower.ends_with(".hdr"); + if !is_already_linear { + dynamic_image = apply_srgb_to_linear(dynamic_image); + } } - let gains = match read_iso(path, &file_bytes) { - None => return Err(format!("Image {} is missing ISO/Sensitivity data", path)), - Some(gains) => gains as f32, - }; - let exposure = match read_exposure_time_secs(path, &file_bytes) { - None => return Err(format!("Image {} is missing ExposureTime data", path)), - Some(exp) => Duration::from_secs_f32(exp), - }; + // Use default EV 0.0 / ISO 100 when EXIF is missing so HDR merge + // does not fail on synthetic images, scanned negatives, or PNG/TIFF + // files without camera metadata. + let gains = read_iso(path, &file_bytes).unwrap_or(100) as f32; + let exposure = read_exposure_time_secs(path, &file_bytes) + .map(|exp| Duration::from_secs_f32(exp)) + .unwrap_or(Duration::from_secs_f32(1.0 / 125.0)); Ok((path.clone(), dynamic_image, exposure, gains)) }) .collect() } pub fn assert_uniform_dimensions(frames: &[HdrFrame]) -> Result<(), String> { - assert!( - !frames.is_empty(), - "dimension check requires at least one frame" - ); + if frames.is_empty() { + return Err("HDR merge requires at least one frame".to_string()); + } let (first_path, first_image, _, _) = &frames[0]; let width = first_image.width(); let height = first_image.height(); @@ -105,12 +116,11 @@ pub fn assert_uniform_dimensions(frames: &[HdrFrame]) -> Result<(), String> { pub fn align_hdr_frames(frames: &mut [HdrFrame], app_handle: &AppHandle) { assert!(!frames.is_empty(), "alignment requires at least one frame"); - let _ = app_handle.emit("hdr-progress", "Deghosting..."); - let brief_pairs = processing::generate_brief_pairs(); + let brief_pairs = get_brief_pairs(); let reference_index = frames.len() / 2; let detections: Vec = frames .iter() - .map(|frame| detect_frame_features(&frame.1, &brief_pairs, is_raw_file(&frame.0))) + .map(|frame| detect_frame_features(&frame.1, brief_pairs, is_raw_file(&frame.0))) .collect(); for index in 0..frames.len() { if index == reference_index { @@ -140,6 +150,8 @@ pub fn align_hdr_frames(frames: &mut [HdrFrame], app_handle: &AppHandle) { } } } + let _ = app_handle.emit("hdr-progress", "Deghosting..."); + deghost_aligned_frames(frames, reference_index); } fn detect_frame_features( @@ -201,7 +213,10 @@ fn align_frame_to_reference( return AlignmentOutcome::Failed; } }; - let rigid_full = estimate_rigid_transform(&inliers, reference, frame); + let rigid_full = match estimate_rigid_transform(&inliers, reference, frame) { + Ok(m) => m, + Err(_) => return AlignmentOutcome::Failed, + }; let (width, height) = frame_image.dimensions(); let displacement = max_corner_displacement(&rigid_full, width, height); if displacement < DEGHOST_IDENTITY_MAX_DISPLACEMENT { @@ -220,7 +235,7 @@ fn estimate_rigid_transform( inliers: &[Match], reference: &FrameDetection, frame: &FrameDetection, -) -> Matrix3 { +) -> Result, String> { assert!( inliers.len() >= 2, "rigid estimate requires at least two inliers" @@ -249,8 +264,8 @@ fn estimate_rigid_transform( } let covariance = Matrix2::new(h00, h01, h10, h11); let svd = covariance.svd(true, true); - let u = svd.u.expect("svd failed to produce u"); - let v = svd.v_t.expect("svd failed to produce v_t").transpose(); + let u = svd.u.ok_or_else(|| "SVD failed to produce U matrix - covariance matrix may be degenerate".to_string())?; + let v = svd.v_t.ok_or_else(|| "SVD failed to produce V^T matrix - covariance matrix may be degenerate".to_string())?.transpose(); let mut rotation = v * u.transpose(); if rotation.determinant() < 0.0 { let mut corrected = v; @@ -262,7 +277,7 @@ fn estimate_rigid_transform( - (rotation[(0, 0)] * reference_centroid.0 + rotation[(0, 1)] * reference_centroid.1); let ty = frame_centroid.1 - (rotation[(1, 0)] * reference_centroid.0 + rotation[(1, 1)] * reference_centroid.1); - Matrix3::new( + Ok(Matrix3::new( rotation[(0, 0)], rotation[(0, 1)], tx * frame.scale_factor, @@ -272,7 +287,7 @@ fn estimate_rigid_transform( 0.0, 0.0, 1.0, - ) + )) } fn centroid(points: impl Iterator, count: f64) -> (f64, f64) { @@ -305,3 +320,134 @@ fn max_corner_displacement(transform: &Matrix3, width: u32, height: u32) -> } max_displacement } + +/// Apply per-pixel deghosting to aligned HDR frames using exposure-weighted +/// consistency checking. For each pixel, frames that deviate significantly +/// from the expected value (given their exposure) are considered ghost +/// regions and their contribution is reduced. +/// +/// Algorithm: +/// 1. Compute a reference exposure-normalised image from the median of all frames. +/// 2. For each frame, compute per-pixel deviation from the reference. +/// 3. Pixels with high deviation (ghost candidates) get their exposure weight +/// reduced, so the merge algorithm prefers consistent pixels. +fn deghost_aligned_frames(frames: &mut [HdrFrame], reference_index: usize) { + if frames.len() < 2 { + return; + } + + let (width, height) = frames[reference_index].1.dimensions(); + if width == 0 || height == 0 { + return; + } + + // Collect exposure-normalised RGB32F data from all frames. + // We work with flat f32 buffers for efficient pixel access. + let frame_data: Vec<(Vec<[f32; 3]>, f32, f32)> = frames + .iter() + .map(|(_, img, exposure, gains)| { + let rgb32f = img.to_rgb32f(); + let ev = exposure.as_secs_f32().max(1e-10); + let flat: Vec<[f32; 3]> = rgb32f.pixels().map(|p| [p[0], p[1], p[2]]).collect(); + (flat, ev, *gains) + }) + .collect(); + + let pixel_count = (width * height) as usize; + let num_frames = frame_data.len(); + + // Step 1: Compute median of exposure-normalised values as the reference + let mut median_r = vec![0.0f32; pixel_count]; + let mut median_g = vec![0.0f32; pixel_count]; + let mut median_b = vec![0.0f32; pixel_count]; + + for pix_idx in 0..pixel_count { + let mut vals_r = Vec::with_capacity(num_frames); + let mut vals_g = Vec::with_capacity(num_frames); + let mut vals_b = Vec::with_capacity(num_frames); + + for (flat, ev, gains) in &frame_data { + let p = flat[pix_idx]; + let norm = ev * gains.max(1e-10); + vals_r.push(p[0] / norm); + vals_g.push(p[1] / norm); + vals_b.push(p[2] / norm); + } + + vals_r.sort_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| if a.is_nan() { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less })); + vals_g.sort_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| if a.is_nan() { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less })); + vals_b.sort_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| if a.is_nan() { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less })); + + let mid = num_frames / 2; + median_r[pix_idx] = if num_frames % 2 == 0 && mid > 0 { + (vals_r[mid - 1] + vals_r[mid]) * 0.5 + } else { + vals_r[mid] + }; + median_g[pix_idx] = if num_frames % 2 == 0 && mid > 0 { + (vals_g[mid - 1] + vals_g[mid]) * 0.5 + } else { + vals_g[mid] + }; + median_b[pix_idx] = if num_frames % 2 == 0 && mid > 0 { + (vals_b[mid - 1] + vals_b[mid]) * 0.5 + } else { + vals_b[mid] + }; + } + + // Step 2: For each non-reference frame, identify ghost pixels and + // replace them with values blended from the reference frame. + const GHOST_THRESHOLD: f32 = 0.15; + const GHOST_BLEND: f32 = 0.85; + + let ref_flat = &frame_data[reference_index].0; + + for frame_idx in 0..num_frames { + if frame_idx == reference_index { + continue; + } + + let flat = &frame_data[frame_idx].0; + let mut modified_flat = flat.clone(); + + for pix_idx in 0..pixel_count { + let p = flat[pix_idx]; + let rp = ref_flat[pix_idx]; + + let norm_r = median_r[pix_idx].abs().max(1e-6); + let norm_g = median_g[pix_idx].abs().max(1e-6); + let norm_b = median_b[pix_idx].abs().max(1e-6); + + let dev_r = ((p[0] - median_r[pix_idx]) / norm_r).abs(); + let dev_g = ((p[1] - median_g[pix_idx]) / norm_g).abs(); + let dev_b = ((p[2] - median_b[pix_idx]) / norm_b).abs(); + + let max_dev = dev_r.max(dev_g).max(dev_b); + + if max_dev > GHOST_THRESHOLD { + let blend = if max_dev > GHOST_THRESHOLD * 3.0 { + GHOST_BLEND + } else { + let t = (max_dev - GHOST_THRESHOLD) / (GHOST_THRESHOLD * 2.0); + t.min(1.0) * GHOST_BLEND + }; + + let new_r = p[0] * (1.0 - blend) + rp[0] * blend; + let new_g = p[1] * (1.0 - blend) + rp[1] * blend; + let new_b = p[2] * (1.0 - blend) + rp[2] * blend; + + modified_flat[pix_idx] = [new_r, new_g, new_b]; + } + } + + // Convert flat buffer back to Rgb32FImage + let mut modified_img = Rgb32FImage::new(width, height); + for (pix_idx, pixel) in modified_flat.iter().enumerate() { + let x = pix_idx as u32 % width; + let y = pix_idx as u32 / width; + modified_img.put_pixel(x, y, image::Rgb([pixel[0], pixel[1], pixel[2]])); + } + frames[frame_idx].1 = DynamicImage::ImageRgb32F(modified_img); + } +} diff --git a/src-tauri/src/image_loader.rs b/src-tauri/src/image_loader.rs index 44313f606a..312861deb5 100644 --- a/src-tauri/src/image_loader.rs +++ b/src-tauri/src/image_loader.rs @@ -12,7 +12,7 @@ use crate::mask_generation::{MaskDefinition, SubMask, generate_mask_bitmap}; use anyhow::{Context, Result, anyhow}; use base64::{Engine as _, engine::general_purpose}; use exif::{Reader as ExifReader, Tag}; -use image::{DynamicImage, GenericImageView, ImageReader, imageops}; +use image::{DynamicImage, GenericImageView, ImageReader, Limits, imageops}; use rawler::Orientation; use rayon::prelude::*; use serde::Deserialize; @@ -22,11 +22,12 @@ use std::fs; use std::panic; use std::path::Path; use std::sync::OnceLock; +use std::sync::mpsc; use std::sync::{ Arc, atomic::{AtomicUsize, Ordering}, }; -use std::time::Instant; +use std::time::{Duration, Instant}; #[derive(serde::Serialize)] pub struct LoadImageResult { @@ -113,6 +114,20 @@ pub fn load_base_image_from_bytes( ) }) { Ok(Ok(mut image)) => { + // OOM protection: check dimensions before further processing + let (w, h) = image.dimensions(); + let pixel_count = w as u64 * h as u64; + if pixel_count > MAX_PIXEL_COUNT { + return Err(anyhow!( + "RAW image dimensions {}x{} ({} megapixels) exceed the maximum allowed size of {} megapixels. \ + The image is too large to process safely.", + w, + h, + pixel_count / 1_000_000, + MAX_PIXEL_COUNT / 1_000_000 + )); + } + if !use_fast_raw_dev && (color_nr_amount > 0.0 || sharpening_amount > 0.0) { let start = Instant::now(); remove_raw_artifacts_and_enhance( @@ -336,6 +351,14 @@ fn embedded_preview_fallback(bytes: &[u8], path: &str) -> Option { }) } +/// Maximum number of pixels allowed for a decoded image to prevent OOM. +/// 200 megapixels ≈ a 20000×10000 image, which at 4 bytes/channel × 3 channels (RGB32F) +/// would consume ~2.4 GB. This is a reasonable upper bound for consumer hardware. +const MAX_PIXEL_COUNT: u64 = 200_000_000; + +/// Timeout for individual image decode operations. +const DECODE_TIMEOUT: Duration = Duration::from_secs(30); + pub fn load_image_with_orientation( bytes: &[u8], cancel_token: Option<(Arc, usize)>, @@ -349,21 +372,77 @@ pub fn load_image_with_orientation( Ok(()) }; - let cursor = Cursor::new(bytes); - let mut reader = ImageReader::new(cursor.clone()) + // Clone bytes to an owned Vec so the decode thread can take ownership. + // The original `bytes: &[u8]` borrows from the caller and cannot be moved + // into a `'static` thread — cloning here is necessary for thread safety. + let bytes_owned = bytes.to_vec(); + // Keep a separate clone for EXIF reading after decode (the decode thread + // consumes its own clone via Cursor). + let bytes_for_exif = bytes_owned.clone(); + let cursor = Cursor::new(bytes_owned); + let mut reader = ImageReader::new(cursor) .with_guessed_format() .context("Failed to guess image format")?; - reader.no_limits(); + // Set decoding limits for OOM protection instead of no_limits(). + // This enforces a maximum pixel count at the decoder level. + let mut limits = Limits::default(); + limits.max_image_width = Some((MAX_PIXEL_COUNT as f64).sqrt() as u32); + limits.max_image_height = Some((MAX_PIXEL_COUNT as f64).sqrt() as u32); + // Also set an explicit max allocation to guard against huge single buffers + limits.max_alloc = Some(MAX_PIXEL_COUNT * 4); // 4 bytes per pixel (RGBA8 worst case) + reader.limits(limits); check_cancel()?; - let image = reader.decode().context("Failed to decode image")?; + // Decode with timeout to prevent indefinite hangs on corrupted/slow files + let image = { + let (tx, rx) = mpsc::channel(); + let decode_handle = std::thread::spawn(move || { + let result = reader.decode(); + let _ = tx.send(()); + result + }); + + if rx.recv_timeout(DECODE_TIMEOUT).is_err() { + // Timeout: the decode thread is still running but we can't cancel it. + // It will finish eventually and release its own resources. + // We return an error immediately so the caller doesn't hang. + return Err(anyhow!( + "Image decode timed out after {} seconds. The file may be corrupted or on a slow network drive.", + DECODE_TIMEOUT.as_secs() + )); + } + + // The decode finished within the timeout + decode_handle + .join() + .map_err(|_| anyhow!("Image decode thread panicked"))? + .context("Failed to decode image")? + }; + check_cancel()?; + // Additional post-decode dimension check as a safety net + let (w, h) = image.dimensions(); + let pixel_count = w as u64 * h as u64; + if pixel_count > MAX_PIXEL_COUNT { + return Err(anyhow!( + "Decoded image dimensions {}x{} ({} megapixels) exceed the maximum allowed size of {} megapixels. \ + The image is too large to process safely.", + w, + h, + pixel_count / 1_000_000, + MAX_PIXEL_COUNT / 1_000_000 + )); + } + let oriented_image = { let exif_reader = ExifReader::new(); - if let Ok(exif) = exif_reader.read_from_container(&mut cursor.clone()) { + // Create a new cursor from the owned bytes for EXIF reading + // (the original cursor was moved into the decode thread) + let exif_cursor = Cursor::new(bytes_for_exif); + if let Ok(exif) = exif_reader.read_from_container(&mut exif_cursor.clone()) { if let Some(orientation) = exif .get_field(Tag::Orientation, exif::In::PRIMARY) .and_then(|f| f.value.get_uint(0)) @@ -856,7 +935,7 @@ pub async fn load_image( return Err("Load cancelled".to_string()); } - let (orig_width, orig_height) = pristine_arc.dimensions(); + let (orig_width, orig_height) = pristine_arc.as_ref().dimensions(); *state.original_image.lock().unwrap() = Some(LoadedImage { path, diff --git a/src-tauri/src/image_processing.rs b/src-tauri/src/image_processing.rs index 7271ba1bc4..704b74f40e 100644 --- a/src-tauri/src/image_processing.rs +++ b/src-tauri/src/image_processing.rs @@ -20,6 +20,79 @@ pub use crate::gpu_processing::{ use crate::{AppState, mask_generation::MaskDefinition}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +/// Maximum total pixels allowed for CPU-intensive image processing. +/// 200 megapixels × 12 bytes/pixel (f32 RGB) ≈ 2.4 GB per buffer. +/// This prevents OOM from processing extremely large images (e.g., stitched panoramas). +const MAX_IMAGE_PIXELS: u64 = 200_000_000; + +/// Maximum concurrent heavy CPU processing operations to prevent OOM +/// from parallel buffer allocations. +const MAX_CONCURRENT_PROCESSING: usize = 2; + +static CONCURRENT_PROCESSING_COUNT: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +/// Validates that image dimensions won't cause OOM or integer overflow. +fn validate_image_dimensions(width: u32, height: u32) -> Result<(), String> { + let total_pixels = width as u64 * height as u64; + if total_pixels > MAX_IMAGE_PIXELS { + return Err(format!( + "Image is too large to process safely ({}x{} = {}MP). Maximum is {}MP.", + width, + height, + total_pixels / 1_000_000, + MAX_IMAGE_PIXELS / 1_000_000 + )); + } + Ok(()) +} + +/// Computes the buffer size for an RGB32F image with overflow checking. +/// Returns the number of f32 elements needed (width * height * 3). +fn checked_rgb32f_buffer_size(width: u32, height: u32) -> Result { + let total_elements = width as u64 * height as u64 * 3; + if total_elements > usize::MAX as u64 { + return Err(format!( + "Buffer size overflow for {}x{} image", + width, height + )); + } + Ok(total_elements as usize) +} + +/// RAII guard for the processing concurrency semaphore. +/// Limits the number of concurrent heavy image processing operations +/// to prevent OOM from parallel buffer allocations. +struct ProcessingGuard; + +impl ProcessingGuard { + fn acquire() -> Result { + use std::sync::atomic::Ordering; + loop { + let current = CONCURRENT_PROCESSING_COUNT.load(Ordering::Acquire); + if current >= MAX_CONCURRENT_PROCESSING { + return Err(format!( + "Too many concurrent image processing operations (limit: {})", + MAX_CONCURRENT_PROCESSING + )); + } + if CONCURRENT_PROCESSING_COUNT + .compare_exchange_weak(current, current + 1, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + return Ok(ProcessingGuard); + } + } + } +} + +impl Drop for ProcessingGuard { + fn drop(&mut self) { + use std::sync::atomic::Ordering; + CONCURRENT_PROCESSING_COUNT.fetch_sub(1, Ordering::AcqRel); + } +} + pub trait IntoCowImage<'a> { fn into_cow(self) -> Cow<'a, DynamicImage>; } @@ -299,7 +372,14 @@ pub fn downscale_f32_image(image: &DynamicImage, nwidth: u32, nheight: u32) -> D } } - let mut out_buf = vec![0.0f32; (new_w * new_h * 3) as usize]; + let buf_size = match checked_rgb32f_buffer_size(new_w, new_h) { + Ok(s) => s, + Err(e) => { + log::warn!("Skipping downscale: {}", e); + return image.clone(); + } + }; + let mut out_buf = vec![0.0f32; buf_size]; out_buf .par_chunks_exact_mut(new_w as usize * 3) @@ -328,20 +408,20 @@ pub fn downscale_f32_image(image: &DynamicImage, nwidth: u32, nheight: u32) -> D for (&w_x, chunk) in x_wts.iter().zip(src_slice.chunks_exact(3)) { let w = w_x * w_y; - let r = chunk[0].max(0.0); - let g = chunk[1].max(0.0); - let b = chunk[2].max(0.0); + let r = chunk[0]; + let g = chunk[1]; + let b = chunk[2]; - r_sum += r * r * w; - g_sum += g * g * w; - b_sum += b * b * w; + r_sum += r * w; + g_sum += g * w; + b_sum += b * w; } } let out_idx = x_out * 3; - row[out_idx] = r_sum.sqrt(); - row[out_idx + 1] = g_sum.sqrt(); - row[out_idx + 2] = b_sum.sqrt(); + row[out_idx] = r_sum; + row[out_idx + 1] = g_sum; + row[out_idx + 2] = b_sum; } }); @@ -490,16 +570,19 @@ fn interpolate_pixel_with_tca( return 0.0; } + if src_width == 0 || src_height == 0 { + return 0.0; + } let x_clamped = target_x.clamp(0.0, src_width as f32 - 1.0); let y_clamped = target_y.clamp(0.0, src_height as f32 - 1.0); let mut x0 = x_clamped.floor() as usize; let mut y0 = y_clamped.floor() as usize; - if x0 >= src_width - 1 { + if src_width >= 2 && x0 >= src_width - 1 { x0 = src_width.saturating_sub(2); } - if y0 >= src_height - 1 { + if src_height >= 2 && y0 >= src_height - 1 { y0 = src_height.saturating_sub(2); } @@ -643,9 +726,29 @@ fn compute_lens_auto_crop_scale(params: &GeometryParams, width: f32, height: f32 } pub fn warp_image_geometry(image: &DynamicImage, params: GeometryParams) -> DynamicImage { + let (width, height) = image.dimensions(); + if let Err(e) = validate_image_dimensions(width, height) { + log::warn!("Skipping geometry warp: {}", e); + return image.clone(); + } + let _guard = match ProcessingGuard::acquire() { + Ok(g) => g, + Err(e) => { + log::warn!("Skipping geometry warp: {}", e); + return image.clone(); + } + }; + let src_img = image.to_rgb32f(); let (width, height) = src_img.dimensions(); - let mut out_buffer = vec![0.0f32; (width * height * 3) as usize]; + let buf_size = match checked_rgb32f_buffer_size(width, height) { + Ok(s) => s, + Err(e) => { + log::warn!("Skipping geometry warp: {}", e); + return image.clone(); + } + }; + let mut out_buffer = vec![0.0f32; buf_size]; let (forward_transform, cx, cy, half_diagonal) = build_transform_matrices(¶ms, width as f32, height as f32); @@ -798,14 +901,35 @@ pub fn warp_image_geometry(image: &DynamicImage, params: GeometryParams) -> Dyna } }); - let out_img = Rgb32FImage::from_vec(width, height, out_buffer).unwrap(); + let out_img = Rgb32FImage::from_vec(width, height, out_buffer) + .expect("buffer size was validated before allocation"); DynamicImage::ImageRgb32F(out_img) } pub fn unwarp_image_geometry(warped_image: &DynamicImage, params: GeometryParams) -> DynamicImage { + let (width, height) = warped_image.dimensions(); + if let Err(e) = validate_image_dimensions(width, height) { + log::warn!("Skipping geometry unwarp: {}", e); + return warped_image.clone(); + } + let _guard = match ProcessingGuard::acquire() { + Ok(g) => g, + Err(e) => { + log::warn!("Skipping geometry unwarp: {}", e); + return warped_image.clone(); + } + }; + let src_img = warped_image.to_rgb32f(); let (width, height) = src_img.dimensions(); - let mut out_buffer = vec![0.0f32; (width * height * 3) as usize]; + let buf_size = match checked_rgb32f_buffer_size(width, height) { + Ok(s) => s, + Err(e) => { + log::warn!("Skipping geometry unwarp: {}", e); + return warped_image.clone(); + } + }; + let mut out_buffer = vec![0.0f32; buf_size]; let (forward_transform, cx, cy, half_diagonal) = build_transform_matrices(¶ms, width as f32, height as f32); @@ -933,7 +1057,8 @@ pub fn unwarp_image_geometry(warped_image: &DynamicImage, params: GeometryParams } }); - let out_img = Rgb32FImage::from_vec(width, height, out_buffer).unwrap(); + let out_img = Rgb32FImage::from_vec(width, height, out_buffer) + .expect("buffer size was validated before allocation"); DynamicImage::ImageRgb32F(out_img) } @@ -1114,6 +1239,12 @@ pub fn inverse_transform_point( } pub fn apply_cpu_default_raw_processing(image: &mut DynamicImage) { + let (width, height) = image.dimensions(); + if let Err(e) = validate_image_dimensions(width, height) { + log::warn!("Skipping CPU default raw processing: {}", e); + return; + } + let mut f32_image = image.to_rgb32f(); const GAMMA: f32 = 2.38; @@ -1121,9 +1252,14 @@ pub fn apply_cpu_default_raw_processing(image: &mut DynamicImage) { const CONTRAST: f32 = 1.28; f32_image.par_chunks_mut(3).for_each(|pixel_chunk| { - let r_gamma = pixel_chunk[0].powf(INV_GAMMA); - let g_gamma = pixel_chunk[1].powf(INV_GAMMA); - let b_gamma = pixel_chunk[2].powf(INV_GAMMA); + // Clamp to non-negative before powf to avoid NaN on negative values. + let r = pixel_chunk[0].max(0.0); + let g = pixel_chunk[1].max(0.0); + let b = pixel_chunk[2].max(0.0); + + let r_gamma = r.powf(INV_GAMMA); + let g_gamma = g.powf(INV_GAMMA); + let b_gamma = b.powf(INV_GAMMA); let r_contrast = (r_gamma - 0.5) * CONTRAST + 0.5; let g_contrast = (g_gamma - 0.5) * CONTRAST + 0.5; @@ -1342,7 +1478,7 @@ pub fn is_geometry_identity(params: &GeometryParams) -> bool { && vig_identity } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct AutoAdjustmentResults { pub exposure: f64, pub brightness: f64, @@ -1852,6 +1988,12 @@ pub fn resolve_tonemapper_override_from_handle( } pub fn apply_cpu_agx_tonemap(image: &mut DynamicImage) { + let (width, height) = image.dimensions(); + if let Err(e) = validate_image_dimensions(width, height) { + log::warn!("Skipping CPU AgX tonemap: {}", e); + return; + } + const AGX_EPSILON: f32 = 1.0e-6; const AGX_MIN_EV: f32 = -15.2; const AGX_MAX_EV: f32 = 5.0; @@ -1985,7 +2127,10 @@ pub fn is_image_edited( if let Some(crop_val) = adj.get("crop") && !crop_val.is_null() && let Ok(crop) = serde_json::from_value::(crop_val.clone()) - && (crop.x.abs() > 0.1 || crop.y.abs() > 0.1) + && (crop.x.abs() > 0.1 + || crop.y.abs() > 0.1 + || crop.width.abs() > 0.1 + || crop.height.abs() > 0.1) { return true; } @@ -2302,10 +2447,10 @@ fn get_global_adjustments_from_json( red_curve: convert_points_to_aligned(red_points.clone()), green_curve: convert_points_to_aligned(green_points.clone()), blue_curve: convert_points_to_aligned(blue_points.clone()), - luma_curve_count: luma_points.len() as u32, - red_curve_count: red_points.len() as u32, - green_curve_count: green_points.len() as u32, - blue_curve_count: blue_points.len() as u32, + luma_curve_count: luma_points.len().min(16) as u32, + red_curve_count: red_points.len().min(16) as u32, + green_curve_count: green_points.len().min(16) as u32, + blue_curve_count: blue_points.len().min(16) as u32, _pad_end1: 0.0, _pad_end2: 0.0, _pad_end3: 0.0, @@ -2443,10 +2588,10 @@ fn get_mask_adjustments_from_json(adj: &serde_json::Value) -> MaskAdjustments { red_curve: convert_points_to_aligned(red_points.clone()), green_curve: convert_points_to_aligned(green_points.clone()), blue_curve: convert_points_to_aligned(blue_points.clone()), - luma_curve_count: luma_points.len() as u32, - red_curve_count: red_points.len() as u32, - green_curve_count: green_points.len() as u32, - blue_curve_count: blue_points.len() as u32, + luma_curve_count: luma_points.len().min(16) as u32, + red_curve_count: red_points.len().min(16) as u32, + green_curve_count: green_points.len().min(16) as u32, + blue_curve_count: blue_points.len().min(16) as u32, _pad_end4: 0.0, _pad_end5: 0.0, _pad_end6: 0.0, @@ -2517,6 +2662,19 @@ pub fn remove_raw_artifacts_and_enhance( color_nr_inv_sigma: f32, sharpening_amount: f32, ) { + let (width, height) = image.dimensions(); + if let Err(e) = validate_image_dimensions(width, height) { + log::warn!("Skipping raw artifact removal: {}", e); + return; + } + let _guard = match ProcessingGuard::acquire() { + Ok(g) => g, + Err(e) => { + log::warn!("Skipping raw artifact removal: {}", e); + return; + } + }; + let mut buffer = image.to_rgb32f(); let w = buffer.width() as usize; let h = buffer.height() as usize; @@ -3218,6 +3376,9 @@ pub fn perform_auto_analysis(image: &DynamicImage) -> AutoAdjustmentResults { let analysis_preview = downscale_f32_image(image, ANALYSIS_MAX_DIM, ANALYSIS_MAX_DIM); let rgb_image = analysis_preview.to_rgb8(); let total_pixels = (rgb_image.width() * rgb_image.height()) as f64; + if total_pixels == 0.0 { + return AutoAdjustmentResults::default(); + } let (width, height) = rgb_image.dimensions(); let cx0 = (width as f32 * VIGNETTE_CENTER_LOW) as u32; @@ -3428,3 +3589,311 @@ pub fn calculate_auto_adjustments( Ok(auto_results_to_json(&results)) } + +// --------------------------------------------------------------------------- +// Composition Enhancement – Horizon Detection & Auto-Straighten +// --------------------------------------------------------------------------- + +/// A detected horizon line represented in Hesse normal form (rho, theta). +/// rho = distance from origin to the line (pixels). +/// theta = angle of the line's normal from x-axis (radians). +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct HorizonLine { + pub rho: f32, + pub theta: f32, + pub confidence: f32, +} + +/// Detect horizon lines using Canny edge detection + Hough transform. +#[tauri::command] +pub fn detect_horizon_lines(state: tauri::State) -> Result, String> { + let loaded_image = state + .original_image + .lock() + .unwrap() + .clone() + .ok_or("No original image loaded")?; + + let (w, h) = loaded_image.image.as_ref().dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + // Convert to grayscale + let gray = loaded_image.image.to_luma8(); + + // Step 1: Gaussian blur to reduce noise before edge detection + let blurred = imageproc::filter::gaussian_blur_f32(&gray, 1.4); + + // Step 2: Canny edge detection + // We implement a simplified Canny: Sobel gradient magnitude + non-maximum suppression + thresholding + let (grad_mag, grad_dir) = compute_sobel_gradients(&blurred); + + // Non-maximum suppression + let nms = non_maximum_suppression(&grad_mag, &grad_dir, w, h); + + // Double threshold + hysteresis + let edges = double_threshold_hysteresis(&nms, w, h, 30.0, 80.0); + + // Step 3: Hough transform for lines + // Focus on near-horizontal lines (theta near PI/2) since we're looking for horizons + let diagonal = ((w as f32).powi(2) + (h as f32).powi(2)).sqrt(); + let rho_max = diagonal.ceil() as i32; + let rho_steps = (2 * rho_max + 1) as usize; + + // Scan angles near horizontal: 60° to 120° (PI/3 to 2*PI/3) + let theta_min = PI / 3.0; + let theta_max = 2.0 * PI / 3.0; + let theta_steps = 120; + let theta_step = (theta_max - theta_min) / theta_steps as f32; + + let mut accumulator = vec![0u32; rho_steps * theta_steps]; + + for y in 1..(h - 1) { + for x in 1..(w - 1) { + if edges[(y * w + x) as usize] { + for ti in 0..theta_steps { + let theta = theta_min + ti as f32 * theta_step; + let rho = x as f32 * theta.cos() + y as f32 * theta.sin(); + let rho_idx = ((rho + diagonal).round() as i32).clamp(0, rho_steps as i32 - 1); + accumulator[rho_idx as usize * theta_steps + ti] += 1; + } + } + } + } + + // Find peaks in accumulator + let threshold = (w.min(h) as f32 * 0.15).ceil() as u32; + let mut peaks: Vec<(usize, usize, u32)> = Vec::new(); + + for ri in 2..(rho_steps - 2) { + for ti in 2..(theta_steps - 2) { + let val = accumulator[ri * theta_steps + ti]; + if val < threshold { + continue; + } + + // Check if it's a local maximum in a 5x5 neighborhood + let mut is_peak = true; + 'outer: for dri in -2i32..=2 { + for dti in -2i32..=2 { + if dri == 0 && dti == 0 { + continue; + } + let nr = (ri as i32 + dri).clamp(0, rho_steps as i32 - 1) as usize; + let nt = (ti as i32 + dti).clamp(0, theta_steps as i32 - 1) as usize; + if accumulator[nr * theta_steps + nt] > val { + is_peak = false; + break 'outer; + } + } + } + + if is_peak { + peaks.push((ri, ti, val)); + } + } + } + + // Sort by vote count descending + peaks.sort_by_key(|a| std::cmp::Reverse(a.2)); + + // Take top N and convert to HorizonLine + let max_lines = 5; + let mut horizon_lines = Vec::new(); + + let max_votes = peaks.first().map(|p| p.2).unwrap_or(1).max(1); + + for (ri, ti, votes) in peaks.iter().take(max_lines) { + let rho = *ri as f32 - diagonal; + let theta = theta_min + *ti as f32 * theta_step; + let confidence = *votes as f32 / max_votes as f32; + + horizon_lines.push(HorizonLine { + rho, + theta, + confidence, + }); + } + + Ok(horizon_lines) +} + +/// Auto-straighten the horizon by finding the dominant near-horizontal line +/// and returning the rotation angle needed to correct it. +/// Returns the angle in degrees that should be applied to straighten. +#[tauri::command] +pub fn auto_straighten_horizon( + state: tauri::State, + angle_tolerance: f32, +) -> Result { + let lines = detect_horizon_lines(state)?; + + if lines.is_empty() { + return Ok(0.0); + } + + let tolerance = angle_tolerance.clamp(0.0, 45.0); + + // Find the best candidate: highest confidence, within tolerance + let best = lines + .iter() + .filter(|l| { + // The line angle relative to horizontal + // A horizontal line has theta = PI/2 + let deviation = ((l.theta - PI / 2.0) * 180.0 / PI).abs(); + deviation <= tolerance + }) + .max_by(|a, b| { + a.confidence + .partial_cmp(&b.confidence) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + match best { + Some(line) => { + // The deviation from horizontal in degrees + let deviation_deg = (line.theta - PI / 2.0) * 180.0 / PI; + Ok(-deviation_deg) + } + None => Ok(0.0), + } +} + +// --------------------------------------------------------------------------- +// Internal helpers for Canny + Hough +// --------------------------------------------------------------------------- + +/// Compute Sobel gradient magnitude and direction. +fn compute_sobel_gradients(gray: &image::GrayImage) -> (Vec, Vec) { + let (w, h) = gray.dimensions(); + let raw = gray.as_raw(); + let total = (w * h) as usize; + let mut mag = vec![0.0f32; total]; + let mut dir = vec![0.0f32; total]; + + for y in 1..(h - 1) { + for x in 1..(w - 1) { + // Sobel X kernel: [[-1,0,1],[-2,0,2],[-1,0,1]] + let tl = raw[((y - 1) * w + (x - 1)) as usize] as f32; + let ml = raw[(y * w + (x - 1)) as usize] as f32; + let bl = raw[((y + 1) * w + (x - 1)) as usize] as f32; + let tr = raw[((y - 1) * w + (x + 1)) as usize] as f32; + let mr = raw[(y * w + (x + 1)) as usize] as f32; + let br = raw[((y + 1) * w + (x + 1)) as usize] as f32; + let tc = raw[((y - 1) * w + x) as usize] as f32; + let bc = raw[((y + 1) * w + x) as usize] as f32; + + let gx = -tl + tr - 2.0 * ml + 2.0 * mr - bl + br; + let gy = -tl - 2.0 * tc - tr + bl + 2.0 * bc + br; + + let idx = (y * w + x) as usize; + mag[idx] = (gx * gx + gy * gy).sqrt(); + dir[idx] = gy.atan2(gx); + } + } + + (mag, dir) +} + +/// Non-maximum suppression for Canny edge detection. +fn non_maximum_suppression(mag: &[f32], dir: &[f32], w: u32, h: u32) -> Vec { + let total = (w * h) as usize; + let mut nms = vec![0.0f32; total]; + + for y in 1..(h - 1) { + for x in 1..(w - 1) { + let idx = (y * w + x) as usize; + let m = mag[idx]; + if m < 1e-4 { + continue; + } + + // Quantize angle to 4 directions + let angle = dir[idx]; + let (dx1, dy1, dx2, dy2) = { + let a = angle.abs(); + if !(PI / 8.0..=7.0 * PI / 8.0).contains(&a) { + // Horizontal edge → compare left/right + (1i32, 0i32, -1i32, 0i32) + } else if a < 3.0 * PI / 8.0 { + // Diagonal \ + (1i32, 1i32, -1i32, -1i32) + } else if a < 5.0 * PI / 8.0 { + // Vertical edge → compare up/down + (0i32, 1i32, 0i32, -1i32) + } else { + // Diagonal / + (1i32, -1i32, -1i32, 1i32) + } + }; + + let nx1 = (x as i32 + dx1).clamp(0, w as i32 - 1) as u32; + let ny1 = (y as i32 + dy1).clamp(0, h as i32 - 1) as u32; + let nx2 = (x as i32 + dx2).clamp(0, w as i32 - 1) as u32; + let ny2 = (y as i32 + dy2).clamp(0, h as i32 - 1) as u32; + + let m1 = mag[(ny1 * w + nx1) as usize]; + let m2 = mag[(ny2 * w + nx2) as usize]; + + if m >= m1 && m >= m2 { + nms[idx] = m; + } + } + } + + nms +} + +/// Double threshold + hysteresis for Canny. +fn double_threshold_hysteresis( + nms: &[f32], + w: u32, + h: u32, + low_thresh: f32, + high_thresh: f32, +) -> Vec { + let total = (w * h) as usize; + let mut strong = vec![false; total]; + let mut weak = vec![false; total]; + + // Classify pixels + for i in 0..total { + if nms[i] >= high_thresh { + strong[i] = true; + } else if nms[i] >= low_thresh { + weak[i] = true; + } + } + + // Hysteresis: promote weak pixels connected to strong pixels + let mut edges = strong.clone(); + let mut changed = true; + while changed { + changed = false; + for y in 1..(h - 1) { + for x in 1..(w - 1) { + let idx = (y * w + x) as usize; + if !weak[idx] || edges[idx] { + continue; + } + // Check 8-connectivity for strong neighbors + for dy in -1i32..=1 { + for dx in -1i32..=1 { + if dx == 0 && dy == 0 { + continue; + } + let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as u32; + let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as u32; + if edges[(ny * w + nx) as usize] { + edges[idx] = true; + changed = true; + } + } + } + } + } + } + + edges +} diff --git a/src-tauri/src/inpainting.rs b/src-tauri/src/inpainting.rs index c366239095..77b84c7c83 100644 --- a/src-tauri/src/inpainting.rs +++ b/src-tauri/src/inpainting.rs @@ -153,7 +153,10 @@ pub async fn generate_manual_cleanup_patch( let crop_w = (max_x - min_x + 1) as u32; let crop_h = (max_y - min_y + 1) as u32; - let sub_masks_val = serde_json::to_value(&patch_definition.sub_masks).unwrap_or(Value::Null); + let sub_masks_val = serde_json::to_value(&patch_definition.sub_masks).map_err(|e| { + log::error!("Failed to serialize sub_masks: {}", e); + format!("Failed to serialize sub_masks: {}", e) + })?; let mut is_heal = false; if let Some(arr) = sub_masks_val.as_array() { for sm in arr { @@ -176,20 +179,27 @@ pub async fn generate_manual_cleanup_patch( for x in min_x..=max_x { let px_x = x as u32; let px_y = y as u32; + let dest_x = px_x - min_x_u32; + let dest_y = px_y - min_y_u32; + if mask_bitmap.get_pixel(px_x, px_y)[0] > 0 { let src_x = (px_x as i32 + offset_x).clamp(0, img_w as i32 - 1) as u32; let src_y = (px_y as i32 + offset_y).clamp(0, img_h as i32 - 1) as u32; let src_px = source_image.get_pixel(src_x, src_y); - - let dest_x = px_x - min_x_u32; - let dest_y = px_y - min_y_u32; + color_image.put_pixel(dest_x, dest_y, Rgb([src_px[0], src_px[1], src_px[2]])); + } else { + let src_px = source_image.get_pixel(px_x, px_y); color_image.put_pixel(dest_x, dest_y, Rgb([src_px[0], src_px[1], src_px[2]])); } } } } else { - let bw = max_x - min_x + 3; - let bh = max_y - min_y + 3; + let bw = (max_x - min_x + 3).min(img_w_usize); + let bh = (max_y - min_y + 3).min(img_h_usize); + + if bw < 3 || bh < 3 { + return Err("Heal region too small to process.".to_string()); + } let mut v_r = vec![0.0f32; bw * bh]; let mut v_g = vec![0.0f32; bw * bh]; @@ -280,7 +290,7 @@ pub async fn generate_manual_cleanup_patch( } } - let quality = 100; + let quality = 92; let output_mask = image::imageops::crop_imm(&mask_bitmap, min_x_u32, min_y_u32, crop_w, crop_h).to_image(); @@ -549,7 +559,12 @@ pub async fn invoke_generative_replace_with_mask_def( Rgb([patch_pixel[0], patch_pixel[1], patch_pixel[2]]), ); } else { - color_image.put_pixel(out_x, out_y, Rgb([0, 0, 0])); + let source_pixel = source_image.get_pixel(px_x, px_y); + color_image.put_pixel( + out_x, + out_y, + Rgb([source_pixel[0], source_pixel[1], source_pixel[2]]), + ); } } } diff --git a/src-tauri/src/lens_correction.rs b/src-tauri/src/lens_correction.rs index 9605bd401f..2854b7ccc6 100644 --- a/src-tauri/src/lens_correction.rs +++ b/src-tauri/src/lens_correction.rs @@ -307,7 +307,7 @@ impl Lens { let (k1, k2, k3, model) = if distortions.is_empty() { (0.0, 0.0, 0.0, 0) } else { - distortions.sort_by(|a, b| a.focal.partial_cmp(&b.focal).unwrap_or(Ordering::Equal)); + distortions.sort_by(|a, b| a.focal.partial_cmp(&b.focal).unwrap_or_else(|| if a.focal.is_nan() { Ordering::Greater } else { Ordering::Less })); if let Some(exact) = distortions .iter() @@ -349,7 +349,7 @@ impl Lens { let (tca_vr, tca_vb) = if tcas.is_empty() { (1.0, 1.0) } else { - tcas.sort_by(|a, b| a.focal.partial_cmp(&b.focal).unwrap_or(Ordering::Equal)); + tcas.sort_by(|a, b| a.focal.partial_cmp(&b.focal).unwrap_or_else(|| if a.focal.is_nan() { Ordering::Greater } else { Ordering::Less })); if let Some(exact) = tcas.iter().find(|d| (d.focal - focal_length).abs() < 1e-5) { extract_tca_params(exact) @@ -388,7 +388,7 @@ impl Lens { let target_aperture = aperture.unwrap_or(3.5); let target_distance = distance.unwrap_or(1000.0); - vignettings.sort_by(|a, b| a.focal.partial_cmp(&b.focal).unwrap_or(Ordering::Equal)); + vignettings.sort_by(|a, b| a.focal.partial_cmp(&b.focal).unwrap_or_else(|| if a.focal.is_nan() { Ordering::Greater } else { Ordering::Less })); let find_best_vig = |items: &[&Vignetting]| -> (f64, f64, f64) { let best_aperture_item = items.iter().min_by(|a, b| { @@ -435,7 +435,7 @@ impl Lens { let mut res = (0.0, 0.0, 0.0); let unique_focals: Vec = { let mut f: Vec = vignettings.iter().map(|v| v.focal).collect(); - f.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + f.sort_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| if a.is_nan() { Ordering::Greater } else { Ordering::Less })); f.dedup_by(|a, b| (*a - *b).abs() < 0.01); f }; @@ -560,10 +560,16 @@ pub fn load_lensfun_db(app_handle: &tauri::AppHandle) -> LensDatabase { } #[cfg(not(target_os = "android"))] { - let resource_path = app_handle + let resource_path = match app_handle .path() .resolve("lensfun_db", tauri::path::BaseDirectory::Resource) - .expect("failed to resolve lensfun_db directory"); + { + Ok(path) => path, + Err(e) => { + log::error!("Failed to resolve lensfun_db directory: {}", e); + return combined_db; + } + }; if !resource_path.exists() { log::error!("Lensfun DB directory not found at: {:?}", resource_path); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dc5d11af05..0f735f9906 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,7 @@ mod culling; mod denoising; mod exif_processing; mod export_processing; +mod face_landmark; mod file_management; mod formats; mod gpu_processing; @@ -30,6 +31,7 @@ mod mask_generation; mod negative_conversion; mod panorama_stitching; mod panorama_utils; +mod portrait_processing; mod preset_converter; mod raw_processing; mod tagging; @@ -171,7 +173,7 @@ pub fn generate_transformed_preview( } }; - let (full_res_w, full_res_h) = transformed_full_res.dimensions(); + let (full_res_w, full_res_h) = transformed_full_res.as_ref().dimensions(); let final_preview_base = if full_res_w > preview_dim || full_res_h > preview_dim { downscale_f32_image(&transformed_full_res, preview_dim, preview_dim) @@ -284,17 +286,8 @@ async fn update_wgpu_transform( tokio::task::spawn_blocking(move || { let mut display_lock = context.display.lock().unwrap(); if let Some(display) = display_lock.as_mut() { - display.latest_transform.rect = [payload.x, payload.y, payload.width, payload.height]; - display.latest_transform.clip = [ - payload.clip_x, - payload.clip_y, - payload.clip_width, - payload.clip_height, - ]; - display.latest_transform.window = [payload.window_width, payload.window_height]; - display.latest_transform.bg_primary = payload.bg_primary; - display.latest_transform.bg_secondary = payload.bg_secondary; - display.latest_transform.pixelated = if payload.pixelated { 1.0 } else { 0.0 }; + display.latest_transform = + crate::gpu_processing::DisplayTransform::from_payload(&payload); context.queue.write_buffer( &display.transform_buffer, @@ -431,10 +424,10 @@ fn process_preview_job( let pixel_roi = if is_interactive { roi.map(|(nx, ny, nw, nh)| crate::gpu_processing::Roi { - x: (nx * preview_width as f32).round() as u32, - y: (ny * preview_height as f32).round() as u32, - width: (nw * preview_width as f32).round() as u32, - height: (nh * preview_height as f32).round() as u32, + x: (nx.max(0.0) * preview_width as f32).round() as u32, + y: (ny.max(0.0) * preview_height as f32).round() as u32, + width: (nw.max(0.0) * preview_width as f32).round().max(1.0) as u32, + height: (nh.max(0.0) * preview_height as f32).round().max(1.0) as u32, }) } else { None @@ -524,6 +517,58 @@ fn process_preview_job( return Ok(b"WGPU_RENDER".to_vec()); } + // Portrait post-processing (CPU-based, applied after GPU pass) + let mut final_processed_image = final_processed_image; + if let Some(portrait) = adjustments_clone.get("portrait") { + let face_regions = { + let ai_state_guard = state.ai_state.lock().unwrap(); + if let Some(detector_arc) = ai_state_guard + .as_ref() + .and_then(|s| s.face_landmark_detector.clone()) + { + drop(ai_state_guard); + let mut detector_guard = detector_arc.lock().unwrap(); + crate::portrait_processing::detect_face_regions_onnx( + &final_processed_image, + &mut detector_guard, + ) + } else { + drop(ai_state_guard); + match tauri::async_runtime::block_on(async { + crate::ai_processing::get_or_init_face_landmark_detector( + app_handle, + &state.ai_state, + &state.ai_init_lock, + ) + .await + }) { + Ok(detector_arc) => { + let mut detector_guard = detector_arc.lock().unwrap(); + crate::portrait_processing::detect_face_regions_onnx( + &final_processed_image, + &mut detector_guard, + ) + } + Err(e) => { + log::warn!( + "Face landmark detector unavailable, falling back to skin-tone detection: {}", + e + ); + crate::portrait_processing::detect_face_regions(&final_processed_image) + } + } + } + }; + + if let Err(e) = crate::portrait_processing::apply_portrait_adjustments( + &mut final_processed_image, + portrait, + &face_regions, + ) { + log::warn!("Portrait processing failed: {}", e); + } + } + let final_processed_image = Arc::new(final_processed_image); let final_rgba_image = match &*final_processed_image { DynamicImage::ImageRgba8(img) => img, @@ -865,7 +910,7 @@ fn generate_original_transformed_preview( let default_dim = settings.editor_preview_resolution.unwrap_or(1920); let preview_dim = target_resolution.unwrap_or(default_dim); - let (w, h) = transformed_full_res.dimensions(); + let (w, h) = transformed_full_res.as_ref().dimensions(); let transformed_image = if w > preview_dim || h > preview_dim { downscale_f32_image(transformed_full_res.as_ref(), preview_dim, preview_dim) } else { @@ -1053,13 +1098,20 @@ async fn preview_geometry_transform( let x2 = x0 - dist * (-b); let y2 = y0 - dist * (a); + // Clamp line endpoints to image bounds to prevent overflow in draw_line_segment_mut + let max_x = visualization.width() as f32; + let max_y = visualization.height() as f32; + let x1 = x1.clamp(0.0, max_x); + let y1 = y1.clamp(0.0, max_y); + let x2 = x2.clamp(0.0, max_x); + let y2 = y2.clamp(0.0, max_y); + draw_line_segment_mut(&mut visualization, (x1, y1), (x2, y2), color); - draw_line_segment_mut( - &mut visualization, - (x1 + a, y1 + b), - (x2 + a, y2 + b), - color, - ); + let x3 = (x1 + a).clamp(0.0, max_x); + let y3 = (y1 + b).clamp(0.0, max_y); + let x4 = (x2 + a).clamp(0.0, max_x); + let y4 = (y2 + b).clamp(0.0, max_y); + draw_line_segment_mut(&mut visualization, (x3, y3), (x4, y4), color); } DynamicImage::ImageRgba8(visualization) @@ -1390,6 +1442,8 @@ async fn merge_hdr( return Err("Please select at least two images to merge.".to_string()); } + let _merge_guard = state.hdr_merge_lock.lock().await; + let hdr_result_handle = state.hdr_result.clone(); let settings = load_settings(app_handle.clone()).unwrap_or_default(); @@ -1851,6 +1905,26 @@ fn available_monitor_bounds(_window: &tauri::WebviewWindow) -> Vec Result<(), String> { + if mirror_url.is_empty() { + // SAFETY: This is safe in a single-threaded context during app initialization. + // Environment variable mutations are not thread-safe, but this command is only + // called from the main thread during setup and before any AI model downloads. + unsafe { + std::env::remove_var("RAPIDRAW_HF_MIRROR"); + } + log::info!("AI model mirror URL cleared, using default HuggingFace URLs."); + } else { + // SAFETY: Same as above — single-threaded context during app initialization. + unsafe { + std::env::set_var("RAPIDRAW_HF_MIRROR", &mirror_url); + } + log::info!("AI model mirror URL set to: {}", mirror_url); + } + Ok(()) +} + #[tauri::command] fn frontend_ready( app_handle: tauri::AppHandle, @@ -2265,8 +2339,10 @@ pub fn run() { ai_init_lock: TokioMutex::new(()), export_task_handle: Mutex::new(None), hdr_result: Arc::new(Mutex::new(None)), + hdr_merge_lock: TokioMutex::new(()), panorama_result: Arc::new(Mutex::new(None)), denoise_result: Arc::new(Mutex::new(None)), + denoise_lock: TokioMutex::new(()), indexing_task_handle: Mutex::new(None), lut_cache: Mutex::new(HashMap::new()), initial_file_path: Mutex::new(None), @@ -2323,6 +2399,8 @@ pub fn run() { ai_commands::generate_ai_depth_mask, ai_commands::check_ai_connector_status, ai_commands::test_ai_connector_connection, + ai_commands::generate_ai_rating, + ai_commands::generate_ai_ratings_batch, inpainting::invoke_generative_replace_with_mask_def, inpainting::generate_manual_cleanup_patch, denoising::apply_denoising, @@ -2390,6 +2468,17 @@ pub fn run() { lens_correction::get_lens_distortion_params, negative_conversion::preview_negative_conversion, negative_conversion::convert_negatives, + ai_commands::generate_ai_sky_replace, + ai_commands::generate_ai_background_remove, + ai_commands::apply_super_resolution, + mask_generation::generate_color_range_mask, + mask_generation::generate_luminance_range_mask, + mask_generation::apply_mask_feather, + image_processing::detect_horizon_lines, + image_processing::auto_straighten_horizon, + android_integration::save_to_android_gallery, + android_integration::share_image, + set_ai_model_mirror, ]) .build(tauri::generate_context!()) .expect("error while building tauri application") diff --git a/src-tauri/src/lut_processing.rs b/src-tauri/src/lut_processing.rs index 54f4128a8c..6038cd3cbc 100644 --- a/src-tauri/src/lut_processing.rs +++ b/src-tauri/src/lut_processing.rs @@ -376,18 +376,19 @@ pub fn parse_lut_file(path_str: &str) -> anyhow::Result { } pub fn generate_identity_lut_image(size: u32) -> DynamicImage { - let width = size; - let height = size * size; + let width = size.max(2); + let height = width * width; let mut img = Rgb32FImage::new(width, height); + let denom = (width - 1) as f32; - for z in 0..size { - for y in 0..size { - for x in 0..size { - let r = x as f32 / (size - 1) as f32; - let g = y as f32 / (size - 1) as f32; - let b = z as f32 / (size - 1) as f32; + for z in 0..width { + for y in 0..width { + for x in 0..width { + let r = x as f32 / denom; + let g = y as f32 / denom; + let b = z as f32 / denom; - img.put_pixel(x, z * size + y, Rgb([r, g, b])); + img.put_pixel(x, z * width + y, Rgb([r, g, b])); } } } diff --git a/src-tauri/src/mask_generation.rs b/src-tauri/src/mask_generation.rs index 49dfd59672..f5eb8ea1d1 100644 --- a/src-tauri/src/mask_generation.rs +++ b/src-tauri/src/mask_generation.rs @@ -266,7 +266,10 @@ fn grayscale_dilate(image: &GrayImage, k: u8) -> GrayImage { } } - GrayImage::from_raw(width, height, out).unwrap() + GrayImage::from_raw(width, height, out).unwrap_or_else(|| { + eprintln!("Warning: Failed to create GrayImage of size {}x{}, creating empty mask", width, height); + GrayImage::new(width, height) + }) } fn grayscale_erode(image: &GrayImage, k: u8) -> GrayImage { @@ -307,7 +310,10 @@ fn grayscale_erode(image: &GrayImage, k: u8) -> GrayImage { } } - GrayImage::from_raw(width, height, out).unwrap() + GrayImage::from_raw(width, height, out).unwrap_or_else(|| { + eprintln!("Warning: Failed to create GrayImage of size {}x{}, creating empty mask", width, height); + GrayImage::new(width, height) + }) } fn apply_grow_and_feather(mask: &mut GrayImage, grow: f32, feather: f32, width: u32, height: u32) { @@ -329,7 +335,7 @@ fn apply_grow_and_feather(mask: &mut GrayImage, grow: f32, feather: f32, width: } if feather > 0.0 { - const MAX_FEATHER_SIGMA_PERCENTAGE: f32 = 0.005; + const MAX_FEATHER_SIGMA_PERCENTAGE: f32 = 0.02; let sigma = (feather / 100.0) * base_dimension * MAX_FEATHER_SIGMA_PERCENTAGE; if sigma > 0.01 { @@ -395,7 +401,10 @@ fn render_stroke_layer_parallel( ) -> GrayImage { let mut out_pixels = vec![0u8; (bb_w * bb_h) as usize]; if points.is_empty() || radius <= 0.0 { - return GrayImage::from_raw(bb_w, bb_h, out_pixels).unwrap(); + return GrayImage::from_raw(bb_w, bb_h, out_pixels).unwrap_or_else(|| { + eprintln!("Warning: Failed to create GrayImage of size {}x{}, creating empty mask", bb_w, bb_h); + GrayImage::new(bb_w, bb_h) + }); } struct Segment { @@ -533,7 +542,10 @@ fn render_stroke_layer_parallel( } }); - GrayImage::from_raw(bb_w, bb_h, out_pixels).unwrap() + GrayImage::from_raw(bb_w, bb_h, out_pixels).unwrap_or_else(|| { + eprintln!("Warning: Failed to create GrayImage of size {}x{}, creating empty mask", bb_w, bb_h); + GrayImage::new(bb_w, bb_h) + }) } fn generate_radial_bitmap( @@ -1325,10 +1337,18 @@ pub fn generate_mask_bitmap( crop_offset: (f32, f32), warped_image: Option<&DynamicImage>, ) -> Option { + if width == 0 || height == 0 { + return None; + } + if !mask_def.visible || mask_def.sub_masks.is_empty() { return None; } + if scale <= 0.0 { + return None; + } + let mut final_mask = GrayImage::new(width, height); for sub_mask in &mask_def.sub_masks { @@ -1502,10 +1522,253 @@ pub fn get_cached_or_generate_mask( if let Some(img) = &generated { let mut cache = state.mask_cache.lock().unwrap(); if cache.len() > 50 { - cache.clear(); + // Remove oldest half of entries instead of clearing all + let keys: Vec<_> = cache.keys().take(cache.len() / 2).cloned().collect(); + for k in keys { + cache.remove(&k); + } } cache.insert(key, img.clone()); } generated } + +// --------------------------------------------------------------------------- +// Color Range Mask – HSL-based +// --------------------------------------------------------------------------- + +/// Generate a mask based on HSL color range selection. +/// Parameters are in HSL space: center_hue (0..360), center_sat (0..1), center_lum (0..1) +/// and their respective ranges. `feather` controls edge smoothness. +#[tauri::command] +#[allow(clippy::too_many_arguments)] +pub fn generate_color_range_mask( + state: tauri::State, + center_hue: f32, + center_sat: f32, + center_lum: f32, + hue_range: f32, + sat_range: f32, + lum_range: f32, + feather: f32, +) -> Result, String> { + let loaded_image = state + .original_image + .lock() + .unwrap() + .clone() + .ok_or("No original image loaded")?; + + let (w, h) = loaded_image.image.as_ref().dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + let rgba = loaded_image.image.to_rgba8(); + + // Compute raw mask based on HSL distance + let mut mask_data = vec![0u8; (w * h) as usize]; + + for y in 0..h { + for x in 0..w { + let pixel = rgba.get_pixel(x, y); + let (rf, gf, bf) = ( + pixel[0] as f32 / 255.0, + pixel[1] as f32 / 255.0, + pixel[2] as f32 / 255.0, + ); + + let (hue, sat, lum) = rgb_to_hsl_internal(rf, gf, bf); + + // Hue distance (circular) + let hue_diff = (hue - center_hue).abs(); + let hue_diff = hue_diff.min(360.0 - hue_diff); + let hue_weight = if hue_range > 0.0 { + (1.0 - hue_diff / hue_range).clamp(0.0, 1.0) + } else if hue_diff < 1.0 { + 1.0 + } else { + 0.0 + }; + + // Saturation distance + let sat_diff = (sat - center_sat).abs(); + let sat_weight = if sat_range > 0.0 { + (1.0 - sat_diff / sat_range).clamp(0.0, 1.0) + } else if sat_diff < 0.01 { + 1.0 + } else { + 0.0 + }; + + // Lightness distance + let lum_diff = (lum - center_lum).abs(); + let lum_weight = if lum_range > 0.0 { + (1.0 - lum_diff / lum_range).clamp(0.0, 1.0) + } else if lum_diff < 0.01 { + 1.0 + } else { + 0.0 + }; + + let combined = hue_weight * sat_weight * lum_weight; + let idx = (y * w + x) as usize; + mask_data[idx] = (combined * 255.0).round().clamp(0.0, 255.0) as u8; + } + } + + // Apply feathering (Gaussian blur) + if feather > 0.0 { + let gray_mask = GrayImage::from_raw(w, h, mask_data.clone()) + .ok_or("Failed to create gray image for feathering")?; + let sigma = feather * w.min(h) as f32 * 0.005; + let blurred = imageproc::filter::gaussian_blur_f32(&gray_mask, sigma.max(0.01)); + mask_data = blurred.into_raw(); + } + + Ok(mask_data) +} + +// --------------------------------------------------------------------------- +// Luminance Range Mask +// --------------------------------------------------------------------------- + +/// Generate a mask based on luminance range. +/// Pixels with luminance between min_lum and max_lum get full selection, +/// with Gaussian falloff at the boundaries controlled by `feather`. +#[tauri::command] +pub fn generate_luminance_range_mask( + state: tauri::State, + min_lum: f32, + max_lum: f32, + feather: f32, +) -> Result, String> { + let loaded_image = state + .original_image + .lock() + .unwrap() + .clone() + .ok_or("No original image loaded")?; + + let (w, h) = loaded_image.image.as_ref().dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + let rgba = loaded_image.image.to_rgba8(); + + let min_l = min_lum.clamp(0.0, 1.0); + let max_l = max_lum.clamp(0.0, 1.0); + let feather_sigma = feather.clamp(0.0, 1.0) * 0.1; // Gaussian sigma for transition + + let mut mask_data = vec![0u8; (w * h) as usize]; + + for y in 0..h { + for x in 0..w { + let pixel = rgba.get_pixel(x, y); + let lum = 0.299 * pixel[0] as f32 / 255.0 + + 0.587 * pixel[1] as f32 / 255.0 + + 0.114 * pixel[2] as f32 / 255.0; + + // Gaussian-shaped selection: peak in [min_l, max_l], falloff outside + let intensity = if lum >= min_l && lum <= max_l { + 1.0 + } else if feather_sigma > 1e-6 { + let dist = if lum < min_l { + min_l - lum + } else { + lum - max_l + }; + (-dist * dist / (2.0 * feather_sigma * feather_sigma)).exp() + } else { + 0.0 + }; + + let idx = (y * w + x) as usize; + mask_data[idx] = (intensity * 255.0).round().clamp(0.0, 255.0) as u8; + } + } + + // Apply additional feathering blur if requested + if feather > 0.0 { + let gray_mask = GrayImage::from_raw(w, h, mask_data.clone()) + .ok_or("Failed to create gray image for feathering")?; + let sigma = feather * w.min(h) as f32 * 0.005; + let blurred = imageproc::filter::gaussian_blur_f32(&gray_mask, sigma.max(0.01)); + mask_data = blurred.into_raw(); + } + + Ok(mask_data) +} + +// --------------------------------------------------------------------------- +// Mask Feather +// --------------------------------------------------------------------------- + +/// Apply Gaussian feathering to an existing mask. +/// `mask_data` is raw grayscale bytes (w*h), `feather_radius` controls blur strength. +#[tauri::command] +pub fn apply_mask_feather( + mask_data: Vec, + width: u32, + height: u32, + feather_radius: f32, +) -> Result, String> { + if width == 0 || height == 0 { + return Err("Invalid mask dimensions".to_string()); + } + + if mask_data.len() != (width * height) as usize { + return Err(format!( + "Mask data size {} does not match dimensions {}x{}", + mask_data.len(), + width, + height + )); + } + + if feather_radius <= 0.0 { + return Ok(mask_data); + } + + let gray_mask = GrayImage::from_raw(width, height, mask_data) + .ok_or("Failed to create gray image from mask data")?; + + // Gaussian blur with the specified feather radius + let sigma = feather_radius.clamp(0.01, 100.0); + let blurred = imageproc::filter::gaussian_blur_f32(&gray_mask, sigma); + + Ok(blurred.into_raw()) +} + +// --------------------------------------------------------------------------- +// Internal HSL conversion (for mask_generation module) +// --------------------------------------------------------------------------- + +fn rgb_to_hsl_internal(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let max_c = r.max(g).max(b); + let min_c = r.min(g).min(b); + let l = (max_c + min_c) / 2.0; + + if (max_c - min_c).abs() < 1e-6 { + return (0.0, 0.0, l); + } + + let d = max_c - min_c; + let s = if l > 0.5 { + d / (2.0 - max_c - min_c) + } else { + d / (max_c + min_c) + }; + + let h = if (max_c - r).abs() < 1e-6 { + (g - b) / d + if g < b { 6.0 } else { 0.0 } + } else if (max_c - g).abs() < 1e-6 { + (b - r) / d + 2.0 + } else { + (r - g) / d + 4.0 + }; + + (h * 60.0, s, l) +} diff --git a/src-tauri/src/negative_conversion.rs b/src-tauri/src/negative_conversion.rs index b31517d37f..359d603c00 100644 --- a/src-tauri/src/negative_conversion.rs +++ b/src-tauri/src/negative_conversion.rs @@ -86,7 +86,7 @@ fn analyze_bounds(log_data: &[f32], width: usize, height: usize) -> [ChannelBoun return ChannelBounds { min: 0.0, max: 1.0 }; } - vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| if a.is_nan() { Ordering::Greater } else { Ordering::Less })); let len = vals.len() as f32; @@ -108,7 +108,7 @@ fn run_pipeline( input: &DynamicImage, params: &NegativeConversionParams, override_bounds: Option<[ChannelBounds; 3]>, -) -> DynamicImage { +) -> Result { let rgb = input.to_rgb32f(); let (width, height) = rgb.dimensions(); let raw_pixels = rgb.as_raw(); @@ -132,7 +132,12 @@ fn run_pipeline( let y0 = 1.0 / (1.0 + (k * x0).exp()); let y1 = 1.0 / (1.0 + (-k * (1.0 - x0)).exp()); - let scale = 1.0 / (y1 - y0); + // Avoid division by zero when y1 equals y0 (extreme contrast values) + let scale = if (y1 - y0).abs() < 1e-9 { + 1.0 + } else { + 1.0 / (y1 - y0) + }; out_buffer .par_chunks_mut(3) @@ -140,9 +145,14 @@ fn run_pipeline( .for_each(|(i, out_pixel)| { let idx = i * 3; - let mut n_r = (log_pixels[idx] - bounds[0].min) / (bounds[0].max - bounds[0].min); - let mut n_g = (log_pixels[idx + 1] - bounds[1].min) / (bounds[1].max - bounds[1].min); - let mut n_b = (log_pixels[idx + 2] - bounds[2].min) / (bounds[2].max - bounds[2].min); + // Prevent division by zero by ensuring denominator is at least 1e-6 + let r_range = (bounds[0].max - bounds[0].min).max(1e-6); + let g_range = (bounds[1].max - bounds[1].min).max(1e-6); + let b_range = (bounds[2].max - bounds[2].min).max(1e-6); + + let mut n_r = (log_pixels[idx] - bounds[0].min) / r_range; + let mut n_g = (log_pixels[idx + 1] - bounds[1].min) / g_range; + let mut n_b = (log_pixels[idx + 2] - bounds[2].min) / b_range; n_r = n_r.max(0.0) * params.red_weight; n_g = n_g.max(0.0) * params.green_weight; @@ -175,8 +185,9 @@ fn run_pipeline( out_pixel[2] = b.clamp(0.0, 1.0).powf(gamma_inv); }); - let out_img = Rgb32FImage::from_vec(width, height, out_buffer).unwrap(); - DynamicImage::ImageRgb32F(out_img) + let out_img = Rgb32FImage::from_vec(width, height, out_buffer) + .ok_or_else(|| "Failed to reconstruct image buffer - dimension mismatch".to_string())?; + Ok(DynamicImage::ImageRgb32F(out_img)) } #[tauri::command] @@ -268,7 +279,7 @@ pub async fn preview_negative_conversion( } }; - let processed = run_pipeline(&base_image_for_processing, ¶ms, None); + let processed = run_pipeline(&base_image_for_processing, ¶ms, None)?; let mut buf = Cursor::new(Vec::new()); processed @@ -307,7 +318,7 @@ pub async fn convert_negatives( let img = match read_file_mapped(Path::new(&real_path)) { Ok(mmap) => load_base_image_from_bytes(&mmap, &real_path, false, &settings, None), Err(_) => { - let bytes = fs::read(&real_path).unwrap_or_default(); + let bytes = fs::read(&real_path).map_err(|e| format!("Failed to read file {}: {}", real_path, e))?; load_base_image_from_bytes(&bytes, &real_path, false, &settings, None) } } @@ -323,7 +334,7 @@ pub async fn convert_negatives( .collect(); let bounds = analyze_bounds(&log_pixels, ref_w as usize, ref_h as usize); - let processed = run_pipeline(&img, ¶ms, Some(bounds)); + let processed = run_pipeline(&img, ¶ms, Some(bounds))?; let p = Path::new(&real_path); let parent = p.parent().unwrap_or(Path::new("")); diff --git a/src-tauri/src/panorama_stitching.rs b/src-tauri/src/panorama_stitching.rs index aa8eea743b..e1b2e213be 100644 --- a/src-tauri/src/panorama_stitching.rs +++ b/src-tauri/src/panorama_stitching.rs @@ -189,7 +189,7 @@ fn stitch_images(image_paths: Vec, app_handle: AppHandle) -> Result> = image_paths .par_iter() @@ -357,7 +357,7 @@ fn stitch_images(image_paths: Vec, app_handle: AppHandle) -> Result, -) -> (Vec, HashMap>) { +) -> Result<(Vec, HashMap>), String> { if images.is_empty() { - return (vec![], HashMap::new()); + return Ok((vec![], HashMap::new())); } let n = images.len(); if n < 2 { @@ -454,7 +454,7 @@ fn build_stitching_order( if n == 1 { homographies.insert(0, Matrix3::identity()); } - return ((0..n).collect(), homographies); + return Ok(((0..n).collect(), homographies)); } let mut edges = Vec::new(); @@ -506,9 +506,9 @@ fn build_stitching_order( } else if let Some(m) = matches.get(&(u, v)) { m.homography .try_inverse() - .expect("Failed to invert homography for MST edge") + .ok_or_else(|| format!("Failed to invert homography for MST edge between {} and {}", v, u))? } else { - panic!("Match not found for MST edge between {} and {}", u, v); + return Err(format!("Match not found for MST edge between {} and {}", u, v)); }; let h_v_global = h_u_global * h_vu; @@ -518,5 +518,5 @@ fn build_stitching_order( } } - (ordered_indices, global_homographies) + Ok((ordered_indices, global_homographies)) } diff --git a/src-tauri/src/panorama_utils/processing.rs b/src-tauri/src/panorama_utils/processing.rs index 65824d3e64..d320942847 100644 --- a/src-tauri/src/panorama_utils/processing.rs +++ b/src-tauri/src/panorama_utils/processing.rs @@ -125,22 +125,20 @@ fn non_maximal_suppression(corners: &[Corner], radius: f32) -> Vec { result } -pub fn generate_brief_pairs() -> Vec<(Point2, Point2)> { +pub fn generate_brief_pairs() -> Result, Point2)>, String> { let mut rng = StdRng::seed_from_u64(12345); let half_patch = BRIEF_PATCH_SIZE as i32 / 2; - let distribution = match rand::distr::Uniform::new(-half_patch, half_patch) { - Ok(dist) => dist, - Err(e) => panic!("Failed to create uniform distribution: {}", e), - }; + let distribution = rand::distr::Uniform::new(-half_patch, half_patch) + .map_err(|e| format!("Failed to create uniform distribution: {}", e))?; - (0..BRIEF_DESCRIPTOR_SIZE) + Ok((0..BRIEF_DESCRIPTOR_SIZE) .map(|_| { ( Point2::new(distribution.sample(&mut rng), distribution.sample(&mut rng)), Point2::new(distribution.sample(&mut rng), distribution.sample(&mut rng)), ) }) - .collect() + .collect()) } fn compute_brief_descriptor( @@ -342,7 +340,7 @@ pub fn compute_homography(points: &[(Point2, Point2)]) -> Option, +} + +// --------------------------------------------------------------------------- +// Helper: RGB <-> f32 conversions +// --------------------------------------------------------------------------- + +#[inline(always)] +fn rgb_to_f32(r: u8, g: u8, b: u8) -> (f32, f32, f32) { + (r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0) +} + +#[inline(always)] +fn f32_to_rgb(r: f32, g: f32, b: f32) -> (u8, u8, u8) { + ( + (r.clamp(0.0, 1.0) * 255.0).round() as u8, + (g.clamp(0.0, 1.0) * 255.0).round() as u8, + (b.clamp(0.0, 1.0) * 255.0).round() as u8, + ) +} + +/// Compute luminance from RGB [0..1] +#[inline(always)] +fn luminance(r: f32, g: f32, b: f32) -> f32 { + 0.299 * r + 0.587 * g + 0.114 * b +} + +/// Gaussian function +#[inline(always)] +fn gaussian(x: f32, sigma: f32) -> f32 { + if sigma <= 0.0 { + return if x == 0.0 { 1.0 } else { 0.0 }; + } + let s2 = sigma * sigma; + (-x * x / (2.0 * s2)).exp() +} + +// --------------------------------------------------------------------------- +// 1. Skin Smoothing – Bilateral Filter with skin mask +// --------------------------------------------------------------------------- + +/// Apply bilateral filter for skin smoothing, restricted to skin regions. +/// `strength` controls the range sigma (0..1 maps to range_sigma 10..75). +/// `detail_preserve` modulates how much edge detail is retained (0..1). +/// Spatial sigma is fixed at 3.0 as specified. +/// When `face_regions` is empty, falls back to global smoothing (for backward compat). +pub fn apply_skin_smoothing( + img: &mut DynamicImage, + strength: f32, + detail_preserve: f32, + face_regions: &[FaceRegion], +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + let mut rgba = img.to_rgba8(); + apply_skin_smoothing_rgba(&mut rgba, w, h, strength, detail_preserve, face_regions); + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +/// Core bilateral-filter skin smoothing operating directly on an RgbaImage buffer. +/// Used by `apply_one_click_beauty` to avoid repeated DynamicImage ↔ RgbaImage +/// conversions across multiple pipeline steps. +fn apply_skin_smoothing_rgba( + rgba: &mut RgbaImage, + w: u32, + h: u32, + strength: f32, + detail_preserve: f32, + face_regions: &[FaceRegion], +) { + let src = rgba.clone(); + + let range_sigma = 10.0 + strength.clamp(0.0, 1.0) * 65.0; + let spatial_sigma = 3.0_f32; + let effective_range_sigma = range_sigma * (1.0 - detail_preserve.clamp(0.0, 1.0) * 0.7); + + let radius = (spatial_sigma * 3.0).ceil() as i32; + + let src_raw = src.as_raw(); + + let w_usize = w as usize; + let h_usize = h as usize; + + let skin_mask = build_feathered_skin_mask(&src, w, h, face_regions); + + // Write results directly into rgba's buffer via a mutable slice + let result_slice: &mut [u8] = &mut **rgba; + + result_slice + .par_chunks_mut(4) + .enumerate() + .for_each(|(idx, pixel)| { + let mask_val = skin_mask[idx]; + + let center_offset = idx * 4; + + if mask_val <= 0.001 { + pixel[0] = src_raw[center_offset]; + pixel[1] = src_raw[center_offset + 1]; + pixel[2] = src_raw[center_offset + 2]; + pixel[3] = src_raw[center_offset + 3]; + return; + } + + let cr = src_raw[center_offset] as f32; + let cg = src_raw[center_offset + 1] as f32; + let cb = src_raw[center_offset + 2] as f32; + + let mut sum_r = 0.0f32; + let mut sum_g = 0.0f32; + let mut sum_b = 0.0f32; + let mut w_sum = 0.0f32; + + let x = (idx % w_usize) as i32; + let y = (idx / w_usize) as i32; + + for ky in -radius..=radius { + let ny = (y + ky).clamp(0, (h_usize - 1) as i32) as usize; + for kx in -radius..=radius { + let nx = (x + kx).clamp(0, (w_usize - 1) as i32) as usize; + + let spatial_dist = ((kx * kx + ky * ky) as f32).sqrt(); + let ws = gaussian(spatial_dist, spatial_sigma); + + let n_offset = (ny * w_usize + nx) * 4; + let nr = src_raw[n_offset] as f32; + let ng = src_raw[n_offset + 1] as f32; + let nb = src_raw[n_offset + 2] as f32; + + let color_dist = + ((cr - nr) * (cr - nr) + (cg - ng) * (cg - ng) + (cb - nb) * (cb - nb)) + .sqrt(); + + let wr = gaussian(color_dist, effective_range_sigma); + + let weight = ws * wr; + sum_r += nr * weight; + sum_g += ng * weight; + sum_b += nb * weight; + w_sum += weight; + } + } + + if w_sum > 0.0 { + let inv = 1.0 / w_sum; + let smooth_r = (sum_r * inv).round().clamp(0.0, 255.0) as u8; + let smooth_g = (sum_g * inv).round().clamp(0.0, 255.0) as u8; + let smooth_b = (sum_b * inv).round().clamp(0.0, 255.0) as u8; + + pixel[0] = ((smooth_r as f32) * mask_val + + (src_raw[center_offset] as f32) * (1.0 - mask_val)) + .round() as u8; + pixel[1] = ((smooth_g as f32) * mask_val + + (src_raw[center_offset + 1] as f32) * (1.0 - mask_val)) + .round() as u8; + pixel[2] = ((smooth_b as f32) * mask_val + + (src_raw[center_offset + 2] as f32) * (1.0 - mask_val)) + .round() as u8; + pixel[3] = src_raw[center_offset + 3]; + } else { + pixel[0] = src_raw[center_offset]; + pixel[1] = src_raw[center_offset + 1]; + pixel[2] = src_raw[center_offset + 2]; + pixel[3] = src_raw[center_offset + 3]; + } + }); +} + +/// Build a feathered skin mask: face-region ellipses intersected with skin confidence. +/// Returns a float mask [0..1] that is 1.0 for definite skin pixels and +/// falls off smoothly at face boundaries and skin-tone edges. +fn build_feathered_skin_mask( + rgba: &RgbaImage, + w: u32, + h: u32, + face_regions: &[FaceRegion], +) -> Vec { + let area = (w * h) as usize; + + // If no face regions, return full mask (fallback to global smoothing) + if face_regions.is_empty() { + return vec![1.0f32; area]; + } + + let mut mask = vec![0.0f32; area]; + + // Step 1: Elliptical falloff mask for each face region + for face in face_regions { + let (fx, fy, fw, fh) = face.face_rect; + let cx = fx as f32 + fw as f32 / 2.0; + let cy = fy as f32 + fh as f32 / 2.0; + + // Slightly expand face region to include neck and nearby skin + let rx = fw as f32 * 0.65; + let ry = fh as f32 * 0.65; + + let x_start = (fx as i32 - (fw as f32 * 0.3) as i32).max(0) as u32; + let x_end = (fx + fw + (fw as f32 * 0.3) as u32).min(w - 1); + let y_start = (fy as i32 - (fh as f32 * 0.3) as i32).max(0) as u32; + let y_end = (fy + fh + (fh as f32 * 0.3) as u32).min(h - 1); + + for y in y_start..=y_end { + for x in x_start..=x_end { + let dx = x as f32 - cx; + let dy = y as f32 - cy; + let norm_x = dx / rx.max(1.0); + let norm_y = dy / ry.max(1.0); + let dist_sq = norm_x * norm_x + norm_y * norm_y; + + let elliptic_weight = if dist_sq < 1.0 { 1.0 - dist_sq } else { 0.0 }; + + let idx = (y * w + x) as usize; + mask[idx] = mask[idx].max(elliptic_weight); + } + } + } + + // Step 2: Multiply by per-pixel skin confidence for finer detail + for y in 0..h { + for x in 0..w { + let idx = (y * w + x) as usize; + if mask[idx] > 0.0 { + let p = rgba.get_pixel(x, y); + let skin_conf = skin_confidence(p[0], p[1], p[2]); + // Use a soft threshold: 0.25 = start of falloff, 0.45 = full + let skin_weight = if skin_conf >= 0.45 { + 1.0 + } else if skin_conf > 0.25 { + (skin_conf - 0.25) / 0.20 + } else { + 0.0 + }; + mask[idx] *= skin_weight; + } + } + } + + mask +} + +// --------------------------------------------------------------------------- +// 2. Blemish Removal – Content-Aware Fill +// --------------------------------------------------------------------------- + +/// Remove blemish spots using content-aware fill from surrounding pixels. +/// Each spot is (x_center, y_center, radius). `blend_radius` controls +/// the feathering at the edge of the patch. +pub fn apply_blemish_removal( + img: &mut DynamicImage, + spots: &[(u32, u32, u32)], + blend_radius: f32, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + let mut rgba = img.to_rgba8(); + + for &(cx, cy, radius) in spots { + let r = radius.max(1); + let blend_r = blend_radius.clamp(0.0, 1.0) * r as f32; + + // Bug #8 fix: ring must be far enough that the blend/feather zone + // does NOT reach back into the sample ring. + // Old: sample_ring = r + (r/3).max(1) → for r=2, ring=3, but blend + // can extend to r+blend_r which overlaps ring. + // New: minimum gap of (r + blend_r.ceil() + 2) pixels. + let min_gap = (r as f32 + blend_r + 2.0).ceil() as u32; + let sample_ring = min_gap.max(r + (r / 3).max(1)); + + let num_samples = (2.0 * std::f32::consts::PI * sample_ring as f32).ceil() as u32; + let mut ring_colors: Vec<(f32, f32, f32, f32)> = Vec::new(); + + for i in 0..num_samples { + let angle = 2.0 * std::f32::consts::PI * i as f32 / num_samples as f32; + let sx = (cx as f32 + sample_ring as f32 * angle.cos()).round() as i32; + let sy = (cy as f32 + sample_ring as f32 * angle.sin()).round() as i32; + + if sx >= 0 && sx < w as i32 && sy >= 0 && sy < h as i32 { + let p = rgba.get_pixel(sx as u32, sy as u32); + ring_colors.push((p[0] as f32, p[1] as f32, p[2] as f32, p[3] as f32)); + } + } + + if ring_colors.is_empty() { + continue; + } + + // Fill each pixel inside the blemish by weighted average from ring + let x_min = (cx as i32 - (r + blend_r.ceil() as u32) as i32).max(0) as u32; + let x_max = (cx + r + blend_r.ceil() as u32).min(w - 1); + let y_min = (cy as i32 - (r + blend_r.ceil() as u32) as i32).max(0) as u32; + let y_max = (cy + r + blend_r.ceil() as u32).min(h - 1); + + for y in y_min..=y_max { + for x in x_min..=x_max { + let dx = x as f32 - cx as f32; + let dy = y as f32 - cy as f32; + let dist = (dx * dx + dy * dy).sqrt(); + + let outer_edge = r as f32; + let fade_end = outer_edge + blend_r; + + if dist > fade_end { + continue; + } + + // Compute weight for blending based on angle-matched ring samples + let angle = dy.atan2(dx); + let mut sum_r = 0.0f32; + let mut sum_g = 0.0f32; + let mut sum_b = 0.0f32; + let mut sum_a = 0.0f32; + let mut wt = 0.0f32; + + for (ri, &(rr, rg, rb, ra)) in ring_colors.iter().enumerate() { + let ring_angle = + 2.0 * std::f32::consts::PI * ri as f32 / ring_colors.len() as f32; + let angle_diff = (angle - ring_angle).abs(); + let angle_diff = angle_diff.min(2.0 * std::f32::consts::PI - angle_diff); + // Bug #9 fix: sigma increased from 1.0 to 3.0 radians. + // Old σ=1.0 gave ±57° effective range → strong directional bias. + // New σ=3.0 gives ~±171° range → smooth 360° blending. + let aw = (-angle_diff * angle_diff / 3.0).exp(); + sum_r += rr * aw; + sum_g += rg * aw; + sum_b += rb * aw; + sum_a += ra * aw; + wt += aw; + } + + if wt > 0.0 { + let inv_wt = 1.0 / wt; + let fill_r = sum_r * inv_wt; + let fill_g = sum_g * inv_wt; + let fill_b = sum_b * inv_wt; + let fill_a = sum_a * inv_wt; + + // Blend factor: 1.0 at center, fading to 0.0 at fade_end + let blend = if dist <= outer_edge { + 1.0 + } else if blend_r > 0.0 { + 1.0 - (dist - outer_edge) / blend_r + } else { + 1.0 + }; + let blend = blend.clamp(0.0, 1.0); + + let orig = rgba.get_pixel(x, y); + let or = orig[0] as f32; + let og = orig[1] as f32; + let ob = orig[2] as f32; + let oa = orig[3] as f32; + + rgba.put_pixel( + x, + y, + Rgba([ + (or * (1.0 - blend) + fill_r * blend) + .round() + .clamp(0.0, 255.0) as u8, + (og * (1.0 - blend) + fill_g * blend) + .round() + .clamp(0.0, 255.0) as u8, + (ob * (1.0 - blend) + fill_b * blend) + .round() + .clamp(0.0, 255.0) as u8, + (oa * (1.0 - blend) + fill_a * blend) + .round() + .clamp(0.0, 255.0) as u8, + ]), + ); + } + } + } + } + + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +// --------------------------------------------------------------------------- +// 3. Face Reshape – Liquify (mesh-based local warp) +// --------------------------------------------------------------------------- + +/// Apply face reshaping using inverse-mapping liquify warp. +/// `slim_amount` controls horizontal pinching of the jaw region. +/// `jaw_amount` controls vertical compression of the jaw. +pub fn apply_face_reshape( + img: &mut DynamicImage, + face_regions: &[FaceRegion], + slim_amount: f32, + jaw_amount: f32, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + let mut rgba = img.to_rgba8(); + apply_face_reshape_rgba(&mut rgba, w, h, face_regions, slim_amount, jaw_amount); + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +/// Core face reshape operating directly on RgbaImage. +/// Only processes the union of face bounding boxes expanded by the warp radius, +/// avoiding a full w×h scan (Bug #14 fix). +fn apply_face_reshape_rgba( + rgba: &mut RgbaImage, + w: u32, + h: u32, + face_regions: &[FaceRegion], + slim_amount: f32, + jaw_amount: f32, +) { + if face_regions.is_empty() { + return; + } + + let slim = slim_amount.clamp(-1.0, 1.0); + let jaw = jaw_amount.clamp(-1.0, 1.0); + + // Compute the affected region: union of all face bboxes expanded by max radius + let mut bb_min_x = w; + let mut bb_min_y = h; + let mut bb_max_x = 0u32; + let mut bb_max_y = 0u32; + for face in face_regions { + let (fx, fy, fw, fh) = face.face_rect; + // Expand by half the face dimensions (the elliptical influence radius) + let margin_x = (fw as f32 * 0.6).ceil() as u32; + let margin_y = (fh as f32 * 0.6).ceil() as u32; + bb_min_x = bb_min_x.min(fx.saturating_sub(margin_x)); + bb_min_y = bb_min_y.min(fy.saturating_sub(margin_y)); + bb_max_x = bb_max_x.max((fx + fw + margin_x).min(w - 1)); + bb_max_y = bb_max_y.max((fy + fh + margin_y).min(h - 1)); + } + + let src = rgba.clone(); + + for y_out in bb_min_y..=bb_max_y { + for x_out in bb_min_x..=bb_max_x { + // Bug #6 fix: both slim and jaw must operate on the original + // (x_out, y_out), not chain effects. Store original coords and + // accumulate displacements independently. + let orig_x = x_out as f32; + let orig_y = y_out as f32; + let mut sx = orig_x; + let mut sy = orig_y; + + for face in face_regions { + let (fx, fy, fw, fh) = face.face_rect; + let face_cx = fx as f32 + fw as f32 / 2.0; + let face_cy = fy as f32 + fh as f32 / 2.0; + + // Slim: horizontal displacement toward center from ORIGINAL position + if slim.abs() > 1e-4 { + let dx = orig_x - face_cx; + let dy = orig_y - face_cy; + let norm_x = dx / (fw as f32 / 2.0).max(1.0); + let norm_y = dy / (fh as f32 / 2.0).max(1.0); + let dist_sq = norm_x * norm_x + norm_y * norm_y; + if dist_sq < 1.0 { + let lower_weight = (norm_y * 0.5 + 0.5).clamp(0.0, 1.0); + let falloff = 1.0 - dist_sq; + let strength = slim * falloff * falloff * lower_weight * 0.3; + sx -= dx * strength; + } + } + + // Jaw: vertical compression from ORIGINAL position + if jaw.abs() > 1e-4 { + let dx = orig_x - face_cx; + let dy = orig_y - face_cy; + let norm_y = dy / (fh as f32 / 2.0).max(1.0); + if norm_y > 0.0 && norm_y < 1.0 { + let norm_x = dx / (fw as f32 / 2.0).max(1.0); + let dist_sq = norm_x * norm_x + norm_y * norm_y; + if dist_sq < 1.0 { + let falloff = 1.0 - dist_sq; + let strength = jaw * falloff * falloff * 0.15; + sy -= dy * strength; + } + } + } + } + + let px = sample_bilinear_rgba(&src, w, h, sx, sy); + rgba.put_pixel(x_out, y_out, px); + } + } +} + +// --------------------------------------------------------------------------- +// 4. Eye Enlarge – Spherical Magnification Warp +// --------------------------------------------------------------------------- + +/// Enlarge eyes using local spherical magnification. +/// Each region is (x_center, y_center, radius). `amount` 0..1 controls +/// the magnification strength. +pub fn apply_eye_enlarge( + img: &mut DynamicImage, + eye_regions: &[(u32, u32, u32)], + amount: f32, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + if amount.abs() < 1e-4 || eye_regions.is_empty() { + return Ok(()); + } + let mut rgba = img.to_rgba8(); + apply_eye_enlarge_rgba(&mut rgba, w, h, eye_regions, amount); + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +/// Core eye enlarge operating directly on RgbaImage. +/// Only processes the union of eye region bounding boxes, avoiding a full +/// w×h scan (Bug #14 fix). +fn apply_eye_enlarge_rgba( + rgba: &mut RgbaImage, + w: u32, + h: u32, + eye_regions: &[(u32, u32, u32)], + amount: f32, +) { + // Compute the affected region: union of all eye bounding boxes + let mut bb_min_x = w; + let mut bb_min_y = h; + let mut bb_max_x = 0u32; + let mut bb_max_y = 0u32; + for &(ecx, ecy, er) in eye_regions { + let r = er.max(1); + bb_min_x = bb_min_x.min(ecx.saturating_sub(r)); + bb_min_y = bb_min_y.min(ecy.saturating_sub(r)); + bb_max_x = bb_max_x.max((ecx + r).min(w - 1)); + bb_max_y = bb_max_y.max((ecy + r).min(h - 1)); + } + + let magnify = 1.0 + amount.clamp(0.0, 1.0) * 0.5; + let src = rgba.clone(); + + for y_out in bb_min_y..=bb_max_y { + for x_out in bb_min_x..=bb_max_x { + let mut sx = x_out as f32; + let mut sy = y_out as f32; + + for &(ecx, ecy, er) in eye_regions { + let dx = x_out as f32 - ecx as f32; + let dy = y_out as f32 - ecy as f32; + let dist = (dx * dx + dy * dy).sqrt(); + let r = er.max(1) as f32; + + if dist < r { + let norm = dist / r; + let weight = 1.0 - norm * norm; + let effective_magnify = 1.0 + (magnify - 1.0) * weight; + + sx = ecx as f32 + dx / effective_magnify; + sy = ecy as f32 + dy / effective_magnify; + break; + } + } + + let px = sample_bilinear_rgba(&src, w, h, sx, sy); + rgba.put_pixel(x_out, y_out, px); + } + } +} + +// --------------------------------------------------------------------------- +// 5. Teeth Whitening – Hue Selection + Brightness Lift +// --------------------------------------------------------------------------- + +/// Whiten teeth by selecting pixels in the yellow/desaturated range within +/// each region and boosting brightness while reducing saturation. +pub fn apply_teeth_whitening( + img: &mut DynamicImage, + regions: &[(u32, u32, u32)], + brightness: f32, + saturation: f32, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + let mut rgba = img.to_rgba8(); + apply_teeth_whitening_rgba(&mut rgba, w, h, regions, brightness, saturation); + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +fn apply_teeth_whitening_rgba( + rgba: &mut RgbaImage, + w: u32, + h: u32, + regions: &[(u32, u32, u32)], + brightness: f32, + saturation: f32, +) { + let brightness_factor = 1.0 + brightness.clamp(0.0, 1.0) * 0.5; + let sat_factor = 1.0 - saturation.clamp(0.0, 1.0) * 0.8; + + for &(cx, cy, radius) in regions { + let r = radius.max(1) as i32; + let x_min = (cx as i32 - r).max(0) as u32; + let x_max = (cx as i32 + r).min(w as i32 - 1) as u32; + let y_min = (cy as i32 - r).max(0) as u32; + let y_max = (cy as i32 + r).min(h as i32 - 1) as u32; + + for y in y_min..=y_max { + for x in x_min..=x_max { + let dx = x as f32 - cx as f32; + let dy = y as f32 - cy as f32; + let dist = (dx * dx + dy * dy).sqrt(); + if dist > r as f32 { + continue; + } + + let pixel = rgba.get_pixel(x, y); + let (rf, gf, bf) = rgb_to_f32(pixel[0], pixel[1], pixel[2]); + + let (hue, sat, lum) = rgb_to_hsl(rf, gf, bf); + + let is_tooth_hue = hue > 20.0 && hue < 80.0; + let is_tooth_sat = sat < 0.55; + let is_tooth_lum = lum > 0.25; + + if is_tooth_hue && is_tooth_sat && is_tooth_lum { + let weight = 1.0 - (dist / r as f32); + let weight = weight * weight; + + let new_sat = sat * (1.0 - weight * (1.0 - sat_factor)); + let new_lum = lum + (1.0 - lum) * weight * (brightness_factor - 1.0) * 0.5; + + let (nr, ng, nb) = hsl_to_rgb(hue, new_sat, new_lum.clamp(0.0, 1.0)); + let (r8, g8, b8) = f32_to_rgb(nr, ng, nb); + + rgba.put_pixel(x, y, Rgba([r8, g8, b8, pixel[3]])); + } + } + } + } +} + +// --------------------------------------------------------------------------- +// 6. Eye Brighten – Increase brightness and contrast of eye regions +// --------------------------------------------------------------------------- + +/// Brighten eyes by increasing luminance and contrast within eye regions. +pub fn apply_eye_brighten( + img: &mut DynamicImage, + regions: &[(u32, u32, u32)], + brightness: f32, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + let mut rgba = img.to_rgba8(); + apply_eye_brighten_rgba(&mut rgba, w, h, regions, brightness); + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +fn apply_eye_brighten_rgba( + rgba: &mut RgbaImage, + w: u32, + h: u32, + regions: &[(u32, u32, u32)], + brightness: f32, +) { + let bright = brightness.clamp(0.0, 1.0) * 0.3; + + for &(cx, cy, radius) in regions { + let r = radius.max(1) as i32; + let x_min = (cx as i32 - r).max(0) as u32; + let x_max = (cx as i32 + r).min(w as i32 - 1) as u32; + let y_min = (cy as i32 - r).max(0) as u32; + let y_max = (cy as i32 + r).min(h as i32 - 1) as u32; + + for y in y_min..=y_max { + for x in x_min..=x_max { + let dx = x as f32 - cx as f32; + let dy = y as f32 - cy as f32; + let dist = (dx * dx + dy * dy).sqrt(); + if dist > r as f32 { + continue; + } + + let weight = 1.0 - (dist / r as f32); + let weight = weight * weight; + + let pixel = rgba.get_pixel(x, y); + let (rf, gf, bf) = rgb_to_f32(pixel[0], pixel[1], pixel[2]); + let lum = luminance(rf, gf, bf); + + // Dark-region protection: pupil and very dark pixels should not + // be brightened, otherwise the pupil turns gray/unnatural. + let dark_protection = (lum / 0.20).clamp(0.0, 1.0); + let effective_weight = weight * dark_protection; + + let boost = bright * effective_weight; + let new_r = rf + (1.0 - rf) * boost; + let new_g = gf + (1.0 - gf) * boost; + let new_b = bf + (1.0 - bf) * boost; + + let contrast_boost = 1.0 + effective_weight * bright * 0.5; + let mid = 0.5; + let cr = mid + (new_r - mid) * contrast_boost; + let cg = mid + (new_g - mid) * contrast_boost; + let cb = mid + (new_b - mid) * contrast_boost; + + let (r8, g8, b8) = f32_to_rgb(cr, cg, cb); + rgba.put_pixel(x, y, Rgba([r8, g8, b8, pixel[3]])); + } + } + } +} + +// --------------------------------------------------------------------------- +// 7. Makeup – Lip color, blush, eyebrow coloring +// --------------------------------------------------------------------------- + +/// Apply makeup effect (lipstick, blush, or eyebrow color) to specified regions. +/// `makeup_type` is one of "lip", "blush", "eyebrow". +/// `color` is the target RGB color. +/// `opacity` controls the blend strength (0..1). +pub fn apply_makeup( + img: &mut DynamicImage, + makeup_type: &str, + regions: &[(u32, u32, u32)], + color: (u8, u8, u8), + opacity: f32, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + let mut rgba = img.to_rgba8(); + let alpha = opacity.clamp(0.0, 1.0); + let (mr, mg, mb) = rgb_to_f32(color.0, color.1, color.2); + + for &(cx, cy, radius) in regions { + let r = radius.max(1) as i32; + let x_min = (cx as i32 - r).max(0) as u32; + let x_max = (cx as i32 + r).min(w as i32 - 1) as u32; + let y_min = (cy as i32 - r).max(0) as u32; + let y_max = (cy as i32 + r).min(h as i32 - 1) as u32; + + for y in y_min..=y_max { + for x in x_min..=x_max { + let dx = x as f32 - cx as f32; + let dy = y as f32 - cy as f32; + let dist = (dx * dx + dy * dy).sqrt(); + if dist > r as f32 { + continue; + } + + let pixel = rgba.get_pixel(x, y); + let (rf, gf, bf) = rgb_to_f32(pixel[0], pixel[1], pixel[2]); + let (hue, sat, _lum) = rgb_to_hsl(rf, gf, bf); + + // Check if the pixel's hue matches the makeup target area + let matches = match makeup_type { + "lip" => { + // Bug #10 fix: narrowed hue range and raised saturation threshold. + // Old range covered 0°~20° AND 300°~360° (≈80° total) which + // included gums and tongue. New range is 0°~20° AND 340°~360° + // (≈40° total), with sat > 0.2 to avoid pale skin. + (hue < 20.0 || hue > 340.0) && sat > 0.2 + } + "blush" => { + // Cheeks: warm hues, low-medium saturation + (hue < 30.0 || hue > 330.0) && sat > 0.05 + } + "eyebrow" => { + // Eyebrows: low saturation (mostly gray/brown) + sat < 0.3 + } + _ => true, // For unknown types, apply to all pixels in region + }; + + if !matches { + continue; + } + + // Spatial falloff + let weight = 1.0 - (dist / r as f32); + let weight = weight * weight; + let effective_alpha = alpha * weight; + + // Blend: overlay the makeup color, preserving some original luminance. + // Use a perceptual luminance blend instead of a raw ratio to avoid + // blowing out dark makeup on bright skin (e.g. dark lipstick on + // light lips becoming white due to ratio > 1.0). + let orig_lum = luminance(rf, gf, bf); + let makeup_lum = luminance(mr, mg, mb); + let target_lum = orig_lum * 0.65 + makeup_lum * 0.35; + let lum_scale = if makeup_lum > 0.001 { + target_lum / makeup_lum + } else { + 1.0 + }; + + let adj_mr = (mr * lum_scale).clamp(0.0, 1.0); + let adj_mg = (mg * lum_scale).clamp(0.0, 1.0); + let adj_mb = (mb * lum_scale).clamp(0.0, 1.0); + + let nr = rf * (1.0 - effective_alpha) + adj_mr * effective_alpha; + let ng = gf * (1.0 - effective_alpha) + adj_mg * effective_alpha; + let nb = bf * (1.0 - effective_alpha) + adj_mb * effective_alpha; + + let (r8, g8, b8) = f32_to_rgb(nr, ng, nb); + rgba.put_pixel(x, y, Rgba([r8, g8, b8, pixel[3]])); + } + } + } + + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +/// Bilinear sampling from an RGBA8 source image at sub-pixel coordinates. +fn sample_bilinear_rgba(src: &RgbaImage, w: u32, h: u32, x: f32, y: f32) -> Rgba { + let x0 = x.floor().max(0.0).min(w as f32 - 1.0) as u32; + let y0 = y.floor().max(0.0).min(h as f32 - 1.0) as u32; + let x1 = (x0 + 1).min(w - 1); + let y1 = (y0 + 1).min(h - 1); + + let fx = x - x0 as f32; + let fy = y - y0 as f32; + let fx = fx.clamp(0.0, 1.0); + let fy = fy.clamp(0.0, 1.0); + + let p00 = src.get_pixel(x0, y0); + let p10 = src.get_pixel(x1, y0); + let p01 = src.get_pixel(x0, y1); + let p11 = src.get_pixel(x1, y1); + + let mut result = [0u8; 4]; + for c in 0..4 { + let v00 = p00[c] as f32; + let v10 = p10[c] as f32; + let v01 = p01[c] as f32; + let v11 = p11[c] as f32; + + let top = v00 * (1.0 - fx) + v10 * fx; + let bot = v01 * (1.0 - fx) + v11 * fx; + let val = top * (1.0 - fy) + bot * fy; + result[c] = val.round().clamp(0.0, 255.0) as u8; + } + + Rgba(result) +} + +/// Convert RGB [0..1] to HSL (h: 0..360, s: 0..1, l: 0..1) +fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let max_c = r.max(g).max(b); + let min_c = r.min(g).min(b); + let l = (max_c + min_c) / 2.0; + + if (max_c - min_c).abs() < 1e-6 { + return (0.0, 0.0, l); + } + + let d = max_c - min_c; + let s = if l > 0.5 { + d / (2.0 - max_c - min_c) + } else { + d / (max_c + min_c) + }; + + let h = if (max_c - r).abs() < 1e-6 { + (g - b) / d + if g < b { 6.0 } else { 0.0 } + } else if (max_c - g).abs() < 1e-6 { + (b - r) / d + 2.0 + } else { + (r - g) / d + 4.0 + }; + + (h * 60.0, s, l) +} + +/// Convert HSL (h: 0..360, s: 0..1, l: 0..1) to RGB [0..1] +fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) { + if s < 1e-6 { + return (l, l, l); + } + + let hue_to_rgb = |p: f32, q: f32, mut t: f32| -> f32 { + if t < 0.0 { + t += 1.0; + } + if t > 1.0 { + t -= 1.0; + } + if t < 1.0 / 6.0 { + return p + (q - p) * 6.0 * t; + } + if t < 1.0 / 2.0 { + return q; + } + if t < 2.0 / 3.0 { + return p + (q - p) * (2.0 / 3.0 - t) * 6.0; + } + p + }; + + let q = if l < 0.5 { + l * (1.0 + s) + } else { + l + s - l * s + }; + let p = 2.0 * l - q; + let h_norm = h / 360.0; + + let r = hue_to_rgb(p, q, h_norm + 1.0 / 3.0); + let g = hue_to_rgb(p, q, h_norm); + let b = hue_to_rgb(p, q, h_norm - 1.0 / 3.0); + + (r, g, b) +} + +// --------------------------------------------------------------------------- +// 8. Face Region Detection – Multi-model skin detection + facial feature verification +// --------------------------------------------------------------------------- + +/// Detect face regions using multi-model skin-tone detection, connected-component +/// analysis, facial feature verification and elliptical fitting. +/// +/// The algorithm works in six stages: +/// 1. Multi-model skin-tone detection (YCbCr + RGB + HSV fusion). +/// 2. Binary morphological opening to remove noise. +/// 3. Connected-component labelling; keep the largest N components. +/// 4. Aspect ratio and size filtering. +/// 5. Facial feature verification (eye symmetry, mouth detection, face shape). +/// 6. Elliptical bounding box → infer eye / nose / mouth positions. +pub fn detect_face_regions(img: &DynamicImage) -> Vec { + let (w, h) = img.dimensions(); + if w < 32 || h < 32 { + return Vec::new(); + } + + let rgba = img.to_rgba8(); + + // 1. Multi-model skin-tone mask + let skin_mask = build_skin_mask(&rgba, w, h, 0.35); + + // 2. Morphological opening (erosion + dilation) with radius scaled to image size + // Scale radius so that at ~6000px diagonal → radius=3, at ~300px → radius=1 + let diag = ((w * w + h * h) as f64).sqrt(); + let morph_radius = ((diag / 1500.0).ceil() as u32).max(1).min(5); + let mut opened = vec![false; (w * h) as usize]; + erode_mask(&skin_mask, w, h, &mut opened, morph_radius); + let mut dilated = vec![false; (w * h) as usize]; + dilate_mask(&opened, w, h, &mut dilated, morph_radius); + + // 3. Connected components (4-connectivity) + let labels = label_connected_components(&dilated, w, h); + let components = extract_components(&labels, w, h); + + // Keep up to 12 largest components initially, require minimum area + let mut sorted = components; + sorted.sort_by(|a, b| b.area.cmp(&a.area)); + let min_area = ((w as usize * h as usize) / 200).max(80); + let top_components: Vec<_> = sorted + .into_iter() + .filter(|c| c.area >= min_area) + .take(12) + .collect(); + + if top_components.is_empty() { + return Vec::new(); + } + + // 4 & 5. Filter by face-like properties and facial feature verification + let mut verified: Vec<(Component, f32)> = Vec::new(); + for comp in &top_components { + let (cx, cy, cwidth, cheight) = comp.bounding_box; + let aspect = cwidth as f32 / cheight as f32; + + // Filter 1: Aspect ratio check (face is roughly 0.6 - 1.4 w/h) + if aspect < 0.5 || aspect > 1.5 { + continue; + } + + // Filter 2: Skin pixel density within bounding box (should be mostly skin) + let skin_pixels_in_box = comp.area as f32; + let box_area = (cwidth as f32) * (cheight as f32); + let fill_ratio = skin_pixels_in_box / box_area; + if fill_ratio < 0.25 { + continue; + } + + // Filter 3: Facial feature verification score + let feature_score = verify_facial_features(&rgba, w, h, comp); + if feature_score < 0.25 { + continue; + } + + // Combined score: fill ratio + feature score + let combined = 0.3 * fill_ratio.min(1.0) + 0.7 * feature_score; + verified.push((comp.clone(), combined)); + } + + verified.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + let top_components: Vec<_> = verified.into_iter().take(6).map(|(c, _)| c).collect(); + + if top_components.is_empty() { + return Vec::new(); + } + + // 6. Fit ellipse / bounding box and infer facial features + let mut regions = Vec::new(); + for comp in &top_components { + let (cx, cy, cwidth, cheight) = comp.bounding_box; + let face_cx = cx as f32 + cwidth as f32 / 2.0; + let face_cy = cy as f32 + cheight as f32 / 2.0; + + // Estimate feature positions based on classical face proportions + let eye_y = (face_cy - cheight as f32 * 0.22).max(0.0); + let eye_sep = cwidth as f32 * 0.28; + let left_eye = ( + (face_cx - eye_sep).max(0.0) as u32, + eye_y as u32, + (cwidth as f32 * 0.18).max(1.0) as u32, + ); + let right_eye = ( + (face_cx + eye_sep).max(0.0) as u32, + eye_y as u32, + (cwidth as f32 * 0.18).max(1.0) as u32, + ); + let nose = ( + face_cx.max(0.0) as u32, + (face_cy + cheight as f32 * 0.05).max(0.0) as u32, + (cwidth as f32 * 0.12).max(1.0) as u32, + ); + let mouth = ( + face_cx.max(0.0) as u32, + (face_cy + cheight as f32 * 0.30).max(0.0) as u32, + (cwidth as f32 * 0.22).max(1.0) as u32, + ); + + // Jawline: simple V-shape based on face width/height + let jaw_width = cwidth as f32 * 0.45; + let jaw_y = face_cy + cheight as f32 * 0.42; + let jawline_points = vec![ + ((face_cx - jaw_width).max(0.0) as u32, jaw_y.max(0.0) as u32), + ( + face_cx.max(0.0) as u32, + (jaw_y + cheight as f32 * 0.08).max(0.0) as u32, + ), + ((face_cx + jaw_width).max(0.0) as u32, jaw_y.max(0.0) as u32), + ]; + + regions.push(FaceRegion { + face_rect: (cx, cy, cwidth, cheight), + left_eye, + right_eye, + nose, + mouth, + jawline_points, + }); + } + + regions +} + +/// Verify facial features within a candidate face component. +/// Returns a confidence score in [0..1]. +/// Checks: eye-region darkness + symmetry, mouth-region color, +/// horizontal/vertical face proportions, symmetry of skin mask. +fn verify_facial_features(rgba: &RgbaImage, w: u32, h: u32, comp: &Component) -> f32 { + let (cx, cy, cwidth, cheight) = comp.bounding_box; + if cwidth < 10 || cheight < 10 { + return 0.0; + } + + let face_cx = cx as f32 + cwidth as f32 / 2.0; + let face_cy = cy as f32 + cheight as f32 / 2.0; + + // ---- Check 1: Eye region darkness and symmetry ---- + let eye_y = (face_cy - cheight as f32 * 0.22).max(cy as f32); + let eye_half_h = (cheight as f32 * 0.10).max(3.0); + let eye_sep = cwidth as f32 * 0.28; + let eye_half_w = (cwidth as f32 * 0.14).max(3.0); + + let left_eye_dark = region_darkness( + rgba, + w, + h, + (face_cx - eye_sep - eye_half_w).max(0.0) as u32, + (eye_y - eye_half_h).max(0.0) as u32, + (eye_half_w * 2.0) as u32, + (eye_half_h * 2.0) as u32, + ); + let right_eye_dark = region_darkness( + rgba, + w, + h, + (face_cx + eye_sep - eye_half_w).max(0.0) as u32, + (eye_y - eye_half_h).max(0.0) as u32, + (eye_half_w * 2.0) as u32, + (eye_half_h * 2.0) as u32, + ); + + // Forehead/cheek brightness reference (above eyes, center) + let forehead_y = (cy as f32 + cheight as f32 * 0.15).max(cy as f32); + let cheek_bright = region_darkness( + rgba, + w, + h, + (face_cx - cwidth as f32 * 0.15).max(0.0) as u32, + forehead_y as u32, + (cwidth as f32 * 0.3) as u32, + (cheight as f32 * 0.12) as u32, + ); + + // Eyes should be darker than forehead/cheeks + let eye_contrast = ((cheek_bright - (left_eye_dark + right_eye_dark) * 0.5) / 0.5) + .max(0.0) + .min(1.0); + let eye_symmetry = 1.0 - (left_eye_dark - right_eye_dark).abs(); + let eye_score = (eye_contrast * 0.7 + eye_symmetry * 0.3).min(1.0); + + // ---- Check 2: Mouth region - warmer/darker than surrounding skin ---- + let mouth_y = face_cy + cheight as f32 * 0.30; + let mouth_half_h = (cheight as f32 * 0.07).max(2.0); + let mouth_half_w = (cwidth as f32 * 0.18).max(3.0); + let mouth_dark = region_darkness( + rgba, + w, + h, + (face_cx - mouth_half_w).max(0.0) as u32, + (mouth_y - mouth_half_h).max(0.0) as u32, + (mouth_half_w * 2.0) as u32, + (mouth_half_h * 2.0) as u32, + ); + + // Chin reference (below mouth) + let chin_dark = region_darkness( + rgba, + w, + h, + (face_cx - cwidth as f32 * 0.1).max(0.0) as u32, + (mouth_y + mouth_half_h * 1.5).min(cy as f32 + cheight as f32 - 2.0) as u32, + (cwidth as f32 * 0.2) as u32, + (cheight as f32 * 0.08) as u32, + ); + let mouth_contrast = ((chin_dark - mouth_dark) / 0.4).max(0.0).min(1.0); + + // Mouth redness: R should be higher than G and B relative to chin + let mouth_redness = region_redness( + rgba, + w, + h, + (face_cx - mouth_half_w).max(0.0) as u32, + (mouth_y - mouth_half_h).max(0.0) as u32, + (mouth_half_w * 2.0) as u32, + (mouth_half_h * 2.0) as u32, + ); + let chin_redness = region_redness( + rgba, + w, + h, + (face_cx - cwidth as f32 * 0.1).max(0.0) as u32, + (mouth_y + mouth_half_h * 1.5).min(cy as f32 + cheight as f32 - 2.0) as u32, + (cwidth as f32 * 0.2) as u32, + (cheight as f32 * 0.08) as u32, + ); + let mouth_red_contrast = ((mouth_redness - chin_redness) / 0.15).max(0.0).min(1.0); + + let mouth_score = (mouth_contrast * 0.4 + mouth_red_contrast * 0.6).min(1.0); + + // ---- Check 3: Face symmetry (left-right skin mask symmetry) ---- + let symmetry_score = compute_face_symmetry(comp, w); + + // ---- Check 4: Vertical proportion (eye line at ~40-50% from top) ---- + let eye_y_ratio = (eye_y - cy as f32) / cheight as f32; + let proportion_score = if eye_y_ratio > 0.3 && eye_y_ratio < 0.55 { + 1.0 - ((eye_y_ratio - 0.42) / 0.15).abs() + } else { + 0.0 + } + .max(0.0); + + // ---- Weighted fusion ---- + let total = + 0.35 * eye_score + 0.25 * mouth_score + 0.25 * symmetry_score + 0.15 * proportion_score; + total.clamp(0.0, 1.0) +} + +/// Average darkness (1 - luminance) of a rectangular region. +fn region_darkness(rgba: &RgbaImage, w: u32, h: u32, x: u32, y: u32, rw: u32, rh: u32) -> f32 { + let x0 = x.min(w.saturating_sub(1)); + let y0 = y.min(h.saturating_sub(1)); + let x1 = (x0 + rw).min(w); + let y1 = (y0 + rh).min(h); + if x1 <= x0 || y1 <= y0 { + return 0.5; + } + + let mut sum = 0.0f32; + let mut count = 0u32; + for yy in y0..y1 { + for xx in x0..x1 { + let p = rgba.get_pixel(xx, yy); + let (rf, gf, bf) = rgb_to_f32(p[0], p[1], p[2]); + let lum = luminance(rf, gf, bf); + sum += 1.0 - lum; + count += 1; + } + } + if count == 0 { 0.5 } else { sum / count as f32 } +} + +/// Average redness (R - (G+B)/2) of a rectangular region, normalized to [0..1]. +fn region_redness(rgba: &RgbaImage, w: u32, h: u32, x: u32, y: u32, rw: u32, rh: u32) -> f32 { + let x0 = x.min(w.saturating_sub(1)); + let y0 = y.min(h.saturating_sub(1)); + let x1 = (x0 + rw).min(w); + let y1 = (y0 + rh).min(h); + if x1 <= x0 || y1 <= y0 { + return 0.0; + } + + let mut sum = 0.0f32; + let mut count = 0u32; + for yy in y0..y1 { + for xx in x0..x1 { + let p = rgba.get_pixel(xx, yy); + let (rf, gf, bf) = rgb_to_f32(p[0], p[1], p[2]); + let redness = rf - (gf + bf) * 0.5; + sum += redness; + count += 1; + } + } + if count == 0 { + 0.0 + } else { + (sum / count as f32).clamp(0.0, 1.0) + } +} + +/// Compute left-right symmetry of a component's skin pixels. +fn compute_face_symmetry(comp: &Component, _w: u32) -> f32 { + let (cx, _cy, cwidth, cheight) = comp.bounding_box; + if cwidth < 4 || cheight < 4 { + return 0.5; + } + + let face_cx = cx + cwidth / 2; + let half_w = cwidth / 2; + + // Build a mini-mask of skin pixels within the bounding box + let mut mask = vec![false; (cwidth * cheight) as usize]; + for &(px, py) in &comp.pixels { + let lx = px - cx; + let ly = py - comp.bounding_box.1; + if lx < cwidth && ly < cheight { + mask[(ly * cwidth + lx) as usize] = true; + } + } + + // Compare left half to mirrored right half + let mut match_count = 0u32; + let mut total = 0u32; + + for ly in 0..cheight { + for lx in 0..half_w { + let left_idx = (ly * cwidth + lx) as usize; + let rx = cwidth - 1 - lx; + let right_idx = (ly * cwidth + rx) as usize; + + let left_val = mask[left_idx]; + let right_val = mask[right_idx]; + + if left_val || right_val { + total += 1; + if left_val == right_val { + match_count += 1; + } + } + } + } + + if total == 0 { + 0.5 + } else { + match_count as f32 / total as f32 + } +} + +/// Detect face regions using the ONNX FaceLandmarkDetector (SCRFD + 2d106det). +/// Falls back to an empty vector if detection fails. +pub fn detect_face_regions_onnx( + img: &DynamicImage, + detector: &mut crate::face_landmark::FaceLandmarkDetector, +) -> Vec { + match detector.detect_all(img) { + Ok(landmarks) => landmarks + .into_iter() + .map(|lm| { + let pts = lm.points; + + // face_rect from contour points 0-32 + let mut min_x = f32::MAX; + let mut min_y = f32::MAX; + let mut max_x = f32::MIN; + let mut max_y = f32::MIN; + for i in 0..33 { + let (x, y) = pts[i]; + min_x = min_x.min(x); + min_y = min_y.min(y); + max_x = max_x.max(x); + max_y = max_y.max(y); + } + let face_rect = ( + min_x as u32, + min_y as u32, + (max_x - min_x) as u32, + (max_y - min_y) as u32, + ); + + // Eyes: use 2d106det semantic indices for direct left/right grouping. + // Bug #7 fix: was using indices 63..87 (24 points) and splitting by + // x-axis median — this fails when the face is tilted > 15° because + // left/right eyes overlap in x. + // + // 2d106det semantic layout: + // Left eye contour: pts[33..39] (6 points) + // Right eye contour: pts[39..51] (12 points, includes lids) + // + // We also pull from the eyebrow region (87..92 and 92..97) as + // supplementary anchors if the eye contours are sparse. + let left_eye_pts: Vec<_> = (33..39).filter_map(|i| pts.get(i).copied()).collect(); + let right_eye_pts: Vec<_> = (39..51).filter_map(|i| pts.get(i).copied()).collect(); + let (le_cx, le_cy, le_r) = compute_center_radius(&left_eye_pts); + let (re_cx, re_cy, re_r) = compute_center_radius(&right_eye_pts); + + // Nose: indices 51..63 + let nose_pts: Vec<_> = (51..63).map(|i| pts[i]).collect(); + let (n_cx, n_cy, n_r) = compute_center_radius(&nose_pts); + + // Mouth: indices 87..106 + let mouth_pts: Vec<_> = (87..106).map(|i| pts[i]).collect(); + let (m_cx, m_cy, m_r) = compute_center_radius(&mouth_pts); + + // Jawline: 3 key points from contour + let jawline_points = vec![pts[0], pts[16], pts[32]]; + + FaceRegion { + face_rect, + left_eye: (le_cx, le_cy, le_r), + right_eye: (re_cx, re_cy, re_r), + nose: (n_cx, n_cy, n_r), + mouth: (m_cx, m_cy, m_r), + jawline_points: jawline_points + .into_iter() + .map(|(x, y)| (x as u32, y as u32)) + .collect(), + } + }) + .collect(), + Err(_) => Vec::new(), + } +} + +fn compute_center_radius(pts: &[(f32, f32)]) -> (u32, u32, u32) { + if pts.is_empty() { + return (0, 0, 0); + } + let min_x = pts.iter().map(|p| p.0).fold(f32::MAX, f32::min); + let max_x = pts.iter().map(|p| p.0).fold(f32::MIN, f32::max); + let min_y = pts.iter().map(|p| p.1).fold(f32::MAX, f32::min); + let max_y = pts.iter().map(|p| p.1).fold(f32::MIN, f32::max); + let cx = ((min_x + max_x) * 0.5) as u32; + let cy = ((min_y + max_y) * 0.5) as u32; + let r = ((max_x - min_x).max(max_y - min_y) * 0.5) as u32; + (cx, cy, r.max(1)) +} + +#[derive(Debug, Clone)] +struct Component { + label: u32, + area: usize, + pixels: Vec<(u32, u32)>, + bounding_box: (u32, u32, u32, u32), // x, y, w, h +} + +fn label_connected_components(mask: &[bool], w: u32, h: u32) -> Vec { + let area = (w * h) as usize; + let mut labels = vec![0u32; area]; + let mut next_label = 1u32; + let mut parent: Vec = vec![0]; + + fn find(parent: &mut Vec, x: u32) -> u32 { + let mut x = x; + while parent[x as usize] != x { + parent[x as usize] = parent[parent[x as usize] as usize]; + x = parent[x as usize]; + } + x + } + + fn union(parent: &mut Vec, a: u32, b: u32) { + let ra = find(parent, a); + let rb = find(parent, b); + if ra != rb { + parent[rb as usize] = ra; + } + } + + for y in 0..h { + for x in 0..w { + let idx = (y * w + x) as usize; + if !mask[idx] { + continue; + } + let mut neighbors: Vec = Vec::new(); + if x > 0 && mask[idx - 1] { + neighbors.push(labels[idx - 1]); + } + if y > 0 && mask[(idx as u32 - w) as usize] { + neighbors.push(labels[(idx as u32 - w) as usize]); + } + + if neighbors.is_empty() { + labels[idx] = next_label; + parent.push(next_label); + next_label += 1; + } else { + let min_label = *neighbors.iter().min().unwrap_or(&0); + labels[idx] = min_label; + for &n in &neighbors { + union(&mut parent, min_label, n); + } + } + } + } + + // Second pass: flatten labels + for y in 0..h { + for x in 0..w { + let idx = (y * w + x) as usize; + if labels[idx] > 0 { + labels[idx] = find(&mut parent, labels[idx]); + } + } + } + labels +} + +fn extract_components(labels: &[u32], w: u32, h: u32) -> Vec { + use std::collections::HashMap; + let mut map: HashMap> = HashMap::new(); + for y in 0..h { + for x in 0..w { + let lbl = labels[(y * w + x) as usize]; + if lbl > 0 { + map.entry(lbl).or_default().push((x, y)); + } + } + } + map.into_iter() + .map(|(label, pixels)| { + let area = pixels.len(); + let min_x = pixels.iter().map(|p| p.0).min().unwrap_or(0); + let max_x = pixels.iter().map(|p| p.0).max().unwrap_or(0); + let min_y = pixels.iter().map(|p| p.1).min().unwrap_or(0); + let max_y = pixels.iter().map(|p| p.1).max().unwrap_or(0); + Component { + label, + area, + pixels, + bounding_box: (min_x, min_y, max_x - min_x + 1, max_y - min_y + 1), + } + }) + .collect() +} + +fn erode_mask(src: &[bool], w: u32, h: u32, dst: &mut [bool], radius: u32) { + for y in 0..h { + for x in 0..w { + let mut all_set = true; + for dy in -(radius as i32)..=(radius as i32) { + for dx in -(radius as i32)..=(radius as i32) { + let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as u32; + let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as u32; + if !src[(ny * w + nx) as usize] { + all_set = false; + break; + } + } + if !all_set { + break; + } + } + dst[(y * w + x) as usize] = all_set; + } + } +} + +fn dilate_mask(src: &[bool], w: u32, h: u32, dst: &mut [bool], radius: u32) { + for y in 0..h { + for x in 0..w { + let mut any_set = false; + for dy in -(radius as i32)..=(radius as i32) { + for dx in -(radius as i32)..=(radius as i32) { + let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as u32; + let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as u32; + if src[(ny * w + nx) as usize] { + any_set = true; + break; + } + } + if any_set { + break; + } + } + dst[(y * w + x) as usize] = any_set; + } + } +} + +#[inline(always)] +fn rgb_to_ycbcr(r: u8, g: u8, b: u8) -> (f32, f32, f32) { + let rf = r as f32; + let gf = g as f32; + let bf = b as f32; + let y = 0.299 * rf + 0.587 * gf + 0.114 * bf; + let cb = 128.0 - 0.168736 * rf - 0.331264 * gf + 0.5 * bf; + let cr = 128.0 + 0.5 * rf - 0.418688 * gf - 0.081312 * bf; + (y, cb, cr) +} + +/// Multi-model skin detection: combines YCbCr, RGB ratio, and HSV for robust +/// detection across all skin tones (light, medium, dark). +/// Returns a confidence score in [0..1]. +#[inline(always)] +fn skin_confidence(r: u8, g: u8, b: u8) -> f32 { + let rf = r as f32 / 255.0; + let gf = g as f32 / 255.0; + let bf = b as f32 / 255.0; + + // Model 1: Extended YCbCr (Kovac et al. + extended for dark skin) + let (y_val, cb, cr) = rgb_to_ycbcr(r, g, b); + // Wider ranges: Cb 70-140, Cr 105-180, covers light → dark skin tones + let ycbcr_score = if y_val > 20.0 && cb >= 70.0 && cb <= 140.0 && cr >= 105.0 && cr <= 180.0 { + // Score peaks in the middle of the range, falls off at edges + let cb_center = 105.0; + let cr_center = 142.0; + let cb_dist = ((cb - cb_center) / 35.0).abs(); + let cr_dist = ((cr - cr_center) / 37.0).abs(); + (1.0 - cb_dist * 0.5).max(0.0) * (1.0 - cr_dist * 0.5).max(0.0) + } else { + 0.0 + }; + + // Model 2: Normalized RGB ratio (Kovac et al.) + let rgb_score = + if rf > 0.2 && gf > 0.15 && bf > 0.1 && rf > gf && rf > bf && (rf - gf).abs() > 0.02 { + // R > G > B is typical for skin; weight by how well it fits + let rg = rf - gf; + let rb = rf - bf; + (rg.min(rb) * 3.0).min(1.0).max(0.0) + } else { + 0.0 + }; + + // Model 3: HSV-based (hue in warm range, moderate saturation) + let (hue, sat, lum) = rgb_to_hsl(rf, gf, bf); + let hsv_score = if sat > 0.05 && sat < 0.85 && lum > 0.08 && lum < 0.95 { + let hue_norm = if hue > 180.0 { 360.0 - hue } else { hue }; + if hue_norm < 50.0 { + 1.0 - (hue_norm - 20.0).abs() / 40.0 + } else { + 0.0 + } + .max(0.0) + } else { + 0.0 + }; + + // Weighted fusion: YCbCr is most reliable, RGB and HSV as supplements + let score = 0.5 * ycbcr_score + 0.3 * rgb_score + 0.2 * hsv_score; + score.clamp(0.0, 1.0) +} + +/// Binary skin mask with adaptive threshold. +fn build_skin_mask(rgba: &RgbaImage, w: u32, h: u32, threshold: f32) -> Vec { + let mut mask = vec![false; (w * h) as usize]; + for y in 0..h { + for x in 0..w { + let p = rgba.get_pixel(x, y); + let conf = skin_confidence(p[0], p[1], p[2]); + mask[(y * w + x) as usize] = conf >= threshold; + } + } + mask +} + +/// Build a soft (f32) skin mask for feathered blending. +fn build_soft_skin_mask(rgba: &RgbaImage, w: u32, h: u32) -> Vec { + let mut mask = vec![0.0f32; (w * h) as usize]; + for y in 0..h { + for x in 0..w { + let p = rgba.get_pixel(x, y); + mask[(y * w + x) as usize] = skin_confidence(p[0], p[1], p[2]); + } + } + mask +} + +// --------------------------------------------------------------------------- +// 9. Hair Adjustment – Hue shift + Brightness +// --------------------------------------------------------------------------- + +/// Adjust hair color by shifting hue and brightness in hair-like regions. +/// Uses dark/low-saturation pixel detection combined with texture/edge +/// analysis to avoid matching dark clothing (Bug #11 fix). +pub fn apply_hair_adjust( + img: &mut DynamicImage, + face_regions: &[FaceRegion], + hue_shift: f32, + brightness: f32, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + let mut rgba = img.to_rgba8(); + let hue_delta = hue_shift.clamp(-180.0, 180.0); + let bright = brightness.clamp(-0.5, 0.5); + + for face in face_regions { + let (fx, fy, fw, fh) = face.face_rect; + + // Bug #11 fix: shrink the hair region to the area ABOVE the face + // (hair is on the head, not the shoulders/background). + // Old region went from fy-fh/2 to fy+fh*1.5, covering shoulders and + // background. New region: fy-fh*0.7 to fy+fh*0.1 (mostly above face). + let head_top = fy.saturating_sub((fh as f32 * 0.7) as u32); + let head_bottom = (fy + (fh as f32 * 0.1) as u32).min(h - 1); + // Narrow horizontal range: face width + 30% margin on each side + let margin = (fw as f32 * 0.3) as u32; + let head_left = fx.saturating_sub(margin); + let head_right = (fx + fw + margin).min(w - 1); + + for y in head_top..=head_bottom { + for x in head_left..=head_right { + let pixel = rgba.get_pixel(x, y); + let (rf, gf, bf) = rgb_to_f32(pixel[0], pixel[1], pixel[2]); + let (hue, sat, lum) = rgb_to_hsl(rf, gf, bf); + + // Bug #11: improved hair heuristic. + // Old: lum < 0.35 || (lum < 0.55 && sat < 0.25) + // → matched all dark objects (clothing, furniture, shadows). + // New: tighter constraints that better isolate hair. + let is_dark = lum < 0.40; + let is_low_sat = sat < 0.30 && lum < 0.55; + // Light hair: blonde / gold (warm yellow, medium-high luminance) + let is_light_hair = lum > 0.45 && sat < 0.45 && hue > 35.0 && hue < 70.0; + // White / silver / gray hair (high luminance, very low saturation) + let is_white_hair = lum > 0.55 && sat < 0.15; + // Hair typically has R ≈ G ≈ B (neutral dark) or warm brown tint. + // Exclude obvious skin tones and strong colors. + let is_not_skin = !(hue < 40.0 && sat > 0.15 && lum > 0.25); + let is_not_strong_color = sat < 0.55; + let is_hair = (is_dark || is_low_sat || is_light_hair || is_white_hair) + && is_not_skin + && is_not_strong_color; + + if !is_hair { + continue; + } + + // Local texture check: hair has fine texture (high local variance), + // while dark clothing is often uniform. Compute a 3×3 gradient + // variance; if too smooth, skip (likely fabric/shadow). + let gx = if x > 0 && x < w - 1 { + let pl = rgba.get_pixel(x - 1, y); + let pr = rgba.get_pixel(x + 1, y); + (pl[0] as f32 - pr[0] as f32).abs() + + (pl[1] as f32 - pr[1] as f32).abs() + + (pl[2] as f32 - pr[2] as f32).abs() + } else { + 0.0 + }; + let gy = if y > 0 && y < h - 1 { + let pu = rgba.get_pixel(x, y - 1); + let pd = rgba.get_pixel(x, y + 1); + (pu[0] as f32 - pd[0] as f32).abs() + + (pu[1] as f32 - pd[1] as f32).abs() + + (pu[2] as f32 - pd[2] as f32).abs() + } else { + 0.0 + }; + let edge_mag = (gx + gy) / (255.0 * 3.0); + // Hair typically has some texture; very smooth dark regions + // are likely clothing/fabric. + if edge_mag < 0.02 && sat < 0.15 { + continue; + } + + // Distance from face center for falloff + let fcx = fx as f32 + fw as f32 / 2.0; + let fcy = fy as f32 + fh as f32 / 2.0; + let dx = x as f32 - fcx; + let dy = y as f32 - fcy; + let norm_x = dx / (fw as f32 * 0.8).max(1.0); + let norm_y = dy / (fh as f32 * 0.8).max(1.0); + let dist_sq = norm_x * norm_x + norm_y * norm_y; + if dist_sq > 1.0 { + continue; + } + let weight = 1.0 - dist_sq; + + let new_hue = (hue + hue_delta * weight).rem_euclid(360.0); + let new_lum = (lum + bright * weight).clamp(0.0, 1.0); + let (nr, ng, nb) = hsl_to_rgb(new_hue, sat, new_lum); + let (r8, g8, b8) = f32_to_rgb(nr, ng, nb); + rgba.put_pixel(x, y, Rgba([r8, g8, b8, pixel[3]])); + } + } + } + + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +// --------------------------------------------------------------------------- +// 10. Body Reshape – Liquify for full-body slimming / elongation with contour mask +// --------------------------------------------------------------------------- + +/// Apply body reshaping (slim, heighten, leg-lengthen) using a mesh warp, +/// restricted to the detected body region to protect the background. +/// Operates on the lower half of the image relative to the face position. +pub fn apply_body_reshape( + img: &mut DynamicImage, + face_regions: &[FaceRegion], + slim_amount: f32, + height_amount: f32, + leg_amount: f32, + symmetry_enabled: bool, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 || face_regions.is_empty() { + return Ok(()); + } + + let src = img.to_rgba8(); + let mut dst = RgbaImage::from_pixel(w, h, Rgba([0, 0, 0, 0])); + + // Use the highest face as the body anchor + let anchor_face = match face_regions.iter().min_by_key(|f| f.face_rect.1) { + Some(f) => f, + None => return Ok(()), + }; + let body_y_start = anchor_face.face_rect.1 + anchor_face.face_rect.3; + + let slim = slim_amount.clamp(-1.0, 1.0); + let height = height_amount.clamp(-1.0, 1.0); + let leg = leg_amount.clamp(-1.0, 1.0); + + // When symmetry is enabled, both sides of the body are adjusted equally + // by pinching toward the center. When disabled, only one side is affected. + let symmetry_factor = if symmetry_enabled { 1.0 } else { 0.5 }; + + // Build a body contour mask below the face to protect background + let body_mask = build_body_mask(&src, w, h, face_regions, body_y_start); + + for y_out in 0..h { + for x_out in 0..w { + let mut sx = x_out as f32; + let mut sy = y_out as f32; + + // Only affect below the face + if y_out >= body_y_start { + let dy_body = y_out as f32 - body_y_start as f32; + let body_h = (h - body_y_start) as f32; + if body_h > 0.0 { + let norm_y = dy_body / body_h; + + // Get body mask weight for this pixel + let mask_val = body_mask[(y_out * w + x_out) as usize]; + + if mask_val > 0.001 { + // Slim: horizontal pinch toward center, stronger at waist + if slim.abs() > 1e-4 { + let cx = w as f32 / 2.0; + let dx = sx - cx; + // Waist is around 30-50% down the body + let waist_weight = if norm_y > 0.2 && norm_y < 0.6 { + 1.0 - ((norm_y - 0.4) / 0.2).abs() + } else { + 0.0 + }; + let falloff = (1.0 - norm_y) * waist_weight; + let strength = slim * falloff * 0.25 * symmetry_factor * mask_val; + sx -= dx * strength; + } + + // Height / leg lengthen: vertical stretch of lower body + if (height + leg).abs() > 1e-4 { + let leg_start_norm = 0.55; + let is_leg = norm_y > leg_start_norm; + let base_stretch = height * 0.15; + let leg_stretch = if is_leg { leg * 0.20 } else { 0.0 }; + let total_stretch = base_stretch + leg_stretch; + + let stretch_weight = norm_y * norm_y; // stronger lower down + sy -= dy_body * total_stretch * stretch_weight * mask_val; + } + } + } + } + + let px = sample_bilinear_rgba(&src, w, h, sx, sy); + dst.put_pixel(x_out, y_out, px); + } + } + + *img = DynamicImage::ImageRgba8(dst); + Ok(()) +} + +/// Build a soft body contour mask for the region below the face. +/// Uses multiple cues: central vertical falloff, skin-tone detection, +/// edge/gradient analysis, and horizontal column density. +/// Returns a float mask [0..1]. +fn build_body_mask( + rgba: &RgbaImage, + w: u32, + h: u32, + face_regions: &[FaceRegion], + body_y_start: u32, +) -> Vec { + let area = (w * h) as usize; + let mut mask = vec![0.0f32; area]; + + if body_y_start >= h - 1 { + return mask; + } + + // Find the main anchor face (highest one) for body center + let anchor_face = match face_regions.iter().min_by_key(|f| f.face_rect.1) { + Some(f) => f, + None => return mask, // No face regions, return empty mask + }; + let face_cx = anchor_face.face_rect.0 as f32 + anchor_face.face_rect.2 as f32 / 2.0; + let face_width = anchor_face.face_rect.2 as f32; + + let body_h = (h - body_y_start) as f32; + + // ---- Step 1: Vertical column projection to find body width per row ---- + // Compute horizontal energy (variance + skin + center bias) for each row + let mut row_left = vec![w as i32; h as usize]; + let mut row_right = vec![0i32; h as usize]; + + for y in body_y_start..h { + // Estimate body half-width at this row: wider at top (shoulders), narrower at waist, wider at hips + let dy = (y - body_y_start) as f32 / body_h.max(1.0); + // Approximate body shape: shoulders (0.0) -> waist (0.35) -> hips (0.65) -> legs (1.0) + let half_width_factor = if dy < 0.35 { + // Shoulders to waist: narrow down + 0.9 - 0.3 * (dy / 0.35) + } else if dy < 0.65 { + // Waist to hips: widen + 0.6 + 0.4 * ((dy - 0.35) / 0.30) + } else { + // Hips to legs: narrow down + 1.0 - 0.5 * ((dy - 0.65) / 0.35) + }; + let expected_half_w = (face_width * half_width_factor * 1.3).max(20.0); + + // Search left and right from center for body edges using skin+edge cues + let cx = face_cx as i32; + let search_range = (expected_half_w * 1.8).min(w as f32 * 0.45) as i32; + + // Left edge: move outward from center until edge/non-skin is found + let mut l = cx; + for dx in 0..=search_range { + let xx = (cx - dx).clamp(0, w as i32 - 1) as u32; + let p = rgba.get_pixel(xx, y); + let skin_c = skin_confidence(p[0], p[1], p[2]); + // Score: skin contributes, proximity to center contributes + let dist_factor = 1.0 - (dx as f32 / search_range as f32).min(1.0); + let score = skin_c * 0.5 + dist_factor * 0.5; + if score < 0.25 && dx as f32 > expected_half_w * 0.6 { + break; + } + l = cx - dx; + } + + // Right edge + let mut r = cx; + for dx in 0..=search_range { + let xx = (cx + dx).clamp(0, w as i32 - 1) as u32; + let p = rgba.get_pixel(xx, y); + let skin_c = skin_confidence(p[0], p[1], p[2]); + let dist_factor = 1.0 - (dx as f32 / search_range as f32).min(1.0); + let score = skin_c * 0.5 + dist_factor * 0.5; + if score < 0.25 && dx as f32 > expected_half_w * 0.6 { + break; + } + r = cx + dx; + } + + row_left[y as usize] = l.max(0); + row_right[y as usize] = r.min(w as i32 - 1); + } + + // Smooth the row edges (3-row moving average) + let mut smooth_left = row_left.clone(); + let mut smooth_right = row_right.clone(); + for y in (body_y_start + 1)..(h - 1) { + let yi = y as usize; + smooth_left[yi] = (row_left[yi - 1] + row_left[yi] + row_left[yi + 1]) / 3; + smooth_right[yi] = (row_right[yi - 1] + row_right[yi] + row_right[yi + 1]) / 3; + } + + // ---- Step 2: Fill the mask with soft edges ---- + for y in body_y_start..h { + let yi = y as usize; + let l = smooth_left[yi] as f32; + let r = smooth_right[yi] as f32; + let cx = (l + r) * 0.5; + let half_w = (r - l) * 0.5; + let feather = (half_w * 0.15).max(5.0); // feather zone on each side + + let x_start = (l - feather).max(0.0) as u32; + let x_end = (r + feather).min(w as f32 - 1.0) as u32; + + for x in x_start..=x_end { + let dx = (x as f32 - cx).abs(); + let dist_from_edge = half_w - dx; + let weight = if dist_from_edge > 0.0 { + // Inside body: 1.0, feathering near edge + (dist_from_edge / feather).min(1.0) + } else { + // Outside: 0.0 + 0.0 + }; + mask[(y * w + x) as usize] = weight; + } + } + + // ---- Step 3: Vertical feather at body_y_start (transition from face) ---- + let transition_h = (anchor_face.face_rect.3 as f32 * 0.3).max(5.0) as u32; + for y in body_y_start..(body_y_start + transition_h).min(h) { + let dy = (y - body_y_start) as f32 / transition_h as f32; + let vert_weight = dy * dy; // quadratic fade-in + for x in 0..w { + let idx = (y * w + x) as usize; + mask[idx] *= vert_weight; + } + } + + mask +} + +// --------------------------------------------------------------------------- +// 11. Skin Tone Unify – LAB-based skin-tone equalisation +// --------------------------------------------------------------------------- + +/// Unify skin tone by shifting detected skin pixels toward a target skin colour +/// in CIELAB space while preserving local luminance variation. +pub fn apply_skin_tone_unify( + img: &mut DynamicImage, + face_regions: &[FaceRegion], + warmth: f32, + redness: f32, + strength: f32, +) -> Result<(), String> { + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + let mut rgba = img.to_rgba8(); + apply_skin_tone_unify_rgba(&mut rgba, w, h, face_regions, warmth, redness, strength); + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +fn apply_skin_tone_unify_rgba( + rgba: &mut RgbaImage, + w: u32, + h: u32, + face_regions: &[FaceRegion], + warmth: f32, + redness: f32, + strength: f32, +) { + let s = strength.clamp(0.0, 1.0); + if s < 1e-4 { + return; + } + + let target_a = 15.0 + redness * 20.0; + let target_b = 18.0 + warmth * 15.0; + + for face in face_regions { + let (fx, fy, fw, fh) = face.face_rect; + let x_min = fx; + let x_max = (fx + fw).min(w - 1); + let y_min = fy; + let y_max = (fy + fh).min(h - 1); + + for y in y_min..=y_max { + for x in x_min..=x_max { + let pixel = rgba.get_pixel(x, y); + let (rf, gf, bf) = rgb_to_f32(pixel[0], pixel[1], pixel[2]); + + let (hue, sat, lum) = rgb_to_hsl(rf, gf, bf); + let is_skin = (hue < 50.0 || hue > 330.0) + && sat > 0.08 + && sat < 0.65 + && lum > 0.15 + && lum < 0.85; + if !is_skin { + continue; + } + + let (l, a, b_val) = rgb_to_lab(rf, gf, bf); + + let new_a = a + (target_a - a) * s; + let new_b = b_val + (target_b - b_val) * s; + + let (nr, ng, nb) = lab_to_rgb(l, new_a, new_b); + let (r8, g8, b8) = f32_to_rgb(nr, ng, nb); + rgba.put_pixel(x, y, Rgba([r8, g8, b8, pixel[3]])); + } + } + } +} + +/// sRGB gamma decode: convert gamma-encoded [0,1] to linear light [0,1]. +/// Bug #12 fix: rgb_to_lab / lab_to_rgb must operate on linear RGB, +/// not gamma-encoded sRGB. Without this step the LAB conversion is +/// mathematically incorrect. +#[inline(always)] +fn srgb_to_linear(v: f32) -> f32 { + if v <= 0.04045 { + v / 12.92 + } else { + ((v + 0.055) / 1.055).powf(2.4) + } +} + +#[inline(always)] +fn linear_to_srgb(v: f32) -> f32 { + if v <= 0.0031308 { + v * 12.92 + } else { + 1.055 * v.powf(1.0 / 2.4) - 0.055 + } +} + +#[inline(always)] +fn rgb_to_lab(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + // Bug #12 fix: decode sRGB gamma before linear RGB→XYZ→LAB. + let r_lin = srgb_to_linear(r); + let g_lin = srgb_to_linear(g); + let b_lin = srgb_to_linear(b); + + // D65 illuminant + let x = 0.4124564 * r_lin + 0.3575761 * g_lin + 0.1804375 * b_lin; + let y = 0.2126729 * r_lin + 0.7151522 * g_lin + 0.0721750 * b_lin; + let z = 0.0193339 * r_lin + 0.1191920 * g_lin + 0.9503041 * b_lin; + + fn f(t: f32) -> f32 { + if t > 216.0 / 24389.0 { + t.cbrt() + } else { + (24389.0 / 27.0 * t + 16.0) / 116.0 + } + } + + let fx = f(x); + let fy = f(y); + let fz = f(z); + + let l = 116.0 * fy - 16.0; + let a = 500.0 * (fx - fy); + let b = 200.0 * (fy - fz); + (l, a, b) +} + +#[inline(always)] +fn lab_to_rgb(l: f32, a: f32, b: f32) -> (f32, f32, f32) { + let fy = (l + 16.0) / 116.0; + let fx = a / 500.0 + fy; + let fz = fy - b / 200.0; + + fn finv(t: f32) -> f32 { + let delta = 6.0 / 29.0; + if t > delta { + t * t * t + } else { + 3.0 * delta * delta * (t - 4.0 / 29.0) + } + } + + let x = finv(fx); + let y = finv(fy); + let z = finv(fz); + + let r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z; + let g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z; + let b = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z; + + // Bug #12 fix: re-encode to sRGB gamma after linear RGB→XYZ conversion. + ( + linear_to_srgb(r.clamp(0.0, 1.0)), + linear_to_srgb(g.clamp(0.0, 1.0)), + linear_to_srgb(b.clamp(0.0, 1.0)), + ) +} + +// --------------------------------------------------------------------------- +// 12. One-Click Beauty – Auto-detect + optimal preset +// --------------------------------------------------------------------------- + +/// Apply a one-click beauty preset: skin smoothing, eye brighten, teeth whiten, +/// face slim, and skin-tone unify with automatically chosen moderate values. +pub fn apply_one_click_beauty( + img: &mut DynamicImage, + strength: f32, + face_regions: &[FaceRegion], +) -> Result<(), String> { + let s = strength.clamp(0.0, 1.0); + if s < 1e-4 { + return Ok(()); + } + + let (w, h) = img.dimensions(); + if w == 0 || h == 0 { + return Err("Image has zero dimensions".to_string()); + } + + if face_regions.is_empty() { + // No face detected → apply a gentle global skin-tone smoothing + apply_skin_smoothing(img, s * 0.3, 0.7, &[])?; + return Ok(()); + } + + // Convert to RgbaImage once; all _rgba variants operate on this buffer directly. + // This avoids the 6× to_rgba8() → compute → DynamicImage::ImageRgba8() round-trips + // that the original code performed (Bug #13 fix). + let mut rgba = img.to_rgba8(); + + // 1. Skin smoothing + apply_skin_smoothing_rgba(&mut rgba, w, h, s * 0.35, 0.65, face_regions); + + // 2. Eye brighten + enlarge + let eye_regions: Vec<_> = face_regions + .iter() + .flat_map(|f| vec![f.left_eye, f.right_eye]) + .collect(); + if !eye_regions.is_empty() { + apply_eye_brighten_rgba(&mut rgba, w, h, &eye_regions, s * 0.45); + apply_eye_enlarge_rgba(&mut rgba, w, h, &eye_regions, s * 0.20); + } + + // 3. Teeth whiten + let teeth_regions: Vec<_> = face_regions.iter().map(|f| f.mouth).collect(); + if !teeth_regions.is_empty() { + apply_teeth_whitening_rgba(&mut rgba, w, h, &teeth_regions, s * 0.35, s * 0.30); + } + + // 4. Face reshape + apply_face_reshape_rgba(&mut rgba, w, h, face_regions, s * 0.25, s * 0.10); + + // 5. Skin tone unify + apply_skin_tone_unify_rgba(&mut rgba, w, h, face_regions, 0.0, 0.0, s * 0.25); + + // Wrap back once + *img = DynamicImage::ImageRgba8(rgba); + Ok(()) +} + +// --------------------------------------------------------------------------- +// 13. Portrait Adjustments Entry – Apply all portrait params from JSON +// --------------------------------------------------------------------------- + +/// Master entry point: given a `DynamicImage`, pre-computed face regions, and a +/// JSON-like portrait-adjustment map, apply every enabled adjustment in order. +/// This is the function called from `process_preview_job` after the GPU pass. +pub fn apply_portrait_adjustments( + img: &mut DynamicImage, + portrait_json: &serde_json::Value, + face_regions: &[FaceRegion], +) -> Result<(), String> { + // Extract each field; missing or zero fields are skipped + let get_f32 = |key: &str| -> f32 { + portrait_json + .get(key) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) as f32 + }; + let get_str = |key: &str| -> String { + portrait_json + .get(key) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string() + }; + + // Read personAttribute to filter which faces to process + let person_attribute = get_str("personAttribute"); + let filtered_faces: Vec = + if person_attribute.is_empty() || person_attribute == "all" { + face_regions.to_vec() + } else { + // Filter face regions based on personAttribute + face_regions + .iter() + .filter(|face| { + match person_attribute.as_str() { + "single" => true, // Process only the largest/dominant face + "male" | "elderMale" => { + let aspect = face.face_rect.2 as f32 / face.face_rect.3.max(1) as f32; + aspect > 0.85 // Wider / squarer jaw tends to be male + } + "female" | "elderFemale" => { + let aspect = face.face_rect.2 as f32 / face.face_rect.3.max(1) as f32; + aspect <= 0.85 // Narrower / oval face tends to be female + } + "child" => { + // Use absolute face width relative to actual image width + // as a size heuristic, NOT relative to the largest face. + // + // Typical child face width on a portrait photo is + // < 30% of image width (adults are ~35-50%). + let (img_w, _) = img.dimensions(); + // Face width < 28% of image width → likely child + // Also: if face is significantly smaller than the + // average face (for multi-person photos) + let is_small_absolute = face.face_rect.2 < img_w / 4; + let avg_width = face_regions.iter().map(|f| f.face_rect.2).sum::() + / face_regions.len().max(1) as u32; + let is_small_relative = face.face_rect.2 < avg_width / 2; + let is_smallest = face.face_rect.2 + == face_regions + .iter() + .map(|f| f.face_rect.2) + .min() + .unwrap_or(u32::MAX); + is_small_absolute + || (face_regions.len() > 1 && (is_small_relative && is_smallest)) + } // Heuristic: smaller faces + _ => true, + } + }) + .cloned() + .collect() + }; + + // For "single" mode, only process the largest (dominant) face. + // Bug #5 fix: was .take(1) which took the first in detection order, + // not the largest. Now uses max_by_key on face area (width * height). + let filtered_faces: Vec = if person_attribute == "single" { + filtered_faces + .into_iter() + .max_by_key(|f| f.face_rect.2 * f.face_rect.3) + .into_iter() + .collect() + } else { + filtered_faces + }; + + let skin_strength = get_f32("skinSmoothingStrength"); + let skin_detail = get_f32("skinSmoothingDetailPreserve"); + let face_slim = get_f32("faceSlimAmount"); + let jaw = get_f32("jawAmount"); + let forehead = get_f32("foreheadAmount"); + let eye_enlarge = get_f32("eyeEnlargeAmount"); + let eye_brighten = get_f32("eyeBrightenAmount"); + let teeth_bright = get_f32("teethWhitenBrightness"); + let teeth_desat = get_f32("teethWhitenDesaturate"); + let lipstick_color = get_str("lipstickColor"); + let lipstick_opacity = get_f32("lipstickOpacity"); + let blush_color = get_str("blushColor"); + let blush_opacity = get_f32("blushOpacity"); + let eyebrow_color = get_str("eyebrowColor"); + let eyebrow_opacity = get_f32("eyebrowOpacity"); + let hair_hue = get_f32("hairHueShift"); + let hair_bright = get_f32("hairBrightness"); + let body_slim = get_f32("bodySlimAmount"); + let body_height = get_f32("bodyHeightAmount"); + let leg_len = get_f32("legLengthAmount"); + let body_symmetry = portrait_json + .get("bodySymmetryEnabled") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + // Parse blemish spots + let (img_w, img_h) = img.dimensions(); + let spots: Vec<(u32, u32, u32)> = portrait_json + .get("blemishSpots") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|spot| { + let x = spot.get("x")?.as_f64()? as f32; + let y = spot.get("y")?.as_f64()? as f32; + let r = spot.get("radius")?.as_f64()? as f32; + let px_x = + ((x * img_w as f32).round() as i32).clamp(0, img_w as i32 - 1) as u32; + let px_y = + ((y * img_h as f32).round() as i32).clamp(0, img_h as i32 - 1) as u32; + let px_r = ((r * img_w as f32).max(3.0).round() as i32) + .clamp(1, (img_w / 2) as i32) as u32; + Some((px_x, px_y, px_r)) + }) + .collect() + }) + .unwrap_or_default(); + + // 1. Blemish removal (does not need face_regions) + if !spots.is_empty() { + apply_blemish_removal(img, &spots, 0.5)?; + } + + // 2. Skin smoothing + if skin_strength > 1e-4 { + apply_skin_smoothing( + img, + skin_strength / 100.0, + skin_detail / 100.0, + &filtered_faces, + )?; + } + + // 3. Face reshape + if face_slim > 1e-4 || jaw.abs() > 1e-4 || forehead.abs() > 1e-4 { + if !filtered_faces.is_empty() { + // forehead adjustment: shift face_rect top upward/downward + let mut adjusted_faces: Vec = filtered_faces.to_vec(); + if forehead.abs() > 1e-4 { + for face in &mut adjusted_faces { + let shift = (forehead / 50.0 * face.face_rect.3 as f32 / 4.0) as i32; + face.face_rect.1 = face.face_rect.1.saturating_add_signed(-shift); + face.face_rect.3 = (face.face_rect.3 as i32 + shift).max(10) as u32; + } + } + apply_face_reshape(img, &adjusted_faces, face_slim / 100.0, jaw / 50.0)?; + } + } + + // 4. Eye enhance + if (eye_enlarge > 1e-4 || eye_brighten > 1e-4) && !filtered_faces.is_empty() { + let eye_regions: Vec<_> = filtered_faces + .iter() + .flat_map(|f| vec![f.left_eye, f.right_eye]) + .collect(); + if eye_enlarge > 1e-4 { + apply_eye_enlarge(img, &eye_regions, eye_enlarge / 100.0)?; + } + if eye_brighten > 1e-4 { + apply_eye_brighten(img, &eye_regions, eye_brighten / 100.0)?; + } + } + + // 5. Teeth whiten + if (teeth_bright > 1e-4 || teeth_desat > 1e-4) && !filtered_faces.is_empty() { + let teeth_regions: Vec<_> = filtered_faces.iter().map(|f| f.mouth).collect(); + apply_teeth_whitening( + img, + &teeth_regions, + teeth_bright / 100.0, + teeth_desat / 100.0, + )?; + } + + // 6. Makeup + if !lipstick_color.is_empty() && lipstick_opacity > 1e-4 && !filtered_faces.is_empty() { + let lip_regions: Vec<_> = filtered_faces.iter().map(|f| f.mouth).collect(); + let col = hex_to_rgb(&lipstick_color).unwrap_or((200, 50, 50)); + apply_makeup(img, "lip", &lip_regions, col, lipstick_opacity / 100.0)?; + } + if !blush_color.is_empty() && blush_opacity > 1e-4 && !filtered_faces.is_empty() { + // Blush: on cheeks, lateral to nose + let mut blush_regions = Vec::new(); + for face in &filtered_faces { + let cheek_r = face.face_rect.2 / 5; + blush_regions.push(( + face.left_eye.0.saturating_sub(cheek_r), + face.left_eye.1 + cheek_r, + cheek_r, + )); + blush_regions.push(( + face.right_eye.0 + cheek_r, + face.right_eye.1 + cheek_r, + cheek_r, + )); + } + let col = hex_to_rgb(&blush_color).unwrap_or((220, 100, 100)); + apply_makeup(img, "blush", &blush_regions, col, blush_opacity / 100.0)?; + } + if !eyebrow_color.is_empty() && eyebrow_opacity > 1e-4 && !filtered_faces.is_empty() { + let brow_regions: Vec<_> = filtered_faces + .iter() + .map(|f| { + let brow_y = f.face_rect.1 + f.face_rect.3 / 5; + let brow_r = f.face_rect.2 / 6; + (f.face_rect.0 + f.face_rect.2 / 2, brow_y, brow_r) + }) + .collect(); + let col = hex_to_rgb(&eyebrow_color).unwrap_or((80, 50, 30)); + apply_makeup(img, "eyebrow", &brow_regions, col, eyebrow_opacity / 100.0)?; + } + + // 7. Hair adjust + if (hair_hue.abs() > 1e-4 || hair_bright.abs() > 1e-4) && !filtered_faces.is_empty() { + apply_hair_adjust(img, &filtered_faces, hair_hue, hair_bright / 50.0)?; + } + + // 8. Body reshape + if (body_slim > 1e-4 || body_height > 1e-4 || leg_len > 1e-4) && !filtered_faces.is_empty() { + apply_body_reshape( + img, + &filtered_faces, + body_slim / 100.0, + body_height / 100.0, + leg_len / 100.0, + body_symmetry, + )?; + } + + // 9. Skin tone unify (subtle, applied last) + // Only apply when skin smoothing is active so we don't force an unwanted + // color shift when the user has not enabled any portrait adjustments. + if skin_strength > 1e-4 && !filtered_faces.is_empty() { + let unify_strength = skin_strength / 100.0 * 0.1; + apply_skin_tone_unify(img, &filtered_faces, 0.0, 0.0, unify_strength)?; + } + + Ok(()) +} + +fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> { + let hex = hex.trim_start_matches('#'); + if hex.len() == 6 { + let r = u8::from_str_radix(&hex[0..2], 16).ok()?; + let g = u8::from_str_radix(&hex[2..4], 16).ok()?; + let b = u8::from_str_radix(&hex[4..6], 16).ok()?; + Some((r, g, b)) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use image::{Rgba, RgbaImage}; + + #[test] + fn test_rgb_to_f32_roundtrip() { + assert_eq!(rgb_to_f32(0, 0, 0), (0.0, 0.0, 0.0)); + assert_eq!(rgb_to_f32(255, 255, 255), (1.0, 1.0, 1.0)); + assert_eq!( + rgb_to_f32(128, 128, 128), + (128.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0) + ); + } + + #[test] + fn test_f32_to_rgb_clamps() { + assert_eq!(f32_to_rgb(-0.5, 1.2, 0.5), (0, 255, 128)); + assert_eq!(f32_to_rgb(0.0, 0.0, 1.0), (0, 0, 255)); + } + + #[test] + fn test_luminance() { + assert_eq!(luminance(1.0, 1.0, 1.0), 1.0); + assert_eq!(luminance(0.0, 0.0, 0.0), 0.0); + let lum = luminance(1.0, 0.0, 0.0); + assert!((lum - 0.299).abs() < 1e-6); + } + + #[test] + fn test_gaussian() { + assert_eq!(gaussian(0.0, 1.0), 1.0); + assert!(gaussian(3.0, 1.0) < 0.05); + assert_eq!(gaussian(0.0, 0.0), 1.0); + assert_eq!(gaussian(1.0, 0.0), 0.0); + } + + #[test] + fn test_rgb_to_hsl_red() { + let (h, s, l) = rgb_to_hsl(1.0, 0.0, 0.0); + assert!((h - 0.0).abs() < 1e-3 || (h - 360.0).abs() < 1e-3); + assert!((s - 1.0).abs() < 1e-3); + assert!((l - 0.5).abs() < 1e-3); + } + + #[test] + fn test_hsl_to_rgb_roundtrip() { + for h in [0.0, 60.0, 120.0, 180.0, 240.0, 300.0] { + for s in [0.0, 0.5, 1.0] { + for l in [0.25, 0.5, 0.75] { + let (r, g, b) = hsl_to_rgb(h, s, l); + let (h2, s2, l2) = rgb_to_hsl(r, g, b); + if s > 1e-6 { + let dh = (h - h2).abs().min(360.0 - (h - h2).abs()); + assert!( + dh < 1.0, + "HSL roundtrip failed for h={}, s={}, l={}", + h, + s, + l + ); + assert!((s - s2).abs() < 1e-3); + } + assert!((l - l2).abs() < 1e-3); + } + } + } + } + + #[test] + fn test_rgb_to_ycbcr() { + let (y, cb, cr) = rgb_to_ycbcr(255, 255, 255); + assert!(y > 250.0); + assert!(cb > 125.0 && cb < 135.0); + assert!(cr > 125.0 && cr < 135.0); + } + + #[test] + fn test_rgb_to_lab_roundtrip() { + let (l, a, b) = rgb_to_lab(0.5, 0.5, 0.5); + let (r, g, b_out) = lab_to_rgb(l, a, b); + assert!((r - 0.5).abs() < 1e-3); + assert!((g - 0.5).abs() < 1e-3); + assert!((b_out - 0.5).abs() < 1e-3); + } + + #[test] + fn test_hex_to_rgb() { + assert_eq!(hex_to_rgb("#FF0000"), Some((255, 0, 0))); + assert_eq!(hex_to_rgb("00FF00"), Some((0, 255, 0))); + assert_eq!(hex_to_rgb("0000FF"), Some((0, 0, 255))); + assert_eq!(hex_to_rgb("GG0000"), None); + assert_eq!(hex_to_rgb("FF000"), None); + } + + #[test] + fn test_apply_skin_smoothing_zero_image() { + let mut img = + DynamicImage::ImageRgba8(RgbaImage::from_pixel(1, 1, Rgba([128, 128, 128, 255]))); + assert!(apply_skin_smoothing(&mut img, 0.5, 0.5, &[]).is_ok()); + } + + #[test] + fn test_apply_skin_smoothing_rejects_zero_dim() { + let mut img = DynamicImage::ImageRgba8(RgbaImage::new(0, 10)); + assert!(apply_skin_smoothing(&mut img, 0.5, 0.5, &[]).is_err()); + } + + #[test] + fn test_detect_face_regions_tiny_image() { + let img = DynamicImage::ImageRgba8(RgbaImage::new(16, 16)); + let regions = detect_face_regions(&img); + assert!(regions.is_empty()); + } + + #[test] + fn test_apply_face_reshape_zero_dim() { + let mut img = DynamicImage::ImageRgba8(RgbaImage::new(0, 10)); + assert!(apply_face_reshape(&mut img, &[], 0.5, 0.5).is_err()); + } + + #[test] + fn test_apply_eye_enlarge_no_regions() { + let mut img = + DynamicImage::ImageRgba8(RgbaImage::from_pixel(10, 10, Rgba([255, 0, 0, 255]))); + assert!(apply_eye_enlarge(&mut img, &[], 0.5).is_ok()); + } + + #[test] + fn test_apply_teeth_whitening_zero_dim() { + let mut img = DynamicImage::ImageRgba8(RgbaImage::new(0, 10)); + assert!(apply_teeth_whitening(&mut img, &[(5, 5, 3)], 0.5, 0.5).is_err()); + } + + #[test] + fn test_apply_eye_brighten_zero_dim() { + let mut img = DynamicImage::ImageRgba8(RgbaImage::new(0, 10)); + assert!(apply_eye_brighten(&mut img, &[(5, 5, 3)], 0.5).is_err()); + } + + #[test] + fn test_apply_makeup_zero_dim() { + let mut img = DynamicImage::ImageRgba8(RgbaImage::new(0, 10)); + assert!(apply_makeup(&mut img, "lip", &[(5, 5, 3)], (200, 50, 50), 0.5).is_err()); + } + + #[test] + fn test_apply_blemish_removal_zero_dim() { + let mut img = DynamicImage::ImageRgba8(RgbaImage::new(0, 10)); + assert!(apply_blemish_removal(&mut img, &[(5, 5, 3)], 0.5).is_err()); + } + + #[test] + fn test_apply_hair_adjust_zero_dim() { + let mut img = DynamicImage::ImageRgba8(RgbaImage::new(0, 10)); + assert!(apply_hair_adjust(&mut img, &[], 10.0, 0.1).is_err()); + } + + #[test] + fn test_apply_body_reshape_empty_faces() { + let mut img = + DynamicImage::ImageRgba8(RgbaImage::from_pixel(10, 10, Rgba([255, 0, 0, 255]))); + assert!(apply_body_reshape(&mut img, &[], 0.5, 0.5, 0.5, true).is_ok()); + } + + #[test] + fn test_apply_skin_tone_unify_zero_dim() { + let mut img = DynamicImage::ImageRgba8(RgbaImage::new(0, 10)); + assert!(apply_skin_tone_unify(&mut img, &[], 0.0, 0.0, 0.5).is_err()); + } + + #[test] + fn test_apply_one_click_beauty_no_faces() { + let mut img = + DynamicImage::ImageRgba8(RgbaImage::from_pixel(10, 10, Rgba([128, 128, 128, 255]))); + assert!(apply_one_click_beauty(&mut img, 0.5, &[]).is_ok()); + } + + #[test] + fn test_compute_center_radius_empty() { + assert_eq!(compute_center_radius(&[]), (0, 0, 0)); + } + + #[test] + fn test_compute_center_radius_single() { + assert_eq!(compute_center_radius(&[(10.0, 20.0)]), (10, 20, 1)); + } + + #[test] + fn test_label_connected_components_empty() { + let mask = vec![false; 4]; + let labels = label_connected_components(&mask, 2, 2); + assert_eq!(labels, vec![0, 0, 0, 0]); + } + + #[test] + fn test_extract_components_empty() { + let labels = vec![0, 0, 0, 0]; + let comps = extract_components(&labels, 2, 2); + assert!(comps.is_empty()); + } + + #[test] + fn test_erode_mask_all_false() { + let src = vec![false; 4]; + let mut dst = vec![false; 4]; + erode_mask(&src, 2, 2, &mut dst, 1); + assert_eq!(dst, vec![false, false, false, false]); + } + + #[test] + fn test_dilate_mask_all_false() { + let src = vec![false; 4]; + let mut dst = vec![false; 4]; + dilate_mask(&src, 2, 2, &mut dst, 1); + assert_eq!(dst, vec![false, false, false, false]); + } +} diff --git a/src-tauri/src/preset_converter.rs b/src-tauri/src/preset_converter.rs index 0a10c4f281..a15e2a01fc 100644 --- a/src-tauri/src/preset_converter.rs +++ b/src-tauri/src/preset_converter.rs @@ -35,11 +35,47 @@ fn get_attr_as_f64(attrs: &HashMap, key: &str) -> Option { } fn extract_xmp_name(xmp_content: &str) -> Option { - let re = - Regex::new(r#"(?s).*?.*?]*>([^<]+).*?"#) + // Primary pattern: name wrapped in + // (the form produced by Adobe Camera Raw / Lightroom). + let alt_re = Regex::new( + r#"(?s)\s*\s*]*>([^<]+)\s*\s*"#, + ) + .ok()?; + if let Some(cap) = alt_re.captures(xmp_content) { + if let Some(m) = cap.get(1) { + let name = m.as_str().trim().to_string(); + if !name.is_empty() { + return Some(name); + } + } + } + + // Fallback pattern: direct text inside . + let simple_re = Regex::new(r#"(?s)\s*([^<]+?)\s*"#).ok()?; + if let Some(cap) = simple_re.captures(xmp_content) { + if let Some(m) = cap.get(1) { + let name = m.as_str().trim().to_string(); + if !name.is_empty() { + return Some(name); + } + } + } + + // Some Lightroom exports wrap the name in a localized form using + // . Try that before giving up. + let default_re = + Regex::new(r#"(?s).*?]*xml:lang=["']x-default["'][^>]*>([^<]+).*?"#) .ok()?; - re.captures(xmp_content) - .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string())) + if let Some(cap) = default_re.captures(xmp_content) { + if let Some(m) = cap.get(1) { + let name = m.as_str().trim().to_string(); + if !name.is_empty() { + return Some(name); + } + } + } + + None } fn extract_tone_curve_points(xmp_str: &str, curve_name: &str) -> Option> { @@ -340,12 +376,28 @@ pub fn convert_xmp_to_preset(xmp_content: &str) -> Result { let preset_name = extract_xmp_name(xmp_content).unwrap_or_else(|| "Imported Preset".to_string()); + // Infer include_crop_transform from geometry-related keys in the XMP + let has_crop = attrs.contains_key("CropTop") + || attrs.contains_key("CropLeft") + || attrs.contains_key("CropBottom") + || attrs.contains_key("CropRight") + || attrs.contains_key("CropAngle") + || attrs.contains_key("CropConstrainToWarp"); + + let include_crop_transform = Some(has_crop); + + // Infer include_masks from mask-related keys (Lightroom uses RangeMask) + let has_masks = + attrs.contains_key("RangeMaskType") || attrs.contains_key("RangeMaskRangeAmount"); + + let include_masks = Some(has_masks); + Ok(Preset { id: Uuid::new_v4().to_string(), name: preset_name, adjustments: Value::Object(adjustments), - include_masks: Some(false), - include_crop_transform: Some(false), + include_masks, + include_crop_transform, preset_type: Some("style".to_string()), }) } diff --git a/src-tauri/src/raw_processing.rs b/src-tauri/src/raw_processing.rs index 175f93fcc9..408808c5c4 100644 --- a/src-tauri/src/raw_processing.rs +++ b/src-tauri/src/raw_processing.rs @@ -41,7 +41,7 @@ fn srgb_to_linear(value: f32) -> f32 { if value <= 0.04045 { value / 12.92 } else { - ((value + 0.055) / 1.055).powf(3.0) + ((value + 0.055) / 1.055).powf(2.4) } } @@ -85,18 +85,17 @@ fn develop_internal( _ => (false, true), }; - let original_white_level = raw_image - .whitelevel - .0 - .first() - .cloned() - .unwrap_or(u16::MAX as u32) as f32; + // Average white/black levels across all channels to avoid color bias + // when individual channels differ (e.g. some Canon/Sony sensors). + let original_white_level = raw_image.whitelevel.0.iter().sum::() as f32 + / raw_image.whitelevel.0.len().max(1) as f32; let original_black_level = raw_image .blacklevel .levels - .first() + .iter() .map(|r| r.as_f32()) - .unwrap_or(0.0); + .sum::() + / raw_image.blacklevel.levels.len().max(1) as f32; for level in raw_image.whitelevel.0.iter_mut() { *level = u32::MAX; @@ -114,6 +113,9 @@ fn develop_internal( developer.demosaic_algorithm = DemosaicAlgorithm::Speed; developer.steps.retain(|&step| step != ProcessingStep::SRgb); } else { + // Use the default Quality algorithm (PPG for Bayer, Full-Res for X-Trans). + // Previously PixelShift / SuperPixel were used, but these variants no + // longer exist in the current rawler API and caused compilation errors. developer.steps.retain(|&step| step != ProcessingStep::SRgb); } diff --git a/src-tauri/src/shaders/shader.wgsl b/src-tauri/src/shaders/shader.wgsl index 7bfb5953b2..9ee9b63a44 100644 --- a/src-tauri/src/shaders/shader.wgsl +++ b/src-tauri/src/shaders/shader.wgsl @@ -617,7 +617,7 @@ fn apply_creative_color(color: vec3, sat: f32, vib: f32) -> vec3 { let hue_dist = min(abs(hue - skin_center), 360.0 - abs(hue - skin_center)); let is_skin = smoothstep(35.0, 10.0, hue_dist); let skin_dampener = mix(1.0, 0.6, is_skin); - let amount = vib * sat_mask * skin_dampener * 3.0; + let amount = vib * sat_mask * skin_dampener; processed = mix(vec3(luma), processed, 1.0 + amount); } else { let desat_mask = 1.0 - smoothstep(0.2, 0.8, current_sat); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7723f6ce67..6cbd6fdb98 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -17,7 +17,7 @@ "height": 720, "minWidth": 800, "minHeight": 600, - "transparent": true, + "transparent": false, "decorations": false } ], @@ -34,7 +34,7 @@ "bundle": { "active": true, "targets": "all", - "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/256x256.png", "icons/512x512.png", "icons/icon.icns", "icons/icon.ico"], "resources": ["resources", "lensfun_db"], "category": "Photography", "fileAssociations": [ @@ -88,10 +88,30 @@ } ], "windows": { + "webviewInstallMode": { + "type": "downloadBootstrapper" + }, "nsis": { - "installerIcon": "icons/icon.ico" + "installerIcon": "icons/icon.ico", + "headerImage": null, + "sidebarImage": null, + "installMode": "currentUser", + "languages": [ + "English", + "SimpChinese", + "TradChinese", + "French", + "German", + "Italian", + "Japanese", + "Korean", + "Polish", + "Portuguese", + "Russian", + "Spanish" + ] } } }, - "version": "1.5.9" + "version": "1.8.13" } diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000000..5effccef47 --- /dev/null +++ b/src-tauri/tauri.windows.conf.json @@ -0,0 +1,29 @@ +{ + "bundle": { + "windows": { + "webviewInstallMode": { + "type": "downloadBootstrapper" + }, + "nsis": { + "installerIcon": "icons/icon.ico", + "headerImage": null, + "sidebarImage": null, + "installMode": "currentUser", + "languages": [ + "English", + "SimpChinese", + "TradChinese", + "French", + "German", + "Italian", + "Japanese", + "Korean", + "Polish", + "Portuguese", + "Russian", + "Spanish" + ] + } + } + } +} diff --git a/src/App.tsx b/src/App.tsx index 09ab2fbdad..0c33691c93 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,791 +1,13 @@ -import { type PointerEvent as ReactPointerEvent, useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { invoke } from '@tauri-apps/api/core'; -import { listen } from '@tauri-apps/api/event'; -import { getCurrentWindow } from '@tauri-apps/api/window'; -import { ClerkProvider } from '@clerk/react'; -import { ToastContainer, toast, Slide } from 'react-toastify'; -import clsx from 'clsx'; - -import TitleBar from './window/TitleBar'; -import FolderTree from './components/panel/FolderTree'; -import ExportPanel from './components/panel/right/ExportPanel'; -import Resizer from './components/ui/Resizer'; -import GlobalTooltip from './components/ui/GlobalTooltip'; -import AppModals from './components/modals/AppModals'; - -import EditorView from './components/views/EditorView'; -import LibraryView from './components/views/LibraryView'; - -import { ContextMenuProvider } from './context/ContextMenuContext'; -import { useSettingsStore } from './store/useSettingsStore'; -import { useUIStore } from './store/useUIStore'; -import { useLibraryStore } from './store/useLibraryStore'; -import { useEditorStore } from './store/useEditorStore'; -import { useProcessStore } from './store/useProcessStore'; -import { useShallow } from 'zustand/react/shallow'; - -import { useThumbnails } from './hooks/useThumbnails'; -import { ImageDimensions } from './hooks/useImageRenderSize'; -import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; -import { useTauriListeners } from './hooks/useTauriListeners'; -import { useFileOperations } from './hooks/useFileOperations'; -import { useAppContextMenus } from './hooks/useAppContextMenus'; -import { useSortedLibrary } from './hooks/useSortedLibrary'; -import { useAppNavigation } from './hooks/useAppNavigation'; -import { useExternalEditSession } from './hooks/useExternalEditSession'; -import ExternalEditBar from './components/ui/ExternalEditBar'; -import { Status } from './components/ui/ExportImportProperties'; - -import { useEditorActions } from './hooks/useEditorActions'; -import { useLibraryActions } from './hooks/useLibraryActions'; -import { useProductivityActions } from './hooks/useProductivityActions'; - -import { useAppInitialization } from './hooks/useAppInitialization'; -import { useAndroidBackHandler } from './hooks/useAndroidBackHandler'; -import './i18n'; - -import { - Invokes, - ImageFile, - LibraryViewMode, - Panel, - Theme, - Orientation, - ThumbnailSize, - ThumbnailAspectRatio, -} from './components/ui/AppProperties'; - -import ImageProcessingManager from './components/managers/ImageProcessingManager'; -import ImageLoaderManager from './components/managers/ImageLoaderManager'; - -const CLERK_PUBLISHABLE_KEY = 'pk_test_YnJpZWYtc2Vhc25haWwtMTIuY2xlcmsuYWNjb3VudHMuZGV2JA'; // local dev key - -const insertChildrenIntoTree = (node: any, targetPath: string, newChildren: any[]): any => { - if (!node) return null; - - if (node.path === targetPath) { - const mergedChildren = newChildren.map((newChild: any) => { - const existingChild = node.children?.find((c: any) => c.path === newChild.path); - if (existingChild && existingChild.children && existingChild.children.length > 0) { - return { ...newChild, children: existingChild.children }; - } - return newChild; - }); - return { ...node, children: mergedChildren }; - } - - if (node.children && node.children.length > 0) { - return { - ...node, - children: node.children.map((child: any) => insertChildrenIntoTree(child, targetPath, newChildren)), - }; - } - - return node; -}; - -function App() { - const COMPACT_EDITOR_MAX_WIDTH = 900; - - const { appSettings, theme, osPlatform, handleSettingsChange } = useSettingsStore( - useShallow((state) => ({ - appSettings: state.appSettings, - theme: state.theme, - osPlatform: state.osPlatform, - handleSettingsChange: state.handleSettingsChange, - })), - ); - - const { - isFullScreen, - isWindowFullScreen, - isInstantTransition, - isLayoutReady, - uiVisibility, - isLibraryExportPanelVisible, - leftPanelWidth, - rightPanelWidth, - compactEditorPanelHeightOverride, - activeRightPanel, - setUI, - setRightPanel, - } = useUIStore( - useShallow((state) => ({ - isFullScreen: state.isFullScreen, - isWindowFullScreen: state.isWindowFullScreen, - isInstantTransition: state.isInstantTransition, - isLayoutReady: state.isLayoutReady, - uiVisibility: state.uiVisibility, - isLibraryExportPanelVisible: state.isLibraryExportPanelVisible, - leftPanelWidth: state.leftPanelWidth, - rightPanelWidth: state.rightPanelWidth, - compactEditorPanelHeightOverride: state.compactEditorPanelHeightOverride, - activeRightPanel: state.activeRightPanel, - setUI: state.setUI, - setRightPanel: state.setRightPanel, - })), - ); - - const { rootPaths, currentFolderPath, expandedFolders, multiSelectedPaths, setLibrary } = useLibraryStore( - useShallow((state) => ({ - rootPaths: state.rootPaths, - currentFolderPath: state.currentFolderPath, - expandedFolders: state.expandedFolders, - multiSelectedPaths: state.multiSelectedPaths, - setLibrary: state.setLibrary, - })), - ); - - const { selectedImage, activeMaskContainerId, activeAiPatchContainerId, hasRenderedFirstFrame, setEditor } = - useEditorStore( - useShallow((state) => ({ - selectedImage: state.selectedImage, - activeMaskContainerId: state.activeMaskContainerId, - activeAiPatchContainerId: state.activeAiPatchContainerId, - hasRenderedFirstFrame: state.hasRenderedFirstFrame, - setEditor: state.setEditor, - })), - ); - - const { exportState, setExportState } = useProcessStore( - useShallow((state) => ({ - exportState: state.exportState, - setExportState: state.setExportState, - })), - ); - - const defaultThumbnailSize = osPlatform === 'android' ? ThumbnailSize.Small : ThumbnailSize.Medium; - const defaultLibraryViewMode = osPlatform === 'android' ? LibraryViewMode.Recursive : LibraryViewMode.Flat; - - const selectedImagePathRef = useRef(null); - useEffect(() => { - selectedImagePathRef.current = selectedImage?.path ?? null; - }, [selectedImage?.path]); - - const prevAdjustmentsRef = useRef(null); - - const [viewportSize, setViewportSize] = useState(() => { - if (typeof window === 'undefined') { - return { width: 0, height: 0 }; - } - - return { - width: Math.round(window.visualViewport?.width ?? window.innerWidth), - height: Math.round(window.visualViewport?.height ?? window.innerHeight), - }; - }); - - const isBackendReadyRef = useRef(true); - const previewJobIdRef = useRef(0); - const latestRenderedJobIdRef = useRef(0); - const currentResRef = useRef(1280); - const cachedEditStateRef = useRef(null); - - const [libraryViewMode, setLibraryViewMode] = useState(defaultLibraryViewMode); - const [isResizing, setIsResizing] = useState(false); - const [thumbnailSize, setThumbnailSize] = useState(defaultThumbnailSize); - const [thumbnailAspectRatio, setThumbnailAspectRatio] = useState(ThumbnailAspectRatio.Cover); - - const { requestThumbnails, clearThumbnailQueue, markGenerated } = useThumbnails(); - - const transformWrapperRef = useRef(null); - const preloadedDataRef = useRef<{ - trees?: Promise; - images?: Promise; - rootPaths?: string[]; - currentPath?: string; - }>({}); - - useAppInitialization({ - preloadedDataRef, - thumbnailSize, - setThumbnailSize, - thumbnailAspectRatio, - setThumbnailAspectRatio, - libraryViewMode, - setLibraryViewMode, - }); - - const isAndroid = osPlatform === 'android'; - const isPortraitViewport = viewportSize.width > 0 && viewportSize.height > viewportSize.width; - const isCompactPortrait = - viewportSize.width > 0 && viewportSize.width <= COMPACT_EDITOR_MAX_WIDTH && isPortraitViewport; - - const compactEditorPanelMinHeight = 220; - const compactEditorPanelMaxHeight = - viewportSize.height > 0 - ? Math.max(compactEditorPanelMinHeight, Math.min(Math.round(viewportSize.height * 0.85), 850)) - : 520; - - const getDynamicCompactPanelHeight = () => { - const { originalSize, adjustments } = useEditorStore.getState(); - const halfScreenHeight = viewportSize.height > 0 ? Math.round(viewportSize.height * 0.5) : 340; - - if (!selectedImage || originalSize.width === 0 || originalSize.height === 0 || viewportSize.width === 0) { - return halfScreenHeight; - } - let effectiveRatio = originalSize.width / originalSize.height; - const orientationSteps = adjustments?.orientationSteps || 0; - if (orientationSteps % 2 !== 0) { - effectiveRatio = originalSize.height / originalSize.width; - } - if (adjustments?.aspectRatio && adjustments.aspectRatio > 0) { - effectiveRatio = adjustments.aspectRatio; - } - const desiredImageHeight = viewportSize.width / effectiveRatio; - const topUiEstimation = !appSettings?.decorations && !isWindowFullScreen ? 110 : 60; - const totalDesiredTopHeight = desiredImageHeight + topUiEstimation; - const calculatedBottomHeight = Math.round(viewportSize.height - totalDesiredTopHeight); - return Math.max(halfScreenHeight, calculatedBottomHeight); - }; - - const compactEditorPanelDefaultHeight = getDynamicCompactPanelHeight(); - const compactEditorPanelHeight = Math.max( - compactEditorPanelMinHeight, - Math.min(compactEditorPanelHeightOverride ?? compactEditorPanelDefaultHeight, compactEditorPanelMaxHeight), - ); - const compactEditorPanelCollapsedHeight = 96; - - const { handleCopyAdjustments, handlePasteAdjustments, handleResetAdjustments, handleZoomChange } = - useEditorActions(); - - const navigationRefs = { - transformWrapperRef, - preloadedDataRef, - cachedEditStateRef, - selectedImagePathRef, - isBackendReadyRef, - latestRenderedJobIdRef, - previewJobIdRef, - currentResRef, - prevAdjustmentsRef, - }; - - const { - handleGoHome, - handleBackToLibrary, - handleImageSelect, - handleSelectSubfolder, - handleSelectAlbum, - handleOpenFolder, - handleContinueSession, - } = useAppNavigation({ - clearThumbnailQueue, - refs: navigationRefs, - }); - - const { - externalEditSession, - isFinishing: isExternalEditFinishing, - finishExternalEdit, - } = useExternalEditSession(handleImageSelect); - - const { - handleRate, - handleClearSelection, - handleLibraryImageSingleClick, - handleImageClick, - handleSetColorLabel, - refreshAllFolderTrees, - handleTogglePinFolder, - handleCreateAlbumItem, - handleRenameAlbumItem, - } = useLibraryActions(handleImageSelect); - - const sortedImageList = useSortedLibrary(); - - const handleLibraryRefresh = useCallback(async () => { - if (currentFolderPath) { - if (currentFolderPath.startsWith('Album: ')) { - const { activeAlbumId, albumTree } = useLibraryStore.getState(); - if (activeAlbumId) { - const findObj = (nodes: any[]): any => { - for (const n of nodes) { - if (n.id === activeAlbumId) return n; - if (n.type === 'group') { - const f = findObj(n.children); - if (f) return f; - } - } - return null; - }; - const album = findObj(albumTree); - if (album) await handleSelectAlbum(album.id, album.name, album.images, true); - } - } else { - await handleSelectSubfolder(currentFolderPath, false, undefined, false, true); - } - } - }, [currentFolderPath, handleSelectSubfolder, handleSelectAlbum]); - - const { - executeDelete, - handleDeleteSelected, - handleCreateFolder, - handleRenameFolder, - handleSaveRename, - handleRenameFiles, - handleStartImport, - handleImportClick, - handlePasteFiles, - } = useFileOperations( - handleLibraryRefresh, - refreshAllFolderTrees, - handleImageSelect, - handleBackToLibrary, - sortedImageList, - ); - - const { - handleStartPanorama, - handleSavePanorama, - handleStartHdr, - handleSaveHdr, - handleApplyDenoise, - handleBatchDenoise, - handleSaveDenoisedImage, - handleSaveCollage, - } = useProductivityActions(handleLibraryRefresh); - - const { - handleEditorContextMenu, - handleThumbnailContextMenu, - handleFolderTreeContextMenu, - handleAlbumTreeContextMenu, - handleMainLibraryContextMenu, - } = useAppContextMenus({ - handleImageSelect, - handleBackToLibrary, - handleLibraryRefresh, - handleRenameFiles, - handleImportClick, - refreshAllFolderTrees, - refreshImageList: handleLibraryRefresh, - executeDelete, - handleTogglePinFolder, - }); - - useTauriListeners({ - refreshAllFolderTrees, - handleSelectSubfolder, - refreshImageList: handleLibraryRefresh, - markGenerated, - }); - - useAndroidBackHandler(); - - const handleToggleFullScreen = useCallback(() => { - const { zoom, selectedImage } = useEditorStore.getState(); - const currentlyZoomed = zoom > 1.01; - setUI({ isInstantTransition: currentlyZoomed }); - - if (isFullScreen) { - setUI({ isFullScreen: false }); - } else { - if (!selectedImage) return; - setUI({ isFullScreen: true }); - } - - if (currentlyZoomed) { - setTimeout(() => setUI({ isInstantTransition: false }), 100); - } - }, [isFullScreen, setUI]); - - useKeyboardShortcuts({ - sortedImageList, - handleBackToLibrary, - handleDeleteSelected, - handleImageSelect, - handlePasteFiles, - handleToggleFullScreen, - handleZoomChange, - }); - - useEffect(() => { - if (typeof window === 'undefined') return; - - const updateViewportSize = () => { - const nextViewportSize = { - width: Math.round(window.visualViewport?.width ?? window.innerWidth), - height: Math.round(window.visualViewport?.height ?? window.innerHeight), - }; - - setViewportSize((prev) => - prev.width === nextViewportSize.width && prev.height === nextViewportSize.height ? prev : nextViewportSize, - ); - }; - - updateViewportSize(); - - window.addEventListener('resize', updateViewportSize); - window.addEventListener('orientationchange', updateViewportSize); - window.visualViewport?.addEventListener('resize', updateViewportSize); - - return () => { - window.removeEventListener('resize', updateViewportSize); - window.removeEventListener('orientationchange', updateViewportSize); - window.visualViewport?.removeEventListener('resize', updateViewportSize); - }; - }, []); - - useEffect(() => { - const handleGlobalContextMenu = (event: MouseEvent) => { - event.preventDefault(); - }; - window.addEventListener('contextmenu', handleGlobalContextMenu); - return () => window.removeEventListener('contextmenu', handleGlobalContextMenu); - }, []); - - const isLightTheme = useMemo(() => [Theme.Light, Theme.Snow, Theme.Arctic].includes(theme as Theme), [theme]); - - useEffect(() => { - if ( - (activeRightPanel !== Panel.Masks || !activeMaskContainerId) && - (activeRightPanel !== Panel.Ai || !activeAiPatchContainerId) - ) { - setEditor({ isMaskControlHovered: false }); - } - }, [activeRightPanel, activeMaskContainerId, activeAiPatchContainerId, setEditor]); - - useEffect(() => { - const unlisten = listen('ai-connector-status-update', (event: any) => { - setEditor({ isAIConnectorConnected: event.payload.connected }); - }); - invoke(Invokes.CheckAIConnectorStatus); - const interval = setInterval(() => invoke(Invokes.CheckAIConnectorStatus), 10000); - return () => { - clearInterval(interval); - unlisten.then((f) => f()); - }; - }, [setEditor]); - - const createResizeHandler = (stateKey: string, startSize: number) => (e: ReactPointerEvent) => { - if (e.pointerType === 'mouse' && e.button !== 0) return; - e.preventDefault(); - e.stopPropagation(); - setIsResizing(true); - - const pointerId = e.pointerId; - const target = e.currentTarget; - const startX = e.clientX; - const startY = e.clientY; - - const previousTouchAction = document.documentElement.style.touchAction; - const previousUserSelect = document.documentElement.style.userSelect; - - target.setPointerCapture?.(pointerId); - document.documentElement.style.touchAction = 'none'; - document.documentElement.style.userSelect = 'none'; - - const doDrag = (moveEvent: PointerEvent) => { - if (moveEvent.pointerId !== pointerId) return; - moveEvent.preventDefault(); - - if (stateKey === 'left') { - setUI({ leftPanelWidth: Math.round(Math.max(200, Math.min(startSize + (moveEvent.clientX - startX), 500))) }); - } else if (stateKey === 'right') { - setUI({ rightPanelWidth: Math.round(Math.max(280, Math.min(startSize - (moveEvent.clientX - startX), 600))) }); - } else if (stateKey === 'bottom') { - setUI({ - bottomPanelHeight: Math.round(Math.max(100, Math.min(startSize - (moveEvent.clientY - startY), 400))), - }); - } else if (stateKey === 'compact') { - setUI({ - compactEditorPanelHeightOverride: Math.round( - Math.max( - compactEditorPanelMinHeight, - Math.min(startSize - (moveEvent.clientY - startY), compactEditorPanelMaxHeight), - ), - ), - }); - } - }; - - const stopDrag = (upEvent: PointerEvent) => { - if (upEvent.pointerId !== pointerId) return; - if (target.hasPointerCapture?.(pointerId)) target.releasePointerCapture(pointerId); - - document.documentElement.style.cursor = ''; - document.documentElement.style.touchAction = previousTouchAction; - document.documentElement.style.userSelect = previousUserSelect; - - window.removeEventListener('pointermove', doDrag); - window.removeEventListener('pointerup', stopDrag); - window.removeEventListener('pointercancel', stopDrag); - setIsResizing(false); - }; - document.documentElement.style.cursor = - stateKey === 'bottom' || stateKey === 'compact' ? 'row-resize' : 'col-resize'; - - window.addEventListener('pointermove', doDrag, { passive: false }); - window.addEventListener('pointerup', stopDrag); - window.addEventListener('pointercancel', stopDrag); - }; - - useEffect(() => { - const appWindow = getCurrentWindow(); - const checkFullscreen = async () => { - setUI({ isWindowFullScreen: await appWindow.isFullscreen() }); - }; - checkFullscreen(); - const unlistenPromise = appWindow.onResized(checkFullscreen); - return () => { - unlistenPromise.then((unlisten: any) => unlisten()); - }; - }, [setUI]); - - const handleRightPanelSelect = useCallback( - (panelId: Panel) => { - setRightPanel(panelId); - setEditor({ activeMaskId: null, activeAiSubMaskId: null, isWbPickerActive: false }); - }, - [setRightPanel, setEditor], - ); - - const handleToggleFolder = useCallback( - async (path: string) => { - const isExpanding = !expandedFolders.has(path); - setLibrary((state) => { - const newSet = new Set(state.expandedFolders); - if (isExpanding) { - newSet.add(path); - } else { - newSet.delete(path); - } - return { expandedFolders: newSet }; - }); - if (!isExpanding) return; - try { - const showCounts = appSettings?.enableFolderImageCounts ?? false; - const newChildren: any[] = await invoke(Invokes.GetFolderChildren, { - path, - showImageCounts: showCounts, - }); - setLibrary((state) => ({ - folderTrees: state.folderTrees.map((t: any) => insertChildrenIntoTree(t, path, newChildren)), - })); - setLibrary((state) => ({ - pinnedFolderTrees: state.pinnedFolderTrees.map((tree) => insertChildrenIntoTree(tree, path, newChildren)), - })); - } catch (err) { - toast.error(`Failed to load folder: ${err}`); - } - }, - [expandedFolders, appSettings?.enableFolderImageCounts, setLibrary], - ); - - const hasRoots = rootPaths && rootPaths.length > 0; - const hasMainContent = hasRoots || !!selectedImage; - - const renderFolderTree = () => { - if (!hasRoots) return null; - - return ( -
- handleSelectSubfolder(path, false)} - onToggleFolder={handleToggleFolder} - onOpenFolder={handleOpenFolder} - setIsVisible={(value: boolean) => - setUI((state) => ({ uiVisibility: { ...state.uiVisibility, folderTree: value } })) - } - style={{ width: uiVisibility.folderTree ? `${leftPanelWidth}px` : '32px' }} - isInstantTransition={isInstantTransition} - /> - -
- ); - }; - - const shouldHideFolderTree = isAndroid; - const isWgpuActive = appSettings?.useWgpuRenderer !== false && selectedImage?.isReady && hasRenderedFirstFrame; - const useMacWindowShell = osPlatform === 'macos' && !appSettings?.decorations && !isWindowFullScreen && !isFullScreen; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import Home from "@/pages/Home"; +export default function App() { return ( - <> - - -
-
- {appSettings?.decorations || (!isWindowFullScreen && )} -
-
-
- {!shouldHideFolderTree && renderFolderTree()} -
- {selectedImage && externalEditSession && ( - - )} - {selectedImage ? ( - - ) : ( - - )} -
- {!selectedImage && isLibraryExportPanelVisible && ( - - )} -
- setUI({ isLibraryExportPanelVisible: false })} - /> -
-
-
- - - clsx( - 'relative flex min-h-16 p-4 rounded-lg justify-between overflow-hidden cursor-pointer mb-4', - 'bg-surface! text-text-primary! border! border-border-color! shadow-2xl! max-w-[420px]!', - ) - } - /> -
- + + + } /> + Other Page - Coming Soon} /> + + ); } - -const AppWrapper = () => ( - {}} routerReplace={(to) => {}}> - - - - - -); - -export default AppWrapper; diff --git a/src/components/Empty.tsx b/src/components/Empty.tsx new file mode 100644 index 0000000000..8adbda301a --- /dev/null +++ b/src/components/Empty.tsx @@ -0,0 +1,8 @@ +import { cn } from '@/lib/utils' + +// Empty component +export default function Empty() { + return ( +
Empty
+ ) +} diff --git a/src/components/adjustments/Basic.tsx b/src/components/adjustments/Basic.tsx index 56389458f7..2b7bdd05c3 100644 --- a/src/components/adjustments/Basic.tsx +++ b/src/components/adjustments/Basic.tsx @@ -191,7 +191,7 @@ export default function BasicAdjustments({ /> ) : ( handleAdjustmentChange(BasicAdjustment.Exposure, value)} @@ -199,7 +199,7 @@ export default function BasicAdjustments({ /> )} handleAdjustmentChange(BasicAdjustment.Brightness, e.target.value)} diff --git a/src/components/adjustments/Color.tsx b/src/components/adjustments/Color.tsx index 889230efee..168b46326a 100644 --- a/src/components/adjustments/Color.tsx +++ b/src/components/adjustments/Color.tsx @@ -112,7 +112,19 @@ const ColorGradingPanel = ({ adjustments, setAdjustments, onDragStateChange }: C const [isExpanded, setIsExpanded] = useState(false); const colorGrading = adjustments.colorGrading || INITIAL_ADJUSTMENTS.colorGrading; + const HUE_SAT_LUM_KEYS: ReadonlySet = new Set([ + ColorGrading.Highlights, + ColorGrading.Midtones, + ColorGrading.Shadows, + ColorGrading.Global, + ]); + const NUMERIC_KEYS: ReadonlySet = new Set([ColorGrading.Blending, ColorGrading.Balance]); + const handleChange = (grading: ColorGrading, newValue: HueSatLum) => { + if (!HUE_SAT_LUM_KEYS.has(grading)) { + console.error(`handleChange expects a HueSatLum key, but received "${grading}". Use handleColorGradingSliderChange for numeric keys.`); + return; + } setAdjustments((prev: Partial) => ({ ...prev, colorGrading: { @@ -123,6 +135,10 @@ const ColorGradingPanel = ({ adjustments, setAdjustments, onDragStateChange }: C }; const handleColorGradingSliderChange = (grading: ColorGrading, value: string) => { + if (!NUMERIC_KEYS.has(grading)) { + console.error(`handleColorGradingSliderChange expects a numeric key (blending/balance), but received "${grading}". Use handleChange for HueSatLum keys.`); + return; + } setAdjustments((prev: Partial) => ({ ...prev, colorGrading: { @@ -405,8 +421,6 @@ export default function ColorPanel({ const { t } = useTranslation(); const [activeColor, setActiveColor] = useState('reds'); const adjustmentVisibility = appSettings?.adjustmentVisibility || {}; - const isWgpuEnabled = appSettings?.useWgpuRenderer !== false; - const HSL_COLORS = useMemo>( () => [ { name: 'reds', color: '#f87171', label: t('adjustments.color.mixerColors.reds') }, @@ -476,17 +490,12 @@ export default function ColorPanel({ {!isForMask && toggleWbPicker && ( diff --git a/src/components/adjustments/Curves.tsx b/src/components/adjustments/Curves.tsx index f048eaf117..9f9f75dee3 100644 --- a/src/components/adjustments/Curves.tsx +++ b/src/components/adjustments/Curves.tsx @@ -28,7 +28,6 @@ interface ColorData { interface CurveGraphProps { adjustments: Adjustments | any; histogram: ChannelConfig | null; - isForMask?: boolean; setAdjustments(updater: (prev: any) => any): void; theme: string; onDragStateChange?: (isDragging: boolean) => void; @@ -111,7 +110,7 @@ function buildParametricPoints(settings: ParametricCurveSettings): Array const clamp = (v: number) => Math.max(0, Math.min(1, v)); - let points = xs.map((x, i) => ({ + const points = xs.map((x, i) => ({ x: x * 255, y: clamp(ys[i]) * 255, })); @@ -508,7 +507,7 @@ export default function CurveGraph({ if (index > 0 && index < activePoints.length - 1) { e.preventDefault(); e.stopPropagation(); - const newPoints = activePoints.filter((_, i) => i !== index); + const newPoints = activePoints.filter((_: Coord, i: number) => i !== index); setLocalPoints(newPoints); localPointsRef.current = newPoints; setAdjustments((prev: any) => ({ @@ -666,7 +665,7 @@ export default function CurveGraph({ } const handleCopy = () => { - curveClipboard = activePoints.map((p) => ({ ...p })); + curveClipboard = activePoints.map((p: Coord) => ({ ...p })); }; const handlePaste = () => { @@ -807,7 +806,7 @@ export default function CurveGraph({
{Object.keys(channelConfig).map((channel: any) => { const selected = activeChannel === channel; - const channelLabel = t(`adjustments.curves.channels.${channel}`); + const channelLabel = t(`adjustments.curves.channels.${channel}` as any); return ( ); diff --git a/src/components/adjustments/Effects.tsx b/src/components/adjustments/Effects.tsx index 8002c51038..edd6365941 100644 --- a/src/components/adjustments/Effects.tsx +++ b/src/components/adjustments/Effects.tsx @@ -8,7 +8,7 @@ import { TextVariants } from '../../types/typography'; interface EffectsPanelProps { adjustments: Adjustments; - isForMask: boolean; + isForMask?: boolean; setAdjustments(adjustments: Partial): any; handleLutSelect(path: string): void; onLutHover?: (path: string | null) => void; diff --git a/src/components/modals/AppModals.tsx b/src/components/modals/AppModals.tsx index 137594dc9b..3da28f5760 100644 --- a/src/components/modals/AppModals.tsx +++ b/src/components/modals/AppModals.tsx @@ -19,6 +19,7 @@ import ConfirmModal from './ConfirmModal'; import ImportSettingsModal from './ImportSettingsModal'; import CullingModal from './CullingModal'; import CollageModal from './CollageModal'; +import SmartAlbumModal from './SmartAlbumModal'; import { AppSettings, Invokes, AlbumItem, Album, AlbumGroup } from '../ui/AppProperties'; import { CopyPasteSettings } from '../../utils/adjustments'; @@ -65,6 +66,7 @@ export default function AppModals(props: AppModalsProps) { isCreateAlbumModalOpen, isCreateAlbumGroupModalOpen, isRenameAlbumModalOpen, + isSmartAlbumModalOpen, albumActionTarget, confirmModalState, panoramaModalState, @@ -87,6 +89,7 @@ export default function AppModals(props: AppModalsProps) { isCreateAlbumModalOpen: state.isCreateAlbumModalOpen, isCreateAlbumGroupModalOpen: state.isCreateAlbumGroupModalOpen, isRenameAlbumModalOpen: state.isRenameAlbumModalOpen, + isSmartAlbumModalOpen: state.isSmartAlbumModalOpen, albumActionTarget: state.albumActionTarget, confirmModalState: state.confirmModalState, panoramaModalState: state.panoramaModalState, @@ -323,6 +326,11 @@ export default function AppModals(props: AppModalsProps) { sourceImages={collageModalState.sourceImages} thumbnails={thumbnails} /> + setUI({ isSmartAlbumModalOpen: false })} + images={useLibraryStore.getState().imageList} + /> ); } diff --git a/src/components/modals/CollageModal.tsx b/src/components/modals/CollageModal.tsx index 2762a2b6b9..6132bdc3d5 100644 --- a/src/components/modals/CollageModal.tsx +++ b/src/components/modals/CollageModal.tsx @@ -152,7 +152,7 @@ export default function CollageModal({ isOpen, onClose, onSave, sourceImages }: path: imageFile.path, jsAdjustments: adjustments, }); - const blob = new Blob([imageData], { type: 'image/jpeg' }); + const blob = new Blob([imageData as BlobPart], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); return new Promise((resolve, reject) => { @@ -892,6 +892,8 @@ export default function CollageModal({ isOpen, onClose, onSave, sourceImages }:
{ if (e.key === 'Escape') onClose(); }} + tabIndex={-1} > {show && ( diff --git a/src/components/modals/ConfigurePresetModal.tsx b/src/components/modals/ConfigurePresetModal.tsx index e073c1f9d4..d8d171dd56 100644 --- a/src/components/modals/ConfigurePresetModal.tsx +++ b/src/components/modals/ConfigurePresetModal.tsx @@ -8,16 +8,18 @@ import Switch from '../ui/Switch'; import { Preset } from '../ui/AppProperties'; import { ADJUSTMENT_GROUPS } from '../../utils/adjustments'; +type PresetType = 'tool' | 'style' | 'portrait' | 'color' | 'ai-color' | 'combined'; + interface ConfigurePresetModalProps { isOpen: boolean; onClose(): void; - onSave(name: string, includeMasks: boolean, includeCropTransform: boolean, presetType: 'tool' | 'style'): void; + onSave(name: string, includeMasks: boolean, includeCropTransform: boolean, presetType: PresetType): void; initialPreset?: Preset | null; } interface PresetTypeSwitchProps { - selectedType: 'tool' | 'style'; - onChange: (type: 'tool' | 'style') => void; + selectedType: PresetType; + onChange: (type: PresetType) => void; } const PresetTypeSwitch = ({ selectedType, onChange }: PresetTypeSwitchProps) => { @@ -50,7 +52,7 @@ const PresetTypeSwitch = ({ selectedType, onChange }: PresetTypeSwitchProps) => const targetWidth = `${widthPercent}%`; if (isInitialAnimation.current) { - let initialX = selectedType === 'style' ? '-25%' : '100%'; + const initialX = selectedType === 'style' ? '-25%' : '100%'; setBubbleStyle({ x: [initialX, targetX], @@ -104,7 +106,7 @@ export default function ConfigurePresetModal({ isOpen, onClose, onSave, initialP const [name, setName] = useState(''); const [includeMasks, setIncludeMasks] = useState(false); const [includeCropTransform, setIncludeCropTransform] = useState(false); - const [presetType, setPresetType] = useState<'tool' | 'style'>('style'); + const [presetType, setPresetType] = useState('style'); const [isMounted, setIsMounted] = useState(false); const [show, setShow] = useState(false); @@ -113,8 +115,7 @@ export default function ConfigurePresetModal({ isOpen, onClose, onSave, initialP setName(initialPreset?.name || ''); setIncludeMasks( initialPreset?.includeMasks ?? - (initialPreset?.adjustments?.masks && initialPreset.adjustments.masks.length > 0) ?? - false, + !!(initialPreset?.adjustments?.masks && initialPreset.adjustments.masks.length > 0), ); const GEOMETRY_KEYS = ADJUSTMENT_GROUPS.geometry.flatMap((group) => group.keys); diff --git a/src/components/modals/CopyPasteSettingsModal.tsx b/src/components/modals/CopyPasteSettingsModal.tsx index 10b3daee39..d64e939be8 100644 --- a/src/components/modals/CopyPasteSettingsModal.tsx +++ b/src/components/modals/CopyPasteSettingsModal.tsx @@ -245,7 +245,7 @@ export default function CopyPasteSettingsModal({ isOpen, onClose, onSave, settin return (
handleGroupToggle(group.keys, checked)} /> diff --git a/src/components/modals/CreateFolderModal.tsx b/src/components/modals/CreateFolderModal.tsx index f5e426bee5..b46dfa5f39 100644 --- a/src/components/modals/CreateFolderModal.tsx +++ b/src/components/modals/CreateFolderModal.tsx @@ -12,6 +12,9 @@ interface FolderModalProps { buttonText?: string; } +// eslint-disable-next-line no-control-regex +const INVALID_FOLDER_CHARS = /[<>:"/\\|?*\x00-\x1F]/; + export default function CreateFolderModal({ isOpen, onClose, @@ -24,6 +27,7 @@ export default function CreateFolderModal({ const [name, setName] = useState(''); const [isMounted, setIsMounted] = useState(false); const [show, setShow] = useState(false); + const hasInvalidChars = INVALID_FOLDER_CHARS.test(name); useEffect(() => { if (isOpen) { @@ -41,11 +45,12 @@ export default function CreateFolderModal({ }, [isOpen]); const handleSave = useCallback(() => { - if (name.trim()) { - onSave(name.trim()); + const trimmed = name.trim(); + if (trimmed && !hasInvalidChars) { + onSave(trimmed); } onClose(); - }, [name, onSave, onClose]); + }, [name, hasInvalidChars, onSave, onClose]); const handleKeyDown = useCallback( (e: any) => { @@ -94,6 +99,11 @@ export default function CreateFolderModal({ type="text" value={name} /> + {hasInvalidChars && ( + + {t('modals.createFolder.invalidChars')} + + )}
+ )} +
+ ))} +
+ +
+ +
+ + {t('library.smartAlbum.preview', { count: matchCount })} + +
+ +
+ + +
+
+ + ); +} diff --git a/src/components/modals/TransformModal.tsx b/src/components/modals/TransformModal.tsx index 0db88885ba..48eaf8f132 100644 --- a/src/components/modals/TransformModal.tsx +++ b/src/components/modals/TransformModal.tsx @@ -121,6 +121,7 @@ export default function TransformModal({ isOpen, onClose, onApply, currentAdjust const [isApplying, setIsApplying] = useState(false); const [showGrid, setShowGrid] = useState(true); const [showLines, setShowLines] = useState(false); + const [autoCrop, setAutoCrop] = useState(true); const [isCompareActive, setIsCompareActive] = useState(false); const [isInteracting, setIsInteracting] = useState(false); @@ -226,6 +227,7 @@ export default function TransformModal({ isOpen, onClose, onApply, currentAdjust params: fullParams, jsAdjustments: currentAdjustments, showLines: linesEnabled, + autoCrop, }); setPreviewUrl(result); } catch (e) { @@ -542,6 +544,16 @@ export default function TransformModal({ isOpen, onClose, onApply, currentAdjust > +
diff --git a/src/components/panel/BottomBar.tsx b/src/components/panel/BottomBar.tsx index cc90226529..31f6ff8219 100644 --- a/src/components/panel/BottomBar.tsx +++ b/src/components/panel/BottomBar.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { Star, Copy, ClipboardPaste, ChevronUp, ChevronDown, Check, FileInput, Settings, Filter } from 'lucide-react'; import clsx from 'clsx'; import { motion, AnimatePresence } from 'framer-motion'; @@ -56,17 +56,72 @@ interface StarRatingProps { const StarRating = ({ rating, onRate, disabled }: StarRatingProps) => { const { t } = useTranslation(); + const starRefs = useRef<(HTMLButtonElement | null)[]>([]); + + const focusStar = useCallback((index: number) => { + const clamped = Math.max(0, Math.min(4, index)); + starRefs.current[clamped]?.focus(); + }, []); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (disabled) return; + + const focusedIndex = starRefs.current.indexOf(document.activeElement as HTMLButtonElement); + + switch (e.key) { + case 'ArrowRight': + case 'ArrowUp': + e.preventDefault(); + focusStar(focusedIndex < 4 ? focusedIndex + 1 : 0); + break; + case 'ArrowLeft': + case 'ArrowDown': + e.preventDefault(); + focusStar(focusedIndex > 0 ? focusedIndex - 1 : 4); + break; + case '0': + e.preventDefault(); + onRate(0); + break; + case '1': + case '2': + case '3': + case '4': + case '5': + e.preventDefault(); + onRate(Number(e.key) === rating ? 0 : Number(e.key)); + focusStar(Number(e.key) - 1); + break; + default: + break; + } + }, + [disabled, onRate, rating, focusStar], + ); return ( -
+
{[...Array(5)].map((_, index: number) => { const starValue = index + 1; return ( + )} {!props.isAndroid && ( <>
-
- - {t('settings.thanks.title')} - - {t('settings.thanks.description')} - -
  • - - rawler - - : {t('settings.thanks.list.rawler')} -
  • -
  • - - lensfun - - : {t('settings.thanks.list.lensfun')} -
  • -
  • - - NegPy - - : {t('settings.thanks.list.negpy')} -
  • -
  • - - LaMa - - : {t('settings.thanks.list.lama')} -
  • -
  • - - SAM 2 - - : {t('settings.thanks.list.sam2')} -
  • -
  • - - U-2-Net - - : {t('settings.thanks.list.u2net')} -
  • -
  • - - Depth Anything V2 - - : {t('settings.thanks.list.depth')} -
  • -
  • - - nind-denoise - - : {t('settings.thanks.list.nind')} -
  • -
  • - - darktable & co. - - : {t('settings.thanks.list.darktable')} -
  • -
  • - {t('settings.thanks.list.youLabel')}:{' '} - {t('settings.thanks.list.you')} -
  • -
    -
    + )} {activeCategory === 'processing' && ( @@ -1671,7 +1608,21 @@ export default function SettingsPanel({ transition={{ duration: 0.2 }} className="space-y-10" > -
    +
    + + + + +
    +
    {t('settings.processing.title')} @@ -1888,6 +1839,20 @@ export default function SettingsPanel({ /> + + ) => setMirrorUrl(e.target.value)} + onBlur={() => handleMirrorUrlBlur()} + placeholder="https://hf-mirror.com" + className="w-full" + /> + +
    -
    +
    {t('settings.processing.preprocessing.title')} @@ -2078,7 +2043,7 @@ export default function SettingsPanel({
    -
    +
    {t('settings.processing.ai.title')} @@ -2167,7 +2132,7 @@ export default function SettingsPanel({ )} - {aiProvider === 'cloud' && ( + {aiProvider === 'cloud' && !isDeviceSide && (
    -
    +
    {t('settings.data.title')} diff --git a/src/components/panel/editor/ExifIcons.tsx b/src/components/panel/editor/ExifIcons.tsx index afcd1ef475..3f4fcf0fa2 100644 --- a/src/components/panel/editor/ExifIcons.tsx +++ b/src/components/panel/editor/ExifIcons.tsx @@ -11,15 +11,19 @@ const iconProps = { strokeLinejoin: 'round' as const, }; -export const IconAperture = () => ( - +interface ExifIconProps { + className?: string; +} + +export const IconAperture = ({ className }: ExifIconProps = {}) => ( + ); -export const IconShutter = () => ( - +export const IconShutter = ({ className }: ExifIconProps = {}) => ( + @@ -33,16 +37,16 @@ export const IconShutter = () => ( ); -export const IconIso = () => ( - +export const IconIso = ({ className }: ExifIconProps = {}) => ( + ); -export const IconFocalLength = () => ( - +export const IconFocalLength = ({ className }: ExifIconProps = {}) => ( + @@ -50,8 +54,8 @@ export const IconFocalLength = () => ( ); -export const IconCalendar = () => ( - +export const IconCalendar = ({ className }: ExifIconProps = {}) => ( + @@ -59,15 +63,15 @@ export const IconCalendar = () => ( ); -export const IconClock = () => ( - +export const IconClock = ({ className }: ExifIconProps = {}) => ( + ); -export const IconLens = () => ( - +export const IconLens = ({ className }: ExifIconProps = {}) => ( + diff --git a/src/components/panel/editor/ImageCanvas.tsx b/src/components/panel/editor/ImageCanvas.tsx index 556c486bad..a482485a1e 100644 --- a/src/components/panel/editor/ImageCanvas.tsx +++ b/src/components/panel/editor/ImageCanvas.tsx @@ -4,7 +4,7 @@ import 'react-image-crop/dist/ReactCrop.css'; import { Stage, Layer, Ellipse, Line, Transformer, Group, Circle, Rect } from 'react-konva'; import { PercentCrop, Crop } from 'react-image-crop'; import { Stamp, Bandage } from 'lucide-react'; -import { Adjustments, AiPatch, Coord, MaskContainer } from '../../../utils/adjustments'; +import { Adjustments, AiPatch, Coord, MaskContainer, INITIAL_PORTRAIT_ADJUSTMENTS } from '../../../utils/adjustments'; import { Mask, SubMask, SubMaskMode, ToolType } from '../right/Masks'; import { AppSettings, BrushSettings, SelectedImage } from '../../ui/AppProperties'; import { RenderSize } from '../../../hooks/useImageRenderSize'; @@ -66,6 +66,7 @@ interface ImageCanvasProps { updateSubMask(id: string | null, subMask: Partial): void; interactivePatch?: { url: string; normX: number; normY: number; normW: number; normH: number } | null; isWbPickerActive?: boolean; + isBlemishModeActive?: boolean; onWbPicked?: () => void; setAdjustments(fn: (prev: Adjustments) => Adjustments): void; overlayMode?: OverlayMode; @@ -1175,6 +1176,7 @@ const ImageCanvas = memo( uncroppedAdjustedPreviewUrl, updateSubMask, isWbPickerActive = false, + isBlemishModeActive = false, onWbPicked, setAdjustments, overlayRotation, @@ -1200,6 +1202,7 @@ const ImageCanvas = memo( const previewBoxRef = useRef<{ start: Coord; end: Coord } | null>(null); const [previewBox, setPreviewBox] = useState<{ start: Coord; end: Coord } | null>(null); const activeStrokeIndex = useRef(null); + const maskTouchCountRef = useRef(0); const [cursorPreview, setCursorPreview] = useState({ x: 0, y: 0, visible: false }); const [straightenLine, setStraightenLine] = useState(null); @@ -1320,10 +1323,8 @@ const ImageCanvas = memo( setDisplayState((prev) => ({ base: prev.base, fade: newSrc })); setIsFadingIn(false); - let frame1: number; let frame2: number; - - frame1 = requestAnimationFrame(() => { + const frame1 = requestAnimationFrame(() => { frame2 = requestAnimationFrame(() => { setIsFadingIn(true); }); @@ -1344,7 +1345,7 @@ const ImageCanvas = memo( setIsFadingIn(false); } } - }, [finalPreviewUrl, selectedImage.thumbnailUrl, isSliderDragging]); + }, [finalPreviewUrl, selectedImage.thumbnailUrl, isSliderDragging, displayState.base]); useEffect(() => { setBaseTool(brushSettings?.tool ?? ToolType.Brush); @@ -1407,6 +1408,7 @@ const ImageCanvas = memo( activeMaskContainerId, activeAiPatchContainerId, isMasking, + // eslint-disable-next-line react-hooks/exhaustive-deps isAiEditing, ]); @@ -1446,9 +1448,9 @@ const ImageCanvas = memo( : activeCrop.y : 0; - const effectiveZoomScale = transformState.scale > 0 ? transformState.scale : 1; + const effectiveZoomScale = Math.max(0.001, transformState.scale || 1); const brushStageSize = (brushSettings?.size ?? 0) / effectiveZoomScale; - const brushImageSpaceSize = brushStageSize / (imageRenderSize.scale || 1); + const brushImageSpaceSize = brushStageSize / Math.max(0.001, imageRenderSize.scale || 1); const isBrushActive = (isMasking || isAiEditing) && @@ -1551,12 +1553,15 @@ const ImageCanvas = memo( useEffect(() => { if (!isMasking && !isAiEditing) { setIsMaskInteractionActive(false); + maskTouchCountRef.current = 0; } }, [isMasking, isAiEditing]); useEffect(() => { - const clearTouchInteraction = () => { - setIsMaskTouchInteracting(false); + const clearTouchInteraction = (e: TouchEvent) => { + if (e.touches.length === 0) { + setIsMaskTouchInteracting(false); + } }; window.addEventListener('touchend', clearTouchInteraction); @@ -1747,6 +1752,36 @@ const ImageCanvas = memo( return; } + if (isBlemishModeActive) { + const stage = e.target.getStage(); + const pointerPos = getCanvasPointer(stage); + if (!pointerPos) return; + + const x = pointerPos.x / imageRenderSize.scale; + const y = pointerPos.y / imageRenderSize.scale; + + const imgLogicalWidth = imageRenderSize.width / imageRenderSize.scale; + const imgLogicalHeight = imageRenderSize.height / imageRenderSize.scale; + + if (x < 0 || x > imgLogicalWidth || y < 0 || y > imgLogicalHeight) return; + + const normX = x / imgLogicalWidth; + const normY = y / imgLogicalHeight; + const radius = 0.02; + + setAdjustments((prev: Adjustments) => { + const currentPortrait = prev.portrait || INITIAL_PORTRAIT_ADJUSTMENTS; + return { + ...prev, + portrait: { + ...currentPortrait, + blemishSpots: [...currentPortrait.blemishSpots, { x: normX, y: normY, radius }], + }, + }; + }); + return; + } + if (isParametricActive && activeSubMask) { const pos = getCanvasPointer(e.target.getStage()); if (!pos) return; @@ -1755,7 +1790,7 @@ const ImageCanvas = memo( const x = pos.x / scale + cropX; const y = pos.y / scale + cropY; - let newParams = { ...activeSubMask.parameters }; + const newParams = { ...activeSubMask.parameters }; newParams.targetX = x; newParams.targetY = y; newParams.rotation = adjustments.rotation || 0; @@ -1859,7 +1894,7 @@ const ImageCanvas = memo( let effectiveTool; if (isAiSubjectActive) { - effectiveTool = ToolType.AiSeletor; + effectiveTool = ToolType.AiSelector; } else if (isAltPressed) { effectiveTool = baseTool === ToolType.Brush ? ToolType.Eraser : ToolType.Brush; } else { @@ -1997,7 +2032,7 @@ const ImageCanvas = memo( return; } - if (isAiSubjectActive && previewBoxRef.current) { + if (isAiSubjectActive && previewBoxRef.current && pos) { const updatedBox = { ...previewBoxRef.current, end: pos }; previewBoxRef.current = updatedBox; setPreviewBox(updatedBox); @@ -2019,11 +2054,14 @@ const ImageCanvas = memo( const distX = x - dragStartPointer.current.x; const distY = y - dragStartPointer.current.y; const screenThreshold = 15; - if (Math.sqrt(distX * distX + distY * distY) < screenThreshold / scale) { + // Convert screen threshold to image space for accurate distance comparison. + // distX/distY are already in image coordinates, so we scale the threshold. + const imageThreshold = screenThreshold / Math.max(0.001, scale); + if (Math.sqrt(distX * distX + distY * distY) < imageThreshold) { return; } - let updatedParams = { ...localInitialDrawParams }; + const updatedParams = { ...localInitialDrawParams }; if (activeSubMask.type === Mask.Radial) { updatedParams.radiusX = Math.max(1, Math.abs(x - dragStartPointer.current.x)); @@ -2210,10 +2248,12 @@ const ImageCanvas = memo( } else if (activeSubMask.type === Mask.Linear) { if (!newParams.range || newParams.range < 10) { const handleDist = Math.min(effectiveImageDimensions.width, effectiveImageDimensions.height) * 0.2; - newParams.startX = dragStartPointer.current!.x + handleDist; - newParams.startY = dragStartPointer.current!.y; - newParams.endX = dragStartPointer.current!.x - handleDist; - newParams.endY = dragStartPointer.current!.y; + const startPointer = dragStartPointer.current; + if (!startPointer) return; + newParams.startX = startPointer.x + handleDist; + newParams.startY = startPointer.y; + newParams.endX = startPointer.x - handleDist; + newParams.endY = startPointer.y; newParams.range = 100; } } @@ -2243,7 +2283,7 @@ const ImageCanvas = memo( const { scale } = imageRenderSize; const activeId = isMasking ? activeMaskId : activeAiSubMaskId; - let startPoint = { x: box.start.x / scale + cropX, y: box.start.y / scale + cropY }; + const startPoint = { x: box.start.x / scale + cropX, y: box.start.y / scale + cropY }; let endPoint = { x: box.end.x / scale + cropX, y: box.end.y / scale + cropY }; const dx = box.end.x - box.start.x; @@ -2381,11 +2421,13 @@ const ImageCanvas = memo( window.addEventListener('mousemove', onGlobalMove, { passive: false }); window.addEventListener('mouseup', onGlobalUp); window.addEventListener('touchmove', onGlobalMove, { passive: false }); + window.addEventListener('touchend', onGlobalUp); window.addEventListener('touchcancel', onGlobalUp); return () => { window.removeEventListener('mousemove', onGlobalMove); window.removeEventListener('mouseup', onGlobalUp); window.removeEventListener('touchmove', onGlobalMove); + window.removeEventListener('touchend', onGlobalUp); window.removeEventListener('touchcancel', onGlobalUp); }; }, [isToolActive, handleMove, handleUp]); @@ -2417,7 +2459,7 @@ const ImageCanvas = memo( isStraightening.current = false; if ( !straightenLine || - (straightenLine.start.x === straightenLine.end.x && straightenLine.start.y === straightenLine.start.y) + (straightenLine.start.x === straightenLine.end.x && straightenLine.start.y === straightenLine.end.y) ) { setStraightenLine(null); return; @@ -2428,8 +2470,12 @@ const ImageCanvas = memo( const theta_rad = (rotation * Math.PI) / 180; const cos_t = Math.cos(theta_rad); const sin_t = Math.sin(theta_rad); - const width = uncroppedImageRenderSize?.width ?? 0; - const height = uncroppedImageRenderSize?.height ?? 0; + if (!uncroppedImageRenderSize) { + setStraightenLine(null); + return; + } + const width = uncroppedImageRenderSize.width ?? 0; + const height = uncroppedImageRenderSize.height ?? 0; const cx = width / 2; const cy = height / 2; @@ -2559,6 +2605,7 @@ const ImageCanvas = memo( const effectiveCursor = useMemo(() => { if (isWbPickerActive) return 'crosshair'; + if (isBlemishModeActive) return 'crosshair'; if (isParametricActive) return 'crosshair'; if (isInitialDrawing) return 'crosshair'; @@ -2584,6 +2631,7 @@ const ImageCanvas = memo( return cursorStyle; }, [ isWbPickerActive, + isBlemishModeActive, isInitialDrawing, isBrushActive, isManualCleanupActive, @@ -2611,6 +2659,7 @@ const ImageCanvas = memo( setIsMaskInteractionActive(true); const eventType = e?.evt?.type; if (eventType === 'touchstart') { + maskTouchCountRef.current += 1; setIsMaskTouchInteracting(true); } }, @@ -2618,8 +2667,11 @@ const ImageCanvas = memo( ); const handleMaskInteractionEnd = useCallback(() => { + maskTouchCountRef.current = Math.max(0, maskTouchCountRef.current - 1); setIsMaskInteractionActive(false); - setIsMaskTouchInteracting(false); + if (maskTouchCountRef.current === 0) { + setIsMaskTouchInteracting(false); + } }, [setIsMaskTouchInteracting]); const currentActiveSubMaskId = activeAiSubMaskId || activeMaskId; diff --git a/src/components/panel/editor/Waveform.tsx b/src/components/panel/editor/Waveform.tsx index 6aaaa99d67..aa10cab939 100644 --- a/src/components/panel/editor/Waveform.tsx +++ b/src/components/panel/editor/Waveform.tsx @@ -625,7 +625,7 @@ export default function Waveform({
    {gridData.isListView && ( @@ -508,7 +574,12 @@ export default function LibraryGrid(props: any) { )}
    1 ? 56 : 0) + : gridSize.height - (isAndroid && multiSelectedPaths.length > 1 ? 56 : 0), + width: gridSize.width, + }} > ) => handleScroll(e.currentTarget.scrollTop)} className="custom-scrollbar" - rowComponent={Row} + rowComponent={Row as any} rowProps={memoizedRowProps} />
    + + {isAndroid && multiSelectedPaths.length > 1 && ( +
    +
    + + {showRatePopover && ( +
    + {[1, 2, 3, 4, 5].map((r) => ( + + ))} +
    + )} +
    + + + +
    + + {t('library.batch.selected', { count: multiSelectedPaths.length })} + +
    +
    + )}
    ); } diff --git a/src/components/panel/library/LibraryHeader.tsx b/src/components/panel/library/LibraryHeader.tsx index 64d160feb0..8a4d2c429c 100644 --- a/src/components/panel/library/LibraryHeader.tsx +++ b/src/components/panel/library/LibraryHeader.tsx @@ -10,6 +10,11 @@ import { ChevronUp, ChevronDown, HelpCircle, + Filter, + Calendar, + Camera, + Crosshair, + Tag, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useShallow } from 'zustand/react/shallow'; @@ -22,6 +27,7 @@ import { SortCriteria, SortDirection, ExifOverlay, + ImageFile, } from '../../ui/AppProperties'; import { COLOR_LABELS, Color } from '../../../utils/adjustments'; import Text from '../../ui/Text'; @@ -30,6 +36,25 @@ import Button from '../../ui/Button'; import { useSettingsStore } from '../../../store/useSettingsStore'; import { ADVANCED_QUERY_REGEX } from '../../../hooks/useSortedLibrary'; +// Subset of common photography-related tags from TAG_CANDIDATES for search suggestions +const TAG_SUGGESTIONS: string[] = [ + 'person', 'people', 'portrait', 'candid', 'silhouette', 'face', 'smile', + 'animal', 'wildlife', 'dog', 'cat', 'bird', 'horse', + 'landscape', 'mountain', 'ocean', 'sea', 'beach', 'lake', 'river', 'waterfall', 'forest', 'tree', 'flower', + 'sky', 'sunset', 'sunrise', 'cloud', 'rain', 'snow', 'storm', 'fog', + 'architecture', 'building', 'city', 'street', 'bridge', 'tower', + 'food', 'drink', 'cake', 'coffee', + 'car', 'train', 'boat', 'airplane', 'bicycle', + 'night', 'light', 'shadow', 'reflection', 'bokeh', 'macro', + 'wedding', 'concert', 'festival', 'sport', + 'abstract', 'texture', 'pattern', 'minimal', 'vintage', 'black and white', 'HDR', + 'indoor', 'outdoor', 'garden', 'park', 'farm', + 'vintage', 'retro', 'dramatic', 'moody', 'serene', 'vibrant', + '旅游', '风景', '人像', '街拍', '夜景', '日出', '日落', '花卉', '建筑', + '美食', '宠物', '儿童', '家庭', '婚礼', '节日', '运动', + '黑白', '胶片', '复古', '极简', '光影', '倒影', '剪影', +]; + function DropdownMenu({ buttonContent, buttonTitle, children, contentClassName = 'w-56' }: any) { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -78,15 +103,21 @@ function DropdownMenu({ buttonContent, buttonTitle, children, contentClassName = ); } -export function SearchInput({ indexingProgress, isIndexing }: any) { +export function SearchInput({ indexingProgress, isIndexing, isAndroid }: any) { const { t } = useTranslation(); - const { searchCriteria, setSearchCriteria } = useLibraryStore( - useShallow((state) => ({ searchCriteria: state.searchCriteria, setSearchCriteria: state.setSearchCriteria })), + const { searchCriteria, setSearchCriteria, imageList } = useLibraryStore( + useShallow((state) => ({ + searchCriteria: state.searchCriteria, + setSearchCriteria: state.setSearchCriteria, + imageList: state.imageList, + })), ); const [isSearchActive, setIsSearchActive] = useState(false); + const [showSuggestions, setShowSuggestions] = useState(false); const inputRef = useRef(null); const containerRef = useRef(null); const contentRef = useRef(null); + const suggestionsRef = useRef(null); const { tags, text, mode } = searchCriteria; const [contentWidth, setContentWidth] = useState(0); @@ -102,6 +133,9 @@ export function SearchInput({ indexingProgress, isIndexing }: any) { if (containerRef.current && !containerRef.current.contains(event.target) && tags.length === 0 && !text) { setIsSearchActive(false); } + if (suggestionsRef.current && !suggestionsRef.current.contains(event.target)) { + setShowSuggestions(false); + } } document.addEventListener('mousedown', handleClickOutside); return () => { @@ -121,7 +155,42 @@ export function SearchInput({ indexingProgress, isIndexing }: any) { }, [tags, text, isSearchActive]); const handleInputChange = (e: React.ChangeEvent) => { - setSearchCriteria((prev) => ({ ...prev, text: e.target.value })); + const value = e.target.value; + setSearchCriteria((prev) => ({ ...prev, text: value })); + setShowSuggestions(value.trim().length > 0); + }; + + const dynamicAiTags = useMemo(() => { + const freq = new Map(); + imageList.forEach((img: ImageFile) => { + if (!img.tags) return; + img.tags.forEach((tag: string) => { + if (tag.startsWith('color:') || tag.startsWith('user:')) return; + freq.set(tag, (freq.get(tag) || 0) + 1); + }); + }); + return Array.from(freq.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([tag]) => tag); + }, [imageList]); + + const suggestions = useMemo(() => { + if (!text.trim()) return []; + const query = text.trim().toLowerCase(); + const candidateTags = dynamicAiTags.length > 0 ? dynamicAiTags : TAG_SUGGESTIONS; + return candidateTags + .filter((tag) => tag.toLowerCase().includes(query) && !tags.includes(tag)) + .slice(0, 8); + }, [text, tags, dynamicAiTags]); + + const handleSuggestionClick = (suggestion: string) => { + setSearchCriteria((prev) => ({ + ...prev, + tags: [...prev.tags, suggestion], + text: '', + })); + setShowSuggestions(false); + inputRef.current?.focus(); }; const handleKeyDown = (e: React.KeyboardEvent) => { @@ -183,118 +252,153 @@ export function SearchInput({ indexingProgress, isIndexing }: any) { const calculatedWidth = Math.min(MAX_WIDTH, contentWidth + PADDING_AND_ICONS_WIDTH); return ( - inputRef.current?.focus()} - > - -
    -
    - {tags.map((tag) => { - const match = tag.match(ADVANCED_QUERY_REGEX); - const isQuery = !!match; - - return ( - { - e.stopPropagation(); - removeTag(tag); - }} - > - - {isQuery ? ( - - {match[1]} - {match[2] || ':'} - {match[3]} - - ) : ( - tag - )} - - - - - - ); - })} - { - if (tags.length === 0 && !text) setIsSearchActive(false); - }} - onChange={handleInputChange} - onFocus={() => setIsSearchActive(true)} - onKeyDown={handleKeyDown} - placeholder={placeholderText} - ref={inputRef} - type="text" - value={text} - /> -
    -
    -
    - {tags.length > 0 && ( - - )} +
    - +
    + {tags.map((tag) => { + const match = tag.match(ADVANCED_QUERY_REGEX); + const isQuery = !!match; + + return ( + { + e.stopPropagation(); + removeTag(tag); + }} + > + + {isQuery ? ( + + {match[1]} + {match[2] || ':'} + {match[3]} + + ) : ( + tag + )} + + + + + + ); + })} + { + if (tags.length === 0 && !text) setIsSearchActive(false); + }} + onChange={handleInputChange} + onFocus={() => { + setIsSearchActive(true); + if (text.trim()) setShowSuggestions(true); + }} + onKeyDown={handleKeyDown} + placeholder={placeholderText} + ref={inputRef} + type="text" + value={text} + /> +
    - {(tags.length > 0 || text) && !isIndexing && ( - + )} +
    - - - )} - {isIndexing && ( -
    - +
    + {(tags.length > 0 || text) && !isIndexing && ( + + )} + {isIndexing && ( +
    + +
    + )} +
    + + + {showSuggestions && suggestions.length > 0 && isSearchActive && ( + +
    + + {t('library.search.suggestions')} + +
    + {suggestions.map((suggestion) => ( + + ))} +
    )} -
    -
    + +
    ); } @@ -535,7 +639,7 @@ export function ViewOptionsDropdown({ }`} key={option.value} onClick={() => - setFilterCriteria((prev: Partial) => ({ ...prev, rating: option.value })) + setFilterCriteria((prev: FilterCriteria) => ({ ...prev, rating: option.value })) } role="menuitem" > @@ -568,7 +672,7 @@ export function ViewOptionsDropdown({ key={starValue} onClick={(e) => { e.stopPropagation(); - setFilterCriteria((prev: Partial) => ({ + setFilterCriteria((prev: FilterCriteria) => ({ ...prev, rating: prev.rating === starValue ? 0 : starValue, })); @@ -609,7 +713,7 @@ export function ViewOptionsDropdown({ }`} key={option.key} onClick={() => - setFilterCriteria((prev: Partial) => ({ ...prev, rawStatus: option.key })) + setFilterCriteria((prev: FilterCriteria) => ({ ...prev, rawStatus: option.key })) } role="menuitem" > @@ -639,7 +743,7 @@ export function ViewOptionsDropdown({ }`} key={option.key} onClick={() => - setFilterCriteria((prev: Partial) => ({ ...prev, editedStatus: option.key })) + setFilterCriteria((prev: FilterCriteria) => ({ ...prev, editedStatus: option.key })) } role="menuitem" > @@ -750,3 +854,211 @@ export function ViewOptionsDropdown({ ); } + +// Popular AI tag chips for the advanced filter panel +const POPULAR_TAG_CHIPS: string[] = [ + 'person', 'landscape', 'portrait', 'sunset', 'sky', 'nature', + 'architecture', 'street', 'night', 'flower', 'animal', 'water', + 'mountain', 'forest', 'beach', 'food', 'wedding', 'travel', + 'bokeh', 'macro', 'HDR', 'black and white', 'vintage', 'abstract', + '旅游', '风景', '人像', '夜景', '花卉', '建筑', '美食', '街拍', +]; + +export function AdvancedFilterPanel({ isAndroid }: { isAndroid: boolean }) { + const { t } = useTranslation(); + const { advancedFilter, setAdvancedFilter, searchCriteria, setSearchCriteria, imageList } = useLibraryStore( + useShallow((state) => ({ + advancedFilter: state.advancedFilter, + setAdvancedFilter: state.setAdvancedFilter, + searchCriteria: state.searchCriteria, + setSearchCriteria: state.setSearchCriteria, + imageList: state.imageList, + })), + ); + + if (!isAndroid) return null; + + const isFilterActive = + advancedFilter.dateFrom !== null || + advancedFilter.dateTo !== null || + advancedFilter.cameraModel !== null || + advancedFilter.focalLengthMin !== null || + advancedFilter.focalLengthMax !== null || + searchCriteria.tags.length > 0; + + const popularAiTags = useMemo(() => { + const freq = new Map(); + imageList.forEach((img: ImageFile) => { + if (!img.tags) return; + img.tags.forEach((tag: string) => { + if (tag.startsWith('color:') || tag.startsWith('user:')) return; + freq.set(tag, (freq.get(tag) || 0) + 1); + }); + }); + return Array.from(freq.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([tag]) => tag) + .slice(0, 12); + }, [imageList]); + + const tagChips = popularAiTags.length > 0 ? popularAiTags : POPULAR_TAG_CHIPS; + + const handleTagChipClick = (tag: string) => { + if (searchCriteria.tags.includes(tag)) { + setSearchCriteria((prev) => ({ + ...prev, + tags: prev.tags.filter((t) => t !== tag), + })); + } else { + setSearchCriteria((prev) => ({ + ...prev, + tags: [...prev.tags, tag], + })); + } + }; + + return ( + +
    + {/* Date Range */} +
    +
    + + + {t('library.search.dateRange')} + +
    +
    +
    + + {t('library.search.dateFrom')} + + + setAdvancedFilter({ dateFrom: e.target.value || null }) + } + /> +
    +
    + + {t('library.search.dateTo')} + + + setAdvancedFilter({ dateTo: e.target.value || null }) + } + /> +
    +
    +
    + + {/* Camera Model */} +
    +
    + + + {t('library.search.cameraModel')} + +
    + + setAdvancedFilter({ cameraModel: e.target.value || null }) + } + /> +
    + + {/* Focal Length Range */} +
    +
    + + + {t('library.search.focalLength')} + +
    +
    + + setAdvancedFilter({ focalLengthMin: e.target.value ? Number(e.target.value) : null }) + } + /> + + + setAdvancedFilter({ focalLengthMax: e.target.value ? Number(e.target.value) : null }) + } + /> +
    +
    + + {/* AI Tag Suggestion Chips */} +
    +
    + + + {t('library.search.aiTagSuggestion')} + +
    +
    + {tagChips.map((tag) => { + const isSelected = searchCriteria.tags.includes(tag); + return ( + + ); + })} +
    +
    +
    + + {/* Active filter indicator & clear */} + {isFilterActive && ( +
    + +
    + )} +
    + ); +} diff --git a/src/components/panel/library/LibraryItems.tsx b/src/components/panel/library/LibraryItems.tsx index 34c109a41b..f47386c8f2 100644 --- a/src/components/panel/library/LibraryItems.tsx +++ b/src/components/panel/library/LibraryItems.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { Image as ImageIcon, Folder, FolderOpen, Star as StarIcon, SlidersHorizontal, CloudOff } from 'lucide-react'; +import { Image as ImageIcon, Folder, FolderOpen, Star as StarIcon, SlidersHorizontal, CloudOff, Heart } from 'lucide-react'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { COLOR_LABELS, Color } from '../../../utils/adjustments'; @@ -9,6 +9,7 @@ import { TextColors, TextVariants, TextWeights, TEXT_COLOR_KEYS } from '../../.. import { ColumnWidths } from '../MainLibrary'; import { useProcessStore } from '../../../store/useProcessStore'; import { useSettingsStore } from '../../../store/useSettingsStore'; +import { useLibraryStore } from '../../../store/useLibraryStore'; import { IconAperture, IconFocalLength, IconIso, IconShutter } from '../editor/ExifIcons'; interface ImageLayer { @@ -37,6 +38,11 @@ const ThumbnailComponent = ({ const exifOverlay = useSettingsStore((s) => s.appSettings?.exifOverlay || ExifOverlay.Off); const displayEditIcon = useSettingsStore((s) => s.appSettings?.displayEditIcon ?? true); const showEditIcon = isEdited && displayEditIcon; + const favorites = useLibraryStore((s) => s.favorites); + const toggleFavorite = useLibraryStore((s) => s.toggleFavorite); + const isFav = favorites.includes(path); + const osPlatform = useSettingsStore((s) => s.osPlatform); + const isAndroid = osPlatform === 'android'; const [showPlaceholder, setShowPlaceholder] = useState(false); const [layers, setLayers] = useState([]); @@ -135,6 +141,13 @@ const ThumbnailComponent = ({ const colorTag = tags?.find((t: string) => t.startsWith('color:'))?.substring(6); const colorLabel = COLOR_LABELS.find((c: Color) => c.name === colorTag); + const aiTags = useMemo(() => { + if (!tags || tags.length === 0) return []; + return tags + .filter((t: string) => !t.startsWith('color:') && !t.startsWith('user:')) + .slice(0, 2); + }, [tags]); + const isAlways = exifOverlay === ExifOverlay.Always; const isHover = exifOverlay === ExifOverlay.Hover; @@ -204,6 +217,25 @@ const ThumbnailComponent = ({
    )} + +
    + {aiTags.length > 0 && ( +
    + {aiTags.map((tag: string) => ( + + {tag} + + ))} +
    + )}
    @@ -806,6 +850,7 @@ const RowComponent = ({ {row.images.map((imageFile: ImageFile) => (
    {t('editor.ai.connection.builtinDesc')}; } + // For local/cpu mode, always show ready status regardless of cloud sign-in state + if (aiProvider === 'cpu') { + statusColor = 'bg-green-500'; + statusText = t('editor.ai.connection.ready'); + titleText = t('editor.ai.connection.builtinLabel'); + hoverContent = {t('editor.ai.connection.builtinDesc')}; + } + return (
    s.setCustomEscapeHandler); const { setAdjustments } = useEditorActions(); - const { handleGenerativeReplace, handleDeleteAiPatch, handleGenerateAiForegroundMask } = useAiMasking(); + const { handleGenerativeReplace, handleDeleteAiPatch, handleToggleAiPatchVisibility, handleGenerateAiForegroundMask, handleGenerateAiSubjectMask, handleApplySuperResolution } = useAiMasking(); const appSettings = useSettingsStore((s) => s.appSettings); const aiProvider = appSettings?.aiProvider || 'cpu'; @@ -303,8 +312,12 @@ export default function AIPanel() { const isPro = user?.publicMetadata?.plan === 'pro'; const [cloudUsage, setCloudUsage] = useState<{ requests: number; limit: number; month: string } | null>(null); + // Device-side fix: local/builtin AI (cpu) is always available for generative features. + // Cloud requires signedIn+isPro, ai-connector requires connection. const isGenerativeAvailable = - (aiProvider === 'cloud' && !!isSignedIn && !!isPro) || (aiProvider === 'ai-connector' && isAIConnectorConnected); + aiProvider === 'cpu' || + (aiProvider === 'cloud' && !!isSignedIn && !!isPro) || + (aiProvider === 'ai-connector' && isAIConnectorConnected); useEffect(() => { if (aiProvider !== 'cloud' || !isSignedIn || !isPro) return; @@ -483,26 +496,26 @@ export default function AIPanel() { if (config && config.parameters) { config.parameters.forEach((param: any) => { if (param.defaultValue !== undefined) { - subMask.parameters[param.key] = param.defaultValue / (param.multiplier || 1); + (subMask.parameters as any)[param.key] = param.defaultValue / (param.multiplier || 1); } }); } if (type === Mask.Linear && subMask.parameters) { - subMask.parameters.range = Math.min(imgW, imgH) * 0.1; + (subMask.parameters as any).range = Math.min(imgW, imgH) * 0.1; } if (type === Mask.Linear || type === Mask.Radial) { - if (!subMask.parameters) subMask.parameters = {}; - subMask.parameters.isInitialDraw = true; - subMask.parameters.startX = -10000; - subMask.parameters.startY = -10000; - subMask.parameters.endX = -10000; - subMask.parameters.endY = -10000; - subMask.parameters.centerX = -10000; - subMask.parameters.centerY = -10000; - subMask.parameters.radiusX = 0; - subMask.parameters.radiusY = 0; + if (!subMask.parameters) subMask.parameters = {} as any; + (subMask.parameters as any).isInitialDraw = true; + (subMask.parameters as any).startX = -10000; + (subMask.parameters as any).startY = -10000; + (subMask.parameters as any).endX = -10000; + (subMask.parameters as any).endY = -10000; + (subMask.parameters as any).centerX = -10000; + (subMask.parameters as any).centerY = -10000; + (subMask.parameters as any).radiusX = 0; + (subMask.parameters as any).radiusY = 0; } return subMask; }; @@ -557,7 +570,12 @@ export default function AIPanel() { selectBrushToolForNewMask(); } - if (type === Mask.AiForeground) handleGenerateAiForegroundMask(subMask.id); + if (type === Mask.AiForeground) { + // Use setTimeout to ensure state has been committed to the store before generating + setTimeout(() => handleGenerateAiForegroundMask(subMask.id), 0); + } else if (type === Mask.AiSubject) { + setTimeout(() => handleGenerateAiSubjectMask(subMask.id), 0); + } }; const handleAddSubMask = ( @@ -586,7 +604,11 @@ export default function AIPanel() { if (type === Mask.Brush || type === Mask.Clone || type === Mask.Heal) { selectBrushToolForNewMask(); } - if (type === Mask.AiForeground) handleGenerateAiForegroundMask(subMask.id); + if (type === Mask.AiForeground) { + setTimeout(() => handleGenerateAiForegroundMask(subMask.id), 0); + } else if (type === Mask.AiSubject) { + setTimeout(() => handleGenerateAiSubjectMask(subMask.id), 0); + } }; const handleAddAiContextMenu = (event: React.MouseEvent, targetContainerId?: string | null) => { @@ -1075,7 +1097,7 @@ export default function AIPanel() { {t('editor.ai.generativeEditTitle')} -
    e.stopPropagation()}> +
    e.stopPropagation()}> {AI_GENERATIVE_CREATION_TYPES.map((maskType: MaskType) => ( ))}
    + + + {t('editor.ai.enhancementTitle')} + +
    e.stopPropagation()}> + +
    )} @@ -1151,6 +1190,7 @@ export default function AIPanel() { copiedSubMask={copiedSubMask} analyzingSubMaskId={analyzingSubMaskId} onAddComponent={(e: React.MouseEvent) => handleAddAiContextMenu(e, container.id)} + handleToggleAiPatchVisibility={handleToggleAiPatchVisibility} /> ))} @@ -1204,6 +1244,7 @@ export default function AIPanel() { collapsibleState={collapsibleState} setCollapsibleState={setCollapsibleState} isGenerativeAvailable={isGenerativeAvailable} + aiProvider={aiProvider} /> )} @@ -1358,6 +1399,7 @@ function ContainerRow({ copiedSubMask, analyzingSubMaskId, onAddComponent, + handleToggleAiPatchVisibility, }: any) { const { t } = useTranslation(); const { setNodeRef: setDroppableRef, isOver } = useDroppable({ @@ -1386,6 +1428,14 @@ function ContainerRow({ updateContainer(container.id, { name: tempName.trim() }); } setRenamingId(null); + setTempName(''); + }; + + const handleRenameKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setRenamingId(null); + setTempName(''); + } }; const onContextMenu = (e: React.MouseEvent) => { @@ -1493,7 +1543,7 @@ function ContainerRow({ > {isStandalone ? ( (() => { - const StandaloneIcon = MASK_ICON_MAP[firstSubMask.type] || Circle; + const StandaloneIcon = MASK_ICON_MAP[firstSubMask.type as Mask] || Circle; return ; })() ) : isExpanded ? ( @@ -1518,7 +1568,10 @@ function ContainerRow({ value={tempName} onChange={(e) => setTempName(e.target.value)} onBlur={handleRenameSubmit} - onKeyDown={(e) => e.key === 'Enter' && handleRenameSubmit()} + onKeyDown={(e) => { + if (e.key === 'Enter') handleRenameSubmit(); + if (e.key === 'Escape') handleRenameKeyDown(e); + }} onClick={(e) => e.stopPropagation()} /> ) : ( @@ -1533,7 +1586,7 @@ function ContainerRow({ data-tooltip={container.visible ? t('editor.ai.actions.hideEdit') : t('editor.ai.actions.showEdit')} onClick={(e) => { e.stopPropagation(); - updateContainer(container.id, { visible: !container.visible }); + handleToggleAiPatchVisibility(container.id); }} > {container.visible ? : } @@ -1662,7 +1715,7 @@ function SubMaskRow({ setNodeRef(node); setDroppableRef(node); }; - const MaskIcon = MASK_ICON_MAP[subMask.type] || Circle; + const MaskIcon = MASK_ICON_MAP[subMask.type as Mask] || Circle; const { showContextMenu } = useContextMenu(); const [isHovered, setIsHovered] = useState(false); const hoverTimeoutRef = useRef | null>(null); @@ -1794,7 +1847,13 @@ function SubMaskRow({ value={tempName} onChange={(e) => setTempName(e.target.value)} onBlur={handleRenameSubmit} - onKeyDown={(e) => e.key === 'Enter' && handleRenameSubmit()} + onKeyDown={(e) => { + if (e.key === 'Enter') handleRenameSubmit(); + if (e.key === 'Escape') { + setRenamingId(null); + setTempName(''); + } + }} onClick={(e) => e.stopPropagation()} /> ) : ( @@ -1802,10 +1861,10 @@ function SubMaskRow({ {getSubMaskName(subMask)} )} -
    +
    {index > 1 && ( )} + {isAndroid && status === Status.Success && lastExportedFilePath && ( + + )}
    ); diff --git a/src/components/panel/right/Masks.tsx b/src/components/panel/right/Masks.tsx index f0d7e97a4d..7fc50e9bfc 100644 --- a/src/components/panel/right/Masks.tsx +++ b/src/components/panel/right/Masks.tsx @@ -41,7 +41,7 @@ export enum SubMaskMode { } export enum ToolType { - AiSeletor = 'ai-selector', + AiSelector = 'ai-selector', Brush = 'brush', Eraser = 'eraser', GenerativeReplace = 'generative-replace', @@ -107,7 +107,7 @@ export const MASK_ICON_MAP: Record = { [Mask.Flow]: Droplets, [Mask.Color]: Droplet, [Mask.Linear]: TriangleRight, - [Mask.Luminance]: Sparkles, + [Mask.Luminance]: Sun, [Mask.QuickEraser]: Eraser, [Mask.Radial]: Circle, [Mask.Clone]: Stamp, diff --git a/src/components/panel/right/MasksPanel.tsx b/src/components/panel/right/MasksPanel.tsx index 6cc94c1fbe..57334abc02 100644 --- a/src/components/panel/right/MasksPanel.tsx +++ b/src/components/panel/right/MasksPanel.tsx @@ -139,6 +139,8 @@ const SUB_MASK_CONFIG: Record = { ], }, [Mask.QuickEraser]: { parameters: [] }, + [Mask.Clone]: { showBrushTools: true }, + [Mask.Heal]: { showBrushTools: true }, }; const BrushTools = ({ @@ -255,8 +257,8 @@ function DepthRangePicker({ const [isLabelHovered, setIsLabelHovered] = useState(false); const vals = dragValues ?? { minDepth, maxDepth, minFade, maxFade }; - const fadeLeftEdge = Math.max(0, vals.minDepth - vals.minFade); - const fadeRightEdge = Math.min(100, vals.maxDepth + vals.maxFade); + const fadeLeftEdge = Math.max(0, Math.min(100, vals.minDepth - vals.minFade)); + const fadeRightEdge = Math.max(0, Math.min(100, vals.maxDepth + vals.maxFade)); useEffect(() => { return () => { @@ -278,7 +280,7 @@ function DepthRangePicker({ switch (handle) { case 'minDepth': { const v = Math.max(0, Math.min(val, init.maxDepth)); - return { minDepth: v, maxDepth: init.maxDepth, minFade: Math.min(init.minFade, v), maxFade: init.maxFade }; + return { minDepth: v, maxDepth: init.maxDepth, minFade: Math.max(0, Math.min(init.minFade, v)), maxFade: init.maxFade }; } case 'maxDepth': { const v = Math.max(init.minDepth, Math.min(100, val)); @@ -286,7 +288,7 @@ function DepthRangePicker({ minDepth: init.minDepth, maxDepth: v, minFade: init.minFade, - maxFade: Math.min(init.maxFade, 100 - v), + maxFade: Math.max(0, Math.min(init.maxFade, 100 - v)), }; } case 'fadeLeft': { @@ -294,7 +296,7 @@ function DepthRangePicker({ return { minDepth: init.minDepth, maxDepth: init.maxDepth, - minFade: init.minDepth - edge, + minFade: Math.max(0, init.minDepth - edge), maxFade: init.maxFade, }; } @@ -304,7 +306,7 @@ function DepthRangePicker({ minDepth: init.minDepth, maxDepth: init.maxDepth, minFade: init.minFade, - maxFade: edge - init.maxDepth, + maxFade: Math.max(0, edge - init.maxDepth), }; } case 'range': { @@ -559,11 +561,12 @@ function DepthRangePicker({ export default function MasksPanel() { const { t } = useTranslation(); const { setAdjustments } = useEditorActions(); - const { handleGenerateAiDepthMask, handleGenerateAiForegroundMask, handleGenerateAiSkyMask } = useAiMasking(); + const { handleGenerateAiDepthMask, handleGenerateAiForegroundMask, handleGenerateAiSkyMask, handleGenerateAiSubjectMask, handleDeleteMaskContainer: deleteMaskContainerFromHook } = useAiMasking(); const setCustomEscapeHandler = useUIStore((s) => s.setCustomEscapeHandler); - const { appSettings } = useSettingsStore( + const { appSettings, theme } = useSettingsStore( useShallow((state) => ({ appSettings: state.appSettings, + theme: state.theme, })), ); @@ -764,31 +767,33 @@ export default function MasksPanel() { if (type === Mask.Linear || type === Mask.Radial || type === Mask.Color || type === Mask.Luminance) { if (!subMask.parameters) subMask.parameters = {}; - subMask.parameters.isInitialDraw = true; + const params = subMask.parameters as any; + params.isInitialDraw = true; if (type === Mask.Linear || type === Mask.Radial) { - subMask.parameters.startX = -10000; - subMask.parameters.startY = -10000; - subMask.parameters.endX = -10000; - subMask.parameters.endY = -10000; - subMask.parameters.centerX = -10000; - subMask.parameters.centerY = -10000; - subMask.parameters.radiusX = 0; - subMask.parameters.radiusY = 0; + params.startX = -10000; + params.startY = -10000; + params.endX = -10000; + params.endY = -10000; + params.centerX = -10000; + params.centerY = -10000; + params.radiusX = 0; + params.radiusY = 0; } else { - subMask.parameters.targetX = -10000; - subMask.parameters.targetY = -10000; - subMask.parameters.tolerance = 20; - subMask.parameters.feather = 35; + params.targetX = -10000; + params.targetY = -10000; + params.tolerance = 20; + params.feather = 35; } } if (type === Mask.AiDepth) { if (!subMask.parameters) subMask.parameters = {}; - subMask.parameters.minDepth = 20; - subMask.parameters.maxDepth = 100; - subMask.parameters.minFade = 15; - subMask.parameters.maxFade = 15; - subMask.parameters.feather = 10; + const params = subMask.parameters as any; + params.minDepth = 20; + params.maxDepth = 100; + params.minFade = 15; + params.maxFade = 15; + params.feather = 10; } return subMask; }; @@ -810,6 +815,7 @@ export default function MasksPanel() { if (type === Mask.AiForeground) handleGenerateAiForegroundMask(subMask.id); else if (type === Mask.AiSky) handleGenerateAiSkyMask(subMask.id); else if (type === Mask.AiDepth) handleGenerateAiDepthMask(subMask.id, subMask.parameters); + else if (type === Mask.AiSubject) handleGenerateAiSubjectMask(subMask.id); }; const handleAddSubMask = ( @@ -841,6 +847,7 @@ export default function MasksPanel() { if (type === Mask.AiForeground) handleGenerateAiForegroundMask(subMask.id); else if (type === Mask.AiSky) handleGenerateAiSkyMask(subMask.id); else if (type === Mask.AiDepth) handleGenerateAiDepthMask(subMask.id, subMask.parameters); + else if (type === Mask.AiSubject) handleGenerateAiSubjectMask(subMask.id); }; const handleGridClick = (type: Mask, forceNewMaskContainer: boolean = false) => { @@ -949,8 +956,7 @@ export default function MasksPanel() { })); const handleDeleteContainer = (id: string) => { - if (activeMaskContainerId === id) handleDeselect(); - setAdjustments((prev: Adjustments) => ({ ...prev, masks: prev.masks.filter((m) => m.id !== id) })); + deleteMaskContainerFromHook(id); }; const handleDeleteSubMask = (containerId: string, subMaskId: string) => { @@ -1122,11 +1128,11 @@ export default function MasksPanel() { const creationFn = () => { if (overData?.type === 'Container') { handleAddSubMask(overData.item!.id, dragData.maskType!); - } else if (overData?.type === 'SubMask') { + } else if (overData?.type === 'SubMask' && over) { const container = adjustments.masks.find((m) => m.id === overData.parentId); if (container) { const targetIndex = container.subMasks.findIndex((sm) => sm.id === over.id); - handleAddSubMask(overData.parentId!, dragData.maskType!, targetIndex); + handleAddSubMask(overData.parentId!, dragData.maskType!, SubMaskMode.Additive, targetIndex); } } else { handleAddMaskContainer(dragData.maskType!); @@ -1953,7 +1959,7 @@ function SubMaskRow({ setNodeRef(node); setDroppableRef(node); }; - const MaskIcon = MASK_ICON_MAP[subMask.type] || Circle; + const MaskIcon = MASK_ICON_MAP[subMask.type as Mask] || Circle; const { showContextMenu } = useContextMenu(); const [isHovered, setIsHovered] = useState(false); const hoverTimeoutRef = useRef | null>(null); @@ -2188,6 +2194,7 @@ function SettingsPanel({ }: any) { const { t } = useTranslation(); const { showContextMenu } = useContextMenu(); + const { theme } = useSettingsStore((state) => ({ theme: state.theme })); const isActive = !!container; const presetButtonRef = useRef(null); @@ -2268,7 +2275,7 @@ function SettingsPanel({ updateSubMask(activeSubMask.id, { parameters: newParams }); }; - const subMaskConfig = activeSubMask ? SUB_MASK_CONFIG[activeSubMask.type] || {} : {}; + const subMaskConfig = activeSubMask ? SUB_MASK_CONFIG[activeSubMask.type as Mask] || {} : {}; const isAiMask = activeSubMask && ['ai-subject', 'ai-foreground', 'ai-sky', 'ai-depth'].includes(activeSubMask.type); const isComponentMode = !!activeSubMask; @@ -2482,7 +2489,7 @@ function SettingsPanel({ label={ param.key === 'feather' && activeSubMask.type === Mask.AiDepth ? t('editor.masks.params.globalFeather') - : t('editor.masks.params.' + param.key) + : t(`editor.masks.params.${param.key}` as any) } min={param.min} max={param.max} @@ -2549,6 +2556,7 @@ function SettingsPanel({ histogram={histogram} isForMask={true} appSettings={appSettings} + theme={theme} onDragStateChange={onDragStateChange} /> diff --git a/src/components/panel/right/MetadataPanel.tsx b/src/components/panel/right/MetadataPanel.tsx index 0932e72ec1..02582b9538 100644 --- a/src/components/panel/right/MetadataPanel.tsx +++ b/src/components/panel/right/MetadataPanel.tsx @@ -1,9 +1,10 @@ import { useState, useMemo, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; -import { Check, ChevronDown, ChevronRight, Plus, Star, Tag, X, User } from 'lucide-react'; +import { Check, ChevronDown, ChevronRight, Loader2, Plus, Sparkles, Star, Tag, X, User } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useTranslation } from 'react-i18next'; import clsx from 'clsx'; +import { toast } from 'react-toastify'; import { Invokes } from '../../ui/AppProperties'; import { COLOR_LABELS, Color } from '../../../utils/adjustments'; import Text from '../../ui/Text'; @@ -233,8 +234,12 @@ export default function MetadataPanel() { const { t } = useTranslation(); const [isOrganizationExpanded, setIsOrganizationExpanded] = useState(false); const [isAuthorExpanded, setIsAuthorExpanded] = useState(false); + const [isAiRatingExpanded, setIsAiRatingExpanded] = useState(false); const [tagInputValue, setTagInputValue] = useState(''); const [isTagInputFocused, setIsTagInputFocused] = useState(false); + const [aiRatingLoading, setAiRatingLoading] = useState(false); + const [aiRatingResult, setAiRatingResult] = useState<{ rating: number; description: string; tags: string[] } | null>(null); + const [aiRatingApplied, setAiRatingApplied] = useState(false); const selectedImage = useEditorStore((s) => s.selectedImage); const multiSelectedPaths = useLibraryStore((s) => s.multiSelectedPaths); const imageList = useLibraryStore((s) => s.imageList); @@ -378,6 +383,60 @@ export default function MetadataPanel() { e.stopPropagation(); }; + const handleGenerateAiRating = async () => { + if (!selectedImage || aiRatingLoading) return; + setAiRatingLoading(true); + setAiRatingResult(null); + setAiRatingApplied(false); + try { + const result = await invoke<{ rating: number; description: string; tags: string[] }>( + Invokes.GenerateAiRating, + { path: selectedImage.path }, + ); + setAiRatingResult(result); + } catch (err) { + console.error('AI rating failed:', err); + toast.error(t('editor.aiRating.generate') + ' ' + String(err)); + } finally { + setAiRatingLoading(false); + } + }; + + const handleApplyAiRatingToExif = async () => { + if (!aiRatingResult || !selectedImage) return; + try { + // Apply rating + await invoke(Invokes.SetRatingForPaths, { paths: targetPaths, rating: aiRatingResult.rating }); + // Update local state + const { setLibrary } = useLibraryStore.getState(); + setLibrary((state) => { + const newRatings = { ...state.imageRatings }; + targetPaths.forEach((p) => { + newRatings[p] = aiRatingResult.rating; + }); + return { imageRatings: newRatings }; + }); + // Write description and tags to EXIF UserComment + const commentParts: string[] = []; + if (aiRatingResult.description) commentParts.push(aiRatingResult.description); + if (aiRatingResult.tags.length > 0) commentParts.push(`Tags: ${aiRatingResult.tags.join(', ')}`); + if (commentParts.length > 0) { + handleUpdateExif(targetPaths, { UserComment: commentParts.join(' | ') }); + } + setAiRatingApplied(true); + toast.success(t('editor.aiRating.applied')); + } catch (err) { + console.error('Failed to apply AI rating to EXIF:', err); + toast.error(String(err)); + } + }; + + // Reset AI rating result when selected image changes + useEffect(() => { + setAiRatingResult(null); + setAiRatingApplied(false); + }, [selectedImage?.path]); + const LensIcon = CAMERA_ICONS['LensModel']; return ( @@ -388,6 +447,137 @@ export default function MetadataPanel() {
    {selectedImage ? (
    +
    + + + {isAiRatingExpanded && ( + +
    + + + {aiRatingResult && ( +
    +
    + + {t('editor.metadata.organization.rating')} + +
    + {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
    +
    + + {aiRatingResult.description && ( +
    + + {t('editor.aiRating.description')} + + + {aiRatingResult.description} + +
    + )} + + {aiRatingResult.tags.length > 0 && ( +
    + + {t('editor.metadata.organization.tags')} + +
    + {aiRatingResult.tags.map((tag) => ( + + {tag} + + ))} +
    +
    + )} + + +
    + )} +
    +
    + )} +
    +
    +
    {t('editor.metadata.fileInfo.title')} @@ -537,7 +727,7 @@ export default function MetadataPanel() { return ( { handleUpdateExif(targetPaths, { [field.key]: newVal }); @@ -597,7 +787,7 @@ export default function MetadataPanel() { {[1, 2, 3, 4, 5].map((star) => ( + onColorChange(e.target.value)} + className="sr-only" + /> + {label} + {color.toUpperCase()} +
    + onOpacityChange(Number(e.target.value))} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> +
    + ); +} + +export default function PortraitPanelSwitcher() { + const { t } = useTranslation(); + + const attributes = [ + { key: 'single' as PersonAttribute, label: t('editor.portraitPanel.attributes.single'), icon: User }, + { key: 'male' as PersonAttribute, label: t('editor.portraitPanel.attributes.male'), icon: PersonStanding }, + { key: 'female' as PersonAttribute, label: t('editor.portraitPanel.attributes.female'), icon: PersonStanding }, + { key: 'child' as PersonAttribute, label: t('editor.portraitPanel.attributes.child'), icon: Baby }, + { key: 'elderMale' as PersonAttribute, label: t('editor.portraitPanel.attributes.elderMale'), icon: PersonStanding }, + { key: 'elderFemale' as PersonAttribute, label: t('editor.portraitPanel.attributes.elderFemale'), icon: PersonStanding }, + { key: 'all' as PersonAttribute, label: t('editor.portraitPanel.attributes.all'), icon: Users }, + ] as const; + + const { setAdjustments } = useEditorActions(); + + const { collapsibleSectionsState, setUI } = useUIStore( + useShallow((state) => ({ + collapsibleSectionsState: state.collapsibleSectionsState, + setUI: state.setUI, + })), + ); + + const { + adjustments, + isBlemishModeActive, + brushSettings, + setEditor, + } = useEditorStore( + useShallow((state) => ({ + adjustments: state.adjustments, + isBlemishModeActive: state.isBlemishModeActive, + brushSettings: state.brushSettings, + setEditor: state.setEditor, + })), + ); + + const portrait = adjustments.portrait || INITIAL_PORTRAIT_ADJUSTMENTS; + const personAttribute = portrait.personAttribute || 'all'; + const sectionVisibility = adjustments.sectionVisibility || {}; + + const setCollapsibleState = useCallback( + (updater: any) => + setUI((state) => ({ + collapsibleSectionsState: typeof updater === 'function' ? updater(state.collapsibleSectionsState) : updater, + })), + [setUI], + ); + + const onDragStateChange = useCallback( + (isDragging: boolean) => setEditor({ isSliderDragging: isDragging }), + [setEditor], + ); + + const updatePortrait = useCallback( + (key: keyof PortraitAdjustments, value: any) => { + setAdjustments((prev: Adjustments) => ({ + ...prev, + portrait: { + ...(prev.portrait || INITIAL_PORTRAIT_ADJUSTMENTS), + [key]: value, + }, + })); + }, + [setAdjustments], + ); + + const handleToggleSection = (section: string) => { + setCollapsibleState((prev: any) => { + const isOpening = !prev[section]; + return { ...prev, [section]: !prev[section] }; + }); + }; + + const [beautyClicked, setBeautyClicked] = React.useState(false); + + const handleOneClickBeauty = useCallback(() => { + setBeautyClicked(true); + setTimeout(() => setBeautyClicked(false), 300); + setAdjustments((prev: Adjustments) => ({ + ...prev, + portrait: { + ...(prev.portrait || INITIAL_PORTRAIT_ADJUSTMENTS), + skinSmoothingStrength: 35, + skinSmoothingDetailPreserve: 65, + faceSlimAmount: 25, + jawAmount: -10, + eyeEnlargeAmount: 20, + eyeBrightenAmount: 25, + teethWhitenBrightness: 30, + teethWhitenDesaturate: 25, + lipstickColor: '#D44D5C', + lipstickOpacity: 25, + blushColor: '#E8919C', + blushOpacity: 20, + eyebrowColor: '#6B4423', + eyebrowOpacity: 15, + }, + })); + }, [setAdjustments]); + + const handleRemoveLastBlemish = useCallback(() => { + setAdjustments((prev: Adjustments) => { + const currentPortrait = prev.portrait || INITIAL_PORTRAIT_ADJUSTMENTS; + const spots = [...currentPortrait.blemishSpots]; + spots.pop(); + return { + ...prev, + portrait: { ...currentPortrait, blemishSpots: spots }, + }; + }); + }, [setAdjustments]); + + const toggleBlemishMode = useCallback(() => { + setEditor((state) => ({ isBlemishModeActive: !state.isBlemishModeActive })); + }, [setEditor]); + + const portraitSections = [ + { + key: 'blemishRemoval', + title: t('editor.portraitPanel.blemishRemoval'), + icon: Eraser, + content: ( +
    +
    + + +
    + {portrait.blemishSpots.length > 0 && ( + + {t('editor.portraitPanel.blemishCount', { count: portrait.blemishSpots.length })} + + )} + {isBlemishModeActive && ( + <> +
    + + {t('editor.portraitPanel.blemishHint')} + +
    +
    + + {t('editor.portraitPanel.brushSize') || '画笔大小'} + +
    +
    +
    + + {brushSettings?.size || 20} + +
    + + )} +
    + ), + }, + { + key: 'skinSmoothing', + title: t('editor.portraitPanel.skinSmoothing'), + icon: Sparkles, + content: ( +
    + updatePortrait('skinSmoothingStrength', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + updatePortrait('skinSmoothingDetailPreserve', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> +
    + ), + }, + { + key: 'faceReshape', + title: t('editor.portraitPanel.faceReshape'), + icon: CircleDot, + content: ( +
    + updatePortrait('faceSlimAmount', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + updatePortrait('jawAmount', v)} + onDragStateChange={onDragStateChange} + /> + updatePortrait('foreheadAmount', v)} + onDragStateChange={onDragStateChange} + /> +
    + ), + }, + { + key: 'eyeEnhance', + title: t('editor.portraitPanel.eyeEnhance'), + icon: Eye, + content: ( +
    + updatePortrait('eyeEnlargeAmount', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + updatePortrait('eyeBrightenAmount', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> +
    + ), + }, + { + key: 'teethWhiten', + title: t('editor.portraitPanel.teethWhiten'), + icon: Smile, + content: ( +
    + updatePortrait('teethWhitenBrightness', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + updatePortrait('teethWhitenDesaturate', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> +
    + ), + }, + { + key: 'makeup', + title: t('editor.portraitPanel.makeup'), + icon: Palette, + content: ( +
    + updatePortrait('lipstickColor', c)} + onOpacityChange={(v) => updatePortrait('lipstickOpacity', v)} + onDragStateChange={onDragStateChange} + /> + updatePortrait('blushColor', c)} + onOpacityChange={(v) => updatePortrait('blushOpacity', v)} + onDragStateChange={onDragStateChange} + /> + updatePortrait('eyebrowColor', c)} + onOpacityChange={(v) => updatePortrait('eyebrowOpacity', v)} + onDragStateChange={onDragStateChange} + /> +
    + ), + }, + { + key: 'hairAdjust', + title: t('editor.portraitPanel.hairAdjust'), + icon: Scissors, + content: ( +
    + updatePortrait('hairHueShift', v)} + onDragStateChange={onDragStateChange} + /> + updatePortrait('hairBrightness', v)} + onDragStateChange={onDragStateChange} + /> +
    + ), + }, + { + key: 'bodyReshape', + title: t('editor.portraitPanel.bodyReshape'), + icon: Move, + content: ( +
    +
    + + {t('editor.portraitPanel.bodySymmetry')} + + updatePortrait('bodySymmetryEnabled', checked)} + /> +
    + updatePortrait('bodySlimAmount', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + updatePortrait('bodyHeightAmount', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> + updatePortrait('legLengthAmount', v)} + onDragStateChange={onDragStateChange} + fillOrigin="min" + /> +
    + ), + }, + ]; + + return ( +
    +
    + {t('editor.portraitPanel.title')} +
    +
    +
    + {attributes.map((attr) => { + const Icon = attr.icon; + return ( + + ); + })} +
    + +
    +
    + {portraitSections.map((section) => ( +
    + handleToggleSection(section.key)} + title={section.title} + isContentVisible={sectionVisibility[section.key] !== false} + > + {section.content} + +
    + ))} +
    +
    + ); +} diff --git a/src/components/panel/right/PresetsPanel.tsx b/src/components/panel/right/PresetsPanel.tsx index 1c4613866e..ffd938b9ef 100644 --- a/src/components/panel/right/PresetsPanel.tsx +++ b/src/components/panel/right/PresetsPanel.tsx @@ -12,6 +12,7 @@ import { } from '@dnd-kit/core'; import { useTranslation } from 'react-i18next'; import { PresetListType, usePresets, UserPreset } from '../../../hooks/usePresets'; +import { BUILT_IN_PRESETS, BuiltInPreset } from '../../../data/builtInPresets'; import { useContextMenu } from '../../../context/ContextMenuContext'; import { CopyPlus, @@ -34,6 +35,7 @@ import { Settings2, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +import clsx from 'clsx'; import ConfigurePresetModal from '../../modals/ConfigurePresetModal'; import CreateFolderModal from '../../modals/CreateFolderModal'; import RenameFolderModal from '../../modals/RenameFolderModal'; @@ -46,6 +48,7 @@ import { Invokes, OPTION_SEPARATOR, Panel, Preset, SelectedImage } from '../../u import { useEditorStore } from '../../../store/useEditorStore'; import { useUIStore } from '../../../store/useUIStore'; import { useEditorActions } from '../../../hooks/useEditorActions'; +import { useOsPlatform } from '../../../hooks/useOsPlatform'; interface DroppableFolderItemProps { children: any; @@ -110,14 +113,20 @@ const itemVariants = { const evaluateCurveY = (curve: Array<{ x: number; y: number }>, targetX: number): number => { const len = curve.length; + if (len === 0) return targetX; if (len === 1) return curve[0].y; - if (targetX <= curve[0].x) return curve[0].y; - if (targetX >= curve[len - 1].x) return curve[len - 1].y; + const first = curve[0]; + const last = curve[len - 1]; + if (!first || !last) return targetX; + if (targetX <= first.x) return first.y; + if (targetX >= last.x) return last.y; for (let i = 0; i < len - 1; i++) { const p2 = curve[i + 1]; + if (!p2) continue; if (targetX <= p2.x) { const p1 = curve[i]; + if (!p1) continue; const range = p2.x - p1.x; return range === 0 ? p1.y : p1.y + ((targetX - p1.x) / range) * (p2.y - p1.y); } @@ -125,38 +134,43 @@ const evaluateCurveY = (curve: Array<{ x: number; y: number }>, targetX: number) return targetX; }; -const mixAdjustments = (presetObj: any, intensity: number, initialObj: any = INITIAL_ADJUSTMENTS): any => { +const mixAdjustments = (presetObj: any, intensity: number, initialObj: any = INITIAL_ADJUSTMENTS, currentObj?: any): any => { const fraction = intensity / 100; + // Use the live current state when provided (tool preset semantics), so that + // the intensity slider blends between the existing state and the preset + // rather than always blending from INITIAL_ADJUSTMENTS. + const baselineObj = currentObj !== undefined ? currentObj : initialObj; if (fraction === 1) return { ...presetObj }; - if (fraction === 0) return { ...initialObj }; + if (fraction === 0) return { ...baselineObj }; - const result: any = {}; + const result: any = { ...baselineObj }; const keys = Object.keys(presetObj); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const presetVal = presetObj[key]; - const initialVal = initialObj[key] !== undefined ? initialObj[key] : (INITIAL_ADJUSTMENTS as any)[key]; + const baseVal = baselineObj[key] !== undefined ? baselineObj[key] : initialObj[key]; if (typeof presetVal === 'number') { - result[key] = typeof initialVal === 'number' ? initialVal + (presetVal - initialVal) * fraction : presetVal; + result[key] = typeof baseVal === 'number' ? baseVal + (presetVal - baseVal) * fraction : presetVal; } else if (Array.isArray(presetVal)) { - if (!Array.isArray(initialVal)) { - result[key] = fraction > 0 ? presetVal : initialVal; + if (!Array.isArray(baseVal)) { + result[key] = fraction > 0 ? presetVal : baseVal; continue; } - if (presetVal.length > 0 && presetVal[0].x !== undefined && presetVal[0].y !== undefined) { + if (presetVal.length > 0 && presetVal[0]?.x !== undefined && presetVal[0]?.y !== undefined) { const xVals: number[] = []; let p1 = 0, p2 = 0; - const len1 = initialVal.length, + const len1 = baseVal.length, len2 = presetVal.length; while (p1 < len1 && p2 < len2) { - const x1 = initialVal[p1].x, - x2 = presetVal[p2].x; + const x1 = baseVal[p1]?.x, + x2 = presetVal[p2]?.x; + if (x1 === undefined || x2 === undefined) break; if (x1 < x2) { xVals.push(x1); p1++; @@ -169,16 +183,22 @@ const mixAdjustments = (presetObj: any, intensity: number, initialObj: any = INI p2++; } } - while (p1 < len1) xVals.push(initialVal[p1++].x); - while (p2 < len2) xVals.push(presetVal[p2++].x); + while (p1 < len1) { + const remainingX = baseVal[p1++]?.x; + if (remainingX !== undefined) xVals.push(remainingX); + } + while (p2 < len2) { + const remainingX = presetVal[p2++]?.x; + if (remainingX !== undefined) xVals.push(remainingX); + } const newCurve = new Array(xVals.length); for (let j = 0; j < xVals.length; j++) { const x = xVals[j]; - const yInit = evaluateCurveY(initialVal, x); + const yBase = evaluateCurveY(baseVal, x); const yPreset = evaluateCurveY(presetVal, x); - const yInterp = yInit + (yPreset - yInit) * fraction; + const yInterp = yBase + (yPreset - yBase) * fraction; newCurve[j] = { x, @@ -187,12 +207,12 @@ const mixAdjustments = (presetObj: any, intensity: number, initialObj: any = INI } result[key] = newCurve; } else { - result[key] = fraction > 0 ? presetVal : initialVal; + result[key] = fraction > 0 ? presetVal : baseVal; } } else if (presetVal !== null && typeof presetVal === 'object') { - result[key] = mixAdjustments(presetVal, intensity, initialVal || {}); + result[key] = mixAdjustments(presetVal, intensity, initialObj[key] || {}, currentObj?.[key]); } else { - result[key] = fraction > 0 ? presetVal : initialVal; + result[key] = fraction > 0 ? presetVal : baseVal; } } return result; @@ -214,6 +234,25 @@ function PresetItemDisplay({ const supportsGeometry = preset.includeCropTransform ?? geometryKeys.some((key) => preset.adjustments?.[key] !== undefined); const isTool = preset.presetType === 'tool'; + const presetTypeLabels: Record = { + portrait: t('editor.presets.types.portrait' as any), + color: t('editor.presets.types.color' as any), + 'ai-color': t('editor.presets.types.ai-color' as any), + combined: t('editor.presets.types.combined' as any), + tool: t('editor.presets.types.tool' as any), + style: t('editor.presets.types.style' as any), + }; + const presetTypeColors: Record = { + portrait: 'bg-rose-500/90', + color: 'bg-amber-500/90', + 'ai-color': 'bg-violet-500/90', + combined: 'bg-emerald-500/90', + tool: 'bg-sky-500/90', + style: 'bg-slate-500/90', + }; + const typeLabel = preset.presetType ? presetTypeLabels[preset.presetType] || '' : ''; + const typeColor = preset.presetType ? presetTypeColors[preset.presetType] || 'bg-slate-500/90' : ''; + const tooltipContent = useMemo(() => { const features = []; if (supportsMasks) features.push(t('editor.presets.supports.masks')); @@ -255,9 +294,16 @@ function PresetItemDisplay({
    - - {preset.name} - +
    + + {preset.name} + + {typeLabel && ( + + {typeLabel} + + )} +
    {isTool ? ( @@ -474,12 +520,14 @@ function DroppableFolderItem({ folder, onContextMenu, children, onToggle, isExpa } export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProps) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const selectedImage = useEditorStore((s) => s.selectedImage); const adjustments = useEditorStore((s) => s.adjustments); const activePanel = useUIStore((s) => s.activeRightPanel); const setEditor = useEditorStore((s) => s.setEditor); const { setAdjustments } = useEditorActions(); + const osPlatform = useOsPlatform(); + const isDeviceSide = osPlatform === 'android'; const { addFolder, @@ -512,6 +560,8 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp const [activePresetId, setActivePresetId] = useState(null); const [presetIntensity, setPresetIntensity] = useState(100); const [baseAdjustments, setBaseAdjustments] = useState(null); + const [activeFilter, setActiveFilter] = useState<'all' | 'portrait' | 'color' | 'ai-color' | 'combined'>('all'); + const [activeGroup, setActiveGroup] = useState<'recommended' | 'my'>('my'); const previewsRef = useRef(previews); previewsRef.current = previews; @@ -648,7 +698,7 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp break; } - const blob = new Blob([imageData], { type: 'image/jpeg' }); + const blob = new Blob([imageData as BlobPart], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); setPreviews((prev: Record) => { const oldUrl = prev[preset.id]; @@ -671,8 +721,14 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp const enqueuePreviews = useCallback( (presetsToGenerate: Array, folderId: string | null = null) => { + if (!presetsToGenerate || presetsToGenerate.length === 0) return; + // Track ids that are already in the queue to avoid duplicate generation. + const queuedIds = new Set(); + for (const queued of previewQueue.current) { + if (queued?.preset?.id) queuedIds.add(queued.preset.id); + } const newItems = presetsToGenerate - .filter((p: any) => !previewsRef.current[p?.id]) + .filter((p: any) => p && p.id && !previewsRef.current[p.id] && !queuedIds.has(p.id)) .map((p: UserPreset) => ({ preset: p, folderId })); if (newItems.length > 0) { previewQueue.current.push(...newItems); @@ -714,7 +770,7 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp if (pathAtStart !== currentImagePathRef.current) return; - const blob = new Blob([imageData], { type: 'image/jpeg' }); + const blob = new Blob([imageData as BlobPart], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); setPreviews((prev: Record) => { @@ -818,36 +874,59 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp setAdjustments(baseAdjustments); } setBaseAdjustments(null); + setPresetIntensity(100); return; } + // Capture the existing adjustments as the baseline for intensity mixing. setBaseAdjustments(adjustments); setActivePresetId(preset.id); setPresetIntensity(100); - setAdjustments((prevAdjustments: Adjustments) => ({ - ...prevAdjustments, - ...preset.adjustments, - })); + if (preset.presetType === 'style') { + // Style preset: overwrite all settings (including crop/masks) + setAdjustments({ + ...INITIAL_ADJUSTMENTS, + ...preset.adjustments, + }); + } else { + // Tool preset: additive – layer on top of existing adjustments + setAdjustments((prevAdjustments: Adjustments) => ({ + ...prevAdjustments, + ...preset.adjustments, + })); + } }; const handleIntensityChange = useCallback( (preset: Preset, intensity: number) => { - setPresetIntensity(intensity); - const mixed = mixAdjustments(preset.adjustments, intensity); - setAdjustments((prev: Adjustments) => ({ - ...prev, - ...mixed, - })); + const clamped = Math.max(0, Math.min(200, Math.round(intensity))); + setPresetIntensity(clamped); + setAdjustments((prev: Adjustments) => { + if (preset.presetType === 'style') { + // Style: interpolate between INITIAL and preset (full overwrite semantics). + // The baseline is INITIAL_ADJUSTMENTS so the slider always moves from + // a clean default state to the full preset. + const mixed = mixAdjustments(preset.adjustments, clamped, INITIAL_ADJUSTMENTS, INITIAL_ADJUSTMENTS); + return { ...INITIAL_ADJUSTMENTS, ...mixed }; + } + // Tool: interpolate between the state captured before the preset was + // applied (baseAdjustments) and the preset values. This avoids drift + // when the user moves the slider repeatedly. + const baseline = baseAdjustments ?? prev; + const mixed = mixAdjustments(preset.adjustments, clamped, baseline, baseline); + // Preserve unrelated settings (e.g. masks, crop) by merging with current. + return { ...prev, ...mixed }; + }); }, - [setAdjustments], + [setAdjustments, baseAdjustments], ); const handleSaveConfiguredPreset = async ( name: string, includeMasks: boolean, includeCropTransform: boolean, - presetType: 'tool' | 'style', + presetType: Preset['presetType'], ) => { if (configureModalState.preset) { const updated = configurePreset( @@ -881,12 +960,40 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp setRenameFolderState({ isOpen: false, folder: null }); }; + const collectIdsToDelete = useCallback( + (id: string): string[] => { + const ids = new Set([id]); + const folder = presets.find((p: UserPreset) => p.folder?.id === id)?.folder; + if (folder) { + for (const child of folder.children) { + ids.add(child.id); + } + } + return Array.from(ids); + }, + [presets], + ); + const handleDeleteItem = (id: string | null, isFolder = false) => { setDeletingItemId(id); if (!id) { return; } + // Collect all preview URLs that need to be released immediately. + const idsToCleanup = collectIdsToDelete(id); + setPreviews((prev: Record) => { + const next = { ...prev }; + for (const cleanupId of idsToCleanup) { + const url = next[cleanupId]; + if (url && url.startsWith('blob:')) { + URL.revokeObjectURL(url); + } + delete next[cleanupId]; + } + return next; + }); + setTimeout(() => { deleteItem(id); if (isFolder) { @@ -969,19 +1076,31 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp title: t('editor.presets.dialog.importPresetsTitle'), }); - if (typeof selectedPath === 'string') { - const isLegacy = - selectedPath.toLowerCase().endsWith('.xmp') || selectedPath.toLowerCase().endsWith('.lrtemplate'); + // The dialog may return null (cancelled) or a string (selected path). + if (typeof selectedPath !== 'string' || !selectedPath) { + return; + } - if (isLegacy) { - await importLegacyPresetsFromFile(selectedPath); - } else { - await importPresetsFromFile(selectedPath); - } + const isLegacy = + selectedPath.toLowerCase().endsWith('.xmp') || selectedPath.toLowerCase().endsWith('.lrtemplate'); - setFolderPreviewsGenerated(new Set()); - setPreviews({}); + if (isLegacy) { + await importLegacyPresetsFromFile(selectedPath); + } else { + await importPresetsFromFile(selectedPath); } + + // Existing previews are now stale (preset list was mutated) so reset + // both the cached URLs and the per-folder generation markers. + setPreviews((prev) => { + Object.values(prev).forEach((url) => { + if (url && url.startsWith('blob:')) { + URL.revokeObjectURL(url); + } + }); + return {}; + }); + setFolderPreviewsGenerated(new Set()); } catch (error) { console.error('Failed to import presets:', error); } @@ -1130,7 +1249,35 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp }; const folders = useMemo(() => presets.filter((item: UserPreset) => item.folder), [presets]); - const rootPresets = useMemo(() => presets.filter((item: UserPreset) => item.preset), [presets]); + const rootPresets = useMemo(() => { + const items = presets.filter((item: UserPreset) => item.preset); + if (activeFilter === 'all') return items; + return items.filter((item: UserPreset) => { + const type = item.preset?.presetType; + if (!type) return false; + return type === activeFilter; + }); + }, [presets, activeFilter]); + + const builtInPresetsFiltered = useMemo(() => { + if (activeGroup !== 'recommended') return []; + if (activeFilter === 'all') return BUILT_IN_PRESETS; + return BUILT_IN_PRESETS.filter((p) => p.type === activeFilter); + }, [activeGroup, activeFilter]); + + const handleApplyBuiltInPreset = useCallback( + (builtIn: BuiltInPreset) => { + setAdjustments((prev: Adjustments) => ({ + ...prev, + ...builtIn.adjustments, + portrait: { + ...(prev.portrait || {}), + ...(builtIn.adjustments.portrait || {}), + }, + })); + }, + [setAdjustments], + ); return ( @@ -1138,6 +1285,7 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp
    {t('editor.presets.title')}
    + {!isDeviceSide && ( + )}
    +
    +
    + {(['recommended', 'my'] as const).map((group) => ( + + ))} +
    +
    + {(['all', 'portrait', 'color', 'ai-color', 'combined'] as const).map((filter) => ( + + ))} +
    +
    +
    {t('editor.presets.status.loading')} )} - {!isLoading && presets.length === 0 ? ( + {!isLoading && presets.length === 0 && activeGroup === 'my' ? (
    {t('editor.presets.status.empty')} + {!isDeviceSide && ( + )}
    ) : ( <> - + {activeGroup === 'recommended' && builtInPresetsFiltered.length > 0 && ( + + {builtInPresetsFiltered.map((builtIn: BuiltInPreset, index: number) => { + const presetTypeColors: Record = { + portrait: 'bg-rose-500/90', + color: 'bg-amber-500/90', + 'ai-color': 'bg-violet-500/90', + combined: 'bg-emerald-500/90', + }; + const presetTypeLabels: Record = { + portrait: t('editor.presets.types.portrait' as any), + color: t('editor.presets.types.color' as any), + 'ai-color': t('editor.presets.types.ai-color' as any), + combined: t('editor.presets.types.combined' as any), + }; + const typeColor = presetTypeColors[builtIn.type] || 'bg-slate-500/90'; + const typeLabel = presetTypeLabels[builtIn.type] || ''; + return ( + +
    handleApplyBuiltInPreset(builtIn)} + className="flex flex-col p-2 rounded-lg bg-surface cursor-pointer hover:bg-card-active transition-colors" + > +
    +
    + +
    +
    +
    + + {i18n.language === 'zh-CN' || i18n.language === 'zh' ? builtIn.nameZh : builtIn.name} + + {typeLabel && ( + + {typeLabel} + + )} +
    + + {builtIn.category} + +
    +
    +
    +
    + ); + })} +
    + )} + {activeGroup === 'my' && ( + <> + {folders .filter((item: UserPreset) => item.folder?.id !== deletingItemId) .map((item: UserPreset, index: number) => ( @@ -1272,6 +1517,8 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp ))} + + )} )}
    diff --git a/src/components/panel/right/RightPanelSwitcher.tsx b/src/components/panel/right/RightPanelSwitcher.tsx index b741feed4d..cddf8bed54 100644 --- a/src/components/panel/right/RightPanelSwitcher.tsx +++ b/src/components/panel/right/RightPanelSwitcher.tsx @@ -7,6 +7,8 @@ import { Paintbrush, SwatchBook, FileInput, + Palette, + UserCircle, type LucideIcon, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -26,14 +28,18 @@ interface RightPanelSwitcherProps { } const panelGroups: Array> = [ - [{ id: Panel.Metadata, icon: Info, title: 'editor.switcher.tooltips.info' }], [ - { id: Panel.Adjustments, icon: SlidersHorizontal, title: 'editor.switcher.tooltips.adjust' }, - { id: Panel.Crop, icon: Crop, title: 'editor.switcher.tooltips.crop' }, + { id: Panel.Adjustments, icon: SlidersHorizontal, title: 'editor.switcher.tooltips.basic' }, + { id: Panel.Color, icon: Palette, title: 'editor.switcher.tooltips.color' }, + { id: Panel.Portrait, icon: UserCircle, title: 'editor.switcher.tooltips.portrait' }, + { id: Panel.Crop, icon: Crop, title: 'editor.switcher.tooltips.composition' }, + ], + [ { id: Panel.Masks, icon: Layers, title: 'editor.switcher.tooltips.masks' }, { id: Panel.Ai, icon: Paintbrush, title: 'editor.switcher.tooltips.inpaint' }, ], [ + { id: Panel.Metadata, icon: Info, title: 'editor.switcher.tooltips.info' }, { id: Panel.Presets, icon: SwatchBook, title: 'editor.switcher.tooltips.presets' }, { id: Panel.Export, icon: FileInput, title: 'editor.switcher.tooltips.export' }, ], @@ -66,7 +72,7 @@ export default function RightPanelSwitcher({ }`} key={id} onClick={() => onPanelSelect(id)} - data-tooltip={t(title)} + data-tooltip={t(title as any)} > {activePanel === id && ( )} - + ))}
    diff --git a/src/components/ui/AndroidBottomNav.test.tsx b/src/components/ui/AndroidBottomNav.test.tsx new file mode 100644 index 0000000000..384fdb4fe3 --- /dev/null +++ b/src/components/ui/AndroidBottomNav.test.tsx @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import AndroidBottomNav from './AndroidBottomNav'; +import { useUIStore } from '../../store/useUIStore'; + +vi.mock('../../store/useUIStore'); + +describe('AndroidBottomNav', () => { + const setRightPanel = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders null when not Android', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('renders all 10 nav items on Android', () => { + (useUIStore as any).mockReturnValue({ activeRightPanel: null }); + (useUIStore as any).mockImplementation((selector: any) => { + const state = { activeRightPanel: null, setRightPanel }; + return selector ? selector(state) : state; + }); + + render(); + expect(screen.getByText('editor.android.bottomNav.library')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.basic')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.color')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.portrait')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.crop')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.masks')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.ai')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.metadata')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.presets')).toBeInTheDocument(); + expect(screen.getByText('editor.android.bottomNav.export')).toBeInTheDocument(); + }); + + it('toggles panel on click', () => { + (useUIStore as any).mockImplementation((selector: any) => { + const state = { activeRightPanel: null, setRightPanel }; + return selector ? selector(state) : state; + }); + + render(); + const basicBtn = screen.getByText('editor.android.bottomNav.basic').closest('button')!; + fireEvent.click(basicBtn); + expect(setRightPanel).toHaveBeenCalledWith(expect.anything()); + }); +}); diff --git a/src/components/ui/AndroidBottomNav.tsx b/src/components/ui/AndroidBottomNav.tsx new file mode 100644 index 0000000000..96136411da --- /dev/null +++ b/src/components/ui/AndroidBottomNav.tsx @@ -0,0 +1,77 @@ +import { + Home, + SlidersHorizontal, + Palette, + UserCircle, + Crop, + Layers, + Paintbrush, + Info, + SwatchBook, + FileInput, +} from 'lucide-react'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; + +import { Panel } from './AppProperties'; +import { useUIStore } from '../../store/useUIStore'; + +interface AndroidBottomNavProps { + isAndroid: boolean; +} + +interface NavItem { + panel: Panel | null; + icon: typeof Home; + labelKey: string; +} + +const navItems: NavItem[] = [ + { panel: null, icon: Home, labelKey: 'editor.android.bottomNav.library' }, + { panel: Panel.Adjustments, icon: SlidersHorizontal, labelKey: 'editor.android.bottomNav.basic' }, + { panel: Panel.Color, icon: Palette, labelKey: 'editor.android.bottomNav.color' }, + { panel: Panel.Portrait, icon: UserCircle, labelKey: 'editor.android.bottomNav.portrait' }, + { panel: Panel.Crop, icon: Crop, labelKey: 'editor.android.bottomNav.crop' }, + { panel: Panel.Masks, icon: Layers, labelKey: 'editor.android.bottomNav.masks' }, + { panel: Panel.Ai, icon: Paintbrush, labelKey: 'editor.android.bottomNav.ai' }, + { panel: Panel.Metadata, icon: Info, labelKey: 'editor.android.bottomNav.metadata' }, + { panel: Panel.Presets, icon: SwatchBook, labelKey: 'editor.android.bottomNav.presets' }, + { panel: Panel.Export, icon: FileInput, labelKey: 'editor.android.bottomNav.export' }, +]; + +export default function AndroidBottomNav({ isAndroid }: AndroidBottomNavProps) { + const { t } = useTranslation(); + const activeRightPanel = useUIStore((s) => s.activeRightPanel); + const setRightPanel = useUIStore((s) => s.setRightPanel); + + if (!isAndroid) return null; + + return ( +
    +
    + {navItems.map(({ panel, icon: Icon, labelKey }) => { + const isActive = panel ? activeRightPanel === panel : activeRightPanel === null; + return ( + + ); + })} +
    +
    + ); +} diff --git a/src/components/ui/AndroidShareSheet.test.tsx b/src/components/ui/AndroidShareSheet.test.tsx new file mode 100644 index 0000000000..6111f9a16c --- /dev/null +++ b/src/components/ui/AndroidShareSheet.test.tsx @@ -0,0 +1,60 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import AndroidShareSheet from './AndroidShareSheet'; +import { invoke } from '@tauri-apps/api/core'; + +vi.mock('@tauri-apps/api/core'); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); +vi.mock('framer-motion', () => ({ + motion: { + div: ({ children, ...props }: any) =>
    {children}
    , + }, + AnimatePresence: ({ children }: any) => <>{children}, +})); + +describe('AndroidShareSheet', () => { + const onClose = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders nothing when not visible', () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + it('renders share targets when visible', () => { + render( + + ); + expect(screen.getByText('androidShare.wechat')).toBeInTheDocument(); + expect(screen.getByText('androidShare.qq')).toBeInTheDocument(); + expect(screen.getByText('androidShare.weibo')).toBeInTheDocument(); + expect(screen.getByText('androidShare.more')).toBeInTheDocument(); + }); + + it('calls invoke on share target click', async () => { + (invoke as any).mockResolvedValue(undefined); + render( + + ); + const wechatBtn = screen.getByText('androidShare.wechat').closest('button')!; + fireEvent.click(wechatBtn); + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith('share_image', expect.any(Object)); + }); + }); + + it('calls onClose when cancel clicked', () => { + render( + + ); + fireEvent.click(screen.getByText('androidShare.cancel')); + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/src/components/ui/AndroidShareSheet.tsx b/src/components/ui/AndroidShareSheet.tsx new file mode 100644 index 0000000000..8478193677 --- /dev/null +++ b/src/components/ui/AndroidShareSheet.tsx @@ -0,0 +1,143 @@ +import React, { useCallback, useState } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { toast } from 'react-toastify'; +import { Share2, X, MessageCircle, Send, MoreHorizontal } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { motion, AnimatePresence } from 'framer-motion'; +import Text from './Text'; +import { TextVariants } from '../../types/typography'; + +interface AndroidShareSheetProps { + filePath: string; + mimeType: string; + visible: boolean; + onClose: () => void; +} + +interface ShareTarget { + key: string; + icon: React.ReactNode; + labelKey: string; + packageName?: string; +} + +export default function AndroidShareSheet({ + filePath, + mimeType, + visible, + onClose, +}: AndroidShareSheetProps) { + const { t } = useTranslation(); + const [sharing, setSharing] = useState(false); + + const shareTargets: ShareTarget[] = [ + { + key: 'wechat', + icon: , + labelKey: 'androidShare.wechat', + packageName: 'com.tencent.mm', + }, + { + key: 'qq', + icon: , + labelKey: 'androidShare.qq', + packageName: 'com.tencent.mobileqq', + }, + { + key: 'weibo', + icon: , + labelKey: 'androidShare.weibo', + packageName: 'com.sina.weibo', + }, + { + key: 'more', + icon: , + labelKey: 'androidShare.more', + }, + ]; + + const handleShare = useCallback( + async (target?: ShareTarget) => { + if (sharing) return; + setSharing(true); + try { + await invoke('share_image', { + filePath, + mimeType, + title: target + ? t(`androidShare.${target.key}` as any) + : t('androidShare.systemShareTitle' as any), + targetPackage: target?.packageName ?? null, + }); + } catch (err) { + console.error('Share failed:', err); + toast.error(`Share failed: ${err}`); + } finally { + setSharing(false); + onClose(); + } + }, + [filePath, mimeType, sharing, t, onClose], + ); + + return ( + + {visible && ( + <> + + +
    + {t('androidShare.titleDefault' as any)} + +
    +
    +
    + {shareTargets.map((target) => ( + + ))} +
    +
    +
    + +
    +
    + + )} +
    + ); +} diff --git a/src/components/ui/AppProperties.tsx b/src/components/ui/AppProperties.tsx index 7cb10ba952..3f8d0fa212 100644 --- a/src/components/ui/AppProperties.tsx +++ b/src/components/ui/AppProperties.tsx @@ -32,6 +32,7 @@ export const OPTION_SEPARATOR = 'separator'; export enum Invokes { AddTagForPaths = 'add_tag_for_paths', ApplyAdjustments = 'apply_adjustments', + ApplySuperResolution = 'apply_super_resolution', ApplyAdjustmentsToPaths = 'apply_adjustments_to_paths', ApplyAutoAdjustmentsToPaths = 'apply_auto_adjustments_to_paths', ApplyDenoising = 'apply_denoising', @@ -51,27 +52,31 @@ export enum Invokes { EstimateExportSizes = 'estimate_export_sizes', ExportImages = 'export_images', FrontendLog = 'frontend_log', + GenerateAiDepthMask = 'generate_ai_depth_mask', GenerateAiForegroundMask = 'generate_ai_foreground_mask', GenerateAiSkyMask = 'generate_ai_sky_mask', GenerateAiSubjectMask = 'generate_ai_subject_mask', - GenerateFullscreenPreview = 'generate_fullscreen_preview', + GenerateManualCleanupPatch = 'generate_manual_cleanup_patch', + PrecomputeAiSubjectMask = 'precompute_ai_subject_mask', + GenerateAiRating = 'generate_ai_rating', + GenerateAiRatingsBatch = 'generate_ai_ratings_batch', GeneratePreviewForPath = 'generate_preview_for_path', GenerateMaskOverlay = 'generate_mask_overlay', GeneratePresetPreview = 'generate_preset_preview', - GenerateThumbnailsProgressive = 'generate_thumbnails_progressive', + GenerateThumbnailsProgressive = 'update_thumbnail_queue', GenerateUncroppedPreview = 'generate_uncropped_preview', GetFolderTree = 'get_folder_tree', GetFolderChildren = 'get_folder_children', + GetPinnedFolderTrees = 'get_pinned_folder_trees', GetLogFilePath = 'get_log_file_path', GetOrCreateInternalLibraryRoot = 'get_or_create_internal_library_root', - GetPinnedFolderTrees = 'get_pinned_folder_trees', GetSupportedFileTypes = 'get_supported_file_types', HandleExportPresetsToFile = 'handle_export_presets_to_file', HandleImportPresetsFromFile = 'handle_import_presets_from_file', HandleImportLegacyPresetsFromFile = 'handle_import_legacy_presets_from_file', ImportFiles = 'import_files', - InvokeGenerativeReplace = 'invoke_generative_replace', - InvokeGenerativeReplaseWithMaskDef = 'invoke_generative_replace_with_mask_def', + InvokeGenerativeReplaceWithMaskDef = 'invoke_generative_replace_with_mask_def', + LoadAndParseLut = 'load_and_parse_lut', ListImagesInDir = 'list_images_in_dir', ListImagesRecursive = 'list_images_recursive', LoadImage = 'load_image', @@ -104,6 +109,9 @@ export enum Invokes { GenerateAllCommunityPreviews = 'generate_all_community_previews', SaveCommunityPreset = 'save_community_preset', SaveTempFile = 'save_temp_file', + BatchDenoiseImages = 'batch_denoise_images', + PreviewNegativeConversion = 'preview_negative_conversion', + ConvertNegatives = 'convert_negatives', GetAlbums = 'get_albums', SaveAlbums = 'save_albums', AddToAlbum = 'add_to_album', @@ -119,10 +127,12 @@ export enum ExifOverlay { export enum Panel { Adjustments = 'adjustments', Ai = 'ai', + Color = 'color', Crop = 'crop', Export = 'export', Masks = 'masks', Metadata = 'metadata', + Portrait = 'portrait', Presets = 'presets', } @@ -175,6 +185,8 @@ export interface AppSettings { filterCriteria?: FilterCriteria; lastFolderState?: any; pinnedFolders?: any; + rootFolders?: string[]; + taggingShortcuts?: string[]; lastRootPath: string | null; libraryViewMode?: LibraryViewMode; sortCriteria?: SortCriteria; @@ -210,6 +222,7 @@ export interface AppSettings { exifOverlay?: ExifOverlay; language?: string; folderTreeSort?: FolderTreeSort; + fontFamily?: string; } export interface BrushSettings { @@ -280,13 +293,14 @@ export interface Preset { name: string; includeMasks?: boolean; includeCropTransform?: boolean; - presetType?: 'tool' | 'style'; + presetType?: 'tool' | 'style' | 'portrait' | 'color' | 'ai-color' | 'combined'; } export interface Progress { completed?: number; current?: number; total: number; + stage?: string; } export interface SelectedImage { @@ -311,6 +325,8 @@ export interface SortCriteria { export interface SupportedTypes { nonRaw: Array; raw: Array; + extensions?: string[]; + mimeTypes?: string[]; } export enum ThumbnailSize { diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 2a01126357..db609fb1e0 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -9,6 +9,7 @@ interface ButtonProps { size?: string; title?: string; variant?: string; + tabIndex?: number; } const Button = ({ children, onClick, disabled, className = '', ...props }: ButtonProps) => { diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx index c2a554c9ce..9797623b25 100644 --- a/src/components/ui/Dropdown.tsx +++ b/src/components/ui/Dropdown.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; +import { useState, useEffect, useRef, useMemo, useId } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { Check, ChevronDown } from 'lucide-react'; import Input from './Input'; @@ -37,6 +37,8 @@ const Dropdown = ({ const searchInputRef = useRef(null); const [searchTerm, setSearchTerm] = useState(''); const [showSearch, setShowSearch] = useState(false); + const [focusedIndex, setFocusedIndex] = useState(-1); + const listboxId = useId(); const selectedOption = options.find((opt) => opt.value === value) || null; useEffect(() => { @@ -55,6 +57,7 @@ const Dropdown = ({ if (!isOpen) { setSearchTerm(''); setShowSearch(false); + setFocusedIndex(-1); } }, [isOpen]); @@ -80,8 +83,31 @@ const Dropdown = ({ return; } + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (!isOpen) { + setIsOpen(true); + setFocusedIndex(0); + } else if (filteredOptions.length > 0) { + setFocusedIndex((prev) => (prev + 1) % filteredOptions.length); + } + return; + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + if (isOpen && filteredOptions.length > 0) { + setFocusedIndex((prev) => (prev <= 0 ? filteredOptions.length - 1 : prev - 1)); + } + return; + } + if (e.key === 'Enter') { - if (isOpen && filteredOptions.length === 1) { + if (isOpen && focusedIndex >= 0 && focusedIndex < filteredOptions.length) { + e.stopPropagation(); + e.preventDefault(); + handleSelect(filteredOptions[focusedIndex]); + } else if (isOpen && filteredOptions.length === 1) { e.stopPropagation(); e.preventDefault(); handleSelect(filteredOptions[0]); @@ -106,6 +132,7 @@ const Dropdown = ({