diff --git a/.gitignore b/.gitignore index c28959c..652574c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,28 @@ -DS_Store -metadata -project -log -config +# OS +.DS_Store + +# Data directories +metadata/ +project/ +log/ +config/ + +# Node +node_modules/ +dist/ + +# Rust/Tauri +src-tauri/target/ +src-tauri/gen/ + +# Python +__pycache__/ +*.pyc + +# IDE +.idea/ +.vscode/ +.metals/ +*.swp + +.venv/ \ No newline at end of file diff --git a/effect/effect_server.py b/effect/effect_server.py new file mode 100644 index 0000000..f1196dc --- /dev/null +++ b/effect/effect_server.py @@ -0,0 +1,180 @@ +""" +FastAPI server wrapping existing apply_effect.py functions. +This is the only new Python file - all 7 effects remain completely unchanged. +""" + +import argparse +import asyncio +import json +import os +import platform +import sys +import traceback +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +import uvicorn + +# Add parent directory to path so we can import apply_effect +sys.path.insert(0, str(Path(__file__).parent)) + +from apply_effect import process_effect, apply_effect, record_error + +app = FastAPI(title="QuantumBrush Effect Server") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +# Track running tasks +running_tasks: dict[str, str] = {} # stroke_id -> status + + +class RunEffectRequest(BaseModel): + stroke_id: str + project_id: str + effect_id: str + user_input: dict + stroke_input: dict + input_image_path: str + + +class RunEffectResponse(BaseModel): + success: bool + stroke_id: str + output_image_path: Optional[str] = None + error: Optional[str] = None + + +@app.get("/health") +async def health(): + return { + "status": "ok", + "python_version": platform.python_version(), + } + + +@app.get("/effects") +async def list_effects(): + """Scan effect/ dirs for *_requirements.json, return list.""" + effect_dir = Path(__file__).parent + effects = [] + + for entry in effect_dir.iterdir(): + if not entry.is_dir(): + continue + if entry.name.startswith("_") or entry.name.startswith("."): + continue + + req_path = entry / f"{entry.name}_requirements.json" + if req_path.exists(): + try: + with open(req_path, "r") as f: + req = json.load(f) + effects.append(req) + except Exception as e: + print(f"Failed to load {req_path}: {e}") + + effects.sort(key=lambda e: e.get("name", "")) + return effects + + +def run_effect_sync(req: RunEffectRequest) -> RunEffectResponse: + """Synchronously run an effect - called in background thread.""" + try: + # Build the instructions dict matching what process_effect() expects + instructions = { + "stroke_id": req.stroke_id, + "project_id": req.project_id, + "effect_id": req.effect_id, + "user_input": req.user_input, + "stroke_input": req.stroke_input, + } + + # process_effect reads the image from disk: + # project//stroke/_input.png + data = process_effect(instructions) + + # apply_effect runs the effect and saves output: + # project//stroke/_output.png + success = apply_effect(data) + + if success: + output_path = str(data["stroke_output_path"]) + return RunEffectResponse( + success=True, + stroke_id=req.stroke_id, + output_image_path=output_path, + ) + else: + return RunEffectResponse( + success=False, + stroke_id=req.stroke_id, + error="Effect returned False", + ) + + except Exception as e: + error_msg = traceback.format_exc() + record_error(e) + return RunEffectResponse( + success=False, + stroke_id=req.stroke_id, + error=str(e), + ) + + +async def run_effect_background(req: RunEffectRequest): + """Run effect in a thread pool to avoid blocking the event loop.""" + running_tasks[req.stroke_id] = "running" + try: + loop = asyncio.get_event_loop() + result = await loop.run_in_executor(None, run_effect_sync, req) + running_tasks[req.stroke_id] = "completed" if result.success else "failed" + return result + except Exception as e: + running_tasks[req.stroke_id] = "failed" + raise + + +@app.post("/run-effect") +async def run_effect_endpoint(req: RunEffectRequest): + """ + Execute an effect on a stroke. + Runs synchronously and returns the result (the Rust side handles async via tokio::spawn). + """ + running_tasks[req.stroke_id] = "running" + + try: + loop = asyncio.get_event_loop() + result = await loop.run_in_executor(None, run_effect_sync, req) + running_tasks[req.stroke_id] = "completed" if result.success else "failed" + return result + except Exception as e: + running_tasks[req.stroke_id] = "failed" + return RunEffectResponse( + success=False, + stroke_id=req.stroke_id, + error=str(e), + ) + + +@app.get("/status/{stroke_id}") +async def get_status(stroke_id: str): + status = running_tasks.get(stroke_id, "unknown") + return {"stroke_id": stroke_id, "status": status} + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="QuantumBrush Effect Server") + parser.add_argument("--port", type=int, default=8787, help="Server port") + parser.add_argument("--host", type=str, default="127.0.0.1", help="Server host") + args = parser.parse_args() + + print(f"Starting QuantumBrush Effect Server on {args.host}:{args.port}") + uvicorn.run(app, host=args.host, port=args.port, log_level="info") diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +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' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..f3d4dd9 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + QuantumBrush + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..235b253 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4178 @@ +{ + "name": "quantum-brush-app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quantum-brush-app", + "version": "0.0.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/cli": "^2.10.0", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", + "@tauri-apps/plugin-shell": "^2.3.5", + "lucide-react": "^0.575.0", + "react": "^19.2.0", + "react-colorful": "^5.6.1", + "react-dom": "^19.2.0", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.2.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.2.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/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.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "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/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz", + "integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz", + "integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-x64": "4.2.0", + "@tailwindcss/oxide-freebsd-x64": "4.2.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-x64-musl": "4.2.0", + "@tailwindcss/oxide-wasm32-wasi": "4.2.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz", + "integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz", + "integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz", + "integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz", + "integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz", + "integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.0.tgz", + "integrity": "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.0.tgz", + "integrity": "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.0.tgz", + "integrity": "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.0.tgz", + "integrity": "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.0.tgz", + "integrity": "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==", + "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.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz", + "integrity": "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.0.tgz", + "integrity": "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.0.tgz", + "integrity": "sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.0", + "@tailwindcss/oxide": "4.2.0", + "tailwindcss": "4.2.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz", + "integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==", + "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.10.0", + "@tauri-apps/cli-darwin-x64": "2.10.0", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.0", + "@tauri-apps/cli-linux-arm64-musl": "2.10.0", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0", + "@tauri-apps/cli-linux-x64-gnu": "2.10.0", + "@tauri-apps/cli-linux-x64-musl": "2.10.0", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.0", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.0", + "@tauri-apps/cli-win32-x64-msvc": "2.10.0" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz", + "integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz", + "integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz", + "integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz", + "integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz", + "integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz", + "integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz", + "integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz", + "integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz", + "integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz", + "integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz", + "integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz", + "integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz", + "integrity": "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==", + "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/@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, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "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": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "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": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "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": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "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/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.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.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.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.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.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.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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-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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "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": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "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==", + "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": { + "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/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/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "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": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@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.12.4", + "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.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "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/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": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/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/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/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" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "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": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/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": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/lucide-react": { + "version": "0.575.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "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/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.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-colorful": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", + "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz", + "integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.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.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "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", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..037d74f --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "quantum-brush-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/cli": "^2.10.0", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", + "@tauri-apps/plugin-shell": "^2.3.5", + "lucide-react": "^0.575.0", + "react": "^19.2.0", + "react-colorful": "^5.6.1", + "react-dom": "^19.2.0", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.2.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.2.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..c8933f0 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,6097 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.116", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.116", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.116", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "deranged" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.116", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.13.0", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.11.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.17.16", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.116", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.116", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quantum-brush" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "glob", + "image", + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-shell", + "tokio", + "uuid", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.116", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.11.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.2", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.116", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.116", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.17.16", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.116", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.116", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.116", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.54.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core 0.5.1", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..d5b6cbf --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "quantum-brush" +version = "0.1.0" +description = "QuantumBrush - Quantum Computing Art Application" +authors = ["QuantumBrush Team"] +edition = "2021" + +[lib] +name = "quantum_brush_lib" +crate-type = ["lib", "cdylib", "staticlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-dialog = "2" +tauri-plugin-fs = "2" +tauri-plugin-shell = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +image = "0.25" +base64 = "0.22" +reqwest = { version = "0.12", features = ["json", "blocking"] } +uuid = { version = "1", features = ["v4"] } +glob = "0.3" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..44380a0 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,19 @@ +{ + "identifier": "default", + "description": "Default capabilities for QuantumBrush", + "windows": ["main"], + "permissions": [ + "core:default", + "core:event:default", + "core:event:allow-emit", + "core:event:allow-listen", + "dialog:default", + "dialog:allow-open", + "dialog:allow-save", + "fs:default", + "fs:allow-read", + "fs:allow-write", + "shell:default", + "shell:allow-open" + ] +} diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..f8514a0 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/commands/effect.rs b/src-tauri/src/commands/effect.rs new file mode 100644 index 0000000..e0b2ebc --- /dev/null +++ b/src-tauri/src/commands/effect.rs @@ -0,0 +1,60 @@ +use crate::models::effect::Effect; +use crate::AppState; +use std::fs; +use std::path::Path; +use tauri::State; + +#[tauri::command] +pub async fn load_effects(state: State<'_, AppState>) -> Result, String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + let effect_dir = Path::new(&app_dir).join("effect"); + + if !effect_dir.exists() { + return Err("Effect directory not found".to_string()); + } + + let mut effects = Vec::new(); + + let entries = fs::read_dir(&effect_dir).map_err(|e| e.to_string())?; + + for entry in entries { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + let dir_name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Skip __pycache__ and hidden directories + if dir_name.starts_with('_') || dir_name.starts_with('.') { + continue; + } + + let req_path = path.join(format!("{}_requirements.json", dir_name)); + + if req_path.exists() { + match fs::read_to_string(&req_path) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(effect) => effects.push(effect), + Err(e) => { + eprintln!("Failed to parse {}: {}", req_path.display(), e); + } + }, + Err(e) => { + eprintln!("Failed to read {}: {}", req_path.display(), e); + } + } + } + } + + // Sort by name for consistent ordering + effects.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(effects) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..ed0e8a8 --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,4 @@ +pub mod effect; +pub mod project; +pub mod python; +pub mod stroke; diff --git a/src-tauri/src/commands/project.rs b/src-tauri/src/commands/project.rs new file mode 100644 index 0000000..bf562e2 --- /dev/null +++ b/src-tauri/src/commands/project.rs @@ -0,0 +1,64 @@ +use crate::models::project::ProjectMetadata; +use crate::services::file_service; +use crate::AppState; +use tauri::State; + +#[tauri::command] +pub async fn new_project( + state: State<'_, AppState>, + name: String, + image_path: String, +) -> Result { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + file_service::create_project(&app_dir, &name, &image_path).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn open_project( + state: State<'_, AppState>, + project_id: String, +) -> Result { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + file_service::load_project_metadata(&app_dir, &project_id).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn list_projects(state: State<'_, AppState>) -> Result, String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + file_service::list_projects(&app_dir).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn delete_project( + state: State<'_, AppState>, + project_id: String, +) -> Result<(), String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + file_service::delete_project(&app_dir, &project_id).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn export_image( + state: State<'_, AppState>, + project_id: String, + export_path: String, +) -> Result<(), String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + file_service::export_image(&app_dir, &project_id, &export_path).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn get_current_image( + state: State<'_, AppState>, + project_id: String, +) -> Result { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + file_service::get_current_image_base64(&app_dir, &project_id).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn set_app_dir(state: State<'_, AppState>, dir: String) -> Result<(), String> { + let mut app_dir = state.app_dir.lock().map_err(|e| e.to_string())?; + *app_dir = dir; + Ok(()) +} diff --git a/src-tauri/src/commands/python.rs b/src-tauri/src/commands/python.rs new file mode 100644 index 0000000..e02e51d --- /dev/null +++ b/src-tauri/src/commands/python.rs @@ -0,0 +1,53 @@ +use crate::services::python_service::PythonServer; +use crate::AppState; +use tauri::State; + +#[tauri::command] +pub async fn start_python_server(state: State<'_, AppState>) -> Result { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + + // Check if already running + { + let server_lock = state.python_server.lock().map_err(|e| e.to_string())?; + if server_lock.is_some() { + return Ok("Server already running".to_string()); + } + } + + // Start in a blocking task so we don't block the async runtime + let server = tokio::task::spawn_blocking(move || { + PythonServer::start(&app_dir) + }) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string())?; + + let mut server_lock = state.python_server.lock().map_err(|e| e.to_string())?; + *server_lock = Some(server); + + Ok("Server started".to_string()) +} + +#[tauri::command] +pub async fn stop_python_server(state: State<'_, AppState>) -> Result { + let mut server_lock = state.python_server.lock().map_err(|e| e.to_string())?; + + if let Some(server) = server_lock.take() { + server.stop().map_err(|e| e.to_string())?; + Ok("Server stopped".to_string()) + } else { + Ok("Server not running".to_string()) + } +} + +#[tauri::command] +pub async fn check_python_server() -> Result { + PythonServer::health_check() + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn detect_python() -> Result { + PythonServer::detect_python().map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/stroke.rs b/src-tauri/src/commands/stroke.rs new file mode 100644 index 0000000..7613439 --- /dev/null +++ b/src-tauri/src/commands/stroke.rs @@ -0,0 +1,445 @@ +use crate::models::stroke::{ + RunEffectRequest, StrokeInfo, StrokeInputForServer, + StrokeStatusResponse, +}; +use crate::services::{file_service, python_service}; +use crate::AppState; +use serde_json::Value; +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use tauri::{Emitter, State, Window}; + +#[tauri::command] +pub async fn create_stroke( + state: State<'_, AppState>, + project_id: String, + effect_id: String, + user_input: HashMap, + paths: Vec>>, + clicks: Vec>, +) -> Result { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + let stroke_id = format!("stroke_{}", chrono_timestamp()); + + let project_dir = Path::new(&app_dir).join("project").join(&project_id); + let stroke_dir = project_dir.join("stroke"); + fs::create_dir_all(&stroke_dir).map_err(|e| e.to_string())?; + + // Flatten paths into single array of points + let flat_path: Vec> = paths.into_iter().flatten().collect(); + + // Save input image (copy current.png) + let current_path = project_dir.join("current.png"); + let input_path = stroke_dir.join(format!("{}_input.png", stroke_id)); + fs::copy(¤t_path, &input_path).map_err(|e| e.to_string())?; + + // Create stroke instructions JSON + let instructions = serde_json::json!({ + "stroke_id": stroke_id, + "project_id": project_id, + "effect_id": effect_id, + "user_input": user_input, + "stroke_input": { + "path": flat_path, + "clicks": clicks, + "image_rgba": "array" + }, + "stroke_output": {}, + "processing_status": "pending", + "created": true, + "effect_received": false, + "effect_processed": false, + "effect_success": false + }); + + let instructions_path = stroke_dir.join(format!("{}_instructions.json", stroke_id)); + let json_str = serde_json::to_string_pretty(&instructions).map_err(|e| e.to_string())?; + fs::write(&instructions_path, json_str).map_err(|e| e.to_string())?; + + Ok(StrokeInfo { + stroke_id, + project_id, + effect_id, + user_input, + processing_status: "pending".to_string(), + has_output: false, + }) +} + +#[tauri::command] +pub async fn run_stroke( + state: State<'_, AppState>, + window: Window, + stroke_id: String, + project_id: String, +) -> Result<(), String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + + // Load stroke instructions + let stroke_dir = Path::new(&app_dir) + .join("project") + .join(&project_id) + .join("stroke"); + let instructions_path = stroke_dir.join(format!("{}_instructions.json", stroke_id)); + + let content = fs::read_to_string(&instructions_path).map_err(|e| e.to_string())?; + let mut instructions: serde_json::Value = + serde_json::from_str(&content).map_err(|e| e.to_string())?; + + // Update status to running + instructions["processing_status"] = serde_json::json!("running"); + let json_str = + serde_json::to_string_pretty(&instructions).map_err(|e| e.to_string())?; + fs::write(&instructions_path, &json_str).map_err(|e| e.to_string())?; + + let effect_id = instructions["effect_id"] + .as_str() + .unwrap_or("") + .to_string(); + let user_input: HashMap = + serde_json::from_value(instructions["user_input"].clone()).unwrap_or_default(); + + let stroke_input_val = &instructions["stroke_input"]; + let path: Vec> = + serde_json::from_value(stroke_input_val["path"].clone()).unwrap_or_default(); + let clicks: Vec> = + serde_json::from_value(stroke_input_val["clicks"].clone()).unwrap_or_default(); + + let input_image_path = stroke_dir + .join(format!("{}_input.png", stroke_id)) + .to_string_lossy() + .to_string(); + + let request = RunEffectRequest { + stroke_id: stroke_id.clone(), + project_id: project_id.clone(), + effect_id, + user_input, + stroke_input: StrokeInputForServer { path, clicks }, + input_image_path, + }; + + // Send to Python server + let sid = stroke_id.clone(); + let pid = project_id.clone(); + let ip = instructions_path.clone(); + + tokio::spawn(async move { + match python_service::PythonServer::run_effect(request).await { + Ok(response) => { + // Update instructions file + if let Ok(content) = fs::read_to_string(&ip) { + if let Ok(mut instr) = serde_json::from_str::(&content) { + if response.success { + instr["processing_status"] = serde_json::json!("completed"); + instr["effect_success"] = serde_json::json!(true); + instr["effect_processed"] = serde_json::json!(true); + instr["effect_received"] = serde_json::json!(true); + } else { + instr["processing_status"] = serde_json::json!("failed"); + instr["effect_success"] = serde_json::json!(false); + } + if let Ok(json_str) = serde_json::to_string_pretty(&instr) { + let _ = fs::write(&ip, json_str); + } + } + } + + let _ = window.emit( + "stroke-completed", + serde_json::json!({ + "stroke_id": sid, + "project_id": pid, + "success": response.success, + "error": response.error, + }), + ); + } + Err(e) => { + // Update instructions file with failure + if let Ok(content) = fs::read_to_string(&ip) { + if let Ok(mut instr) = serde_json::from_str::(&content) { + instr["processing_status"] = serde_json::json!("failed"); + instr["effect_success"] = serde_json::json!(false); + if let Ok(json_str) = serde_json::to_string_pretty(&instr) { + let _ = fs::write(&ip, json_str); + } + } + } + + let _ = window.emit( + "stroke-completed", + serde_json::json!({ + "stroke_id": sid, + "project_id": pid, + "success": false, + "error": e.to_string(), + }), + ); + } + } + }); + + Ok(()) +} + +#[tauri::command] +pub async fn apply_stroke( + state: State<'_, AppState>, + project_id: String, + stroke_id: String, +) -> Result { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + let project_dir = Path::new(&app_dir).join("project").join(&project_id); + let stroke_dir = project_dir.join("stroke"); + + let output_path = stroke_dir.join(format!("{}_output.png", stroke_id)); + let current_path = project_dir.join("current.png"); + + if !output_path.exists() { + return Err("Stroke output not found. Run the stroke first.".to_string()); + } + + // Alpha-blend output onto current image + let current_img = image::open(¤t_path) + .map_err(|e| e.to_string())? + .to_rgba8(); + let output_img = image::open(&output_path) + .map_err(|e| e.to_string())? + .to_rgba8(); + + let mut result = current_img.clone(); + + for (x, y, pixel) in output_img.enumerate_pixels() { + if pixel[3] > 0 { + // Non-transparent pixel from output + if x < result.width() && y < result.height() { + let bg = result.get_pixel(x, y); + let blended = alpha_blend(bg, pixel); + result.put_pixel(x, y, blended); + } + } + } + + result.save(¤t_path).map_err(|e| e.to_string())?; + + // Return base64 of new current image + file_service::get_current_image_base64(&app_dir, &project_id).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn delete_stroke( + state: State<'_, AppState>, + project_id: String, + stroke_id: String, +) -> Result<(), String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + let stroke_dir = Path::new(&app_dir) + .join("project") + .join(&project_id) + .join("stroke"); + + // Delete all files for this stroke + for suffix in &["_instructions.json", "_input.png", "_output.png"] { + let path = stroke_dir.join(format!("{}{}", stroke_id, suffix)); + if path.exists() { + let _ = fs::remove_file(path); + } + } + + Ok(()) +} + +#[tauri::command] +pub async fn cancel_stroke( + state: State<'_, AppState>, + project_id: String, + stroke_id: String, +) -> Result<(), String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + let stroke_dir = Path::new(&app_dir) + .join("project") + .join(&project_id) + .join("stroke"); + let instructions_path = stroke_dir.join(format!("{}_instructions.json", stroke_id)); + + if instructions_path.exists() { + let content = fs::read_to_string(&instructions_path).map_err(|e| e.to_string())?; + let mut instr: serde_json::Value = + serde_json::from_str(&content).map_err(|e| e.to_string())?; + instr["processing_status"] = serde_json::json!("canceled"); + let json_str = serde_json::to_string_pretty(&instr).map_err(|e| e.to_string())?; + fs::write(&instructions_path, json_str).map_err(|e| e.to_string())?; + } + + Ok(()) +} + +#[tauri::command] +pub async fn get_stroke_status( + state: State<'_, AppState>, + project_id: String, + stroke_id: String, +) -> Result { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + + // Try Python server first + if let Ok(status) = python_service::PythonServer::get_status(&stroke_id).await { + return Ok(status); + } + + // Fall back to reading instructions file + let instructions_path = Path::new(&app_dir) + .join("project") + .join(&project_id) + .join("stroke") + .join(format!("{}_instructions.json", stroke_id)); + + if instructions_path.exists() { + let content = fs::read_to_string(&instructions_path).map_err(|e| e.to_string())?; + let instr: serde_json::Value = + serde_json::from_str(&content).map_err(|e| e.to_string())?; + Ok(StrokeStatusResponse { + stroke_id, + status: instr["processing_status"] + .as_str() + .unwrap_or("unknown") + .to_string(), + }) + } else { + Ok(StrokeStatusResponse { + stroke_id, + status: "unknown".to_string(), + }) + } +} + +#[tauri::command] +pub async fn list_strokes( + state: State<'_, AppState>, + project_id: String, +) -> Result, String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + let stroke_dir = Path::new(&app_dir) + .join("project") + .join(&project_id) + .join("stroke"); + + if !stroke_dir.exists() { + return Ok(Vec::new()); + } + + let mut strokes = Vec::new(); + + let entries = fs::read_dir(&stroke_dir).map_err(|e| e.to_string())?; + + for entry in entries { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + + if path + .file_name() + .map(|n| n.to_string_lossy().ends_with("_instructions.json")) + .unwrap_or(false) + { + if let Ok(content) = fs::read_to_string(&path) { + if let Ok(instr) = serde_json::from_str::(&content) { + let sid = instr["stroke_id"].as_str().unwrap_or("").to_string(); + let output_path = + stroke_dir.join(format!("{}_output.png", sid)); + + strokes.push(StrokeInfo { + stroke_id: sid, + project_id: instr["project_id"] + .as_str() + .unwrap_or("") + .to_string(), + effect_id: instr["effect_id"] + .as_str() + .unwrap_or("") + .to_string(), + user_input: serde_json::from_value(instr["user_input"].clone()) + .unwrap_or_default(), + processing_status: instr["processing_status"] + .as_str() + .unwrap_or("pending") + .to_string(), + has_output: output_path.exists(), + }); + } + } + } + } + + // Sort by stroke_id (timestamp-based) + strokes.sort_by(|a, b| a.stroke_id.cmp(&b.stroke_id)); + + Ok(strokes) +} + +#[tauri::command] +pub async fn update_stroke_params( + state: State<'_, AppState>, + project_id: String, + stroke_id: String, + user_input: HashMap, +) -> Result<(), String> { + let app_dir = state.app_dir.lock().map_err(|e| e.to_string())?.clone(); + let instructions_path = Path::new(&app_dir) + .join("project") + .join(&project_id) + .join("stroke") + .join(format!("{}_instructions.json", stroke_id)); + + let content = fs::read_to_string(&instructions_path).map_err(|e| e.to_string())?; + let mut instr: serde_json::Value = + serde_json::from_str(&content).map_err(|e| e.to_string())?; + + instr["user_input"] = serde_json::to_value(&user_input).map_err(|e| e.to_string())?; + instr["processing_status"] = serde_json::json!("pending"); + instr["effect_received"] = serde_json::json!(false); + instr["effect_processed"] = serde_json::json!(false); + instr["effect_success"] = serde_json::json!(false); + + let json_str = serde_json::to_string_pretty(&instr).map_err(|e| e.to_string())?; + fs::write(&instructions_path, json_str).map_err(|e| e.to_string())?; + + // Delete old output + let output_path = instructions_path + .parent() + .unwrap() + .join(format!("{}_output.png", stroke_id)); + if output_path.exists() { + let _ = fs::remove_file(output_path); + } + + Ok(()) +} + +fn alpha_blend( + bg: &image::Rgba, + fg: &image::Rgba, +) -> image::Rgba { + let fg_a = fg[3] as f32 / 255.0; + let bg_a = bg[3] as f32 / 255.0; + let out_a = fg_a + bg_a * (1.0 - fg_a); + + if out_a == 0.0 { + return image::Rgba([0, 0, 0, 0]); + } + + let r = ((fg[0] as f32 * fg_a + bg[0] as f32 * bg_a * (1.0 - fg_a)) / out_a) as u8; + let g = ((fg[1] as f32 * fg_a + bg[1] as f32 * bg_a * (1.0 - fg_a)) / out_a) as u8; + let b = ((fg[2] as f32 * fg_a + bg[2] as f32 * bg_a * (1.0 - fg_a)) / out_a) as u8; + let a = (out_a * 255.0) as u8; + + image::Rgba([r, g, b, a]) +} + +fn chrono_timestamp() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64 +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..f157e75 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,84 @@ +mod commands; +mod models; +mod services; + +use commands::{effect, project, python, stroke}; +use services::python_service::PythonServer; +use std::sync::Mutex; +use tauri::Manager; + +pub struct AppState { + pub python_server: Mutex>, + pub app_dir: Mutex, +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_shell::init()) + .setup(|app| { + // Determine the app root directory. + // In dev mode, cwd is the project root (where package.json lives). + // We detect this by checking if effect/ directory exists. + let cwd = std::env::current_dir().unwrap_or_default(); + let app_dir = if cwd.join("effect").exists() { + cwd.to_string_lossy().to_string() + } else if cwd.parent().map(|p| p.join("effect").exists()).unwrap_or(false) { + // cwd is src-tauri/, go up one level + cwd.parent().unwrap().to_string_lossy().to_string() + } else { + cwd.to_string_lossy().to_string() + }; + eprintln!("QuantumBrush app_dir: {}", app_dir); + + app.manage(AppState { + python_server: Mutex::new(None), + app_dir: Mutex::new(app_dir), + }); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + // Project commands + project::new_project, + project::open_project, + project::list_projects, + project::delete_project, + project::export_image, + project::get_current_image, + project::set_app_dir, + // Effect commands + effect::load_effects, + // Python commands + python::start_python_server, + python::stop_python_server, + python::check_python_server, + python::detect_python, + // Stroke commands + stroke::create_stroke, + stroke::run_stroke, + stroke::apply_stroke, + stroke::delete_stroke, + stroke::cancel_stroke, + stroke::get_stroke_status, + stroke::list_strokes, + stroke::update_stroke_params, + ]) + .on_window_event(|window, event| { + if let tauri::WindowEvent::Destroyed = event { + // Stop Python server on window close + let app = window.app_handle(); + if let Some(state) = app.try_state::() { + if let Ok(mut server) = state.python_server.lock() { + if let Some(srv) = server.take() { + let _ = srv.stop(); + } + } + } + } + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..ed2a520 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + quantum_brush_lib::run() +} diff --git a/src-tauri/src/models/effect.rs b/src-tauri/src/models/effect.rs new file mode 100644 index 0000000..bfffe85 --- /dev/null +++ b/src-tauri/src/models/effect.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Effect { + pub name: String, + pub id: String, + #[serde(default)] + pub author: String, + #[serde(default)] + pub version: String, + #[serde(default)] + pub description: String, + #[serde(default)] + pub long_description: Option, + #[serde(default)] + pub dependencies: HashMap, + pub user_input: HashMap, + pub stroke_input: HashMap, + #[serde(default)] + pub flags: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ParamSpec { + #[serde(rename = "type")] + pub param_type: String, + #[serde(default)] + pub min: Option, + #[serde(default)] + pub max: Option, + pub default: serde_json::Value, +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs new file mode 100644 index 0000000..cea786d --- /dev/null +++ b/src-tauri/src/models/mod.rs @@ -0,0 +1,3 @@ +pub mod effect; +pub mod project; +pub mod stroke; diff --git a/src-tauri/src/models/project.rs b/src-tauri/src/models/project.rs new file mode 100644 index 0000000..9a41f9d --- /dev/null +++ b/src-tauri/src/models/project.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectMetadata { + pub project_name: String, + pub project_id: String, + pub created_time: u64, + pub modified_time: u64, + #[serde(default = "default_status")] + pub status: String, +} + +fn default_status() -> String { + "normal".to_string() +} diff --git a/src-tauri/src/models/stroke.rs b/src-tauri/src/models/stroke.rs new file mode 100644 index 0000000..4fb5574 --- /dev/null +++ b/src-tauri/src/models/stroke.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Stroke { + pub stroke_id: String, + pub project_id: String, + pub effect_id: String, + pub user_input: HashMap, + pub stroke_input: StrokeInput, + #[serde(default)] + pub processing_status: String, + #[serde(default)] + pub created: bool, + #[serde(default)] + pub effect_received: bool, + #[serde(default)] + pub effect_processed: bool, + #[serde(default)] + pub effect_success: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StrokeInput { + #[serde(default)] + pub path: Vec>, + #[serde(default)] + pub clicks: Vec>, + #[serde(skip_serializing_if = "Option::is_none")] + pub image_rgba: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RunEffectRequest { + pub stroke_id: String, + pub project_id: String, + pub effect_id: String, + pub user_input: HashMap, + pub stroke_input: StrokeInputForServer, + pub input_image_path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StrokeInputForServer { + pub path: Vec>, + #[serde(default)] + pub clicks: Vec>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RunEffectResponse { + pub success: bool, + pub stroke_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_image_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StrokeStatusResponse { + pub stroke_id: String, + pub status: String, +} + +/// Stroke info sent to the frontend (without large data) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StrokeInfo { + pub stroke_id: String, + pub project_id: String, + pub effect_id: String, + pub user_input: HashMap, + pub processing_status: String, + pub has_output: bool, +} diff --git a/src-tauri/src/services/file_service.rs b/src-tauri/src/services/file_service.rs new file mode 100644 index 0000000..7bbf26c --- /dev/null +++ b/src-tauri/src/services/file_service.rs @@ -0,0 +1,170 @@ +use crate::models::project::ProjectMetadata; +use std::fs; +use std::path::Path; + +pub fn create_project( + app_dir: &str, + name: &str, + image_path: &str, +) -> Result> { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_millis() as u64; + + let project_id = format!("project_{}", timestamp); + let project_dir = Path::new(app_dir).join("project").join(&project_id); + let stroke_dir = project_dir.join("stroke"); + let metadata_dir = Path::new(app_dir).join("metadata"); + + fs::create_dir_all(&stroke_dir)?; + fs::create_dir_all(&metadata_dir)?; + + // Copy source image as both original.png and current.png + let source = Path::new(image_path); + if !source.exists() { + return Err(format!("Image file not found: {}", image_path).into()); + } + + fs::copy(source, project_dir.join("original.png"))?; + fs::copy(source, project_dir.join("current.png"))?; + + let metadata = ProjectMetadata { + project_name: name.to_string(), + project_id: project_id.clone(), + created_time: timestamp, + modified_time: timestamp, + status: "normal".to_string(), + }; + + let metadata_path = metadata_dir.join(format!("{}.json", project_id)); + let json = serde_json::to_string_pretty(&metadata)?; + fs::write(metadata_path, json)?; + + Ok(metadata) +} + +pub fn load_project_metadata( + app_dir: &str, + project_id: &str, +) -> Result> { + let metadata_path = Path::new(app_dir) + .join("metadata") + .join(format!("{}.json", project_id)); + + let content = fs::read_to_string(metadata_path)?; + let metadata: ProjectMetadata = serde_json::from_str(&content)?; + Ok(metadata) +} + +pub fn list_projects( + app_dir: &str, +) -> Result, Box> { + let metadata_dir = Path::new(app_dir).join("metadata"); + + if !metadata_dir.exists() { + return Ok(Vec::new()); + } + + let mut projects = Vec::new(); + + for entry in fs::read_dir(metadata_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().map(|e| e == "json").unwrap_or(false) { + if let Ok(content) = fs::read_to_string(&path) { + if let Ok(mut metadata) = serde_json::from_str::(&content) { + // Validate project directory exists + let project_dir = + Path::new(app_dir).join("project").join(&metadata.project_id); + if !project_dir.exists() { + metadata.status = "missing_project_dir".to_string(); + } + projects.push(metadata); + } + } + } + } + + // Sort by modified_time descending (most recent first) + projects.sort_by(|a, b| b.modified_time.cmp(&a.modified_time)); + + Ok(projects) +} + +pub fn delete_project( + app_dir: &str, + project_id: &str, +) -> Result<(), Box> { + let project_dir = Path::new(app_dir).join("project").join(project_id); + let metadata_path = Path::new(app_dir) + .join("metadata") + .join(format!("{}.json", project_id)); + + if project_dir.exists() { + fs::remove_dir_all(project_dir)?; + } + if metadata_path.exists() { + fs::remove_file(metadata_path)?; + } + + Ok(()) +} + +pub fn export_image( + app_dir: &str, + project_id: &str, + export_path: &str, +) -> Result<(), Box> { + let current_path = Path::new(app_dir) + .join("project") + .join(project_id) + .join("current.png"); + + if !current_path.exists() { + return Err("Current image not found".into()); + } + + fs::copy(current_path, export_path)?; + Ok(()) +} + +pub fn get_current_image_base64( + app_dir: &str, + project_id: &str, +) -> Result> { + let current_path = Path::new(app_dir) + .join("project") + .join(project_id) + .join("current.png"); + + if !current_path.exists() { + return Err("Current image not found".into()); + } + + let bytes = fs::read(current_path)?; + use base64::Engine; + let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes); + Ok(format!("data:image/png;base64,{}", b64)) +} + +pub fn update_project_modified_time( + app_dir: &str, + project_id: &str, +) -> Result<(), Box> { + let metadata_path = Path::new(app_dir) + .join("metadata") + .join(format!("{}.json", project_id)); + + if metadata_path.exists() { + let content = fs::read_to_string(&metadata_path)?; + let mut metadata: ProjectMetadata = serde_json::from_str(&content)?; + metadata.modified_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_millis() as u64; + let json = serde_json::to_string_pretty(&metadata)?; + fs::write(metadata_path, json)?; + } + + Ok(()) +} diff --git a/src-tauri/src/services/image_service.rs b/src-tauri/src/services/image_service.rs new file mode 100644 index 0000000..373ce66 --- /dev/null +++ b/src-tauri/src/services/image_service.rs @@ -0,0 +1,61 @@ +use image::{GenericImageView, RgbaImage}; +use std::path::Path; + +/// Load an image from disk and return it as an RGBA image +pub fn load_image(path: &str) -> Result> { + let img = image::open(Path::new(path))?.to_rgba8(); + Ok(img) +} + +/// Alpha-blend a foreground image onto a background image +pub fn composite_images( + background: &RgbaImage, + foreground: &RgbaImage, +) -> Result> { + let mut result = background.clone(); + + let (fg_w, fg_h) = foreground.dimensions(); + let (bg_w, bg_h) = background.dimensions(); + + let width = fg_w.min(bg_w); + let height = fg_h.min(bg_h); + + for y in 0..height { + for x in 0..width { + let fg_pixel = foreground.get_pixel(x, y); + if fg_pixel[3] > 0 { + let bg_pixel = result.get_pixel(x, y); + let blended = alpha_blend(bg_pixel, fg_pixel); + result.put_pixel(x, y, blended); + } + } + } + + Ok(result) +} + +fn alpha_blend( + bg: &image::Rgba, + fg: &image::Rgba, +) -> image::Rgba { + let fg_a = fg[3] as f32 / 255.0; + let bg_a = bg[3] as f32 / 255.0; + let out_a = fg_a + bg_a * (1.0 - fg_a); + + if out_a == 0.0 { + return image::Rgba([0, 0, 0, 0]); + } + + let r = ((fg[0] as f32 * fg_a + bg[0] as f32 * bg_a * (1.0 - fg_a)) / out_a) as u8; + let g = ((fg[1] as f32 * fg_a + bg[1] as f32 * bg_a * (1.0 - fg_a)) / out_a) as u8; + let b = ((fg[2] as f32 * fg_a + bg[2] as f32 * bg_a * (1.0 - fg_a)) / out_a) as u8; + let a = (out_a * 255.0) as u8; + + image::Rgba([r, g, b, a]) +} + +/// Get image dimensions without loading the full image +pub fn get_image_dimensions(path: &str) -> Result<(u32, u32), Box> { + let img = image::open(Path::new(path))?; + Ok(img.dimensions()) +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs new file mode 100644 index 0000000..221560f --- /dev/null +++ b/src-tauri/src/services/mod.rs @@ -0,0 +1,3 @@ +pub mod file_service; +pub mod image_service; +pub mod python_service; diff --git a/src-tauri/src/services/python_service.rs b/src-tauri/src/services/python_service.rs new file mode 100644 index 0000000..10a284c --- /dev/null +++ b/src-tauri/src/services/python_service.rs @@ -0,0 +1,162 @@ +use crate::models::stroke::{RunEffectRequest, RunEffectResponse, StrokeStatusResponse}; +use std::net::TcpStream; +use std::process::{Child, Command}; + +const SERVER_PORT: u16 = 8787; +const SERVER_URL: &str = "http://localhost:8787"; + +pub struct PythonServer { + process: Child, +} + +impl PythonServer { + pub fn detect_python() -> Result> { + // Check config file for custom path first + let config_path = std::path::Path::new("config/python_path.txt"); + if config_path.exists() { + if let Ok(path) = std::fs::read_to_string(config_path) { + let path = path.trim().to_string(); + if !path.is_empty() { + return Ok(path); + } + } + } + + // Try common Python 3 executables + let candidates = vec![ + "python3", + "python", + "python3.12", + "python3.11", + "python3.10", + ]; + + for candidate in candidates { + if let Ok(output) = Command::new(candidate).arg("--version").output() { + if output.status.success() { + let version = String::from_utf8_lossy(&output.stdout).to_string(); + let version = version.trim().to_string(); + + // Check it's Python 3.10+ + if let Some(ver_str) = version.strip_prefix("Python ") { + let parts: Vec<&str> = ver_str.split('.').collect(); + if parts.len() >= 2 { + if let (Ok(major), Ok(minor)) = + (parts[0].parse::(), parts[1].parse::()) + { + if major == 3 && minor >= 10 { + return Ok(candidate.to_string()); + } + } + } + } + } + } + } + + Err("Python 3.10+ not found. Please install Python 3.10 or newer.".into()) + } + + fn detect_python_for_app(app_dir: &str) -> Result> { + // Prefer .venv inside the app directory + let venv_python = std::path::Path::new(app_dir).join(".venv/bin/python3"); + if venv_python.exists() { + return Ok(venv_python.to_string_lossy().to_string()); + } + let venv_python = std::path::Path::new(app_dir).join(".venv/bin/python"); + if venv_python.exists() { + return Ok(venv_python.to_string_lossy().to_string()); + } + // Fall back to system detection + Self::detect_python() + } + + pub fn start(app_dir: &str) -> Result> { + let python = Self::detect_python_for_app(app_dir)?; + let server_script = std::path::Path::new(app_dir).join("effect/effect_server.py"); + + if !server_script.exists() { + return Err(format!( + "Server script not found at {}", + server_script.display() + ) + .into()); + } + + let process = Command::new(&python) + .arg(&server_script) + .arg("--port") + .arg(SERVER_PORT.to_string()) + .current_dir(app_dir) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn()?; + + // Wait for server to become available using a plain TCP connect + // (avoids reqwest::blocking which can't run inside tokio) + let start = std::time::Instant::now(); + let timeout = std::time::Duration::from_secs(15); + let addr = format!("127.0.0.1:{}", SERVER_PORT); + + while start.elapsed() < timeout { + std::thread::sleep(std::time::Duration::from_millis(500)); + + if TcpStream::connect_timeout( + &addr.parse().unwrap(), + std::time::Duration::from_secs(1), + ) + .is_ok() + { + eprintln!("Python server is accepting connections on port {}", SERVER_PORT); + return Ok(PythonServer { process }); + } + } + + Err("Python server failed to start within 15 seconds".into()) + } + + pub fn stop(mut self) -> Result<(), Box> { + let _ = self.process.kill(); + let _ = self.process.wait(); + Ok(()) + } + + pub async fn health_check() -> Result> { + let client = reqwest::Client::new(); + let response = client + .get(format!("{}/health", SERVER_URL)) + .timeout(std::time::Duration::from_secs(5)) + .send() + .await?; + Ok(response.status().is_success()) + } + + pub async fn run_effect( + request: RunEffectRequest, + ) -> Result> { + let client = reqwest::Client::new(); + let response = client + .post(format!("{}/run-effect", SERVER_URL)) + .json(&request) + .timeout(std::time::Duration::from_secs(600)) // 10 min timeout for long effects + .send() + .await?; + + let result: RunEffectResponse = response.json().await?; + Ok(result) + } + + pub async fn get_status( + stroke_id: &str, + ) -> Result> { + let client = reqwest::Client::new(); + let response = client + .get(format!("{}/status/{}", SERVER_URL, stroke_id)) + .timeout(std::time::Duration::from_secs(5)) + .send() + .await?; + + let result: StrokeStatusResponse = response.json().await?; + Ok(result) + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..5c6887c --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json", + "productName": "QuantumBrush", + "version": "0.1.0", + "identifier": "com.quantumbrush.app", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:1420", + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build" + }, + "app": { + "windows": [ + { + "title": "QuantumBrush", + "width": 1400, + "height": 900, + "resizable": true, + "minWidth": 800, + "minHeight": 600 + } + ], + "security": { + "csp": null + } + }, + "plugins": { + "shell": { + "open": true + } + } +} diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index da18d44..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -/CanvasManager.class -/DebugLogger$1.class -/DebugLogger$2.class -/DebugLogger.class -/Effect.class -/EffectManager.class -/FileManager.class -/PVector.class -/Path.class -/QuantumBrush$ProjectItem.class -/QuantumBrush$ProjectListCellRenderer.class -/QuantumBrush$ProjectState.class -/QuantumBrush.class -/StrokeManager$1.class -/StrokeManager$ProcessingCallback.class -/StrokeManager$Stroke.class -/StrokeManager.class -/UIManager$1.class -/UIManager$StrokeItem.class -/UIManager$StrokeListCellRenderer.class -/UIManager.class -/QuantumBrush$1.class -/QuantumBrush$2.class -/QuantumBrush$3.class -/QuantumBrush$4.class -/QuantumBrush$5.class -/UIManager$2.class -/.DS_Store -/.project diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..c8f68c2 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { AppShell } from "./components/layout/AppShell"; +import { useStore } from "./store"; +import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; +import * as api from "./lib/tauriApi"; + +function App() { + const [serverStatus, setServerStatus] = useState< + "starting" | "running" | "error" + >("starting"); + const [serverError, setServerError] = useState(null); + + const { setEffects, updateStroke } = useStore(); + + // Register keyboard shortcuts (Ctrl+Z, Ctrl+Shift+Z, Ctrl+/-/0) + useKeyboardShortcuts(); + + // Initialize app + useEffect(() => { + async function init() { + try { + // Start Python server + try { + await api.startPythonServer(); + setServerStatus("running"); + } catch (e) { + console.warn("Python server start failed:", e); + setServerStatus("error"); + setServerError(String(e)); + } + + // Load effects + try { + const effects = await api.loadEffects(); + setEffects(effects); + } catch (e) { + console.warn("Failed to load effects:", e); + } + } catch (e) { + console.error("Init error:", e); + } + } + + init(); + + // Cleanup on unmount + return () => { + api.stopPythonServer().catch(console.warn); + }; + }, []); + + // Listen for stroke completion events + useEffect(() => { + const unlisten = listen<{ + stroke_id: string; + project_id: string; + success: boolean; + error?: string; + }>("stroke-completed", (event) => { + const { stroke_id, success } = event.payload; + updateStroke(stroke_id, { + processing_status: success ? "completed" : "failed", + has_output: success, + }); + }); + + return () => { + unlisten.then((fn) => fn()); + }; + }, [updateStroke]); + + return ( +
+ +
+ ); +} + +export default App; diff --git a/src/CanvasManager.java b/src/CanvasManager.java deleted file mode 100644 index 7d94dae..0000000 --- a/src/CanvasManager.java +++ /dev/null @@ -1,277 +0,0 @@ -import processing.core.PApplet; -import processing.core.PImage; - -import java.util.ArrayList; -import java.util.concurrent.CopyOnWriteArrayList; - -public class CanvasManager { - private PApplet app; - private CopyOnWriteArrayList paths; - private Path currentPath; - private ArrayList> undoHistory; - private int historyIndex; - - public CanvasManager(PApplet app) { - this.app = app; - this.paths = new CopyOnWriteArrayList<>(); - this.currentPath = null; - this.undoHistory = new ArrayList<>(); - this.historyIndex = -1; - } - - public void startNewPath() { - currentPath = new Path(); - } - - public void setClickPoint(float x, float y) { - if (currentPath != null) { - currentPath.setClickPoint(x, y); - } - } - - /* - public void addPointToCurrentPath(float x, float y) { - if (currentPath != null) { - currentPath.addPoint(x, y); - } - } - */ - - // WITH this new method: - public void addPointToCurrentPath(float x, float y, boolean shiftPressed) { - if (currentPath != null) { - // Pass the boolean flag down to the Path object - currentPath.addPoint(x, y, shiftPressed); - } - } - - public void finishCurrentPath() { - if (currentPath != null && currentPath.hasPoints()) { - paths.add(currentPath); - currentPath = null; - - notifyPathChanged(); - } - } - - public void draw(float zoomLevel, float panX, float panY) { - try { - app.pushMatrix(); - app.translate(panX, panY); - app.scale(zoomLevel); - - for (Path path : paths) { - if (path != null) { - path.draw(app, zoomLevel); - } - } - - if (currentPath != null) { - currentPath.draw(app, zoomLevel); - } - - app.popMatrix(); - } catch (Exception e) { - System.err.println("Error in CanvasManager.draw(): " + e.getMessage()); - } - } - - public void clearPaths() { - paths.clear(); - currentPath = null; - - notifyPathChanged(); - } - - public ArrayList getPaths() { - return new ArrayList<>(paths); - } - - public boolean hasPath() { - return !paths.isEmpty(); - } - - private void notifyPathChanged() { - if (app instanceof QuantumBrush) { - QuantumBrush quantumApp = (QuantumBrush) app; - if (quantumApp.getUIManager() != null) { - quantumApp.getUIManager().enableCreateButton(); - } - } - } - - // ... existing code ... - - public void setPaths(ArrayList newPaths) { - paths.clear(); - if (newPaths != null) { - for (Path path : newPaths) { - if (path != null) { - paths.add(path.copy()); - } - } - } - currentPath = null; - - notifyPathChanged(); - } -} - -class Path { - private ArrayList points; - private PVector clickPoint; - - public Path() { - this.points = new ArrayList<>(); - } - - public Path(float x, float y) { - this(); - // addPoint(x, y); - addPoint(x, y, false); // Add 'false' - } - - /* - public void addPoint(float x, float y) { - if (clickPoint != null && points.size() > 0) { - // Calculate delta from click point - float deltaX = x - clickPoint.x; - float deltaY = y - clickPoint.y; - - // Compare magnitudes to decide horizontal or vertical - if (Math.abs(deltaX) > Math.abs(deltaY)) { - // Horizontal line - lock Y to click point Y - points.add(new PVector(x, clickPoint.y)); - } else { - // Vertical line - lock X to click point X - points.add(new PVector(clickPoint.x, y)); - } - } else { - // Normal drawing without constraint - points.add(new PVector(x, y)); - } - } - */ - - // WITH this new, corrected method: - public void addPoint(float x, float y, boolean shiftPressed) { - // The clickPoint is the starting "anchor" for the constraint - // We also check points.size() > 0 because the *very first* point - // (added in mousePressed) should just be at (x, y) - if (shiftPressed && clickPoint != null && points.size() > 0) { - - // Calculate delta from the *original* click point - float deltaX = x - clickPoint.x; - float deltaY = y - clickPoint.y; - - // Compare absolute magnitudes to decide horizontal or vertical - if (Math.abs(deltaX) > Math.abs(deltaY)) { - // Horizontal line: Lock Y to the click point's Y - points.clear(); - points.add(clickPoint); - points.add(new PVector(x, clickPoint.y)); - } else { - // Vertical line: Lock X to the click point's X - points.clear(); - points.add(clickPoint); - points.add(new PVector(clickPoint.x, y)); - } - } else { - // Normal drawing (if Shift is not pressed OR this is the very first point) - points.add(new PVector(x, y)); - } - } - - public void setClickPoint(float x, float y) { - clickPoint = new PVector(x, y); - } - - public PVector getClickPoint() { - return clickPoint; - } - - public ArrayList getPoints() { - return points; - } - - public boolean hasPoints() { - return !points.isEmpty(); - } - - public void draw(PApplet app, float zoomLevel) { - if (clickPoint != null) { - app.pushStyle(); - - app.fill(0, 0, 0); - app.noStroke(); - float borderSize = 12.0f / zoomLevel; - app.ellipse(clickPoint.x, clickPoint.y, borderSize, borderSize); - - app.fill(255, 255, 0); - float dotSize = 8.0f / zoomLevel; - app.ellipse(clickPoint.x, clickPoint.y, dotSize, dotSize); - - app.popStyle(); - } - - if (points.size() >= 2) { - app.pushStyle(); - - try { - app.stroke(255, 255, 0); - app.strokeWeight(6.0f / zoomLevel); - app.strokeCap(app.ROUND); - app.strokeJoin(app.ROUND); - - for (int i = 0; i < points.size() - 1; i++) { - PVector p1 = points.get(i); - PVector p2 = points.get(i + 1); - if (p1 != null && p2 != null) { - app.line(p1.x, p1.y, p2.x, p2.y); - } - } - - app.stroke(255, 0, 0); - app.strokeWeight(3.0f / zoomLevel); - - for (int i = 0; i < points.size() - 1; i++) { - PVector p1 = points.get(i); - PVector p2 = points.get(i + 1); - if (p1 != null && p2 != null) { - app.line(p1.x, p1.y, p2.x, p2.y); - } - } - - } catch (Exception e) { - System.err.println("Concurrent modification in Path.draw(), skipping frame"); - } - - app.popStyle(); - } - } - - public Path copy() { - Path newPath = new Path(); - synchronized(points) { - for (PVector p : points) { - if (p != null) { - // newPath.addPoint(p.x, p.y); - newPath.addPoint(p.x, p.y, false); // Add 'false' - } - } - } - if (clickPoint != null) { - newPath.setClickPoint(clickPoint.x, clickPoint.y); - } - return newPath; - } -} - -class PVector { - public float x, y; - - public PVector(float x, float y) { - this.x = x; - this.y = y; - } -} \ No newline at end of file diff --git a/src/DebugLogger.java b/src/DebugLogger.java deleted file mode 100644 index 6185c18..0000000 --- a/src/DebugLogger.java +++ /dev/null @@ -1,189 +0,0 @@ -import java.util.*; -import java.io.*; - -public class DebugLogger { - private static final List logs = Collections.synchronizedList(new ArrayList<>()); - private static final int MAX_LOGS = 1000; // Limit to prevent memory issues - private static PrintStream originalOut; - private static PrintStream originalErr; - private static boolean isRedirecting = false; - - static { - // Store original streams - originalOut = System.out; - originalErr = System.err; - - // Start redirecting immediately - startRedirection(); - } - - /** - * Start redirecting System.out and System.err to the debug logger - */ - public static void startRedirection() { - if (isRedirecting) return; - - isRedirecting = true; - - // Create custom PrintStream that captures output - PrintStream customOut = new PrintStream(new OutputStream() { - private StringBuilder buffer = new StringBuilder(); - - @Override - public void write(int b) throws IOException { - char c = (char) b; - if (c == '\n') { - // Line is complete, log it - String line = buffer.toString(); - if (!line.trim().isEmpty()) { - addLogEntry("[OUT] " + line); - } - buffer.setLength(0); - } else if (c != '\r') { // Ignore carriage returns - buffer.append(c); - } - - // Also write to original output - originalOut.write(b); - } - }); - - PrintStream customErr = new PrintStream(new OutputStream() { - private StringBuilder buffer = new StringBuilder(); - - @Override - public void write(int b) throws IOException { - char c = (char) b; - if (c == '\n') { - // Line is complete, log it - String line = buffer.toString(); - if (!line.trim().isEmpty()) { - addLogEntry("[ERR] " + line); - } - buffer.setLength(0); - } else if (c != '\r') { // Ignore carriage returns - buffer.append(c); - } - - // Also write to original error stream - originalErr.write(b); - } - }); - - // Replace System.out and System.err - System.setOut(customOut); - System.setErr(customErr); - - log("=== DEBUG LOGGER STARTED ==="); - log("All System.out.println() and System.err.println() calls will now appear in the debug viewer"); - } - - /** - * Stop redirecting and restore original streams - */ - public static void stopRedirection() { - if (!isRedirecting) return; - - log("=== STOPPING DEBUG LOGGER ==="); - - System.setOut(originalOut); - System.setErr(originalErr); - isRedirecting = false; - } - - /** - * Add a log entry with timestamp - */ - public static void log(String message) { - addLogEntry("[LOG] " + message); - } - - /** - * Internal method to add log entries - */ - private static void addLogEntry(String message) { - String timestamp = java.time.LocalTime.now().format( - java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss.SSS") - ); - String logEntry = "[" + timestamp + "] " + message; - - synchronized (logs) { - logs.add(logEntry); - - // Remove old entries if we exceed the limit - while (logs.size() > MAX_LOGS) { - logs.remove(0); - } - } - } - - /** - * Get all logs as a single string - */ - public static String getAllLogs() { - synchronized (logs) { - if (logs.isEmpty()) { - return "No logs available yet.\n\nLogs will appear here when:\n" + - "- You create effects\n" + - "- You process strokes\n" + - "- The application performs various operations\n" + - "- Any System.out.println() or System.err.println() calls are made\n\n" + - "Try creating an effect to see parameter processing details!"; - } - - StringBuilder sb = new StringBuilder(); - for (String log : logs) { - sb.append(log).append("\n"); - } - return sb.toString(); - } - } - - /** - * Get the number of log entries - */ - public static int getLogCount() { - synchronized (logs) { - return logs.size(); - } - } - - /** - * Clear all logs - */ - public static void clearLogs() { - synchronized (logs) { - logs.clear(); - } - log("Debug log cleared"); - } - - /** - * Add test logs for debugging the logger itself - */ - public static void addTestLog() { - log("=== TEST LOG ENTRY ==="); - log("This is a test log entry added at: " + new java.util.Date()); - System.out.println("This is a test System.out.println() call"); - System.err.println("This is a test System.err.println() call"); - log("Test completed - you should see multiple entries above"); - } - - /** - * Get original output streams (for cases where you need direct access) - */ - public static PrintStream getOriginalOut() { - return originalOut; - } - - public static PrintStream getOriginalErr() { - return originalErr; - } - - /** - * Check if redirection is active - */ - public static boolean isRedirecting() { - return isRedirecting; - } -} diff --git a/src/EffectManager.java b/src/EffectManager.java deleted file mode 100644 index 886286c..0000000 --- a/src/EffectManager.java +++ /dev/null @@ -1,228 +0,0 @@ -import processing.core.*; -import processing.data.*; -import java.io.*; -import java.util.*; - -public class EffectManager { - private QuantumBrush app; - private HashMap effects; - - public EffectManager(QuantumBrush app) { - this.app = app; - this.effects = new HashMap<>(); - loadEffects(); - } - - public void loadEffects() { - // Clear existing effects to prevent contamination - effects.clear(); - - File effectsDir = new File("effect"); - if (!effectsDir.exists() || !effectsDir.isDirectory()) { - System.err.println("Error: effect directory not found or not a directory"); - return; - } - - File[] effectFolders = effectsDir.listFiles(File::isDirectory); - - if (effectFolders == null || effectFolders.length == 0) { - System.out.println("No effect folders found in effect directory"); - return; - } - - for (File folder : effectFolders) { - String folderName = folder.getName(); - - // Look for requirements JSON files - File[] jsonFiles = folder.listFiles( - (dir, name) -> name.toLowerCase().endsWith("_requirements.json") - ); - - if (jsonFiles != null && jsonFiles.length > 0) { - for (File jsonFile : jsonFiles) { - try { - // ✅ FIXED: Load JSON fresh each time to prevent contamination - JSONObject requirements = app.loadJSONObject(jsonFile.getAbsolutePath()); - - if (requirements == null) { - System.err.println("Failed to load JSON from: " + jsonFile.getAbsolutePath()); - continue; - } - - // Get the effect ID from the JSON file - String effectId = requirements.getString("id", folderName); - - // ✅ FIXED: Create a completely new Effect object with clean state - Effect effect = new Effect(effectId, folderName, requirements); - - // Store the effect using its ID as the key - effects.put(effectId, effect); - - System.out.println("Loaded effect: " + effectId + " from folder: " + folderName); - - } catch (Exception e) { - System.err.println( - "Error loading effect from " + jsonFile.getName() + - ": " + e.getMessage() - ); - e.printStackTrace(); - } - } - } else { - System.out.println("No requirements JSON files found in folder: " + folderName); - } - } - } - - public Effect getEffect(String id) { - return effects.get(id); - } - - public Set getEffectNames() { - return effects.keySet(); - } - - public ArrayList getEffects() { - return new ArrayList<>(effects.values()); - } -} - -class Effect { - private String id; - private String folderName; - private JSONObject requirements; - - public Effect(String id, String folderName, JSONObject requirements) { - this.id = id; - this.folderName = folderName; - // ✅ FIXED: Make a deep copy of the requirements to prevent contamination - this.requirements = copyJSONObject(requirements); - } - - // ✅ FIXED: Deep copy JSON to prevent cross-contamination between effects - private JSONObject copyJSONObject(JSONObject original) { - if (original == null) return new JSONObject(); - - try { - // Convert to string and back to create a deep copy - String jsonString = original.toString(); - return JSONObject.parse(jsonString); - } catch (Exception e) { - System.err.println("Error copying JSON object: " + e.getMessage()); - return new JSONObject(); - } - } - - public String getId() { - return id; - } - - public String getFolderName() { - return folderName; - } - - public String getName() { - return requirements.getString("name", id); - } - - public JSONObject getRequirements() { - return requirements; - } - - public JSONObject getUserInputRequirements() { - if (requirements.hasKey("user_input")) { - return requirements.getJSONObject("user_input"); - } - return new JSONObject(); - } - - /** - * Gets the parameter specification for a specific parameter. - */ - public JSONObject getParamSpec(String paramName) { - JSONObject userInput = getUserInputRequirements(); - if (userInput.hasKey(paramName)) { - Object paramValue = userInput.get(paramName); - if (paramValue instanceof JSONObject) { - return (JSONObject) paramValue; - } - } - return null; - } - - /** - * Gets the parameter type for a specific parameter. - */ - public String getParamType(String paramName) { - JSONObject paramSpec = getParamSpec(paramName); - if (paramSpec != null && paramSpec.hasKey("type")) { - return paramSpec.getString("type"); - } - return null; - } - - /** - * Gets the minimum value for a numeric parameter. - */ - public float getParamMin(String paramName, float defaultMin) { - JSONObject paramSpec = getParamSpec(paramName); - if (paramSpec != null && paramSpec.hasKey("min")) { - return paramSpec.getFloat("min"); - } - return defaultMin; - } - - /** - * Gets the maximum value for a numeric parameter. - */ - public float getParamMax(String paramName, float defaultMax) { - JSONObject paramSpec = getParamSpec(paramName); - if (paramSpec != null && paramSpec.hasKey("max")) { - return paramSpec.getFloat("max"); - } - return defaultMax; - } - - /** - * Gets the default value for a parameter. - */ - public Object getParamDefault(String paramName) { - JSONObject paramSpec = getParamSpec(paramName); - - if (paramSpec != null && paramSpec.hasKey("default")) { - String type = getParamType(paramName); - - try { - if ("int".equals(type)) { - return paramSpec.getInt("default"); - } else if ("float".equals(type)) { - return paramSpec.getFloat("default"); - } else if ("bool".equals(type) || "boolean".equals(type)) { - return paramSpec.getBoolean("default"); - } else { - return paramSpec.get("default"); - } - } catch (Exception e) { - System.err.println("Error reading default value for " + paramName + ": " + e.getMessage()); - return paramSpec.get("default"); - } - } - - return null; - } - - public Map getDefaultParameters() { - Map params = new HashMap<>(); - JSONObject userInput = getUserInputRequirements(); - - for (Object key : userInput.keys()) { - String paramName = (String) key; - Object defaultValue = getParamDefault(paramName); - if (defaultValue != null) { - params.put(paramName, defaultValue); - } - } - - return params; - } -} diff --git a/src/FileManager.java b/src/FileManager.java deleted file mode 100644 index 033874f..0000000 --- a/src/FileManager.java +++ /dev/null @@ -1,333 +0,0 @@ -import processing.core.*; -import processing.data.*; -import java.io.*; -import java.util.*; -import java.nio.file.*; -import javax.swing.*; - -public class FileManager { - private QuantumBrush app; - - public FileManager(QuantumBrush app) { - this.app = app; - - // Create project directory if it doesn't exist - ensureDirectoryExists("project"); - } - - public void saveProject(String projectId, PImage originalImage) { - String projectPath = "project/" + projectId; - ensureDirectoryExists(projectPath); - - // Save original image - if (originalImage != null) { - originalImage.save(projectPath + "/original.png"); - } - - // IMPORTANT: Also save the current state (with all effects applied) - PImage currentImage = app.getCurrentImage(); - if (currentImage != null) { - currentImage.save(projectPath + "/current.png"); - System.out.println("Saved current state to: " + projectPath + "/current.png"); - } - } - - public boolean ensureDirectoryExists(String path) { - File directory = new File(path); - if (!directory.exists()) { - return directory.mkdirs(); - } - return true; - } - - public void saveStroke( - String projectId, - String strokeId, - JSONObject instructions, - PImage inputImage - ) { - String strokeDirPath = "project/" + projectId + "/stroke"; - ensureDirectoryExists(strokeDirPath); - - // Save instructions - String instructionsPath = strokeDirPath + "/" + strokeId + "_instructions.json"; - app.saveJSONObject(instructions, instructionsPath); - - // Save input image - if (inputImage != null) { - String imagePath = strokeDirPath + "/" + strokeId + "_input.png"; - inputImage.save(imagePath); - } - } - - public JSONObject loadEffectRequirements(String effectName) { - File requirementsFile = new File( - "effect/" + effectName + "/" + effectName + "_requirements.json" - ); - if (requirementsFile.exists()) { - try { - return app.loadJSONObject(requirementsFile.getAbsolutePath()); - } catch (Exception e) { - System.err.println("Error loading effect requirements: " + e.getMessage()); - } - } - return null; - } - - public void createProjectMetadata(String projectId, String projectName) { - // Create metadata directory if it doesn't exist - ensureDirectoryExists("metadata"); - - // Create metadata JSON - JSONObject metadata = new JSONObject(); - metadata.setString("project_name", projectName); - metadata.setString("project_id", projectId); - - // Set timestamps - long currentTime = System.currentTimeMillis(); - metadata.setLong("created_time", currentTime); - metadata.setLong("modified_time", currentTime); - - // Save metadata - app.saveJSONObject(metadata, "metadata/" + projectId + ".json"); - } - - public void updateProjectMetadata(String projectId) { - String metadataPath = "metadata/" + projectId + ".json"; - File metadataFile = new File(metadataPath); - - if (metadataFile.exists()) { - try { - JSONObject metadata = app.loadJSONObject(metadataPath); - metadata.setLong("modified_time", System.currentTimeMillis()); - app.saveJSONObject(metadata, metadataPath); - } catch (Exception e) { - System.err.println("Error updating metadata: " + e.getMessage()); - } - } - } - - public ArrayList getProjectsMetadata() { - ArrayList projects = new ArrayList<>(); - - File metadataDir = new File("metadata"); - if (!metadataDir.exists()) { - ensureDirectoryExists("metadata"); - return projects; - } - - File[] metadataFiles = metadataDir.listFiles( - (dir, name) -> name.toLowerCase().endsWith(".json") - ); - - if (metadataFiles != null) { - for (File file : metadataFiles) { - try { - // Check if the corresponding project directory exists - JSONObject metadata = app.loadJSONObject(file.getAbsolutePath()); - String projectId = metadata.getString("project_id", ""); - File projectDir = new File("project/" + projectId); - - // Only add to the list if the project directory exists - if (projectDir.exists() && projectDir.isDirectory()) { - projects.add(metadata); - } else { - // ✅ FIXED: Just log the issue, don't delete the metadata file - System.out.println("Warning: Found metadata for project '" + projectId + - "' but project directory is missing or invalid. " + - "Metadata file preserved at: " + file.getAbsolutePath()); - - // Still add to the list but mark it as problematic - metadata.setString("status", "missing_project_dir"); - metadata.setString("metadata_file", file.getAbsolutePath()); - projects.add(metadata); - } - } catch (Exception e) { - System.err.println( - "Error loading metadata from " + file.getName() + - ": " + e.getMessage() + " (file preserved)" - ); - - // Create a placeholder metadata object for corrupted files - JSONObject errorMetadata = new JSONObject(); - errorMetadata.setString("project_name", "Corrupted Project (" + file.getName() + ")"); - errorMetadata.setString("project_id", "error_" + System.currentTimeMillis()); - errorMetadata.setString("status", "corrupted_metadata"); - errorMetadata.setString("metadata_file", file.getAbsolutePath()); - errorMetadata.setString("error_message", e.getMessage()); - errorMetadata.setLong("created_time", file.lastModified()); - errorMetadata.setLong("modified_time", file.lastModified()); - projects.add(errorMetadata); - } - } - } - - // Sort by modified time (most recent first), but handle missing timestamps - projects.sort((a, b) -> { - long timeA = a.getLong("modified_time", 0); - long timeB = b.getLong("modified_time", 0); - return Long.compare(timeB, timeA); // Reverse order for most recent first - }); - - return projects; - } - - public JSONObject getProjectMetadata(String projectId) { - String metadataPath = "metadata/" + projectId + ".json"; - File metadataFile = new File(metadataPath); - - if (metadataFile.exists()) { - try { - return app.loadJSONObject(metadataPath); - } catch (Exception e) { - System.err.println("Error loading metadata: " + e.getMessage()); - } - } - - return null; - } - - // Add a method to load strokes when a project is loaded - public boolean loadProject(String projectId) { - String projectPath = "project/" + projectId; - File projectDir = new File(projectPath); - - if (!projectDir.exists()) { - return false; - } - - // Try to load current state first (with all effects applied) - File currentImageFile = new File(projectPath + "/current.png"); - File originalImageFile = new File(projectPath + "/original.png"); - - PImage loadedImage = null; - - // Prefer current state if it exists, fallback to original - if (currentImageFile.exists()) { - loadedImage = app.loadImage(currentImageFile.getAbsolutePath()); - System.out.println("Loaded current state from: " + currentImageFile.getAbsolutePath()); - } else if (originalImageFile.exists()) { - loadedImage = app.loadImage(originalImageFile.getAbsolutePath()); - System.out.println("Loaded original image from: " + originalImageFile.getAbsolutePath()); - } - - if (loadedImage != null) { - app.setCurrentImage(loadedImage); - - // CRITICAL: Ensure proper window sizing - app.getSurface().setSize(Math.min(loadedImage.width, 1200), Math.min(loadedImage.height, 800)); - - // Update metadata - updateProjectMetadata(projectId); - - // Load existing strokes for this project - app.getStrokeManager().loadExistingStrokes(); - - return true; - } - - return false; - } - - public String formatTimestamp(long timestamp) { - java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - return sdf.format(new java.util.Date(timestamp)); - } - - /** - * Deletes a project and all its associated files and metadata. - * - * @param projectId The ID of the project to delete - * @return true if the project was successfully deleted, false otherwise - */ - public boolean deleteProject(String projectId) { - if (projectId == null || projectId.isEmpty()) { - return false; - } - - boolean success = true; - - try { - // Delete project directory and all its contents - File projectDir = new File("project/" + projectId); - if (projectDir.exists()) { - success &= deleteDirectory(projectDir); - } - - // Delete metadata file - File metadataFile = new File("metadata/" + projectId + ".json"); - if (metadataFile.exists()) { - success &= metadataFile.delete(); - } - - return success; - } catch (Exception e) { - System.err.println("Error deleting project: " + e.getMessage()); - e.printStackTrace(); - return false; - } - } - - /** - * Recursively deletes a directory and all its contents. - * - * @param directory The directory to delete - * @return true if the directory was successfully deleted, false otherwise - */ - private boolean deleteDirectory(File directory) { - if (directory.exists()) { - File[] files = directory.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory()) { - deleteDirectory(file); - } else { - file.delete(); - } - } - } - } - return directory.delete(); - } - - /** - * Manually clean up orphaned metadata files after user confirmation. - * This should only be called when the user explicitly requests cleanup. - */ - public int cleanupOrphanedMetadata() { - int cleanedCount = 0; - - File metadataDir = new File("metadata"); - if (!metadataDir.exists()) { - return 0; - } - - File[] metadataFiles = metadataDir.listFiles( - (dir, name) -> name.toLowerCase().endsWith(".json") - ); - - if (metadataFiles != null) { - for (File file : metadataFiles) { - try { - JSONObject metadata = app.loadJSONObject(file.getAbsolutePath()); - String projectId = metadata.getString("project_id", ""); - File projectDir = new File("project/" + projectId); - - // Only delete if project directory truly doesn't exist - if (!projectDir.exists()) { - if (file.delete()) { - System.out.println("Cleaned up orphaned metadata for project: " + projectId); - cleanedCount++; - } else { - System.err.println("Failed to delete orphaned metadata: " + file.getAbsolutePath()); - } - } - } catch (Exception e) { - System.err.println("Error checking metadata file " + file.getName() + ": " + e.getMessage()); - } - } - } - - return cleanedCount; - } -} diff --git a/src/QuantumBrush.java b/src/QuantumBrush.java deleted file mode 100644 index e565bcf..0000000 --- a/src/QuantumBrush.java +++ /dev/null @@ -1,1228 +0,0 @@ -import processing.core.*; -import processing.awt.PSurfaceAWT; -import processing.data.*; -import javax.swing.*; -import java.awt.*; -import java.awt.event.*; -import java.util.*; -import java.io.*; - -public class QuantumBrush extends PApplet { - // Managers - private CanvasManager canvas; - private EffectManager effects; - private StrokeManager strokes; - private FileManager files; - private UIManager ui; - - // UI components - private JFrame controlFrame; - private JFrame canvasFrame; - private JMenuBar menuBar; - private JComboBox effectsDropdown; - private JButton createButton; - private JPanel effectParameterContainer; // Container for effect parameters - - // Canvas state - private PImage currentImage; - private boolean isDrawing = false; - private String projectId = null; - private boolean ignoreNextClick = false; - - // Zoom and Pan state - private float zoomLevel = 1.0f; - private float panX = 0; - private float panY = 0; - private boolean isPanning = false; - private float lastMouseX, lastMouseY; - private static final float MIN_ZOOM = 0.1f; - private static final float MAX_ZOOM = 10.0f; - private static final int CANVAS_WIDTH = 800; - private static final int CANVAS_HEIGHT = 600; - - // Project history for undo/redo - private ArrayList projectHistory; - private int projectHistoryIndex; - private static final int MAX_HISTORY_SIZE = 20; - - // Drawing state - private int strokeColor = color(255, 0, 0); // Red - private float strokeWeight = 2.0f; - - public static void main(String[] args) { - PApplet.main("QuantumBrush"); - } - - public void settings() { - size(CANVAS_WIDTH, CANVAS_HEIGHT); - } - - public void setup() { - frameRate(90); - - canvas = new CanvasManager(this); - effects = new EffectManager(this); - files = new FileManager(this); - ui = new UIManager(this); - strokes = new StrokeManager(this); - - // Load effects - effects.loadEffects(); - DebugLogger.log("Effects loaded: " + effects.getEffectNames().size() + " effects found"); - - // Setup UI - setupUI(); - - - stroke(strokeColor); - strokeWeight(strokeWeight); - - // Add shutdown hook to clean up resources - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - if (strokes != null) { - strokes.shutdown(); - } - })); - - // Clean up any temporary files on startup - cleanupTempFiles(); - - // Initialize project history - projectHistory = new ArrayList<>(); - projectHistoryIndex = -1; - - } - - private void cleanupTempFiles() { - try { - // Clean up temp directory - File tempDir = new File("temp"); - if (tempDir.exists() && tempDir.isDirectory()) { - File[] tempFiles = tempDir.listFiles(); - if (tempFiles != null) { - for (File file : tempFiles) { - file.delete(); - } - } - } - - // Clean up any lock files - File[] lockFiles = new File(".").listFiles((dir, name) -> name.endsWith(".lock")); - if (lockFiles != null) { - for (File file : lockFiles) { - file.delete(); - } - } - - // Clean up any temp files - File[] tmpFiles = new File(".").listFiles((dir, name) -> name.endsWith(".tmp")); - if (tmpFiles != null) { - for (File file : tmpFiles) { - file.delete(); - } - } - } catch (Exception e) { - System.err.println("Error cleaning up temporary files: " + e.getMessage()); - } - } - - private void setupUI() { - // Get the JFrame from Processing for canvas - PSurfaceAWT.SmoothCanvas smoothCanvas = (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative(); - canvasFrame = (JFrame) smoothCanvas.getFrame(); - canvasFrame.setTitle("Quantum Brush - Canvas"); - canvasFrame.addWindowFocusListener(new WindowAdapter() { - @Override - public void windowGainedFocus(WindowEvent e) { - ignoreNextClick = true; - } - }); - - // Position the canvas frame on the right side of the screen - Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - canvasFrame.setLocation(screenSize.width/2, screenSize.height/4); - - // Create control frame - createControlFrame(); - - // Add keyboard shortcuts for undo/redo and zoom/pan - KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() { - @Override - public boolean dispatchKeyEvent(KeyEvent e) { - if (e.getID() == KeyEvent.KEY_PRESSED) { - int keyCode = e.getKeyCode(); - boolean isCtrlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0 || - (e.getModifiersEx() & InputEvent.META_DOWN_MASK) != 0; - boolean isShiftDown = (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0; - - if (isCtrlDown) { - if (keyCode == KeyEvent.VK_Z) { - if (isShiftDown) { - // Redo (Ctrl/Cmd + Shift + Z) - redoProject(); - } else { - // Undo (Ctrl/Cmd + Z) - undoProject(); - } - return true; - } else if (keyCode == KeyEvent.VK_D) { - // Clear drawing paths (Ctrl/Cmd + D) - canvas.clearPaths(); - return true; - } else if (keyCode == KeyEvent.VK_0) { - // Reset zoom (Ctrl/Cmd + 0) - resetZoom(); - return true; - } else if (keyCode == KeyEvent.VK_EQUALS || keyCode == KeyEvent.VK_PLUS) { - // Zoom in (Ctrl/Cmd + +) - zoomIn(); - return true; - } else if (keyCode == KeyEvent.VK_MINUS) { - // Zoom out (Ctrl/Cmd + -) - zoomOut(); - return true; - } - } - - // Space bar for panning (when held down) - if (keyCode == KeyEvent.VK_SPACE) { - // This will be handled in keyPressed/keyReleased - return false; - } - } - return false; - } - }); - - canvasFrame.setVisible(true); - } - - // Zoom and Pan methods - private void resetZoom() { - if (currentImage != null) { - calculateInitialZoomAndPan(); - } else { - zoomLevel = 1.0f; - panX = 0; - panY = 0; - } - } - - private void zoomIn() { - float newZoom = zoomLevel * 1.2f; - setZoom(newZoom, width/2, height/2); - } - - private void zoomOut() { - float newZoom = zoomLevel / 1.2f; - setZoom(newZoom, width/2, height/2); - } - - private void setZoom(float newZoom, float centerX, float centerY) { - newZoom = constrain(newZoom, MIN_ZOOM, MAX_ZOOM); - - if (currentImage != null) { - // Calculate the image point that should remain at the center - float imageX = (centerX - panX) / zoomLevel; - float imageY = (centerY - panY) / zoomLevel; - - // Update zoom - zoomLevel = newZoom; - - // Recalculate pan to keep the same image point at the center - panX = centerX - imageX * zoomLevel; - panY = centerY - imageY * zoomLevel; - } else { - zoomLevel = newZoom; - } - } - - private void calculateInitialZoomAndPan() { - if (currentImage == null) return; - - // Calculate zoom to fit image in canvas with some margin - float fitZoomX = (width * 0.9f) / currentImage.width; - float fitZoomY = (height * 0.9f) / currentImage.height; - zoomLevel = Math.min(fitZoomX, fitZoomY); - zoomLevel = constrain(zoomLevel, MIN_ZOOM, MAX_ZOOM); - - // Center the image - panX = (width - currentImage.width * zoomLevel) / 2; - panY = (height - currentImage.height * zoomLevel) / 2; - } - - // Coordinate transformation methods - private PVector screenToImage(float screenX, float screenY) { - if (currentImage == null) return new PVector(screenX, screenY); - - float imageX = (screenX - panX) / zoomLevel; - float imageY = (screenY - panY) / zoomLevel; - - // Clamp to image bounds - imageX = constrain(imageX, 0, currentImage.width); - imageY = constrain(imageY, 0, currentImage.height); - - return new PVector(imageX, imageY); - } - - private PVector imageToScreen(float imageX, float imageY) { - float screenX = imageX * zoomLevel + panX; - float screenY = imageY * zoomLevel + panY; - return new PVector(screenX, screenY); - } - - private boolean isPointInImage(float screenX, float screenY) { - if (currentImage == null) return false; - - PVector imagePoint = screenToImage(screenX, screenY); - return imagePoint.x >= 0 && imagePoint.x <= currentImage.width && - imagePoint.y >= 0 && imagePoint.y <= currentImage.height; - } - - private void createControlFrame() { - // Create main control window - controlFrame = new JFrame("Quantum Brush - Control Panel"); - Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - int controlWidth = Math.min(500, (int)(screenSize.width * 0.4)); - int controlHeight = Math.min(700, (int)(screenSize.height * 0.8)); - controlFrame.setSize(controlWidth, controlHeight); - controlFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - - // Position the control frame on the left side of the screen - controlFrame.setLocation(screenSize.width/4, screenSize.height/4); - - // Set references for UI manager - ui.setMainControlFrame(controlFrame); - - // Create menu bar - menuBar = new JMenuBar(); - - // File menu - JMenu fileMenu = new JMenu("File"); - JMenuItem newItem = new JMenuItem("New"); - JMenuItem openItem = new JMenuItem("Open"); - JMenuItem exportItem = new JMenuItem("Export"); - JMenuItem exitItem = new JMenuItem("Exit"); - - // Add keyboard shortcuts - newItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); - openItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); - exportItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); - - newItem.addActionListener(e -> newFile()); - openItem.addActionListener(e -> openFile()); - exportItem.addActionListener(e -> exportFile()); - exitItem.addActionListener(e -> exit()); - - fileMenu.add(newItem); - fileMenu.add(openItem); - fileMenu.add(exportItem); - fileMenu.addSeparator(); - fileMenu.add(exitItem); - - // Edit menu - JMenu editMenu = new JMenu("Edit"); - JMenuItem undoItem = new JMenuItem("Undo"); - JMenuItem redoItem = new JMenuItem("Redo"); - JMenuItem clearItem = new JMenuItem("Clear Drawing Paths"); - - undoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); - redoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK)); - - undoItem.addActionListener(e -> undoProject()); - redoItem.addActionListener(e -> redoProject()); - clearItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); - clearItem.addActionListener(e -> canvas.clearPaths()); - - editMenu.add(undoItem); - editMenu.add(redoItem); - editMenu.addSeparator(); - editMenu.add(clearItem); - - // View menu - JMenu viewMenu = new JMenu("View"); - JMenuItem zoomInItem = new JMenuItem("Zoom In"); - JMenuItem zoomOutItem = new JMenuItem("Zoom Out"); - JMenuItem resetZoomItem = new JMenuItem("Reset Zoom"); - JMenuItem fitToWindowItem = new JMenuItem("Fit to Window"); - - zoomInItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); - zoomOutItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); - resetZoomItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_0, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); - - zoomInItem.addActionListener(e -> zoomIn()); - zoomOutItem.addActionListener(e -> zoomOut()); - resetZoomItem.addActionListener(e -> resetZoom()); - fitToWindowItem.addActionListener(e -> resetZoom()); - - viewMenu.add(zoomInItem); - viewMenu.add(zoomOutItem); - viewMenu.add(resetZoomItem); - viewMenu.addSeparator(); - viewMenu.add(fitToWindowItem); - - // Tools menu - JMenu toolsMenu = new JMenu("Tools"); - JMenuItem strokeManagerItem = new JMenuItem("Stroke Manager"); - JMenuItem pythonConfigItem = new JMenuItem("Python Configuration"); - JMenuItem debugViewerItem = new JMenuItem("View Live Debug Log"); - JMenuItem testDebugItem = new JMenuItem("Test Debug Logging"); - - strokeManagerItem.addActionListener(e -> strokes.showStrokeManager()); - pythonConfigItem.addActionListener(e -> strokes.showPythonConfigDialog()); - debugViewerItem.addActionListener(e -> showLiveDebugViewer()); - testDebugItem.addActionListener(e -> { - DebugLogger.addTestLog(); - JOptionPane.showMessageDialog(controlFrame, - "Test logs added! Check the debug viewer to see if they appear.", - "Test Debug", - JOptionPane.INFORMATION_MESSAGE); - }); - - toolsMenu.add(strokeManagerItem); - toolsMenu.add(pythonConfigItem); - toolsMenu.addSeparator(); - toolsMenu.add(debugViewerItem); - toolsMenu.add(testDebugItem); - - menuBar.add(fileMenu); - menuBar.add(editMenu); - menuBar.add(viewMenu); - menuBar.add(toolsMenu); - - controlFrame.setJMenuBar(menuBar); - - // Create main panel with BorderLayout - JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); - mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); - - // Create effects panel without titled border - JPanel effectsPanel = new JPanel(new BorderLayout(10, 10)); - - // Effects dropdown - effectsDropdown = new JComboBox<>(); - effectsDropdown.addItem("Select an effect..."); - - // Populate effects dropdown with NAMES, not IDs - for (String effectId : effects.getEffectNames()) { - Effect effect = effects.getEffect(effectId); - if (effect != null) { - String displayName = effect.getName(); // Use name instead of ID - effectsDropdown.addItem(displayName); - } - } - - effectsDropdown.addActionListener(e -> { - String selectedName = (String) effectsDropdown.getSelectedItem(); - if (selectedName != null && !selectedName.equals("Select an effect...")) { - // Find effect by name, not ID - Effect selectedEffect = null; - for (String effectId : effects.getEffectNames()) { - Effect effect = effects.getEffect(effectId); - if (effect != null && effect.getName().equals(selectedName)) { - selectedEffect = effect; - break; - } - } - - if (selectedEffect != null) { - DebugLogger.log("User selected effect: " + selectedName + " (ID: " + selectedEffect.getId() + ")"); - ui.createEffectWindow(selectedEffect); - } - } - }); - - effectsPanel.add(effectsDropdown, BorderLayout.NORTH); - - // Create effect parameter container WITHOUT titled border - effectParameterContainer = new JPanel(new BorderLayout()); - ui.setEffectParameterContainer(effectParameterContainer); - - effectsPanel.add(effectParameterContainer, BorderLayout.CENTER); - - // Add zoom info panel - JPanel zoomInfoPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - zoomInfoPanel.setBorder(BorderFactory.createTitledBorder("Canvas Info")); - - JLabel zoomLabel = new JLabel("Zoom: 100% | Pan: (0, 0)"); - zoomInfoPanel.add(zoomLabel); - - // Update zoom label periodically - javax.swing.Timer zoomTimer = new javax.swing.Timer(100, e -> { - if (currentImage != null) { - zoomLabel.setText(String.format("Zoom: %.0f%% | Pan: (%.0f, %.0f) | Image: %dx%d", - zoomLevel * 100, panX, panY, currentImage.width, currentImage.height)); - } else { - zoomLabel.setText("No image loaded"); - } - }); - zoomTimer.start(); - - // Add only the effects panel and zoom info to main panel - mainPanel.add(effectsPanel, BorderLayout.CENTER); - mainPanel.add(zoomInfoPanel, BorderLayout.SOUTH); - - controlFrame.add(mainPanel); - controlFrame.setVisible(true); - } - - private void showLiveDebugViewer() { - JDialog debugDialog = new JDialog((Frame)null, "Live Debug Log Viewer", false); // Non-modal - debugDialog.setSize(600, 400); - debugDialog.setLocationRelativeTo(controlFrame); - - JTextArea debugTextArea = new JTextArea(); - debugTextArea.setFont(new Font("Courier New", Font.PLAIN, 13)); - debugTextArea.setEditable(false); - debugTextArea.setBackground(new Color(248, 248, 248)); - - // Load current debug log content - debugTextArea.setText(DebugLogger.getAllLogs()); - - // Auto-scroll to bottom - debugTextArea.setCaretPosition(debugTextArea.getDocument().getLength()); - - JScrollPane scrollPane = new JScrollPane(debugTextArea); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); - - // Create control panel - JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - - JButton refreshButton = new JButton("Refresh"); - JButton clearButton = new JButton("Clear Log"); - JButton copyButton = new JButton("Copy All"); - JButton testButton = new JButton("Add Test Log"); - JCheckBox autoRefreshBox = new JCheckBox("Auto-refresh", true); - - controlPanel.add(refreshButton); - controlPanel.add(clearButton); - controlPanel.add(copyButton); - controlPanel.add(testButton); - controlPanel.add(autoRefreshBox); - - // Add status label - JLabel statusLabel = new JLabel("Live debug log - " + DebugLogger.getLogCount() + " entries"); - controlPanel.add(Box.createHorizontalStrut(20)); - controlPanel.add(statusLabel); - - // Button actions - refreshButton.addActionListener(e -> { - String logContent = DebugLogger.getAllLogs(); - debugTextArea.setText(logContent); - debugTextArea.setCaretPosition(debugTextArea.getDocument().getLength()); - statusLabel.setText("Live debug log - " + DebugLogger.getLogCount() + " entries"); - }); - - clearButton.addActionListener(e -> { - DebugLogger.clearLogs(); - debugTextArea.setText(""); - statusLabel.setText("Live debug log - 0 entries"); - }); - - copyButton.addActionListener(e -> { - debugTextArea.selectAll(); - debugTextArea.copy(); - debugTextArea.setCaretPosition(debugTextArea.getDocument().getLength()); - JOptionPane.showMessageDialog(debugDialog, "Debug log copied to clipboard!", "Copied", JOptionPane.INFORMATION_MESSAGE); - }); - - testButton.addActionListener(e -> { - DebugLogger.addTestLog(); - // Immediately refresh the display - debugTextArea.setText(DebugLogger.getAllLogs()); - debugTextArea.setCaretPosition(debugTextArea.getDocument().getLength()); - statusLabel.setText("Live debug log - " + DebugLogger.getLogCount() + " entries"); - }); - - // Auto-refresh timer - javax.swing.Timer refreshTimer = new javax.swing.Timer(1000, e -> { - if (autoRefreshBox.isSelected()) { - String currentText = debugTextArea.getText(); - String newText = DebugLogger.getAllLogs(); - if (!currentText.equals(newText)) { - debugTextArea.setText(newText); - debugTextArea.setCaretPosition(debugTextArea.getDocument().getLength()); - statusLabel.setText("Live debug log - " + DebugLogger.getLogCount() + " entries"); - } - } - }); - refreshTimer.start(); - - // Stop timer when dialog is closed - debugDialog.addWindowListener(new java.awt.event.WindowAdapter() { - @Override - public void windowClosing(java.awt.event.WindowEvent e) { - refreshTimer.stop(); - } - }); - - debugDialog.add(scrollPane, BorderLayout.CENTER); - debugDialog.add(controlPanel, BorderLayout.SOUTH); - debugDialog.setVisible(true); - } - - private void newFile() { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setFileFilter(new javax.swing.filechooser.FileFilter() { - public boolean accept(File f) { - return f.isDirectory() || f.getName().toLowerCase().endsWith(".png") || - f.getName().toLowerCase().endsWith(".jpg") || f.getName().toLowerCase().endsWith(".jpeg"); - } - public String getDescription() { - return "Image files (*.png, *.jpg, *.jpeg)"; - } - }); - - if (fileChooser.showOpenDialog(controlFrame) == JFileChooser.APPROVE_OPTION) { - File selectedFile = fileChooser.getSelectedFile(); - PImage loadedImage = loadImage(selectedFile.getAbsolutePath()); - - if (loadedImage != null) { - currentImage = loadedImage; - - // Calculate initial zoom and pan to fit image nicely - calculateInitialZoomAndPan(); - - // Ask user for project name - String defaultProjectName = selectedFile.getName(); - // Remove file extension from default name - int lastDotIndex = defaultProjectName.lastIndexOf('.'); - if (lastDotIndex > 0) { - defaultProjectName = defaultProjectName.substring(0, lastDotIndex); - } - - String projectName = (String) JOptionPane.showInputDialog( - controlFrame, - "Enter a name for this project:", - "New Project", - JOptionPane.QUESTION_MESSAGE, - null, - null, - defaultProjectName - ); - - // If user cancels or enters empty name, use default - if (projectName == null || projectName.trim().isEmpty()) { - projectName = defaultProjectName; - } - - // Generate new project ID - projectId = "project_" + System.currentTimeMillis(); - - // Save project - files.saveProject(projectId, currentImage); - files.createProjectMetadata(projectId, projectName.trim()); - - // Clear canvas and strokes - canvas.clearPaths(); - strokes.clearStrokes(); - - // Save initial project state (after loading new image) - saveProjectStateAfterImageChange(); - - DebugLogger.log("New project created: " + projectName + " (ID: " + projectId + ")"); - println("New project created: " + projectName + " (ID: " + projectId + ")"); - } else { - JOptionPane.showMessageDialog(controlFrame, "Failed to load image.", "Error", JOptionPane.ERROR_MESSAGE); - } - } - } - - private void openFile() { - // Show enhanced project selection dialog with delete option - showProjectManagerDialog(); - } - - private void showProjectManagerDialog() { - // Get projects metadata - ArrayList projects = files.getProjectsMetadata(); - - if (projects.isEmpty()) { - JOptionPane.showMessageDialog(controlFrame, "No projects found.", "No Projects", JOptionPane.INFORMATION_MESSAGE); - return; - } - - // Create a custom dialog for project management - JDialog projectDialog = new JDialog(controlFrame, "Project Manager", true); - Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - int dialogWidth = Math.min(480, (int)(screenSize.width * 0.5)); - int dialogHeight = Math.min(450, (int)(screenSize.height * 0.6)); - projectDialog.setSize(dialogWidth, dialogHeight); - projectDialog.setLocationRelativeTo(controlFrame); - projectDialog.setLayout(new BorderLayout(10, 10)); - projectDialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - - // Create project list model and JList - DefaultListModel listModel = new DefaultListModel<>(); - for (JSONObject project : projects) { - String name = project.getString("project_name", "Unknown"); - String id = project.getString("project_id", ""); - long modified = project.getLong("modified_time", 0); - String timeStr = files.formatTimestamp(modified); - String status = project.getString("status", "normal"); - - // Add status indicator to the name if there are issues - if ("missing_project_dir".equals(status)) { - name = "⚠️ " + name + " (Missing Project Files)"; - } else if ("corrupted_metadata".equals(status)) { - name = "❌ " + name + " (Corrupted Metadata)"; - } - - listModel.addElement(new ProjectItem(name, id, timeStr, status)); - } - - JList projectList = new JList<>(listModel); - projectList.setCellRenderer(new ProjectListCellRenderer()); - projectList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - - // Add list to scroll pane - JScrollPane scrollPane = new JScrollPane(projectList); - scrollPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - // Create buttons panel - JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 10)); - - JButton openButton = new JButton("Open"); - JButton deleteButton = new JButton("Delete"); - JButton cleanupButton = new JButton("Cleanup Orphaned"); - JButton cancelButton = new JButton("Cancel"); - - // ✅ FIXED: Make cleanup button consistent with others - // Remove special styling to match other buttons - - // Add cleanup action - cleanupButton.addActionListener(e -> { - int confirm = JOptionPane.showConfirmDialog( - projectDialog, - "This will permanently delete metadata files for projects that don't have corresponding project folders.\n" + - "This action cannot be undone. Continue?", - "Confirm Cleanup", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE - ); - - if (confirm == JOptionPane.YES_OPTION) { - int cleanedCount = files.cleanupOrphanedMetadata(); - - JOptionPane.showMessageDialog( - projectDialog, - "Cleanup completed. Removed " + cleanedCount + " orphaned metadata files.", - "Cleanup Complete", - JOptionPane.INFORMATION_MESSAGE - ); - - // Refresh the project list - projectDialog.dispose(); - showProjectManagerDialog(); - } - }); - - // Update open button to handle problematic projects - openButton.addActionListener(e -> { - ProjectItem selectedItem = projectList.getSelectedValue(); - if (selectedItem != null) { - if (!"normal".equals(selectedItem.status)) { - JOptionPane.showMessageDialog(projectDialog, - "Cannot open this project due to issues:\n" + - "Status: " + selectedItem.status + "\n\n" + - "Please check the project files or use the cleanup function.", - "Cannot Open Project", - JOptionPane.WARNING_MESSAGE); - return; - } - - String selectedProjectId = selectedItem.id; - - if (files.loadProject(selectedProjectId)) { - projectId = selectedProjectId; - - // Calculate initial zoom and pan for loaded image - calculateInitialZoomAndPan(); - - println("Project loaded: " + selectedItem.name + " (ID: " + projectId + ")"); - - // Don't save project state immediately after loading! - // This was overwriting the current.png file with the loaded state - // Instead, just initialize the undo history without saving to disk - initializeProjectHistoryAfterLoad(); - - projectDialog.dispose(); - } else { - JOptionPane.showMessageDialog(projectDialog, - "Failed to load project.", "Error", JOptionPane.ERROR_MESSAGE); - } - } else { - JOptionPane.showMessageDialog(projectDialog, - "Please select a project to open.", "No Selection", JOptionPane.INFORMATION_MESSAGE); - } - }); - - deleteButton.addActionListener(e -> { - ProjectItem selectedItem = projectList.getSelectedValue(); - if (selectedItem != null) { - // Confirm deletion - int confirm = JOptionPane.showConfirmDialog( - projectDialog, - "Are you sure you want to delete the project \"" + selectedItem.name + "\"?\n" + - "This action cannot be undone.", - "Confirm Deletion", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE - ); - - if (confirm == JOptionPane.YES_OPTION) { - String selectedProjectId = selectedItem.id; - - // Delete the project - boolean success = files.deleteProject(selectedProjectId); - - if (success) { - // Remove from list - listModel.removeElement(selectedItem); - - // Show success message - JOptionPane.showMessageDialog(projectDialog, - "Project \"" + selectedItem.name + "\" deleted successfully.", - "Project Deleted", - JOptionPane.INFORMATION_MESSAGE); - - // If no more projects, close dialog - if (listModel.isEmpty()) { - JOptionPane.showMessageDialog(projectDialog, - "No more projects available.", - "No Projects", - JOptionPane.INFORMATION_MESSAGE); - projectDialog.dispose(); - } - } else { - JOptionPane.showMessageDialog(projectDialog, - "Failed to delete project.", - "Error", - JOptionPane.ERROR_MESSAGE); - } - } - } else { - JOptionPane.showMessageDialog(projectDialog, - "Please select a project to delete.", - "No Selection", - JOptionPane.INFORMATION_MESSAGE); - } - }); - - cancelButton.addActionListener(e -> projectDialog.dispose()); - - // Enable open button on double-click - projectList.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - openButton.doClick(); - } - } - }); - - // Add buttons to panel - buttonsPanel.add(openButton); - buttonsPanel.add(deleteButton); - buttonsPanel.add(cleanupButton); - buttonsPanel.add(cancelButton); - - // Add components to dialog - JLabel titleLabel = new JLabel("Select a project:"); - titleLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 5, 10)); - titleLabel.setFont(new Font("Arial", Font.BOLD, 14)); - - projectDialog.add(titleLabel, BorderLayout.NORTH); - projectDialog.add(scrollPane, BorderLayout.CENTER); - projectDialog.add(buttonsPanel, BorderLayout.SOUTH); - - // Show dialog - projectDialog.setVisible(true); - } - - // Helper class for project items in the list - private static class ProjectItem { - public final String name; - public final String id; - public final String timestamp; - public final String status; - - public ProjectItem(String name, String id, String timestamp, String status) { - this.name = name; - this.id = id; - this.timestamp = timestamp; - this.status = status != null ? status : "normal"; - } - - @Override - public String toString() { - return name + " (" + timestamp + ")"; - } - } - - // Custom cell renderer for project list - private static class ProjectListCellRenderer extends DefaultListCellRenderer { - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - - JLabel label = (JLabel) super.getListCellRendererComponent( - list, value, index, isSelected, cellHasFocus); - - if (value instanceof ProjectItem) { - ProjectItem item = (ProjectItem) value; - - // Set text with HTML formatting - label.setText("" + item.name + "
" + - "Last modified: " + item.timestamp + ""); - - // Add more padding - label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - } - - return label; - } - } - - private void exportFile() { - if (currentImage != null) { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setFileFilter(new javax.swing.filechooser.FileFilter() { - public boolean accept(File f) { - return f.isDirectory() || f.getName().toLowerCase().endsWith(".png") || - f.getName().toLowerCase().endsWith(".jpg") || f.getName().toLowerCase().endsWith(".jpeg"); - } - public String getDescription() { - return "Image files (*.png, *.jpg, *.jpeg)"; - } - }); - - // Set default filename - if (projectId != null) { - fileChooser.setSelectedFile(new File(projectId + ".png")); - } else { - fileChooser.setSelectedFile(new File("quantum_brush_image.png")); - } - - if (fileChooser.showSaveDialog(controlFrame) == JFileChooser.APPROVE_OPTION) { - File selectedFile = fileChooser.getSelectedFile(); - String filePath = selectedFile.getAbsolutePath(); - - // Add .png extension if not present - if (!filePath.toLowerCase().endsWith(".png") && - !filePath.toLowerCase().endsWith(".jpg") && - !filePath.toLowerCase().endsWith(".jpeg")) { - filePath += ".png"; - selectedFile = new File(filePath); - } - - try { - currentImage.save(filePath); - JOptionPane.showMessageDialog(controlFrame, - "Image exported successfully to: " + filePath, - "Export Successful", - JOptionPane.INFORMATION_MESSAGE); - - // Also save project if we have one - if (projectId != null) { - files.saveProject(projectId, currentImage); // This will now save both original and current - files.updateProjectMetadata(projectId); - } - } catch (Exception e) { - JOptionPane.showMessageDialog(controlFrame, - "Error exporting image: " + e.getMessage(), - "Export Error", - JOptionPane.ERROR_MESSAGE); - } - } - } else { - JOptionPane.showMessageDialog(controlFrame, "No image to export.", "Error", JOptionPane.WARNING_MESSAGE); - } - } - - // New method that initializes undo history without overwriting current.png - private void initializeProjectHistoryAfterLoad() { - if (projectHistory == null) { - projectHistory = new ArrayList<>(); - } - - // Clear existing history - projectHistory.clear(); - - // Add the current loaded state to history (but don't save to disk) - ProjectState state = new ProjectState(currentImage, new ArrayList(), projectId); - projectHistory.add(state); - projectHistoryIndex = 0; - - System.out.println("Initialized project history after loading (without overwriting current.png)"); - } - - /** - * ✅ NEW: Centralized method for saving the current image to disk. - * This ensures consistency for all operations that modify the image. - */ - private void saveCurrentImageToDisk() { - if (projectId != null && currentImage != null) { - String projectPath = "project/" + projectId; - File projectDir = new File(projectPath); - if (projectDir.exists()) { - currentImage.save(projectPath + "/current.png"); - System.out.println("Saved current image state to disk: project/" + projectId + "/current.png"); - } - } - } - - // Project state management - only save when image changes, not when drawing - public void saveProjectStateAfterImageChange() { - if (projectHistory == null) return; - - // Remove any states after current index - while (projectHistory.size() > projectHistoryIndex + 1) { - projectHistory.remove(projectHistory.size() - 1); - } - - // Create new project state (only save image, not paths) - ProjectState state = new ProjectState(currentImage, new ArrayList(), projectId); - projectHistory.add(state); - projectHistoryIndex = projectHistory.size() - 1; - - // Limit history size - while (projectHistory.size() > MAX_HISTORY_SIZE) { - projectHistory.remove(0); - projectHistoryIndex--; - } - - // ✅ FIXED: Call the new method to save the image to disk - saveCurrentImageToDisk(); - - System.out.println("Saved project state " + projectHistoryIndex + " (image changes only)"); - } - - private void undoProject() { - if (projectHistoryIndex > 0) { - projectHistoryIndex--; - ProjectState state = projectHistory.get(projectHistoryIndex); - restoreProjectState(state); - println("Undo: Restored to state " + projectHistoryIndex); - } else { - println("Undo: No more states to undo"); - } - } - - private void redoProject() { - if (projectHistoryIndex < projectHistory.size() - 1) { - projectHistoryIndex++; - ProjectState state = projectHistory.get(projectHistoryIndex); - restoreProjectState(state); - System.out.println("Redo: Restored to state " + projectHistoryIndex); - } else { - System.out.println("Redo: No more states to redo (current: " + projectHistoryIndex + ", max: " + (projectHistory.size() - 1) + ")"); - } - } - - private void restoreProjectState(ProjectState state) { - if (state.image != null) { - currentImage = state.image.copy(); - // Recalculate zoom and pan for the restored image - calculateInitialZoomAndPan(); - } - canvas.setPaths(state.paths); - projectId = state.projectId; - - // ✅ FIXED: Save the restored image state to disk so it persists - saveCurrentImageToDisk(); - - // Force a redraw - redraw(); - - println("Restored project state - Image: " + (currentImage != null ? "Yes" : "No") + - ", Paths: " + (state.paths != null ? state.paths.size() : 0) + - ", Project ID: " + projectId); - } - - public void draw() { - background(50); // Dark gray background - - if (currentImage != null) { - pushMatrix(); - - // Apply zoom and pan transformations - translate(panX, panY); - scale(zoomLevel); - - // Draw the image at original size - image(currentImage, 0, 0); - - popMatrix(); - - // Draw paths with transformations - canvas.draw(zoomLevel, panX, panY); - } else { - // Show "no image" message - fill(200); - textAlign(CENTER, CENTER); - textSize(18); - text("Load an image or project to begin", width/2, height/2); - - fill(150); - textSize(14); - text("Use File > New to load an image", width/2, height/2 + 30); - text("Use keyboard shortcuts to zoom: Ctrl/Cmd + Plus/Minus", width/2, height/2 + 50); - } - } - - // Mouse event handling with coordinate transformation - public void mousePressed() { - if (ignoreNextClick) { - ignoreNextClick = false; - return; - } - if (currentImage == null) { - return; // Don't allow drawing if no image is loaded - } - - if (mouseButton == LEFT && !isPanning) { - // Check if mouse is over the image - if (isPointInImage(mouseX, mouseY)) { - isDrawing = true; - PVector imagePoint = screenToImage(mouseX, mouseY); - canvas.startNewPath(); - canvas.setClickPoint(imagePoint.x, imagePoint.y); - - // Check if Shift is pressed ('keyPressed' and 'keyCode' are built-in PApplet variables) - boolean isShiftDown = (keyPressed && keyCode == java.awt.event.KeyEvent.VK_SHIFT); - canvas.addPointToCurrentPath(imagePoint.x, imagePoint.y, isShiftDown); - // canvas.addPointToCurrentPath(imagePoint.x, imagePoint.y); - } - } else if (mouseButton == RIGHT || (mouseButton == LEFT && keyPressed && key == ' ')) { - // Start panning - isPanning = true; - lastMouseX = mouseX; - lastMouseY = mouseY; - } - } - - public void mouseDragged() { - if (currentImage == null) { - return; - } - - if (isPanning) { - // Pan the view - float deltaX = mouseX - lastMouseX; - float deltaY = mouseY - lastMouseY; - panX += deltaX; - panY += deltaY; - lastMouseX = mouseX; - lastMouseY = mouseY; - } else if (isDrawing && mouseButton == LEFT) { - // Continue drawing stroke - if (isPointInImage(mouseX, mouseY)) { - PVector imagePoint = screenToImage(mouseX, mouseY); - - // canvas.addPointToCurrentPath(imagePoint.x, imagePoint.y); - - // --- ADD THIS LOGIC --- - // Check if the Shift key is pressed. - boolean isShiftDown = (keyPressed && keyCode == java.awt.event.KeyEvent.VK_SHIFT); - - // --- MODIFY THIS LINE --- - // Pass the 'isShiftDown' boolean to the manager - canvas.addPointToCurrentPath(imagePoint.x, imagePoint.y, isShiftDown); - } - } - } - - public void mouseReleased() { - if (currentImage == null) { - return; - } - - if (isDrawing && mouseButton == LEFT) { - // Finish drawing stroke - canvas.finishCurrentPath(); - isDrawing = false; - - // Enable create button if there are paths - if (canvas.hasPath()) { - ui.enableCreateButton(); - } - } else if (isPanning) { - // Stop panning - isPanning = false; - } - } - - // Getter methods for managers - public CanvasManager getCanvasManager() { - return canvas; - } - - public EffectManager getEffectManager() { - return effects; - } - - public StrokeManager getStrokeManager() { - return strokes; - } - - public FileManager getFileManager() { - return files; - } - - public UIManager getUIManager() { - return ui; - } - - // Getter methods for state - public PImage getCurrentImage() { - return currentImage; - } - - public void setCurrentImage(PImage image) { - this.currentImage = image; - if (image != null) { - calculateInitialZoomAndPan(); - } - } - - public String getProjectId() { - return projectId; - } - - public void setProjectId(String id) { - this.projectId = id; - } - - // ProjectState inner class - private static class ProjectState { - public final PImage image; - public final ArrayList paths; - public final String projectId; - - public ProjectState(PImage image, ArrayList paths, String projectId) { - this.image = image != null ? image.copy() : null; - this.paths = new ArrayList<>(); - if (paths != null) { - for (Path path : paths) { - this.paths.add(path.copy()); - } - } - this.projectId = projectId; - } - } - - @Override - public void exit() { - System.exit(0); - } -} diff --git a/src/StrokeManager.java b/src/StrokeManager.java deleted file mode 100644 index 15e6855..0000000 --- a/src/StrokeManager.java +++ /dev/null @@ -1,1794 +0,0 @@ -import processing.core.*; -import processing.data.*; -import java.util.*; -import java.io.*; -import javax.swing.*; -import java.awt.*; -import java.nio.file.Files; -import java.util.concurrent.*; - -public class StrokeManager { - private QuantumBrush app; - private ArrayList strokes; - private int currentStrokeIndex = -1; - private String pythonCommand = null; - private File pythonExecutable = null; - - // Thread pool for asynchronous processing - private ExecutorService executorService; - - // Map to track which strokes are currently being processed - private Map> processingStrokes; - - // Callback interface for UI updates - public interface ProcessingCallback { - void onProcessingComplete(String strokeId, boolean success); - } - - private java.util.List callbacks; - - public StrokeManager(QuantumBrush app) { - this.app = app; - this.strokes = new ArrayList<>(); - this.executorService = Executors.newFixedThreadPool(3); // Allow up to 3 concurrent processes - this.processingStrokes = new ConcurrentHashMap<>(); - this.callbacks = new ArrayList<>(); - - // Initialize Python command on startup - initializePythonCommand(); - } - - private void initializePythonCommand() { - try { - // First check if there's a saved custom Python path - String customPath = loadCustomPythonPath(); - if (customPath != null && !customPath.isEmpty()) { - File customPython = new File(customPath); - if (customPython.exists() && customPython.canExecute()) { - pythonExecutable = customPython; - pythonCommand = customPython.getAbsolutePath(); - System.out.println("Using custom Python path: " + pythonCommand); - return; - } else { - System.err.println("Custom Python path is invalid: " + customPath); - } - } - - // If no custom path or it's invalid, try to find Python automatically - boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - - // Try python3 command first (for macOS/Linux) - if (!isWindows) { - if (findPythonExecutable("python3")) { - return; - } - } - - // Try python command (for Windows or as fallback) - if (findPythonExecutable("python")) { - return; - } - - // Try specific python3.10+ commands as last resort - String[] pythonCommands = { - "python3.12", "python3.11", "python3.10", - "python312", "python311", "python310" - }; - - for (String cmd : pythonCommands) { - if (findPythonExecutable(cmd)) { - return; - } - } - - // If we get here, we couldn't find a suitable Python version - System.err.println( - "WARNING: Could not find Python 3.10 or higher. " + - "The application may not work correctly." - ); - pythonCommand = isWindows ? "python" : "python3"; // Default fallback - - } catch (Exception e) { - System.err.println("Error initializing Python command: " + e.getMessage()); - e.printStackTrace(); - pythonCommand = System.getProperty("os.name").toLowerCase().contains("win") ? - "python" : "python3"; - } - } - - private boolean findPythonExecutable(String command) { - try { - // First try to find the full path of the executable - ProcessBuilder whichBuilder; - boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - - if (isWindows) { - whichBuilder = new ProcessBuilder("where", command); - } else { - whichBuilder = new ProcessBuilder("which", command); - } - - whichBuilder.redirectErrorStream(true); - Process whichProcess = whichBuilder.start(); - BufferedReader whichReader = new BufferedReader( - new InputStreamReader(whichProcess.getInputStream()) - ); - String executablePath = whichReader.readLine(); // Get the first result - whichProcess.waitFor(); - - if (executablePath != null && !executablePath.isEmpty()) { - File executable = new File(executablePath); - if (executable.exists() && executable.canExecute()) { - System.out.println("Found Python executable: " + executablePath); - - // Now check the version - ProcessBuilder versionBuilder = new ProcessBuilder(executablePath, "--version"); - versionBuilder.redirectErrorStream(true); - Process versionProcess = versionBuilder.start(); - BufferedReader versionReader = new BufferedReader( - new InputStreamReader(versionProcess.getInputStream()) - ); - String versionLine = versionReader.readLine(); - versionProcess.waitFor(); - - if (versionLine != null && versionLine.toLowerCase().contains("python")) { - System.out.println("Python version: " + versionLine); - - // Extract version numbers - String[] parts = versionLine.split(" ")[1].split("\\."); - if (parts.length >= 2) { - int major = Integer.parseInt(parts[0]); - int minor = Integer.parseInt(parts[1]); - - boolean isCompatible = (major > 3) || - (major == 3 && minor >= 10); - - if (isCompatible) { - System.out.println( - "Using compatible Python: " + executablePath + - " (" + versionLine + ")" - ); - pythonExecutable = executable; - pythonCommand = executablePath; - return true; - } else { - System.out.println( - "Python version too old: " + versionLine + - " (need 3.10 or higher)" - ); - } - } - } - } - } - - return false; - } catch (Exception e) { - System.out.println( - "Error finding Python executable for " + command + ": " + e.getMessage() - ); - return false; - } - } - - public void showPythonConfigDialog() { - JDialog dialog = new JDialog((Frame)null, "Python Configuration", true); - dialog.setSize(600, 300); - dialog.setLocationRelativeTo(null); - - JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); - mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); - - // Current Python info - JPanel infoPanel = new JPanel(new GridLayout(3, 1, 5, 5)); - infoPanel.setBorder(BorderFactory.createTitledBorder("Current Python Information")); - - String currentPath = pythonExecutable != null ? - pythonExecutable.getAbsolutePath() : pythonCommand; - JLabel pathLabel = new JLabel("Path: " + currentPath); - - String versionInfo = "Unknown"; - try { - ProcessBuilder pb = new ProcessBuilder(pythonCommand, "--version"); - pb.redirectErrorStream(true); - Process process = pb.start(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()) - ); - versionInfo = reader.readLine(); - process.waitFor(); - } catch (Exception e) { - versionInfo = "Error: " + e.getMessage(); - } - - JLabel versionLabel = new JLabel("Version: " + versionInfo); - - boolean isCompatible = versionInfo.matches(".*Python 3\\.1[0-9].*") || - versionInfo.matches(".*Python 3\\.[2-9][0-9].*"); - String compatibilityMsg = isCompatible ? - "✓ Compatible with match-case syntax" : - "✗ Not compatible with match-case syntax (needs Python 3.10+)"; - JLabel compatLabel = new JLabel(compatibilityMsg); - compatLabel.setForeground(isCompatible ? new Color(0, 150, 0) : Color.RED); - - infoPanel.add(pathLabel); - infoPanel.add(versionLabel); - infoPanel.add(compatLabel); - - // Custom path selection - JPanel customPanel = new JPanel(new BorderLayout(5, 5)); - customPanel.setBorder(BorderFactory.createTitledBorder("Custom Python Path")); - - JTextField pathField = new JTextField(20); - if (pythonExecutable != null) { - pathField.setText(pythonExecutable.getAbsolutePath()); - } - - JButton browseButton = new JButton("Browse..."); - browseButton.addActionListener(e -> { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle("Select Python Executable"); - - // Set file filter based on OS - boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - if (isWindows) { - fileChooser.setFileFilter(new javax.swing.filechooser.FileFilter() { - public boolean accept(File f) { - return f.isDirectory() || f.getName().toLowerCase().endsWith(".exe"); - } - public String getDescription() { - return "Executable files (*.exe)"; - } - }); - } else { - fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - } - - if (fileChooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) { - File selectedFile = fileChooser.getSelectedFile(); - pathField.setText(selectedFile.getAbsolutePath()); - } - }); - - JPanel pathPanel = new JPanel(new BorderLayout(5, 0)); - pathPanel.add(pathField, BorderLayout.CENTER); - pathPanel.add(browseButton, BorderLayout.EAST); - - JButton testButton = new JButton("Test Selected Python"); - testButton.addActionListener(e -> { - String path = pathField.getText().trim(); - if (path.isEmpty()) { - JOptionPane.showMessageDialog( - dialog, - "Please enter a Python path first.", - "No Path", - JOptionPane.WARNING_MESSAGE - ); - return; - } - - File pythonFile = new File(path); - if (!pythonFile.exists()) { - JOptionPane.showMessageDialog( - dialog, - "The specified file does not exist: " + path, - "File Not Found", - JOptionPane.ERROR_MESSAGE - ); - return; - } - - if (!pythonFile.canExecute()) { - JOptionPane.showMessageDialog( - dialog, - "The specified file is not executable: " + path, - "Not Executable", - JOptionPane.ERROR_MESSAGE - ); - return; - } - - try { - ProcessBuilder pb = new ProcessBuilder(path, "--version"); - pb.redirectErrorStream(true); - Process process = pb.start(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()) - ); - String version = reader.readLine(); - int exitCode = process.waitFor(); - - if (exitCode == 0 && version != null && version.toLowerCase().contains("python")) { - boolean pythonCompatible = version.matches(".*Python 3\\.1[0-9].*") || - version.matches(".*Python 3\\.[2-9][0-9].*"); - - if (pythonCompatible) { - JOptionPane.showMessageDialog( - dialog, - "Python test successful!\n" + version + - "\n\nThis version is compatible with match-case syntax.", - "Test Successful", - JOptionPane.INFORMATION_MESSAGE - ); - } else { - JOptionPane.showMessageDialog( - dialog, - "Python test successful, but version may be incompatible.\n" + - version + - "\n\nThis version may NOT support match-case syntax (needs Python 3.10+).", - "Version Warning", - JOptionPane.WARNING_MESSAGE - ); - } - } else { - JOptionPane.showMessageDialog( - dialog, - "Failed to get Python version. Output: " + version, - "Test Failed", - JOptionPane.ERROR_MESSAGE - ); - } - } catch (Exception ex) { - JOptionPane.showMessageDialog( - dialog, - "Error testing Python: " + ex.getMessage(), - "Test Error", - JOptionPane.ERROR_MESSAGE - ); - } - }); - - customPanel.add(pathPanel, BorderLayout.NORTH); - customPanel.add(testButton, BorderLayout.SOUTH); - - // Button panel - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0)); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> dialog.dispose()); - - JButton saveButton = new JButton("Save"); - saveButton.addActionListener(e -> { - String path = pathField.getText().trim(); - if (path.isEmpty()) { - JOptionPane.showMessageDialog( - dialog, - "Please enter a Python path first.", - "No Path", - JOptionPane.WARNING_MESSAGE - ); - return; - } - - File pythonFile = new File(path); - if (!pythonFile.exists() || !pythonFile.canExecute()) { - JOptionPane.showMessageDialog( - dialog, - "The specified file is not a valid executable: " + path, - "Invalid Path", - JOptionPane.ERROR_MESSAGE - ); - return; - } - - // Save the custom path - saveCustomPythonPath(path); - - // Update the current Python command - pythonExecutable = pythonFile; - pythonCommand = path; - - JOptionPane.showMessageDialog( - dialog, - "Python path saved successfully. It will be used for all future operations.", - "Path Saved", - JOptionPane.INFORMATION_MESSAGE - ); - - dialog.dispose(); - }); - - buttonPanel.add(cancelButton); - buttonPanel.add(saveButton); - - // Add all panels to main panel - mainPanel.add(infoPanel, BorderLayout.NORTH); - mainPanel.add(customPanel, BorderLayout.CENTER); - mainPanel.add(buttonPanel, BorderLayout.SOUTH); - - dialog.add(mainPanel); - dialog.setVisible(true); - } - - private String loadCustomPythonPath() { - try { - File configFile = new File("config/python_path.txt"); - if (configFile.exists()) { - return new String(Files.readAllBytes(configFile.toPath())).trim(); - } - } catch (Exception e) { - System.err.println("Error loading custom Python path: " + e.getMessage()); - } - return null; - } - - private void saveCustomPythonPath(String path) { - try { - File configDir = new File("config"); - if (!configDir.exists()) { - configDir.mkdirs(); - } - - File configFile = new File("config/python_path.txt"); - Files.write(configFile.toPath(), path.getBytes()); - System.out.println("Saved custom Python path: " + path); - } catch (Exception e) { - System.err.println("Error saving custom Python path: " + e.getMessage()); - } - } - - public String createStroke(Effect effect, Map parameters) { - // Generate stroke ID - String strokeId = "stroke_" + System.currentTimeMillis(); - - // ✅ FIXED: Create stroke with EXACT paths from canvas (not connected) - ArrayList canvasPaths = app.getCanvasManager().getPaths(); - - // Create stroke and add to list - Stroke stroke = new Stroke( - strokeId, - effect, - parameters, - canvasPaths // Use exact paths from canvas - ); - - // DEBUG: Show what parameters the stroke received - DebugLogger.log("\n=== STROKE CREATION DEBUG ==="); - DebugLogger.log("Stroke ID: " + strokeId); - DebugLogger.log("Effect: " + effect.getName()); - DebugLogger.log("Number of separate paths: " + canvasPaths.size()); - for (int i = 0; i < canvasPaths.size(); i++) { - Path path = canvasPaths.get(i); - DebugLogger.log(" Path " + i + ": " + path.getPoints().size() + " points, click at " + path.getClickPoint()); - } - DebugLogger.log("Parameters received by Stroke:"); - for (Map.Entry entry : parameters.entrySet()) { - Object value = entry.getValue(); - DebugLogger.log(" " + entry.getKey() + " = " + value + " (" + (value != null ? value.getClass().getSimpleName() : "null") + ")"); - } - DebugLogger.log("=== END STROKE CREATION DEBUG ===\n"); - - strokes.add(stroke); - - // Get project ID - String projectId = app.getProjectId(); - if (projectId == null) { - System.err.println("Error: No project ID available"); - return null; - } - - // Create project directory structure - File projectDir = new File("project/" + projectId); - if (!projectDir.exists()) { - projectDir.mkdirs(); - - // Save original image - if (app.getCurrentImage() != null) { - String originalPath = projectDir.getPath() + "/original.png"; - app.getCurrentImage().save(originalPath); - } - } - - // Create stroke directory - File strokeDir = new File(projectDir, "stroke"); - if (!strokeDir.exists()) { - strokeDir.mkdirs(); - } - - // Generate and save JSON instructions - String instructionsPath = strokeDir.getPath() + "/" + strokeId + "_instructions.json"; - JSONObject instructions = stroke.generateJSON(projectId); - app.saveJSONObject(instructions, instructionsPath); - - // DEBUG: Show the final JSON that was saved - DebugLogger.log("\n=== FINAL SAVED JSON DEBUG ==="); - DebugLogger.log("JSON saved to: " + instructionsPath); - DebugLogger.log("Final JSON content (user_input section):"); - if (instructions.hasKey("user_input")) { - JSONObject savedUserInput = instructions.getJSONObject("user_input"); - for (Object key : savedUserInput.keys()) { - String paramName = (String) key; - Object savedValue = savedUserInput.get(paramName); - DebugLogger.log(" " + paramName + " = " + savedValue + " (" + (savedValue != null ? savedValue.getClass().getSimpleName() : "null") + ")"); - } - } else { - DebugLogger.log(" No user_input section found in JSON!"); - } - DebugLogger.log("=== END FINAL SAVED JSON DEBUG ===\n"); - - // Save input image - if (app.getCurrentImage() != null) { - String inputPath = strokeDir.getPath() + "/" + strokeId + "_input.png"; - app.getCurrentImage().save(inputPath); - - // Update JSON with image paths - instructions = app.loadJSONObject(instructionsPath); - JSONObject strokeInput = instructions.getJSONObject("stroke_input"); - strokeInput.setString("input_location", inputPath); - strokeInput.setString( - "output_location", - strokeDir.getPath() + "/" + strokeId + "_output.png" - ); - instructions.setJSONObject("stroke_input", strokeInput); - app.saveJSONObject(instructions, instructionsPath); - } - - // Set current stroke index - currentStrokeIndex = strokes.size() - 1; - - return strokeId; - } - - // ✅ FIXED: Completely rewrite path loading to preserve separate strokes - public void loadExistingStrokes() { - if (app.getProjectId() == null) { - return; - } - - String projectId = app.getProjectId(); - File strokeDir = new File("project/" + projectId + "/stroke"); - - if (!strokeDir.exists() || !strokeDir.isDirectory()) { - return; - } - - // Get all instruction JSON files - File[] instructionFiles = strokeDir.listFiles((dir, name) -> - name.endsWith("_instructions.json")); - - if (instructionFiles == null || instructionFiles.length == 0) { - return; - } - - // Clear existing strokes if we're reloading - if (!strokes.isEmpty()) { - strokes.clear(); - } - - // Process each instruction file - for (File file : instructionFiles) { - try { - JSONObject instructions = app.loadJSONObject(file.getAbsolutePath()); - String strokeId = instructions.getString("stroke_id", ""); - String effectId = instructions.getString("effect_id", ""); - - if (strokeId.isEmpty() || effectId.isEmpty()) { - continue; - } - - // Get the effect - Effect effect = app.getEffectManager().getEffect(effectId); - if (effect == null) { - System.err.println("Effect not found: " + effectId); - continue; - } - - // ✅ FIXED: Extract parameters with ORIGINAL types from requirements, not JSON types - JSONObject userInput = instructions.getJSONObject("user_input"); - Map parameters = new HashMap<>(); - - System.out.println("\n=== LOADING STROKE PARAMETERS DEBUG ==="); - System.out.println("Loading stroke: " + strokeId); - System.out.println("Effect: " + effect.getName()); - - for (Object key : userInput.keys()) { - String paramName = (String) key; - Object jsonValue = userInput.get(paramName); - - // ✅ CRITICAL FIX: Convert JSON types back to the ORIGINAL requirement types - String originalType = effect.getParamType(paramName); - Object convertedValue = jsonValue; - - if (originalType != null) { - switch (originalType) { - case "int": - // Convert Integer/Double back to int - if (jsonValue instanceof Number) { - convertedValue = ((Number) jsonValue).intValue(); - } - break; - case "float": - // Convert Integer/Double back to float - if (jsonValue instanceof Number) { - convertedValue = ((Number) jsonValue).floatValue(); - } - break; - case "bool": - case "boolean": - // Keep boolean as-is - convertedValue = jsonValue; - break; - default: - // Keep string/color as-is - convertedValue = jsonValue; - break; - } - } - - parameters.put(paramName, convertedValue); - - System.out.println(" Loaded parameter: " + paramName + " = " + convertedValue + - " (JSON type: " + (jsonValue != null ? jsonValue.getClass().getSimpleName() : "null") + - ", converted to: " + (convertedValue != null ? convertedValue.getClass().getSimpleName() : "null") + ")"); - } - System.out.println("=== END LOADING PARAMETERS DEBUG ===\n"); - - // ✅ FIXED: Extract path data correctly - preserve separate paths - // ✅ CRITICAL FIX: Reconstruct SEPARATE paths correctly - JSONObject strokeInput = instructions.getJSONObject("stroke_input"); - JSONArray pathArray = strokeInput.getJSONArray("path"); - JSONArray clicksArray = strokeInput.getJSONArray("clicks"); - - ArrayList paths = new ArrayList<>(); - - System.out.println("\n=== LOADING PATH DATA DEBUG ==="); - System.out.println("Path array size: " + (pathArray != null ? pathArray.size() : 0)); - System.out.println("Clicks array size: " + (clicksArray != null ? clicksArray.size() : 0)); - - // ✅ CRITICAL FIX: We need to reconstruct the ORIGINAL separate paths - // The issue is that the JSON stores all points in one array, but we need to - // figure out where each separate path begins and ends - - if (pathArray != null && pathArray.size() > 0 && clicksArray != null && clicksArray.size() > 0) { - // For now, create separate paths based on click points - // Each click point represents the start of a new path - - int currentPointIndex = 0; - - for (int clickIndex = 0; clickIndex < clicksArray.size(); clickIndex++) { - JSONArray clickPoint = clicksArray.getJSONArray(clickIndex); - if (clickPoint != null && clickPoint.size() >= 2) { - float clickX = clickPoint.getFloat(0); - float clickY = clickPoint.getFloat(1); - - Path newPath = new Path(); - newPath.setClickPoint(clickX, clickY); - - System.out.println("Creating path " + clickIndex + " with click at (" + clickX + ", " + clickY + ")"); - - // Find all points that belong to this path - // We'll take points until we find the next click point or reach the end - int pointsAdded = 0; - while (currentPointIndex < pathArray.size()) { - JSONArray point = pathArray.getJSONArray(currentPointIndex); - if (point != null && point.size() >= 2) { - float x = point.getFloat(0); - float y = point.getFloat(1); - - // Check if this point is close to the current click point (start of path) - // or if it's a continuation of the current path - if (pointsAdded == 0) { - // First point should be close to click point - float distance = (float)Math.sqrt((x - clickX) * (x - clickX) + (y - clickY) * (y - clickY)); - if (distance < 50) { // Within 50 pixels of click - // newPath.addPoint(x, y); - newPath.addPoint(x, y, false); // Add 'false' - // pointsAdded++; - pointsAdded++; - currentPointIndex++; - System.out.println(" Added first point " + pointsAdded + ": (" + x + ", " + y + ")"); - } else { - // This point doesn't belong to this path - break; - } - } else { - // Check if we've reached the next click point - boolean isNextClickPoint = false; - if (clickIndex + 1 < clicksArray.size()) { - JSONArray nextClick = clicksArray.getJSONArray(clickIndex + 1); - if (nextClick != null && nextClick.size() >= 2) { - float nextClickX = nextClick.getFloat(0); - float nextClickY = nextClick.getFloat(1); - float distanceToNextClick = (float)Math.sqrt( - (x - nextClickX) * (x - nextClickX) + (y - nextClickY) * (y - nextClickY) - ); - if (distanceToNextClick < 20) { // Very close to next click point - isNextClickPoint = true; - } - } - } - - if (isNextClickPoint) { - // This point belongs to the next path - break; - } else { - // This point belongs to current path - newPath.addPoint(x, y, false); - pointsAdded++; - currentPointIndex++; - System.out.println(" Added point " + pointsAdded + ": (" + x + ", " + y + ")"); - } - } - } else { - currentPointIndex++; - } - } - - if (newPath.hasPoints()) { - paths.add(newPath); - System.out.println(" Created path with " + newPath.getPoints().size() + " points"); - } - } - } - } else if (pathArray != null && pathArray.size() > 0) { - // Fallback: if no clicks, create one path with all points - Path singlePath = new Path(); - for (int i = 0; i < pathArray.size(); i++) { - JSONArray point = pathArray.getJSONArray(i); - if (point != null && point.size() >= 2) { - float x = point.getFloat(0); - float y = point.getFloat(1); - singlePath.addPoint(x, y, false); - } - } - if (singlePath.hasPoints()) { - paths.add(singlePath); - } - } - - System.out.println("Total paths reconstructed: " + paths.size()); - System.out.println("=== END LOADING PATH DATA DEBUG ===\n"); - - // Create the stroke object and add it to the list - Stroke stroke = new Stroke(strokeId, effect, parameters, paths); - strokes.add(stroke); - - } catch (Exception e) { - System.err.println("Error loading stroke from " + file.getName() + ": " + e.getMessage()); - e.printStackTrace(); - } - } - - // After loading all strokes, sort them by timestamp (most recent last) - if (!strokes.isEmpty()) { - strokes.sort((a, b) -> { - // Extract timestamp from stroke ID (format: stroke_timestamp) - long timeA = Long.parseLong(a.getId().substring(a.getId().indexOf('_') + 1)); - long timeB = Long.parseLong(b.getId().substring(b.getId().indexOf('_') + 1)); - return Long.compare(timeA, timeB); - }); - - // Set current stroke index to the most recent stroke - currentStrokeIndex = strokes.size() - 1; - } - } - - // Modify the showStrokeManager method to load existing strokes - public void showStrokeManager() { - // Load existing strokes first - loadExistingStrokes(); - - if (!strokes.isEmpty()) { - app.getUIManager().createStrokeManagerWindow(this); - } else { - // Instead of showing a message that blocks interaction, show a non-modal dialog - JOptionPane optionPane = new JOptionPane( - "No strokes available. Create a stroke first by drawing a path and clicking 'Create'.", - JOptionPane.INFORMATION_MESSAGE - ); - JDialog dialog = optionPane.createDialog("No Strokes"); - dialog.setModal(false); - dialog.setVisible(true); - - // Auto-close the dialog after 3 seconds - javax.swing.Timer timer = new javax.swing.Timer(3000, e -> dialog.dispose()); - timer.setRepeats(false); - timer.start(); - } - } - - public Stroke getCurrentStroke() { - if (currentStrokeIndex >= 0 && currentStrokeIndex < strokes.size()) { - return strokes.get(currentStrokeIndex); - } - return null; - } - - public void nextStroke() { - if (currentStrokeIndex < strokes.size() - 1) { - currentStrokeIndex++; - } - } - - public void previousStroke() { - if (currentStrokeIndex > 0) { - currentStrokeIndex--; - } - } - - public void runCurrentStroke() { - Stroke stroke = getCurrentStroke(); - if (stroke != null) { - runStroke(stroke); - } else { - JOptionPane.showMessageDialog( - null, - "No stroke selected or available.", - "No Stroke", - JOptionPane.WARNING_MESSAGE - ); - } - } - - /** - * Runs the effect processing for a stroke asynchronously. - * This method will not block the UI thread. - */ -public void runStroke(Stroke stroke) { - try { - final String strokeId = stroke.getId(); - - // Check if this stroke is already being processed - if (processingStrokes.containsKey(strokeId)) { - JOptionPane.showMessageDialog( - null, - "This stroke is already being processed. Please wait for it to complete.", - "Processing in Progress", - JOptionPane.INFORMATION_MESSAGE - ); - return; - } - - Effect effect = stroke.getEffect(); - String effectId = effect.getId(); - String folderName = effect.getFolderName(); - final String projectId = app.getProjectId(); - - // Define instructionsPath once here to use throughout the method - final String instructionsPath = "project/" + projectId + "/stroke/" + strokeId + "_instructions.json"; - File instructionsFile = new File(instructionsPath); - - // Check if the stroke is in a failed state and needs cleanup - if (instructionsFile.exists()) { - JSONObject instructions = app.loadJSONObject(instructionsPath); - String processingStatus = instructions.getString("processing_status", ""); - - // If the stroke is in a "running" state but not in our processing map, - // it means the previous run crashed or was interrupted - if ("running".equals(processingStatus) && !processingStrokes.containsKey(strokeId)) { - // Reset the status to allow reprocessing - instructions.setString("processing_status", "pending"); - app.saveJSONObject(instructions, instructionsPath); - } - } - - // Check if Python script exists - File pythonScript = new File("effect/" + folderName + "/" + effectId + ".py"); - if (!pythonScript.exists()) { - // Try with folder name as fallback - pythonScript = new File("effect/" + folderName + "/" + folderName + ".py"); - if (!pythonScript.exists()) { - JOptionPane.showMessageDialog( - null, - "Python script not found: " + pythonScript.getAbsolutePath(), - "Error", - JOptionPane.ERROR_MESSAGE - ); - return; - } - } - - // Ensure directories exist - File projectDir = new File("project/" + projectId); - File strokeDir = new File(projectDir, "stroke"); - if (!projectDir.exists()) projectDir.mkdirs(); - if (!strokeDir.exists()) strokeDir.mkdirs(); - - // Ensure instructions file exists - if (!instructionsFile.exists()) { - JSONObject instructions = stroke.generateJSON(projectId); - app.saveJSONObject(instructions, instructionsPath); - } - - // Update status fields to indicate processing has started - JSONObject instructions = app.loadJSONObject(instructionsPath); - instructions.setBoolean("created", true); - instructions.setString("effect_received", "null"); - instructions.setString("effect_processed", "null"); - instructions.setString("effect_success", "null"); - instructions.setString("processing_status", "running"); - app.saveJSONObject(instructions, instructionsPath); - - // Create a task to execute the Python script asynchronously - Runnable task = () -> { - boolean success = false; - - try { - // Execute Python script with absolute path - success = executeApplyEffectScript(instructionsFile.getAbsolutePath()); - - // Update status based on result - JSONObject updatedInstructions = app.loadJSONObject(instructionsPath); - if (success) { - updatedInstructions.setString("effect_received", "true"); - updatedInstructions.setString("effect_processed", "true"); - updatedInstructions.setString("effect_success", "true"); - updatedInstructions.setString("processing_status", "completed"); - app.saveJSONObject(updatedInstructions, instructionsPath); - - // Notify on the Event Dispatch Thread - SwingUtilities.invokeLater(() -> { - notifyProcessingComplete(strokeId, true); - }); - } else { - updatedInstructions.setString("effect_received", "true"); - updatedInstructions.setString("effect_processed", "true"); - updatedInstructions.setString("effect_success", "false"); - updatedInstructions.setString("processing_status", "failed"); - app.saveJSONObject(updatedInstructions, instructionsPath); - - // Notify on the Event Dispatch Thread - SwingUtilities.invokeLater(() -> { - notifyProcessingComplete(strokeId, false); - }); - } - } catch (InterruptedException e) { - // This exception is thrown when the thread is interrupted (e.g., when cancelling) - System.err.println("Process execution interrupted"); - - // Update the instructions file to reflect cancellation - try { - JSONObject updatedInstructions = app.loadJSONObject(instructionsPath); - updatedInstructions.setString("effect_success", "false"); - updatedInstructions.setString("processing_status", "canceled"); - updatedInstructions.setString("error_message", "Process was cancelled by user"); - app.saveJSONObject(updatedInstructions, instructionsPath); - } catch (Exception ex) { - System.err.println("Error updating instructions file after cancellation: " + ex.getMessage()); - } - - // Notify on the Event Dispatch Thread - SwingUtilities.invokeLater(() -> { - notifyProcessingComplete(strokeId, false); - }); - - // Preserve interrupt status - Thread.currentThread().interrupt(); - } catch (Exception e) { - System.err.println("Error in background processing: " + e.getMessage()); - e.printStackTrace(); - - try { - JSONObject updatedInstructions = app.loadJSONObject(instructionsPath); - updatedInstructions.setString("effect_success", "false"); - updatedInstructions.setString("processing_status", "failed"); - updatedInstructions.setString("error_message", e.getMessage()); - app.saveJSONObject(updatedInstructions, instructionsPath); - } catch (Exception ex) { - System.err.println("Error updating instructions file: " + ex.getMessage()); - } - - // Notify on the Event Dispatch Thread - SwingUtilities.invokeLater(() -> { - notifyProcessingComplete(strokeId, false); - }); - } - }; - - // Submit the task to the executor service - Future future = executorService.submit(task); - - // Store the future for potential cancellation - processingStrokes.put(strokeId, future); - - // Show simple, non-dramatic processing notification - SwingUtilities.invokeLater(() -> { - // ✅ FIXED: Call the correct renamed method - app.getUIManager().refreshStrokeManagerContent(this); - - // Show a simple toast notification - JWindow notification = new JWindow(); - notification.setSize(200, 50); - notification.setLocationRelativeTo(null); - - JPanel notificationPanel = new JPanel(new BorderLayout()); - notificationPanel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(Color.BLUE, 2), - BorderFactory.createEmptyBorder(8, 12, 8, 12) - )); - notificationPanel.setBackground(new Color(240, 248, 255)); - - JLabel messageLabel = new JLabel("Processing..."); - messageLabel.setHorizontalAlignment(JLabel.CENTER); - messageLabel.setFont(new Font("Arial", Font.PLAIN, 12)); - notificationPanel.add(messageLabel, BorderLayout.CENTER); - - notification.add(notificationPanel); - notification.setVisible(true); - - // Auto-close after 1.5 seconds - javax.swing.Timer timer = new javax.swing.Timer(1500, evt -> notification.dispose()); - timer.setRepeats(false); - timer.start(); - }); - - } catch (Exception e) { - System.err.println("Error starting effect processing: " + e.getMessage()); - e.printStackTrace(); - JOptionPane.showMessageDialog( - null, - "Error starting effect processing: " + e.getMessage(), - "Error", - JOptionPane.ERROR_MESSAGE - ); - } -} - - /** - * Cancels the processing of a stroke if it's currently running. - * * @param strokeId The ID of the stroke to cancel - * @return true if the stroke was canceled, false if it wasn't running or couldn't be canceled - */ - public boolean cancelStrokeProcessing(String strokeId) { - Future future = processingStrokes.get(strokeId); - if (future != null && !future.isDone()) { - // Cancel the future with interruption - boolean canceled = future.cancel(true); - - if (canceled) { - // Remove from processing map immediately to prevent race conditions - processingStrokes.remove(strokeId); - - // Update the status in the instructions file - try { - String projectId = app.getProjectId(); - String instructionsPath = "project/" + projectId + "/stroke/" + strokeId + "_instructions.json"; - JSONObject instructions = app.loadJSONObject(instructionsPath); - instructions.setString("processing_status", "canceled"); - instructions.setString("effect_success", "false"); - instructions.setString("error_message", "Process was cancelled by user"); - app.saveJSONObject(instructions, instructionsPath); - } catch (Exception e) { - System.err.println("Error updating instructions file: " + e.getMessage()); - } - - // Notify the UI - SwingUtilities.invokeLater(() -> { - // ✅ FIXED: Call the correct renamed method - app.getUIManager().refreshStrokeManagerContent(this); - - // Show a non-blocking notification - JOptionPane optionPane = new JOptionPane( - "Effect processing has been cancelled.", - JOptionPane.INFORMATION_MESSAGE - ); - JDialog dialog = optionPane.createDialog("Processing Cancelled"); - dialog.setModal(false); - dialog.setVisible(true); - - // Auto-close after 2 seconds - javax.swing.Timer timer = new javax.swing.Timer(2000, e -> dialog.dispose()); - timer.setRepeats(false); - timer.start(); - }); - } - return canceled; - } - return false; - } - - /** - * Adds a callback to be notified when stroke processing completes. - */ - public void addProcessingCallback(ProcessingCallback callback) { - if (!callbacks.contains(callback)) { - callbacks.add(callback); - } - } - - /** - * Removes a processing callback. - */ - public void removeProcessingCallback(ProcessingCallback callback) { - callbacks.remove(callback); - } - - /** - * ✅ NEW: Enhanced notification with auto-refresh and focus - * Notifies all registered callbacks that processing has completed. - */ - private void notifyProcessingComplete(String strokeId, boolean success) { - // Remove from processing map before notifying callbacks - processingStrokes.remove(strokeId); - - // ✅ NEW: Auto-refresh Stroke Manager and focus on the completed stroke - SwingUtilities.invokeLater(() -> { - app.getUIManager().refreshStrokeManagerAndFocus(strokeId); - }); - - // Only show success notification, not failure (to avoid spam) - if (success) { - // Show a single, non-blocking success notification - SwingUtilities.invokeLater(() -> { - // Create a simple toast notification that doesn't block - JWindow notification = new JWindow(); - notification.setSize(280, 60); - notification.setLocationRelativeTo(null); - - JPanel notificationPanel = new JPanel(new BorderLayout()); - notificationPanel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(new Color(0, 150, 0), 2), - BorderFactory.createEmptyBorder(8, 12, 8, 12) - )); - notificationPanel.setBackground(new Color(240, 255, 240)); - - JLabel messageLabel = new JLabel("Processing completed successfully!"); - messageLabel.setHorizontalAlignment(JLabel.CENTER); - messageLabel.setForeground(new Color(0, 100, 0)); - messageLabel.setFont(new Font("Arial", Font.BOLD, 12)); - notificationPanel.add(messageLabel, BorderLayout.CENTER); - - notification.add(notificationPanel); - notification.setVisible(true); - - // Auto-close after 2.5 seconds - javax.swing.Timer timer = new javax.swing.Timer(2500, evt -> notification.dispose()); - timer.setRepeats(false); - timer.start(); - }); - } - - // Now notify all registered callbacks (but don't show additional dialogs) - for (ProcessingCallback callback : callbacks) { - try { - callback.onProcessingComplete(strokeId, success); - } catch (Exception e) { - System.err.println("Error in processing callback: " + e.getMessage()); - } - } - } - - /** - * Checks if a stroke is currently being processed. - */ - public boolean isStrokeProcessing(String strokeId) { - Future future = processingStrokes.get(strokeId); - return future != null && !future.isDone(); - } - - /** - * Gets the processing status of a stroke from its instructions file. - */ - public String getStrokeProcessingStatus(String strokeId) { - try { - String projectId = app.getProjectId(); - String instructionsPath = "project/" + projectId + "/stroke/" + strokeId + "_instructions.json"; - File instructionsFile = new File(instructionsPath); - - if (instructionsFile.exists()) { - JSONObject instructions = app.loadJSONObject(instructionsFile); - return instructions.getString("processing_status", "unknown"); - } - } catch (Exception e) { - System.err.println("Error getting stroke status: " + e.getMessage()); - } - return "unknown"; - } - - private boolean executeApplyEffectScript(String instructionsFilePath) throws InterruptedException, IOException { - Process process = null; - try { - // Check if we need to re-initialize Python command - if (pythonCommand == null) { - initializePythonCommand(); - } - - // Create log directory if it doesn't exist - File logDir = new File("log"); - if (!logDir.exists()) { - logDir.mkdirs(); - } - - // Create log files for stdout and stderr - File stdoutLog = new File("log/python_stdout.log"); - File stderrLog = new File("log/python_stderr.log"); - - // Display Python version information - ProcessBuilder versionProcessBuilder = new ProcessBuilder(pythonCommand, "--version"); - versionProcessBuilder.redirectErrorStream(true); - - Process versionProcess = versionProcessBuilder.start(); - BufferedReader versionReader = new BufferedReader( - new InputStreamReader(versionProcess.getInputStream()) - ); - String versionLine; - while ((versionLine = versionReader.readLine()) != null) { - System.out.println( - "Using Python: " + versionLine + " (from: " + pythonCommand + ")" - ); - - // Check if version is compatible with match-case syntax (Python 3.10+) - if (versionLine.matches(".*Python 3\\.[0-9](\\..*)?")) { - String minorVersionStr = versionLine.replaceAll( - ".*Python 3\\.([0-9])(\\..*)?", "$1" - ); - try { - int minorVersion = Integer.parseInt(minorVersionStr); - if (minorVersion < 10) { - System.err.println( - "WARNING: Python version is " + versionLine + - " but match-case syntax requires Python 3.10 or higher!" - ); - - // Don't show dialog in background thread - return false; - } - } catch (NumberFormatException e) { - System.err.println("Could not parse Python version: " + versionLine); - } - } - } - versionProcess.waitFor(); - - // Create command to execute Python script - ProcessBuilder processBuilder = new ProcessBuilder( - pythonCommand, "effect/apply_effect.py", instructionsFilePath - ); - processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(stdoutLog)); - processBuilder.redirectError(ProcessBuilder.Redirect.appendTo(stderrLog)); - - // Execute process - process = processBuilder.start(); - - // ✅ FIXED: NO TIMEOUT - wait indefinitely for completion - // This allows quantum operations and high-resolution processing to complete - System.out.println("Starting effect processing..."); - process.waitFor(); // Wait indefinitely - - // Get exit code - int exitCode = process.exitValue(); - - // Check the effect_success flag in the JSON file - JSONObject instructions = app.loadJSONObject(instructionsFilePath); - String projectId = instructions.getString("project_id", ""); - String strokeId = instructions.getString("stroke_id", ""); - String outputPath = "project/" + projectId + "/stroke/" + strokeId + "_output.png"; - File outputFile = new File(outputPath); - String effectSuccess = instructions.getString("effect_success", "false"); - - boolean success = exitCode == 0 && "true".equals(effectSuccess) && outputFile.exists(); - - if (!success) { - System.err.println("Python script execution failed with exit code: " + exitCode); - System.err.println("Effect success flag: " + effectSuccess); - System.err.println("Output file exists: " + outputFile.exists()); - - // Read error log - if (stderrLog.exists()) { - try (BufferedReader reader = new BufferedReader(new FileReader(stderrLog))) { - String line; - System.err.println("Python error log:"); - while ((line = reader.readLine()) != null) { - System.err.println(" " + line); - } - } catch (IOException e) { - System.err.println("Could not read error log: " + e.getMessage()); - } - } - } - - return success; - - } catch (InterruptedException e) { - // Handle cancellation properly - System.err.println("Process execution interrupted"); - - if (process != null && process.isAlive()) { - process.destroyForcibly(); - System.out.println("Forcibly terminated process due to cancellation"); - } - - // Update the instructions file to reflect cancellation - try { - JSONObject instructions = app.loadJSONObject(instructionsFilePath); - instructions.setString("effect_success", "false"); - instructions.setString("processing_status", "canceled"); - instructions.setString("error_message", "Process was cancelled by user"); - app.saveJSONObject(instructions, instructionsFilePath); - } catch (Exception ex) { - System.err.println("Error updating instructions file after cancellation: " + ex.getMessage()); - } - - throw e; - } - } - - /** - * FIXED: Apply effect to canvas by properly layering on top of current image - * This method now correctly preserves previous effects and creates proper undo states - */ - public boolean applyEffectToCanvas(String strokeId) { - String projectId = app.getProjectId(); - if (projectId == null || strokeId == null) { - System.err.println("Error: Project ID or Stroke ID is null"); - return false; - } - - // Check if the effect was successful - String instructionsPath = "project/" + projectId + "/stroke/" + - strokeId + "_instructions.json"; - File instructionsFile = new File(instructionsPath); - - if (!instructionsFile.exists()) { - JOptionPane.showMessageDialog( - null, - "Effect instructions not found. The stroke may be corrupted.", - "Error", - JOptionPane.ERROR_MESSAGE - ); - return false; - } - - JSONObject instructions = app.loadJSONObject(instructionsPath); - String effectSuccess = instructions.getString("effect_success", "false"); - - if (!"true".equals(effectSuccess)) { - JOptionPane.showMessageDialog( - null, - "Effect was not successfully processed. Please run the effect first.", - "Error", - JOptionPane.ERROR_MESSAGE - ); - return false; - } - - // Get the effect output image (just the effect, not layered) - String outputPath = "project/" + projectId + "/stroke/" + strokeId + "_output.png"; - PImage effectImage = app.loadImage(outputPath); - - if (effectImage == null) { - System.err.println("Failed to load effect output image: " + outputPath); - JOptionPane.showMessageDialog( - null, - "Failed to load effect output image.", - "Error", - JOptionPane.ERROR_MESSAGE - ); - return false; - } - - // Get the current image (which may already have previous effects applied) - PImage currentImage = app.getCurrentImage(); - if (currentImage == null) { - System.err.println("No current image available"); - JOptionPane.showMessageDialog( - null, - "No current image available.", - "Error", - JOptionPane.ERROR_MESSAGE - ); - return false; - } - - // IMPORTANT: Create a new image by blending the effect on top of the current image - // This preserves all previous effects and changes - PImage resultImage = app.createImage( - currentImage.width, - currentImage.height, - PConstants.ARGB - ); - - // Copy the current image as the base (this preserves all previous effects) - resultImage.copy( - currentImage, - 0, 0, currentImage.width, currentImage.height, - 0, 0, resultImage.width, resultImage.height - ); - - // Blend the new effect image on top of the existing image - // This adds the new effect while preserving everything underneath - resultImage.blend( - effectImage, - 0, 0, effectImage.width, effectImage.height, - 0, 0, resultImage.width, resultImage.height, - PConstants.BLEND - ); - - // Set the result as the new current image - app.setCurrentImage(resultImage); - - // CRITICAL: Save project state AFTER applying the effect - // This creates a new undo point with the combined result - // The user can now undo to the state before this effect was applied - app.saveProjectStateAfterImageChange(); - - // Update project metadata to reflect the change - app.getFileManager().updateProjectMetadata(projectId); - - // IMPORTANT: Save the current state to disk - if (projectId != null) { - String projectPath = "project/" + projectId; - File projectDir = new File(projectPath); - if (projectDir.exists()) { - resultImage.save(projectPath + "/current.png"); - System.out.println("Saved current state after applying effect"); - } - } - - // Clear drawing paths (but don't save this as a project state) - app.getCanvasManager().clearPaths(); - - System.out.println("Effect applied to canvas successfully! New undo state created."); - - return true; - } - - // ✅ FIXED: Add delete stroke functionality - public boolean deleteStroke(String strokeId) { - if (strokeId == null || strokeId.isEmpty()) { - return false; - } - - try { - String projectId = app.getProjectId(); - if (projectId == null) { - return false; - } - - // Cancel processing if the stroke is currently running - if (isStrokeProcessing(strokeId)) { - cancelStrokeProcessing(strokeId); - } - - // Remove from strokes list - strokes.removeIf(stroke -> stroke.getId().equals(strokeId)); - - // Delete stroke files - String strokeDir = "project/" + projectId + "/stroke/"; - File instructionsFile = new File(strokeDir + strokeId + "_instructions.json"); - File inputFile = new File(strokeDir + strokeId + "_input.png"); - File outputFile = new File(strokeDir + strokeId + "_output.png"); - - boolean success = true; - if (instructionsFile.exists()) { - success &= instructionsFile.delete(); - } - if (inputFile.exists()) { - success &= inputFile.delete(); - } - if (outputFile.exists()) { - success &= outputFile.delete(); - } - - // Update current stroke index if needed - if (currentStrokeIndex >= strokes.size()) { - currentStrokeIndex = strokes.size() - 1; - } - - return success; - - } catch (Exception e) { - System.err.println("Error deleting stroke: " + e.getMessage()); - e.printStackTrace(); - return false; - } - } - - // ✅ FIXED: Add methods to support UIManager - public int getStrokeCount() { - return strokes.size(); - } - - public Stroke getStroke(int index) { - if (index >= 0 && index < strokes.size()) { - return strokes.get(index); - } - return null; - } - - public boolean hasStrokes() { - return !strokes.isEmpty(); - } - - public void clearStrokes() { - strokes.clear(); - currentStrokeIndex = -1; - processingStrokes.clear(); - } - - /** - * Cleans up resources when the application is closing. - * This should be called when the application is shutting down. - */ - public void shutdown() { - // Cancel all running tasks - for (Map.Entry> entry : processingStrokes.entrySet()) { - entry.getValue().cancel(true); - } - - // Shutdown the executor service - executorService.shutdownNow(); - try { - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - System.err.println("Executor service did not terminate in the specified time."); - } - } catch (InterruptedException e) { - System.err.println("Executor service shutdown interrupted: " + e.getMessage()); - } - } - - /** - * ✅ NEW: Updates a stroke's parameters and saves the changes to its JSON file. - * This allows parameters to be edited from the Stroke Manager UI. - * - * @param strokeId The ID of the stroke to update. - * @param newParameters A map containing the new parameters. - * @return true if the update was successful, false otherwise. - */ - public boolean updateStroke(String strokeId, Map newParameters) { - Stroke strokeToUpdate = null; - for (Stroke s : strokes) { - if (s.getId().equals(strokeId)) { - strokeToUpdate = s; - break; - } - } - - if (strokeToUpdate != null) { - try { - // Update the parameters in the stroke object - strokeToUpdate.setParameters(newParameters); - - // Regenerate and save the JSON - String projectId = app.getProjectId(); - JSONObject instructions = strokeToUpdate.generateJSON(projectId); - String instructionsPath = "project/" + projectId + "/stroke/" + strokeId + "_instructions.json"; - - // Mark the stroke as pending again, as its parameters have changed. - // This is important so it can be re-run. - instructions.setString("processing_status", "pending"); - instructions.setString("effect_success", "null"); // Reset success flag - - app.saveJSONObject(instructions, instructionsPath); - - System.out.println("Stroke " + strokeId + " updated successfully. Its status has been reset to 'pending'."); - return true; - } catch (Exception e) { - System.err.println("Failed to update stroke " + strokeId + ": " + e.getMessage()); - e.printStackTrace(); - return false; - } - } else { - System.err.println("Could not find stroke to update with ID: " + strokeId); - return false; - } - } - - public static class Stroke { - private String id; - private Effect effect; - private Map parameters; - private ArrayList paths; - - public Stroke( - String id, - Effect effect, - Map parameters, - ArrayList paths - ) { - this.id = id; - this.effect = effect; - this.parameters = parameters; - this.paths = new ArrayList<>(paths); // Make a copy of the paths - } - - public String getId() { - return id; - } - - public Effect getEffect() { - return effect; - } - - public Map getParameters() { - return parameters; - } - - /** - * ✅ NEW: Sets the parameters for the stroke. - * @param parameters The new map of parameters. - */ - public void setParameters(Map parameters) { - this.parameters = new HashMap<>(parameters); // Store a copy to prevent external modification - } - - public ArrayList getPaths() { - return paths; - } - - public JSONObject generateJSON(String projectId) { - DebugLogger.log("\n=== JSON GENERATION DEBUG START ==="); - DebugLogger.log("Generating JSON for stroke: " + id); - DebugLogger.log("Effect: " + effect.getName() + " (ID: " + effect.getId() + ")"); - DebugLogger.log("Project ID: " + projectId); - - DebugLogger.log("\nParameters to convert to JSON:"); - for (Map.Entry entry : parameters.entrySet()) { - Object value = entry.getValue(); - DebugLogger.log(" " + entry.getKey() + " = " + value + " (" + (value != null ? value.getClass().getSimpleName() : "null") + ")"); - } - - JSONObject json = new JSONObject(); - - // Add basic info - json.setString("stroke_id", id); - json.setString("project_id", projectId); - json.setString("effect_id", effect.getId()); - - // ✅ FIXED: Use the AUTOMATIC requirements from the loaded JSON files - // The Effect class already loads these automatically from effect/{name}/{name}_requirements.json - JSONObject userInput = new JSONObject(); - - DebugLogger.log("\nProcessing parameters using AUTOMATIC requirements:"); - - for (Map.Entry entry : parameters.entrySet()) { - String paramName = entry.getKey(); - Object value = entry.getValue(); - - DebugLogger.log("\n--- Processing parameter: " + paramName + " ---"); - DebugLogger.log("Input value: " + value + " (" + (value != null ? value.getClass().getSimpleName() : "null") + ")"); - - // ✅ Use the AUTOMATIC type detection from the loaded requirements - String paramType = effect.getParamType(paramName); - DebugLogger.log("Automatic type from requirements: " + paramType); - - if (paramType != null) { - switch (paramType) { - case "bool": - case "boolean": - boolean boolValue = false; - if (value instanceof Boolean) { - boolValue = (Boolean) value; - } else if (value instanceof String) { - String strValue = value.toString().toLowerCase().trim(); - boolValue = "true".equals(strValue) || "on".equals(strValue); - } else { - boolValue = Boolean.parseBoolean(value.toString()); - } - userInput.setBoolean(paramName, boolValue); - DebugLogger.log("Set as boolean: " + boolValue); - - // ✅ CRITICAL DEBUG: Check what was actually stored - Object storedValue = userInput.get(paramName); - DebugLogger.log("CRITICAL: What was actually stored in JSON: " + storedValue + " (type: " + (storedValue != null ? storedValue.getClass().getSimpleName() : "null") + ")"); - - break; - - case "int": - int intValue = 0; - if (value instanceof Number) { - intValue = ((Number) value).intValue(); - } else { - try { - intValue = Integer.parseInt(value.toString()); - } catch (NumberFormatException e) { - Object defaultValue = effect.getParamDefault(paramName); - intValue = defaultValue instanceof Number ? ((Number)defaultValue).intValue() : 0; - } - } - userInput.setInt(paramName, intValue); - DebugLogger.log("Set as int: " + intValue); - break; - - case "float": - float floatValue = 0.0f; - if (value instanceof Number) { - floatValue = ((Number) value).floatValue(); - } else { - try { - floatValue = Float.parseFloat(value.toString()); - } catch (NumberFormatException e) { - Object defaultValue = effect.getParamDefault(paramName); - floatValue = defaultValue instanceof Number ? ((Number)defaultValue).floatValue() : 0.0f; - } - } - userInput.setFloat(paramName, floatValue); - DebugLogger.log("Set as float: " + floatValue); - break; - - case "color": - case "string": - default: - String stringValue = value.toString(); - userInput.setString(paramName, stringValue); - DebugLogger.log("Set as string: " + stringValue); - break; - } - } else { - // Fallback if type not found in requirements - DebugLogger.log("No type found in requirements, using fallback"); - if (value instanceof Boolean) { - userInput.setBoolean(paramName, (Boolean) value); - } else if (value instanceof Integer) { - userInput.setInt(paramName, (Integer) value); - } else if (value instanceof Float) { - userInput.setFloat(paramName, (Float) value); - } else { - userInput.setString(paramName, value.toString()); - } - } - } - - json.setJSONObject("user_input", userInput); - - DebugLogger.log("\nFinal JSON user_input object:"); - for (Object key : userInput.keys()) { - String paramName = (String) key; - Object jsonValue = userInput.get(paramName); - DebugLogger.log(" " + paramName + " = " + jsonValue + " (" + (jsonValue != null ? jsonValue.getClass().getSimpleName() : "null") + ")"); - } - - // ✅ FIXED: Add stroke input with SEPARATE paths (not connected) - JSONObject strokeInput = new JSONObject(); - strokeInput.setBoolean("real_hardware", false); - - // ✅ CRITICAL FIX: Create path array preserving SEPARATE paths - JSONArray pathArray = new JSONArray(); - JSONArray clicksArray = new JSONArray(); - - DebugLogger.log("\n=== PATH SERIALIZATION DEBUG ==="); - DebugLogger.log("Number of separate paths to serialize: " + paths.size()); - - for (int pathIndex = 0; pathIndex < paths.size(); pathIndex++) { - Path path = paths.get(pathIndex); - DebugLogger.log("Processing path " + pathIndex + " with " + path.getPoints().size() + " points"); - - // Add click point for this path - PVector clickPoint = path.getClickPoint(); - if (clickPoint != null) { - JSONArray clickArray = new JSONArray(); - clickArray.append(Math.round(clickPoint.x)); - clickArray.append(Math.round(clickPoint.y)); - clicksArray.append(clickArray); - DebugLogger.log(" Added click point: (" + clickPoint.x + ", " + clickPoint.y + ")"); - } - - // Add all points from this path - ArrayList points = path.getPoints(); - for (PVector point : points) { - JSONArray pointArray = new JSONArray(); - pointArray.append(Math.round(point.x)); - pointArray.append(Math.round(point.y)); - pathArray.append(pointArray); - } - DebugLogger.log(" Added " + points.size() + " points from path " + pathIndex); - } - - strokeInput.setJSONArray("path", pathArray); - strokeInput.setJSONArray("clicks", clicksArray); - - DebugLogger.log("Total points in path array: " + pathArray.size()); - DebugLogger.log("Total click points: " + clicksArray.size()); - DebugLogger.log("=== END PATH SERIALIZATION DEBUG ==="); - - json.setJSONObject("stroke_input", strokeInput); - - // Add status fields - json.setBoolean("created", true); - json.setString("effect_received", "null"); - json.setString("effect_processed", "null"); - json.setString("effect_success", "null"); - json.setString("processing_status", "pending"); - - DebugLogger.log("\n=== JSON GENERATION DEBUG END ==="); - DebugLogger.log("Complete JSON structure created for stroke: " + id); - - DebugLogger.log("\n=== FINAL JSON STRING DEBUG ==="); - DebugLogger.log("Complete JSON as string:"); - DebugLogger.log(json.toString()); - DebugLogger.log("=== END FINAL JSON STRING DEBUG ===\n"); - - return json; - } - } -} diff --git a/src/UIManager.java b/src/UIManager.java deleted file mode 100644 index 69a486b..0000000 --- a/src/UIManager.java +++ /dev/null @@ -1,679 +0,0 @@ -import processing.core.*; -import processing.data.*; -import javax.swing.*; -import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; -import java.util.*; - -public class UIManager { - private QuantumBrush app; - private JFrame mainControlFrame; - private JPanel effectParameterContainer; - private JButton createButton; - private Map currentEffectParameters; - private Effect currentEffect; - - // ✅ NEW: Track single Stroke Manager instance - private JFrame strokeManagerFrame = null; - private DefaultListModel strokeListModel = null; - private JList strokeList = null; - private JPanel strokeDetailsContent = null; - private JScrollPane strokeDetailsScrollPane = null; - - public UIManager(QuantumBrush app) { - this.app = app; - this.currentEffectParameters = new HashMap<>(); - } - - public void setMainControlFrame(JFrame frame) { - this.mainControlFrame = frame; - } - - public void setEffectParameterContainer(JPanel container) { - this.effectParameterContainer = container; - } - - public void createEffectWindow(Effect effect) { - this.currentEffect = effect; - - // Clear parameters completely when switching effects - currentEffectParameters.clear(); - - effectParameterContainer.removeAll(); - - JPanel contentPanel = new JPanel(); - contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); - - // Add effect name and description - JLabel nameLabel = new JLabel(effect.getName()); - nameLabel.setFont(new Font("Arial", Font.BOLD, 16)); - nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - contentPanel.add(nameLabel); - - String description = getEffectDescription(effect); - if (description != null && !description.trim().isEmpty()) { - JTextArea descriptionArea = new JTextArea(description); - descriptionArea.setFont(new Font("Arial", Font.PLAIN, 12)); - descriptionArea.setLineWrap(true); - descriptionArea.setWrapStyleWord(true); - descriptionArea.setEditable(false); - descriptionArea.setOpaque(false); - descriptionArea.setAlignmentX(Component.LEFT_ALIGNMENT); - descriptionArea.setBorder(BorderFactory.createEmptyBorder(5, 0, 10, 0)); - contentPanel.add(descriptionArea); - } - - contentPanel.add(Box.createVerticalStrut(10)); - - // Get ONLY the parameters that exist in THIS effect's JSON - JSONObject userInputReqs = effect.getUserInputRequirements(); - - // Create UI components ONLY for parameters that exist in the JSON - for (Object key : userInputReqs.keys()) { - String paramName = (String) key; - JSONObject paramSpec = effect.getParamSpec(paramName); - - if (paramSpec != null) { - // ✅ REFACTORED: Use the generic panel creation method - JPanel paramPanel = createGenericParameterPanel(paramName, paramSpec, effect, currentEffectParameters, this::updateCreateButtonState); - if (paramPanel != null) { - contentPanel.add(paramPanel); - contentPanel.add(Box.createVerticalStrut(8)); - } - } - } - - // Create button - createButton = new JButton("Create"); - createButton.setEnabled(false); - createButton.setAlignmentX(Component.LEFT_ALIGNMENT); - createButton.addActionListener(e -> { - String strokeId = app.getStrokeManager().createStroke(effect, currentEffectParameters); - if (strokeId != null && !strokeId.startsWith("pending:")) { - JOptionPane.showMessageDialog( - mainControlFrame, - "Stroke created successfully! Use Tools > Stroke Manager to run it.", - "Stroke Created", - JOptionPane.INFORMATION_MESSAGE - ); - - // NEW: Auto-refresh Stroke Manager if it's open - refreshStrokeManagerIfOpen(); - } - }); - - contentPanel.add(Box.createVerticalStrut(10)); - contentPanel.add(createButton); - - // Add content to the parameter container - effectParameterContainer.add(contentPanel, BorderLayout.CENTER); - - // Update button state - updateCreateButtonState(); - - // Refresh the UI - effectParameterContainer.revalidate(); - effectParameterContainer.repaint(); - - System.out.println("Effect window created for: " + effect.getName()); - } - - private String getEffectDescription(Effect effect) { - JSONObject requirements = effect.getRequirements(); - if (requirements != null && requirements.hasKey("description")) { - return requirements.getString("description"); - } - return null; - } - - /** - * REFACTORED: Creates a generic UI panel for a single parameter. - * This method is now used by both the main Control Panel and the Stroke Manager. - * - * @param paramName The name of the parameter. - * @param paramSpec The JSON specification for the parameter. - * @param effect The effect the parameter belongs to. - * @param parametersMap The map where the parameter's value is stored and updated. - * @param onUpdateCallback A runnable to execute when the parameter's value changes (can be null). - * @return A JPanel containing the UI controls for the parameter. - */ - private JPanel createGenericParameterPanel(String paramName, JSONObject paramSpec, Effect effect, Map parametersMap, Runnable onUpdateCallback) { - String type = paramSpec.getString("type", "string"); - - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); - panel.setAlignmentX(Component.LEFT_ALIGNMENT); - - // Create label - JLabel label = new JLabel(paramName + ":"); - label.setPreferredSize(new Dimension(120, 25)); - panel.add(label); - - // Get initial value: use value from map if it exists, otherwise use effect default - Object initialValue = parametersMap.get(paramName); - if (initialValue == null) { - initialValue = effect.getParamDefault(paramName); - parametersMap.put(paramName, initialValue); // Ensure map is populated with default - } - - try { - switch (type) { - case "bool": - case "boolean": - JCheckBox checkBox = new JCheckBox(); - boolean boolValue = false; - if (initialValue instanceof Boolean) { - boolValue = (Boolean) initialValue; - } else if (initialValue != null) { - boolValue = Boolean.parseBoolean(initialValue.toString()); - } - checkBox.setSelected(boolValue); - checkBox.addActionListener(e -> { - parametersMap.put(paramName, checkBox.isSelected()); - if (onUpdateCallback != null) onUpdateCallback.run(); - }); - panel.add(checkBox); - break; - - case "int": - float minInt = effect.getParamMin(paramName, 0); - float maxInt = effect.getParamMax(paramName, 100); - int intValue = 0; - if (initialValue instanceof Number) { - intValue = ((Number) initialValue).intValue(); - } else if (initialValue != null) { - try { - intValue = Integer.parseInt(initialValue.toString()); - } catch (NumberFormatException ex) { - intValue = (int) minInt; - } - } - - JSlider intSlider = new JSlider((int) minInt, (int) maxInt, intValue); - intSlider.setPreferredSize(new Dimension(200, 25)); - JLabel intValueLabel = new JLabel(String.valueOf(intValue)); - intValueLabel.setPreferredSize(new Dimension(50, 25)); - - intSlider.addChangeListener(e -> { - int value = intSlider.getValue(); - intValueLabel.setText(String.valueOf(value)); - parametersMap.put(paramName, value); - if (onUpdateCallback != null) onUpdateCallback.run(); - }); - - panel.add(intSlider); - panel.add(intValueLabel); - break; - - case "float": - float minFloat = effect.getParamMin(paramName, 0.0f); - float maxFloat = effect.getParamMax(paramName, 1.0f); - float floatValue = 0.0f; - if (initialValue instanceof Number) { - floatValue = ((Number) initialValue).floatValue(); - } else if (initialValue != null) { - try { - floatValue = Float.parseFloat(initialValue.toString()); - } catch (NumberFormatException ex) { - floatValue = minFloat; - } - } - - int sliderMax = 1000; - int sliderValue = (int) ((floatValue - minFloat) / (maxFloat - minFloat) * sliderMax); - - JSlider floatSlider = new JSlider(0, sliderMax, sliderValue); - floatSlider.setPreferredSize(new Dimension(200, 25)); - JLabel floatValueLabel = new JLabel(String.format("%.3f", floatValue)); - floatValueLabel.setPreferredSize(new Dimension(60, 25)); - - floatSlider.addChangeListener(e -> { - float value = minFloat + (floatSlider.getValue() / (float) sliderMax) * (maxFloat - minFloat); - floatValueLabel.setText(String.format("%.3f", value)); - parametersMap.put(paramName, value); - if (onUpdateCallback != null) onUpdateCallback.run(); - }); - - panel.add(floatSlider); - panel.add(floatValueLabel); - break; - - case "color": - String colorValue = initialValue != null ? initialValue.toString() : "#FF0000"; - JTextField colorField = new JTextField(colorValue, 8); - JButton colorButton = new JButton("Choose"); - - JPanel colorPreview = new JPanel(); - colorPreview.setPreferredSize(new Dimension(25, 20)); - colorPreview.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1)); - try { - colorPreview.setBackground(Color.decode(colorValue)); - } catch (Exception ex) { - colorPreview.setBackground(Color.WHITE); - } - - ActionListener updateColor = e -> { - String hexColor = colorField.getText(); - try { - colorPreview.setBackground(Color.decode(hexColor)); - parametersMap.put(paramName, hexColor); - if (onUpdateCallback != null) onUpdateCallback.run(); - } catch (Exception ex) { - // handle invalid color string if necessary - } - }; - colorField.addActionListener(updateColor); - - colorButton.addActionListener(e -> { - Color currentColor = Color.RED; - try { - currentColor = Color.decode(colorField.getText()); - } catch (NumberFormatException ex) { /* use default */ } - - Color newColor = JColorChooser.showDialog(panel, "Choose Color", currentColor); - if (newColor != null) { - String hexColor = String.format("#%02X%02X%02X", newColor.getRed(), newColor.getGreen(), newColor.getBlue()); - colorField.setText(hexColor); - updateColor.actionPerformed(null); // Trigger update - } - }); - - panel.add(colorField); - panel.add(colorPreview); - panel.add(colorButton); - break; - - case "string": - default: - String stringValue = initialValue != null ? initialValue.toString() : ""; - JTextField textField = new JTextField(stringValue, 15); - textField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { - public void insertUpdate(javax.swing.event.DocumentEvent e) { update(); } - public void removeUpdate(javax.swing.event.DocumentEvent e) { update(); } - public void changedUpdate(javax.swing.event.DocumentEvent e) { update(); } - private void update() { - parametersMap.put(paramName, textField.getText()); - if (onUpdateCallback != null) onUpdateCallback.run(); - } - }); - panel.add(textField); - break; - } - return panel; - } catch (Exception e) { - System.err.println("Error creating parameter panel for " + paramName + ": " + e.getMessage()); - e.printStackTrace(); - return null; - } - } - - private void updateCreateButtonState() { - if (createButton != null) { - boolean hasPath = app.getCanvasManager().hasPath(); - createButton.setEnabled(hasPath); - } - } - - public void enableCreateButton() { - if (createButton != null) { - updateCreateButtonState(); - } - } - - public void createStrokeManagerWindow(StrokeManager strokeManager) { - if (strokeManagerFrame != null && strokeManagerFrame.isDisplayable()) { - strokeManagerFrame.toFront(); - strokeManagerFrame.requestFocus(); - refreshStrokeManagerContent(strokeManager); - return; - } - - strokeManagerFrame = new JFrame("Stroke Manager"); - strokeManagerFrame.setSize(1200, 700); - strokeManagerFrame.setLocationRelativeTo(mainControlFrame); - strokeManagerFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - - strokeManagerFrame.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - strokeManagerFrame = null; - strokeListModel = null; - strokeList = null; - strokeDetailsContent = null; - strokeDetailsScrollPane = null; - } - }); - - JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); - splitPane.setDividerLocation(350); - - JPanel leftPanel = new JPanel(new BorderLayout()); - strokeListModel = new DefaultListModel<>(); - strokeList = new JList<>(strokeListModel); - strokeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - strokeList.setCellRenderer(new StrokeListCellRenderer()); - updateStrokeList(strokeListModel, strokeManager); - JScrollPane strokeScrollPane = new JScrollPane(strokeList); - leftPanel.add(strokeScrollPane, BorderLayout.CENTER); - - strokeDetailsContent = new JPanel(); - strokeDetailsContent.setLayout(new BoxLayout(strokeDetailsContent, BoxLayout.Y_AXIS)); - strokeDetailsContent.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - strokeDetailsScrollPane = new JScrollPane(strokeDetailsContent); - - splitPane.setLeftComponent(leftPanel); - splitPane.setRightComponent(strokeDetailsScrollPane); - - strokeList.addListSelectionListener(e -> { - if (!e.getValueIsAdjusting()) { - StrokeItem selectedItem = strokeList.getSelectedValue(); - if (selectedItem != null) { - updateStrokeDetails(strokeDetailsContent, selectedItem.stroke, strokeManager); - } else { - strokeDetailsContent.removeAll(); - strokeDetailsContent.revalidate(); - strokeDetailsContent.repaint(); - } - } - }); - - strokeManagerFrame.add(splitPane, BorderLayout.CENTER); - strokeManagerFrame.setVisible(true); - - if (strokeListModel.getSize() > 0) { - strokeList.setSelectedIndex(strokeListModel.getSize() - 1); // Select most recent - } - } - - /** - * ✅ FIXED: Changed visibility from private to public. - * This allows the StrokeManager to call this method to refresh the UI. - */ - public void refreshStrokeManagerContent(StrokeManager strokeManager) { - if (strokeListModel != null && strokeList != null) { - StrokeItem selectedItem = strokeList.getSelectedValue(); - String selectedId = selectedItem != null ? selectedItem.stroke.getId() : null; - - updateStrokeList(strokeListModel, strokeManager); - - if (selectedId != null) { - for (int i = 0; i < strokeListModel.getSize(); i++) { - if (strokeListModel.getElementAt(i).stroke.getId().equals(selectedId)) { - strokeList.setSelectedIndex(i); - return; - } - } - } - - if (strokeListModel.getSize() > 0) { - strokeList.setSelectedIndex(strokeListModel.getSize() - 1); - } - } - } - - private void refreshStrokeManagerIfOpen() { - if (strokeManagerFrame != null && strokeManagerFrame.isDisplayable()) { - SwingUtilities.invokeLater(() -> refreshStrokeManagerContent(app.getStrokeManager())); - } - } - - public void refreshStrokeManagerAndFocus(String strokeId) { - if (strokeManagerFrame != null && strokeManagerFrame.isDisplayable()) { - SwingUtilities.invokeLater(() -> { - updateStrokeList(strokeListModel, app.getStrokeManager()); - for (int i = 0; i < strokeListModel.getSize(); i++) { - if (strokeListModel.getElementAt(i).stroke.getId().equals(strokeId)) { - strokeList.setSelectedIndex(i); - strokeList.ensureIndexIsVisible(i); - strokeManagerFrame.toFront(); - strokeManagerFrame.requestFocus(); - break; - } - } - }); - } - } - - private void updateStrokeList(DefaultListModel listModel, StrokeManager strokeManager) { - listModel.clear(); - strokeManager.loadExistingStrokes(); - for (int i = 0; i < strokeManager.getStrokeCount(); i++) { - StrokeManager.Stroke stroke = strokeManager.getStroke(i); - if (stroke != null) { - String status = strokeManager.getStrokeProcessingStatus(stroke.getId()); - listModel.addElement(new StrokeItem(stroke, status)); - } - } - } - - /** - * ✅ UPDATED: Renders the details of a selected stroke, now with editable parameters and image previews. - */ - private void updateStrokeDetails(JPanel detailsPanel, StrokeManager.Stroke stroke, StrokeManager strokeManager) { - detailsPanel.removeAll(); - - // --- Static Info Panel --- - JPanel infoPanel = new JPanel(); - infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS)); - infoPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - - JLabel idLabel = new JLabel("ID: " + stroke.getId()); - idLabel.setFont(new Font("Monospaced", Font.PLAIN, 12)); - infoPanel.add(idLabel); - - JLabel effectLabel = new JLabel("Effect: " + stroke.getEffect().getName()); - effectLabel.setFont(new Font("Arial", Font.BOLD, 14)); - infoPanel.add(effectLabel); - - String status = strokeManager.getStrokeProcessingStatus(stroke.getId()); - JLabel statusLabel = new JLabel("Status: " + status); - statusLabel.setFont(new Font("Arial", Font.PLAIN, 12)); - switch (status) { - case "completed": statusLabel.setForeground(new Color(0, 150, 0)); break; - case "failed": case "canceled": statusLabel.setForeground(Color.RED); break; - case "running": statusLabel.setForeground(new Color(255, 140, 0)); break; - default: statusLabel.setForeground(Color.GRAY); break; - } - infoPanel.add(statusLabel); - detailsPanel.add(infoPanel); - detailsPanel.add(Box.createVerticalStrut(10)); - - // --- Buttons Panel --- - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - JButton runButton = new JButton("Run"); - JButton applyButton = new JButton("Apply to Canvas"); - JButton deleteButton = new JButton("Delete"); - - runButton.addActionListener(e -> strokeManager.runStroke(stroke)); - applyButton.addActionListener(e -> { - if (strokeManager.applyEffectToCanvas(stroke.getId())) { - JOptionPane.showMessageDialog(detailsPanel, "Effect applied!", "Success", JOptionPane.INFORMATION_MESSAGE); - } - }); - deleteButton.addActionListener(e -> { - int confirm = JOptionPane.showConfirmDialog(detailsPanel, "Delete this stroke?", "Confirm", JOptionPane.YES_NO_OPTION); - if (confirm == JOptionPane.YES_OPTION && strokeManager.deleteStroke(stroke.getId())) { - refreshStrokeManagerContent(strokeManager); - } - }); - - buttonPanel.add(runButton); - buttonPanel.add(applyButton); - buttonPanel.add(deleteButton); - - if ("running".equals(status)) { - JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> strokeManager.cancelStrokeProcessing(stroke.getId())); - buttonPanel.add(cancelButton); - } - detailsPanel.add(buttonPanel); - detailsPanel.add(Box.createVerticalStrut(10)); - - // --- Image Previews Panel --- - JPanel imagesPanel = new JPanel(new GridLayout(1, 2, 10, 10)); - imagesPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - String projectId = app.getProjectId(); - if (projectId != null) { - // Input Image - String inputPath = "project/" + projectId + "/stroke/" + stroke.getId() + "_input.png"; - addImageWithOverlay(imagesPanel, inputPath, "Input", stroke); - - // Output Image - String outputPath = "project/" + projectId + "/stroke/" + stroke.getId() + "_output.png"; - addImageWithOverlay(imagesPanel, outputPath, "Output", null); - } - detailsPanel.add(imagesPanel); - detailsPanel.add(Box.createVerticalStrut(10)); - - // --- Editable Parameters Panel --- - JPanel paramsContainer = new JPanel(); - paramsContainer.setLayout(new BoxLayout(paramsContainer, BoxLayout.Y_AXIS)); - paramsContainer.setBorder(BorderFactory.createTitledBorder("Editable Parameters")); - paramsContainer.setAlignmentX(Component.LEFT_ALIGNMENT); - - Map modifiedParameters = new HashMap<>(stroke.getParameters()); - Effect effect = stroke.getEffect(); - JSONObject userInputReqs = effect.getUserInputRequirements(); - - for (Object key : userInputReqs.keys()) { - String paramName = (String) key; - JSONObject paramSpec = effect.getParamSpec(paramName); - if (paramSpec != null) { - JPanel paramPanel = createGenericParameterPanel(paramName, paramSpec, effect, modifiedParameters, null); - if (paramPanel != null) { - paramsContainer.add(paramPanel); - } - } - } - - JButton updateButton = new JButton("Save Parameter Changes"); - updateButton.addActionListener(e -> { - if (strokeManager.updateStroke(stroke.getId(), modifiedParameters)) { - JOptionPane.showMessageDialog(detailsPanel, "Parameters updated and saved.", "Success", JOptionPane.INFORMATION_MESSAGE); - refreshStrokeManagerAndFocus(stroke.getId()); - } else { - JOptionPane.showMessageDialog(detailsPanel, "Failed to update parameters.", "Error", JOptionPane.ERROR_MESSAGE); - } - }); - paramsContainer.add(Box.createVerticalStrut(10)); - paramsContainer.add(updateButton); - detailsPanel.add(paramsContainer); - - detailsPanel.add(Box.createVerticalGlue()); - detailsPanel.revalidate(); - detailsPanel.repaint(); - } - - private void addImageWithOverlay(JPanel container, String imagePath, String title, StrokeManager.Stroke stroke) { - java.io.File imageFile = new java.io.File(imagePath); - if (imageFile.exists()) { - try { - BufferedImage bImg = javax.imageio.ImageIO.read(imageFile); - if (stroke != null) { // Add path overlay only for input image - bImg = createImageWithExactPathOverlay(bImg, stroke); - } - - // Scale image for display - int maxSize = 300; - float scale = Math.min(1, (float)maxSize / Math.max(bImg.getWidth(), bImg.getHeight())); - int displayWidth = (int)(bImg.getWidth() * scale); - int displayHeight = (int)(bImg.getHeight() * scale); - - ImageIcon icon = new ImageIcon(bImg.getScaledInstance(displayWidth, displayHeight, Image.SCALE_SMOOTH)); - JLabel label = new JLabel(icon); - label.setBorder(BorderFactory.createTitledBorder(title)); - container.add(label); - } catch (Exception e) { - addImagePlaceholder(container, "Error loading image", title); - } - } else { - String message = title.equals("Output") ? "Not processed yet" : "Image not found"; - addImagePlaceholder(container, message, title); - } - } - - private BufferedImage createImageWithExactPathOverlay(BufferedImage baseImage, StrokeManager.Stroke stroke) { - BufferedImage overlayImage = new BufferedImage(baseImage.getWidth(), baseImage.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = overlayImage.createGraphics(); - g2d.drawImage(baseImage, 0, 0, null); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - for (Path path : stroke.getPaths()) { - // Draw path line - g2d.setColor(Color.RED); - g2d.setStroke(new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - ArrayList points = path.getPoints(); - for (int i = 0; i < points.size() - 1; i++) { - PVector p1 = points.get(i); - PVector p2 = points.get(i + 1); - g2d.drawLine((int)p1.x, (int)p1.y, (int)p2.x, (int)p2.y); - } - // Draw click point - PVector clickPoint = path.getClickPoint(); - if (clickPoint != null) { - g2d.setColor(Color.YELLOW); - g2d.fillOval((int)clickPoint.x - 5, (int)clickPoint.y - 5, 10, 10); - g2d.setColor(Color.BLACK); - g2d.drawOval((int)clickPoint.x - 5, (int)clickPoint.y - 5, 10, 10); - } - } - g2d.dispose(); - return overlayImage; - } - - private void addImagePlaceholder(JPanel container, String message, String title) { - JLabel placeholder = new JLabel(message, SwingConstants.CENTER); - placeholder.setPreferredSize(new Dimension(300, 300)); - placeholder.setBorder(BorderFactory.createTitledBorder(title)); - placeholder.setOpaque(true); - placeholder.setBackground(Color.LIGHT_GRAY); - container.add(placeholder); - } - - private static class StrokeItem { - public final StrokeManager.Stroke stroke; - public final String status; - - public StrokeItem(StrokeManager.Stroke stroke, String status) { - this.stroke = stroke; - this.status = status; - } - - @Override - public String toString() { - return stroke.getEffect().getName() + " (" + status + ")"; - } - } - - private static class StrokeListCellRenderer extends DefaultListCellRenderer { - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - - JLabel label = (JLabel) super.getListCellRendererComponent( - list, value, index, isSelected, cellHasFocus); - - if (value instanceof StrokeItem) { - StrokeItem item = (StrokeItem) value; - - String statusColor = "gray"; - switch (item.status) { - case "completed": statusColor = "green"; break; - case "failed": case "canceled": statusColor = "red"; break; - case "running": statusColor = "orange"; break; - } - - String timestamp = new java.text.SimpleDateFormat("HH:mm:ss") - .format(new Date(Long.parseLong(item.stroke.getId().substring(7)))); - - label.setText("" + item.stroke.getEffect().getName() + "
" + - "Status: " + item.status + "
" + - "" + timestamp + ""); - - label.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - } - return label; - } - } -} diff --git a/src/components/canvas/CanvasView.tsx b/src/components/canvas/CanvasView.tsx new file mode 100644 index 0000000..075ac27 --- /dev/null +++ b/src/components/canvas/CanvasView.tsx @@ -0,0 +1,34 @@ +import { useRef } from "react"; +import { DrawingCanvas } from "./DrawingCanvas"; +import { ImageLayer } from "./ImageLayer"; +import { PathOverlay } from "./PathOverlay"; +import { useStore } from "../../store"; +import { useZoomPan } from "../../hooks/useZoomPan"; + +export function CanvasView() { + const containerRef = useRef(null); + const { zoom, panX, panY, currentImage } = useStore(); + const { handleWheel, handleSpaceDrag, isPanning } = useZoomPan(containerRef); + + return ( +
+
+ {currentImage && } + + +
+
+ ); +} diff --git a/src/components/canvas/DrawingCanvas.tsx b/src/components/canvas/DrawingCanvas.tsx new file mode 100644 index 0000000..3a6a56a --- /dev/null +++ b/src/components/canvas/DrawingCanvas.tsx @@ -0,0 +1,304 @@ +import { useRef, useCallback, useEffect, useState } from "react"; +import { useStore } from "../../store"; +import { constrainToAxis } from "../../lib/pathUtils"; +import type { CanvasPath } from "../../store/slices/canvasSlice"; + +interface DrawingCanvasProps { + onSpaceDrag: (dx: number, dy: number) => void; +} + +export function DrawingCanvas({ onSpaceDrag }: DrawingCanvasProps) { + const canvasRef = useRef(null); + const [isDrawing, setIsDrawing] = useState(false); + const [spaceHeld, setSpaceHeld] = useState(false); + const lastPanPos = useRef<{ x: number; y: number } | null>(null); + const clickAnchor = useRef<[number, number] | null>(null); + + const { + imageWidth, + imageHeight, + zoom, + activeTool, + strokeColor, + strokeSize, + strokeOpacity, + addPath, + currentPath, + setCurrentPath, + paths, + removePath, + } = useStore(); + + // Handle keyboard events for space bar + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === "Space" && !e.repeat) { + e.preventDefault(); + setSpaceHeld(true); + } + }; + const handleKeyUp = (e: KeyboardEvent) => { + if (e.code === "Space") { + setSpaceHeld(false); + lastPanPos.current = null; + } + }; + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, []); + + const getCanvasCoords = useCallback( + (e: React.MouseEvent): [number, number] => { + const canvas = canvasRef.current; + if (!canvas) return [0, 0]; + const rect = canvas.getBoundingClientRect(); + return [(e.clientX - rect.left) / zoom, (e.clientY - rect.top) / zoom]; + }, + [zoom] + ); + + const makePath = useCallback( + (points: [number, number][], clickPoint: [number, number]): CanvasPath => ({ + points, + clickPoint, + tool: activeTool, + color: strokeColor, + size: strokeSize, + opacity: strokeOpacity, + }), + [activeTool, strokeColor, strokeSize, strokeOpacity] + ); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + if (e.button !== 0) return; + + if (spaceHeld) { + lastPanPos.current = { x: e.clientX, y: e.clientY }; + return; + } + + const [x, y] = getCanvasCoords(e); + + // Eraser: find and remove the nearest path under the click + if (activeTool === "eraser") { + const hitIndex = findPathUnderPoint(paths, x, y, 10); + if (hitIndex >= 0) { + removePath(hitIndex); + } + return; + } + + // Dot: place a single dot immediately + if (activeTool === "dot") { + const dotPath = makePath([[x, y]], [x, y]); + addPath(dotPath); + return; + } + + // Brush or Line: start drawing + clickAnchor.current = [x, y]; + setIsDrawing(true); + setCurrentPath(makePath([[x, y]], [x, y])); + }, + [spaceHeld, getCanvasCoords, activeTool, paths, removePath, makePath, addPath, setCurrentPath] + ); + + const handleMouseMove = useCallback( + (e: React.MouseEvent) => { + if (spaceHeld && lastPanPos.current) { + const dx = e.clientX - lastPanPos.current.x; + const dy = e.clientY - lastPanPos.current.y; + lastPanPos.current = { x: e.clientX, y: e.clientY }; + onSpaceDrag(dx, dy); + return; + } + + if (!isDrawing || !currentPath) return; + + let [x, y] = getCanvasCoords(e); + + // Shift-key constraint + if (e.shiftKey && clickAnchor.current) { + [x, y] = constrainToAxis( + clickAnchor.current[0], + clickAnchor.current[1], + x, + y + ); + } + + if (activeTool === "brush") { + // Freehand: append points + setCurrentPath({ + ...currentPath, + points: [...currentPath.points, [x, y]], + }); + } else if (activeTool === "line") { + // Line: always just start + current end (2 points) + setCurrentPath({ + ...currentPath, + points: [currentPath.clickPoint, [x, y]], + }); + } + }, + [isDrawing, spaceHeld, currentPath, getCanvasCoords, activeTool, onSpaceDrag, setCurrentPath] + ); + + const handleMouseUp = useCallback(() => { + if (spaceHeld) { + lastPanPos.current = null; + return; + } + + if (isDrawing && currentPath) { + // For brush, need at least 2 points; for line, always has 2 + if (currentPath.points.length >= 2) { + addPath(currentPath); + } + } + setIsDrawing(false); + setCurrentPath(null); + clickAnchor.current = null; + }, [isDrawing, spaceHeld, currentPath, addPath, setCurrentPath]); + + // Draw current stroke in progress + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (!currentPath || currentPath.points.length < 1) return; + + const pts = currentPath.points; + const color = currentPath.color; + const size = currentPath.size; + + ctx.globalAlpha = currentPath.opacity; + + if (currentPath.tool === "dot" || pts.length < 2) { + // Single dot + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(pts[0][0], pts[0][1], size / 2, 0, Math.PI * 2); + ctx.fill(); + } else { + // Outer stroke (highlight) + ctx.strokeStyle = adjustBrightness(color, 40); + ctx.lineWidth = size + 3; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (let i = 1; i < pts.length; i++) { + ctx.lineTo(pts[i][0], pts[i][1]); + } + ctx.stroke(); + + // Inner stroke (main color) + ctx.strokeStyle = color; + ctx.lineWidth = size; + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (let i = 1; i < pts.length; i++) { + ctx.lineTo(pts[i][0], pts[i][1]); + } + ctx.stroke(); + } + + ctx.globalAlpha = 1; + + // Click point indicator + const cp = currentPath.clickPoint; + ctx.strokeStyle = "#000000"; + ctx.lineWidth = 2; + ctx.fillStyle = "#FFFF00"; + ctx.beginPath(); + ctx.arc(cp[0], cp[1], 6, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + }, [currentPath]); + + if (imageWidth === 0 || imageHeight === 0) return null; + + return ( + + ); +} + +/** Find the index of the path closest to (x, y) within threshold, or -1 */ +function findPathUnderPoint( + paths: { points: [number, number][]; tool: string }[], + x: number, + y: number, + threshold: number +): number { + for (let i = paths.length - 1; i >= 0; i--) { + const pts = paths[i].points; + if (paths[i].tool === "dot") { + // Check distance to the single point + const dx = pts[0][0] - x; + const dy = pts[0][1] - y; + if (Math.sqrt(dx * dx + dy * dy) < threshold) return i; + } else { + // Check distance to each segment + for (let j = 0; j < pts.length - 1; j++) { + if (distToSegment(x, y, pts[j], pts[j + 1]) < threshold) return i; + } + } + } + return -1; +} + +/** Distance from point (px, py) to line segment (a, b) */ +function distToSegment( + px: number, + py: number, + a: [number, number], + b: [number, number] +): number { + const dx = b[0] - a[0]; + const dy = b[1] - a[1]; + const lenSq = dx * dx + dy * dy; + if (lenSq === 0) { + const ex = px - a[0]; + const ey = py - a[1]; + return Math.sqrt(ex * ex + ey * ey); + } + let t = ((px - a[0]) * dx + (py - a[1]) * dy) / lenSq; + t = Math.max(0, Math.min(1, t)); + const closestX = a[0] + t * dx; + const closestY = a[1] + t * dy; + const ex = px - closestX; + const ey = py - closestY; + return Math.sqrt(ex * ex + ey * ey); +} + +/** Lighten or darken a hex color */ +function adjustBrightness(hex: string, amount: number): string { + const num = parseInt(hex.replace("#", ""), 16); + const r = Math.min(255, Math.max(0, ((num >> 16) & 0xff) + amount)); + const g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) + amount)); + const b = Math.min(255, Math.max(0, (num & 0xff) + amount)); + return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`; +} diff --git a/src/components/canvas/ImageLayer.tsx b/src/components/canvas/ImageLayer.tsx new file mode 100644 index 0000000..b1d6e1e --- /dev/null +++ b/src/components/canvas/ImageLayer.tsx @@ -0,0 +1,29 @@ +import { useRef, useEffect } from "react"; +import { useStore } from "../../store"; + +export function ImageLayer() { + const imgRef = useRef(null); + const { currentImage, setImageDimensions } = useStore(); + + useEffect(() => { + if (imgRef.current && currentImage) { + const img = imgRef.current; + img.onload = () => { + setImageDimensions(img.naturalWidth, img.naturalHeight); + }; + } + }, [currentImage, setImageDimensions]); + + if (!currentImage) return null; + + return ( + Canvas + ); +} diff --git a/src/components/canvas/PathOverlay.tsx b/src/components/canvas/PathOverlay.tsx new file mode 100644 index 0000000..910f922 --- /dev/null +++ b/src/components/canvas/PathOverlay.tsx @@ -0,0 +1,91 @@ +import { useStore } from "../../store"; + +export function PathOverlay() { + const { paths, imageWidth, imageHeight } = useStore(); + + if (paths.length === 0 || imageWidth === 0) return null; + + return ( + + {paths.map((path, i) => { + const color = path.color; + const size = path.size; + const opacity = path.opacity; + + // Dot tool: render a filled circle + if (path.tool === "dot" || path.points.length === 1) { + const [cx, cy] = path.points[0]; + return ( + + + {/* Click point */} + + + ); + } + + if (path.points.length < 2) return null; + + const d = path.points + .map((p, j) => `${j === 0 ? "M" : "L"} ${p[0]} ${p[1]}`) + .join(" "); + + // Brighter outer color + const outerColor = adjustBrightness(color, 40); + + return ( + + {/* Outer highlight line */} + + {/* Inner main color line */} + + {/* Click point */} + + + ); + })} + + ); +} + +function adjustBrightness(hex: string, amount: number): string { + const num = parseInt(hex.replace("#", ""), 16); + if (isNaN(num)) return hex; + const r = Math.min(255, Math.max(0, ((num >> 16) & 0xff) + amount)); + const g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) + amount)); + const b = Math.min(255, Math.max(0, (num & 0xff) + amount)); + return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`; +} diff --git a/src/components/common/ColorPicker.tsx b/src/components/common/ColorPicker.tsx new file mode 100644 index 0000000..0989d97 --- /dev/null +++ b/src/components/common/ColorPicker.tsx @@ -0,0 +1,47 @@ +import { useState } from "react"; +import { HexColorPicker } from "react-colorful"; + +interface ColorPickerProps { + value: string; + onChange: (color: string) => void; +} + +export function ColorPicker({ value, onChange }: ColorPickerProps) { + const [showPicker, setShowPicker] = useState(false); + + return ( +
+
+
+ + {showPicker && ( +
+
setShowPicker(false)} + /> +
+ +
+
+ )} +
+ ); +} diff --git a/src/components/common/Dialog.tsx b/src/components/common/Dialog.tsx new file mode 100644 index 0000000..9ae0474 --- /dev/null +++ b/src/components/common/Dialog.tsx @@ -0,0 +1,46 @@ +import { useEffect, useRef } from "react"; +import { X } from "lucide-react"; + +interface DialogProps { + open: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; +} + +export function Dialog({ open, onClose, title, children }: DialogProps) { + const dialogRef = useRef(null); + + useEffect(() => { + if (open) { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + } + }, [open, onClose]); + + if (!open) return null; + + return ( +
+
+
+
+

{title}

+ +
+
{children}
+
+
+ ); +} diff --git a/src/components/common/FloatingPanel.tsx b/src/components/common/FloatingPanel.tsx new file mode 100644 index 0000000..57ef5b6 --- /dev/null +++ b/src/components/common/FloatingPanel.tsx @@ -0,0 +1,37 @@ +import { X } from "lucide-react"; + +interface FloatingPanelProps { + title: string; + onClose: () => void; + children: React.ReactNode; + width?: number; +} + +export function FloatingPanel({ title, onClose, children, width = 200 }: FloatingPanelProps) { + return ( +
+
+ + {title} + + +
+
{children}
+
+ ); +} diff --git a/src/components/common/Slider.tsx b/src/components/common/Slider.tsx new file mode 100644 index 0000000..826d5d5 --- /dev/null +++ b/src/components/common/Slider.tsx @@ -0,0 +1,38 @@ +interface SliderProps { + min: number; + max: number; + step: number; + value: number; + onChange: (value: number) => void; +} + +export function Slider({ min, max, step, value, onChange }: SliderProps) { + const percentage = ((value - min) / (max - min)) * 100; + + return ( +
+ onChange(parseFloat(e.target.value))} + className="w-full h-1.5 bg-bg-hover rounded-full appearance-none cursor-pointer + [&::-webkit-slider-thumb]:appearance-none + [&::-webkit-slider-thumb]:w-3.5 + [&::-webkit-slider-thumb]:h-3.5 + [&::-webkit-slider-thumb]:rounded-full + [&::-webkit-slider-thumb]:bg-accent + [&::-webkit-slider-thumb]:border-2 + [&::-webkit-slider-thumb]:border-bg-primary + [&::-webkit-slider-thumb]:cursor-pointer + [&::-webkit-slider-thumb]:transition-transform + [&::-webkit-slider-thumb]:hover:scale-110" + style={{ + background: `linear-gradient(to right, var(--color-accent) ${percentage}%, var(--color-bg-hover) ${percentage}%)`, + }} + /> +
+ ); +} diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..283d057 --- /dev/null +++ b/src/components/layout/AppShell.tsx @@ -0,0 +1,73 @@ +import { useState } from "react"; +import { MenuBar } from "./MenuBar"; +import { CanvasView } from "../canvas/CanvasView"; +import { ToolsPanel } from "../panels/ToolsPanel"; +import { PropertiesPanel } from "../panels/PropertiesPanel"; +import { LayersPanel } from "../panels/LayersPanel"; +import { useStore } from "../../store"; + +interface AppShellProps { + serverStatus: "starting" | "running" | "error"; + serverError: string | null; +} + +export function AppShell({ serverStatus, serverError }: AppShellProps) { + const { currentProject } = useStore(); + const [panels, setPanels] = useState({ + tools: true, + properties: true, + layers: true, + }); + + const togglePanel = (panel: "tools" | "properties" | "layers") => { + setPanels((prev) => ({ ...prev, [panel]: !prev[panel] })); + }; + + return ( + <> + + +
+ {/* Canvas fills the entire area */} + {currentProject ? ( + + ) : ( +
+
+

+ Welcome to QuantumBrush +

+

+ Click + New to get started +

+ {serverStatus === "error" && serverError && ( +

+ Python server error: {serverError} +

+ )} +
+
+ )} + + {/* Floating panels positioned over the canvas */} + {panels.tools && ( +
+ togglePanel("tools")} /> +
+ )} + + {panels.properties && ( +
+ togglePanel("properties")} /> +
+ )} + + {panels.layers && ( +
+ togglePanel("layers")} /> +
+ )} +
+ + ); +} diff --git a/src/components/layout/MenuBar.tsx b/src/components/layout/MenuBar.tsx new file mode 100644 index 0000000..0a2a07e --- /dev/null +++ b/src/components/layout/MenuBar.tsx @@ -0,0 +1,175 @@ +import { open, save } from "@tauri-apps/plugin-dialog"; +import { useStore } from "../../store"; +import * as api from "../../lib/tauriApi"; +import { Plus, Download, Upload } from "lucide-react"; + +interface MenuBarProps { + serverStatus: "starting" | "running" | "error"; + panels: { tools: boolean; properties: boolean; layers: boolean }; + togglePanel: (panel: "tools" | "properties" | "layers") => void; +} + +export function MenuBar({ serverStatus, panels, togglePanel }: MenuBarProps) { + const { + currentProject, + setCurrentProject, + setCurrentImage, + setStrokes, + clearPaths, + } = useStore(); + + const handleNewProject = async () => { + const filePath = await open({ + title: "Select Image", + filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "bmp", "gif"] }], + }); + if (!filePath) return; + + const name = (filePath as string).split("/").pop()?.replace(/\.[^.]+$/, "") || "Untitled"; + try { + const project = await api.newProject(name, filePath as string); + setCurrentProject(project); + const image = await api.getCurrentImage(project.project_id); + setCurrentImage(image); + clearPaths(); + setStrokes([]); + } catch (e) { + console.error("Failed to create project:", e); + } + }; + + const handleImport = async () => { + const filePath = await open({ + title: "Import Image", + filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "bmp", "gif"] }], + }); + if (!filePath) return; + + const name = (filePath as string).split("/").pop()?.replace(/\.[^.]+$/, "") || "Untitled"; + try { + const project = await api.newProject(name, filePath as string); + setCurrentProject(project); + const image = await api.getCurrentImage(project.project_id); + setCurrentImage(image); + clearPaths(); + setStrokes([]); + } catch (e) { + console.error("Failed to import:", e); + } + }; + + const handleExport = async () => { + if (!currentProject) return; + const filePath = await save({ + title: "Export Image", + filters: [{ name: "PNG", extensions: ["png"] }], + defaultPath: `${currentProject.project_name}_export.png`, + }); + if (!filePath) return; + try { + await api.exportImage(currentProject.project_id, filePath); + } catch (e) { + console.error("Failed to export:", e); + } + }; + + return ( +
+ {/* Logo */} +
+
+ Q +
+ + QUANTUM + BRUSH + +
+ + {/* Divider */} +
+ + {/* Actions */} +
+ + + +
+ + {/* Spacer */} +
+ + {/* Server status */} +
+
+
+ + {/* Divider */} +
+ + {/* Panel toggles */} +
+ + Windows: + + {(["tools", "properties", "layers"] as const).map((panel) => ( + + ))} +
+
+ ); +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..5071109 --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,12 @@ +import { ControlPanel } from "../panels/ControlPanel"; +import { StrokePanel } from "../panels/StrokePanel"; + +export function Sidebar() { + return ( +
+ +
+ +
+ ); +} diff --git a/src/components/panels/ControlPanel.tsx b/src/components/panels/ControlPanel.tsx new file mode 100644 index 0000000..abdc3ac --- /dev/null +++ b/src/components/panels/ControlPanel.tsx @@ -0,0 +1,83 @@ +import { EffectSelector } from "./EffectSelector"; +import { ParameterForm } from "./ParameterForm"; +import { useStore } from "../../store"; +import * as api from "../../lib/tauriApi"; +import { Paintbrush } from "lucide-react"; + +export function ControlPanel() { + const { + selectedEffect, + paramValues, + paths, + currentProject, + addStroke, + clearPaths, + } = useStore(); + + const canCreate = selectedEffect && paths.length > 0 && currentProject; + + const handleCreate = async () => { + if (!selectedEffect || !currentProject || paths.length === 0) return; + + const pathArrays = paths.map((p) => p.points); + const clicks = paths.map((p) => p.clickPoint); + + try { + const stroke = await api.createStroke( + currentProject.project_id, + selectedEffect.id, + paramValues as Record, + pathArrays, + clicks + ); + + addStroke(stroke); + clearPaths(); + + // Auto-run the stroke + await api.runStroke(stroke.stroke_id, currentProject.project_id); + // Update status to running + useStore.getState().updateStroke(stroke.stroke_id, { + processing_status: "running", + }); + } catch (e) { + console.error("Failed to create/run stroke:", e); + } + }; + + return ( +
+
+ +

+ Effects +

+
+ + + + {selectedEffect && } + + + + {paths.length > 0 && ( + + )} +
+ ); +} diff --git a/src/components/panels/EffectSelector.tsx b/src/components/panels/EffectSelector.tsx new file mode 100644 index 0000000..b9cdc63 --- /dev/null +++ b/src/components/panels/EffectSelector.tsx @@ -0,0 +1,64 @@ +import { useState, useRef, useEffect } from "react"; +import { useStore } from "../../store"; +import { ChevronDown } from "lucide-react"; + +export function EffectSelector() { + const { effects, selectedEffect, setSelectedEffect } = useStore(); + const [isOpen, setIsOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setIsOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ + + {isOpen && ( +
+ {effects.length === 0 ? ( +
No effects loaded
+ ) : ( + effects.map((effect) => ( + + )) + )} +
+ )} +
+ ); +} diff --git a/src/components/panels/LayersPanel.tsx b/src/components/panels/LayersPanel.tsx new file mode 100644 index 0000000..7136d04 --- /dev/null +++ b/src/components/panels/LayersPanel.tsx @@ -0,0 +1,138 @@ +import { FloatingPanel } from "../common/FloatingPanel"; +import { useStore } from "../../store"; +import { Eye, Trash2 } from "lucide-react"; +import type { CanvasPath } from "../../store/slices/canvasSlice"; + +interface LayersPanelProps { + onClose: () => void; +} + +const toolLabel: Record = { + brush: "Brush Stroke", + line: "Line Stroke", + dot: "Dot", + eraser: "Eraser", +}; + +/** Generate a short hex-like ID from the path index + first point */ +function shortId(path: CanvasPath, index: number): string { + const x = path.clickPoint[0] | 0; + const y = path.clickPoint[1] | 0; + const hash = ((index * 2654435761) ^ (x * 73856093) ^ (y * 19349663)) >>> 0; + return hash.toString(16).slice(-5); +} + +export function LayersPanel({ onClose }: LayersPanelProps) { + const { paths, removePath, clearPaths } = useStore(); + + return ( + + {paths.length === 0 ? ( +

+ No strokes yet. +

+ ) : ( +
+ {paths.map((path, i) => { + const label = toolLabel[path.tool] || "Stroke"; + const id = shortId(path, i); + + return ( +
+ {/* Color swatch */} +
+ + {/* Label + ID */} +
+
+ {label} +
+
+ {id} +
+
+ + {/* Actions */} + + +
+ ); + })} +
+ )} + + {paths.length > 0 && ( + <> +
+ + + )} + + ); +} diff --git a/src/components/panels/ParamField.tsx b/src/components/panels/ParamField.tsx new file mode 100644 index 0000000..39b1fbe --- /dev/null +++ b/src/components/panels/ParamField.tsx @@ -0,0 +1,94 @@ +import type { ParamSpec } from "../../types/effect"; +import { Slider } from "../common/Slider"; +import { ColorPicker } from "../common/ColorPicker"; + +interface ParamFieldProps { + name: string; + spec: ParamSpec; + value: unknown; + onChange: (value: unknown) => void; +} + +export function ParamField({ name, spec, value, onChange }: ParamFieldProps) { + switch (spec.type) { + case "int": + return ( +
+ + onChange(Math.round(v))} + /> +
+ ); + + case "float": + return ( +
+ + +
+ ); + + case "bool": + return ( + + ); + + case "color": + return ( +
+ + onChange(c)} + /> +
+ ); + + case "string": + return ( +
+ + onChange(e.target.value)} + className="w-full bg-bg-surface border border-border rounded px-2 py-1 text-sm text-text-primary focus:outline-none focus:border-accent" + /> +
+ ); + + default: + return ( +
+ Unknown type: {spec.type} for {name} +
+ ); + } +} diff --git a/src/components/panels/ParameterForm.tsx b/src/components/panels/ParameterForm.tsx new file mode 100644 index 0000000..a721201 --- /dev/null +++ b/src/components/panels/ParameterForm.tsx @@ -0,0 +1,40 @@ +import { useStore } from "../../store"; +import { ParamField } from "./ParamField"; + +export function ParameterForm() { + const { selectedEffect, paramValues, setParamValue, resetParamDefaults } = + useStore(); + + if (!selectedEffect) return null; + + const entries = Object.entries(selectedEffect.user_input); + + return ( +
+ {selectedEffect.description && ( +

+ {selectedEffect.description} +

+ )} + + {entries.map(([key, spec]) => ( + setParamValue(key, val)} + /> + ))} + + {entries.length > 0 && ( + + )} +
+ ); +} diff --git a/src/components/panels/PropertiesPanel.tsx b/src/components/panels/PropertiesPanel.tsx new file mode 100644 index 0000000..5668f28 --- /dev/null +++ b/src/components/panels/PropertiesPanel.tsx @@ -0,0 +1,292 @@ +import { FloatingPanel } from "../common/FloatingPanel"; +import { useStore } from "../../store"; +import { EffectSelector } from "./EffectSelector"; +import { ParamField } from "./ParamField"; +import { HexColorPicker } from "react-colorful"; +import { useState } from "react"; +import * as api from "../../lib/tauriApi"; + +interface PropertiesPanelProps { + onClose: () => void; +} + +const colorSwatches = [ + "#ffffff", "#000000", "#6b7280", "#ef4444", "#f97316", "#eab308", + "#22c55e", "#3b82f6", "#8b5cf6", "#ec4899", "#06b6d4", "#f43f5e", +]; + +export function PropertiesPanel({ onClose }: PropertiesPanelProps) { + const { + selectedEffect, + paramValues, + setParamValue, + strokeColor, + setStrokeColor, + strokeSize, + setStrokeSize, + strokeOpacity, + setStrokeOpacity, + } = useStore(); + const [showPicker, setShowPicker] = useState(false); + + // Find color param if it exists (to sync with effect) + const colorKey = selectedEffect + ? Object.keys(selectedEffect.user_input).find( + (k) => selectedEffect.user_input[k].type === "color" + ) + : null; + const colorValue = strokeColor; + + return ( + + {/* Effect selector */} +
+ +
+ + {/* Dynamic params (non-color) */} + {selectedEffect && ( +
+ {Object.entries(selectedEffect.user_input) + .filter(([, spec]) => spec.type !== "color") + .map(([key, spec]) => ( + setParamValue(key, val)} + /> + ))} +
+ )} + + {/* Divider */} +
+ + {/* SIZE */} +
+
+ + Size + + + {strokeSize}px + +
+ setStrokeSize(Number(e.target.value))} + style={{ width: "100%", accentColor: "var(--color-accent)" }} + /> +
+ + {/* OPACITY */} +
+
+ + Opacity + + + {Math.round(strokeOpacity * 100)}% + +
+ setStrokeOpacity(Number(e.target.value) / 100)} + style={{ width: "100%", accentColor: "var(--color-accent)" }} + /> +
+ + {/* Divider */} +
+ + {/* Stroke color section */} +
+
+ Stroke Color +
+ + {/* Swatch grid */} +
+ {colorSwatches.map((c) => ( +
+ + {/* Hex input + swatch */} +
+
setShowPicker(!showPicker)} + /> + { + if (/^#[0-9A-Fa-f]{0,6}$/.test(e.target.value)) { + setStrokeColor(e.target.value); + if (colorKey) setParamValue(colorKey, e.target.value); + } + }} + className="flex-1 text-xs font-mono" + style={{ + background: "var(--color-bg-surface)", + border: "1px solid var(--color-border)", + borderRadius: 4, + padding: "4px 8px", + color: "var(--color-text-primary)", + outline: "none", + }} + maxLength={7} + /> +
+ + {/* Color picker popup */} + {showPicker && ( +
+ { + setStrokeColor(c); + if (colorKey) setParamValue(colorKey, c); + }} + style={{ width: "100%" }} + /> +
+ )} +
+ + {/* Create stroke button */} + + + ); +} + +function CreateStrokeButton() { + const { selectedEffect, paramValues, paths, currentProject, addStroke, clearPaths } = + useStore(); + + const canCreate = selectedEffect && paths.length > 0 && currentProject; + + const handleCreate = async () => { + if (!selectedEffect || !currentProject || paths.length === 0) return; + + const pathArrays = paths.map((p) => p.points); + const clicks = paths.map((p) => p.clickPoint); + + try { + const stroke = await api.createStroke( + currentProject.project_id, + selectedEffect.id, + paramValues as Record, + pathArrays, + clicks + ); + addStroke(stroke); + clearPaths(); + await api.runStroke(stroke.stroke_id, currentProject.project_id); + useStore.getState().updateStroke(stroke.stroke_id, { processing_status: "running" }); + } catch (e) { + console.error("Failed to create/run stroke:", e); + } + }; + + return ( +
+ + {paths.length > 0 && ( + + )} +
+ ); +} diff --git a/src/components/panels/StrokeDetail.tsx b/src/components/panels/StrokeDetail.tsx new file mode 100644 index 0000000..25a5cbb --- /dev/null +++ b/src/components/panels/StrokeDetail.tsx @@ -0,0 +1,199 @@ +import { useState } from "react"; +import { useStore } from "../../store"; +import { ParamField } from "./ParamField"; +import * as api from "../../lib/tauriApi"; +import { Play, Check, Trash2, X, Save } from "lucide-react"; + +export function StrokeDetail() { + const { + selectedStroke, + currentProject, + effects, + updateStroke, + removeStroke, + setSelectedStroke, + setCurrentImage, + pushUndo, + clearRedo, + currentImage, + } = useStore(); + + const [editedParams, setEditedParams] = useState | null>(null); + + if (!selectedStroke || !currentProject) return null; + + const effect = effects.find((e) => e.id === selectedStroke.effect_id); + const params = editedParams || (selectedStroke.user_input as Record); + const hasEdits = editedParams !== null; + + const handleRun = async () => { + try { + updateStroke(selectedStroke.stroke_id, { processing_status: "running" }); + await api.runStroke(selectedStroke.stroke_id, currentProject.project_id); + } catch (e) { + console.error("Failed to run stroke:", e); + updateStroke(selectedStroke.stroke_id, { processing_status: "failed" }); + } + }; + + const handleApply = async () => { + try { + if (currentImage) { + pushUndo(currentImage); + clearRedo(); + } + const newImage = await api.applyStroke( + currentProject.project_id, + selectedStroke.stroke_id + ); + setCurrentImage(newImage); + } catch (e) { + console.error("Failed to apply stroke:", e); + } + }; + + const handleDelete = async () => { + try { + await api.deleteStroke(currentProject.project_id, selectedStroke.stroke_id); + removeStroke(selectedStroke.stroke_id); + setSelectedStroke(null); + } catch (e) { + console.error("Failed to delete stroke:", e); + } + }; + + const handleCancel = async () => { + try { + await api.cancelStroke(currentProject.project_id, selectedStroke.stroke_id); + updateStroke(selectedStroke.stroke_id, { processing_status: "canceled" }); + } catch (e) { + console.error("Failed to cancel stroke:", e); + } + }; + + const handleSaveParams = async () => { + if (!editedParams) return; + try { + await api.updateStrokeParams( + currentProject.project_id, + selectedStroke.stroke_id, + editedParams + ); + updateStroke(selectedStroke.stroke_id, { + user_input: editedParams, + processing_status: "pending", + has_output: false, + }); + setEditedParams(null); + } catch (e) { + console.error("Failed to save params:", e); + } + }; + + const status = selectedStroke.processing_status; + + return ( +
+
+

+ Stroke Detail +

+ +
+ + {/* Effect info */} +
+
{effect?.name || selectedStroke.effect_id}
+
+ {selectedStroke.stroke_id.replace("stroke_", "#")} +
+
+ {status} +
+
+ + {/* Parameters */} + {effect && ( +
+

Parameters

+ {Object.entries(effect.user_input).map(([key, spec]) => ( + { + setEditedParams({ ...params, [key]: val }); + }} + /> + ))} + {hasEdits && ( + + )} +
+ )} + + {/* Action buttons */} +
+ {(status === "pending" || status === "failed" || status === "canceled") && ( + + )} + + {status === "running" && ( + + )} + + {status === "completed" && selectedStroke.has_output && ( + + )} + + +
+
+ ); +} diff --git a/src/components/panels/StrokeList.tsx b/src/components/panels/StrokeList.tsx new file mode 100644 index 0000000..9651ab9 --- /dev/null +++ b/src/components/panels/StrokeList.tsx @@ -0,0 +1,59 @@ +import { useStore } from "../../store"; +import { Circle, CheckCircle, XCircle, Loader, Clock } from "lucide-react"; + +const statusIcons: Record = { + completed: { icon: CheckCircle, color: "text-success" }, + failed: { icon: XCircle, color: "text-error" }, + running: { icon: Loader, color: "text-running" }, + pending: { icon: Clock, color: "text-pending" }, + canceled: { icon: XCircle, color: "text-text-muted" }, +}; + +export function StrokeList() { + const { strokes, selectedStroke, setSelectedStroke, effects } = useStore(); + + if (strokes.length === 0) { + return ( +

+ No strokes yet. Draw a path and create a stroke to get started. +

+ ); + } + + return ( +
+ {strokes.map((stroke) => { + const isSelected = selectedStroke?.stroke_id === stroke.stroke_id; + const statusDef = statusIcons[stroke.processing_status] || statusIcons.pending; + const StatusIcon = statusDef.icon; + const effectName = + effects.find((e) => e.id === stroke.effect_id)?.name || stroke.effect_id; + + return ( + + ); + })} +
+ ); +} diff --git a/src/components/panels/StrokePanel.tsx b/src/components/panels/StrokePanel.tsx new file mode 100644 index 0000000..c1cc245 --- /dev/null +++ b/src/components/panels/StrokePanel.tsx @@ -0,0 +1,16 @@ +import { StrokeList } from "./StrokeList"; +import { Layers } from "lucide-react"; + +export function StrokePanel() { + return ( +
+
+ +

+ Strokes +

+
+ +
+ ); +} diff --git a/src/components/panels/ToolsPanel.tsx b/src/components/panels/ToolsPanel.tsx new file mode 100644 index 0000000..450240b --- /dev/null +++ b/src/components/panels/ToolsPanel.tsx @@ -0,0 +1,48 @@ +import { FloatingPanel } from "../common/FloatingPanel"; +import { useStore } from "../../store"; +import type { ToolType } from "../../store/slices/canvasSlice"; +import { Paintbrush, Minus, Circle, Eraser } from "lucide-react"; + +interface ToolsPanelProps { + onClose: () => void; +} + +const tools: { id: ToolType; label: string; icon: typeof Paintbrush }[] = [ + { id: "brush", label: "Brush", icon: Paintbrush }, + { id: "line", label: "Line", icon: Minus }, + { id: "dot", label: "Dot", icon: Circle }, + { id: "eraser", label: "Eraser", icon: Eraser }, +]; + +export function ToolsPanel({ onClose }: ToolsPanelProps) { + const { activeTool, setActiveTool } = useStore(); + + return ( + +
+ {tools.map((tool) => { + const Icon = tool.icon; + const isActive = tool.id === activeTool; + return ( + + ); + })} +
+
+ ); +} diff --git a/src/hooks/useCanvas.ts b/src/hooks/useCanvas.ts new file mode 100644 index 0000000..1a344f4 --- /dev/null +++ b/src/hooks/useCanvas.ts @@ -0,0 +1,49 @@ +import { useCallback } from "react"; +import { useStore } from "../store"; +import * as api from "../lib/tauriApi"; + +/** Hook for canvas operations that involve Tauri backend calls */ +export function useCanvas() { + const { + currentProject, + currentImage, + setCurrentImage, + pushUndo, + clearRedo, + } = useStore(); + + const applyStroke = useCallback( + async (strokeId: string) => { + if (!currentProject) return; + + // Save current image to undo stack before applying + if (currentImage) { + pushUndo(currentImage); + clearRedo(); + } + + try { + const newImage = await api.applyStroke( + currentProject.project_id, + strokeId + ); + setCurrentImage(newImage); + } catch (e) { + console.error("Failed to apply stroke:", e); + } + }, + [currentProject, currentImage, setCurrentImage, pushUndo, clearRedo] + ); + + const refreshImage = useCallback(async () => { + if (!currentProject) return; + try { + const image = await api.getCurrentImage(currentProject.project_id); + setCurrentImage(image); + } catch (e) { + console.error("Failed to refresh image:", e); + } + }, [currentProject, setCurrentImage]); + + return { applyStroke, refreshImage }; +} diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..1c4f377 --- /dev/null +++ b/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,64 @@ +import { useEffect } from "react"; +import { useStore } from "../store"; + +export function useKeyboardShortcuts() { + const { + zoom, + setZoom, + currentImage, + undoStack, + redoStack, + popUndo, + popRedo, + pushUndo, + pushRedo, + setCurrentImage, + } = useStore(); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const ctrl = e.ctrlKey || e.metaKey; + + if (ctrl && e.key === "=") { + e.preventDefault(); + setZoom(zoom * 1.25); + } else if (ctrl && e.key === "-") { + e.preventDefault(); + setZoom(zoom / 1.25); + } else if (ctrl && e.key === "0") { + e.preventDefault(); + setZoom(1); + } else if (ctrl && e.shiftKey && e.key === "Z") { + e.preventDefault(); + // Redo + if (redoStack.length > 0 && currentImage) { + pushUndo(currentImage); + const next = popRedo(); + if (next) setCurrentImage(next); + } + } else if (ctrl && e.key === "z") { + e.preventDefault(); + // Undo + if (undoStack.length > 0 && currentImage) { + pushRedo(currentImage); + const prev = popUndo(); + if (prev) setCurrentImage(prev); + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + zoom, + setZoom, + currentImage, + undoStack, + redoStack, + popUndo, + popRedo, + pushUndo, + pushRedo, + setCurrentImage, + ]); +} diff --git a/src/hooks/useZoomPan.ts b/src/hooks/useZoomPan.ts new file mode 100644 index 0000000..db851d2 --- /dev/null +++ b/src/hooks/useZoomPan.ts @@ -0,0 +1,51 @@ +import { useCallback, useState, useEffect } from "react"; +import { useStore } from "../store"; + +export function useZoomPan(containerRef: React.RefObject) { + const { zoom, setZoom, panX, panY, setPan } = useStore(); + const [isPanning, setIsPanning] = useState(false); + + const handleWheel = useCallback( + (e: React.WheelEvent) => { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + const delta = e.deltaY > 0 ? 0.9 : 1.1; + const newZoom = Math.max(0.1, Math.min(10, zoom * delta)); + + // Zoom toward mouse position + const rect = containerRef.current?.getBoundingClientRect(); + if (rect) { + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + const scale = newZoom / zoom; + const newPanX = mouseX - (mouseX - panX) * scale; + const newPanY = mouseY - (mouseY - panY) * scale; + setPan(newPanX, newPanY); + } + + setZoom(newZoom); + } else { + // Scroll to pan + setPan(panX - e.deltaX, panY - e.deltaY); + } + }, + [zoom, panX, panY, setZoom, setPan, containerRef] + ); + + const handleSpaceDrag = useCallback( + (dx: number, dy: number) => { + setPan(panX + dx, panY + dy); + setIsPanning(true); + }, + [panX, panY, setPan] + ); + + // Reset panning state when mouse is released + useEffect(() => { + const handleMouseUp = () => setIsPanning(false); + window.addEventListener("mouseup", handleMouseUp); + return () => window.removeEventListener("mouseup", handleMouseUp); + }, []); + + return { handleWheel, handleSpaceDrag, isPanning }; +} diff --git a/src/lib/pathUtils.ts b/src/lib/pathUtils.ts new file mode 100644 index 0000000..9f2098a --- /dev/null +++ b/src/lib/pathUtils.ts @@ -0,0 +1,50 @@ +/** Convert screen coordinates to image coordinates */ +export function screenToImage( + screenX: number, + screenY: number, + zoom: number, + panX: number, + panY: number +): [number, number] { + return [(screenX - panX) / zoom, (screenY - panY) / zoom]; +} + +/** Convert image coordinates to screen coordinates */ +export function imageToScreen( + imageX: number, + imageY: number, + zoom: number, + panX: number, + panY: number +): [number, number] { + return [imageX * zoom + panX, imageY * zoom + panY]; +} + +/** Constrain movement to horizontal or vertical based on shift key */ +export function constrainToAxis( + anchorX: number, + anchorY: number, + currentX: number, + currentY: number +): [number, number] { + const dx = Math.abs(currentX - anchorX); + const dy = Math.abs(currentY - anchorY); + + if (dx > dy) { + // Constrain to horizontal + return [currentX, anchorY]; + } else { + // Constrain to vertical + return [anchorX, currentY]; + } +} + +/** Calculate distance between two points */ +export function distance( + x1: number, + y1: number, + x2: number, + y2: number +): number { + return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); +} diff --git a/src/lib/tauriApi.ts b/src/lib/tauriApi.ts new file mode 100644 index 0000000..babd9b9 --- /dev/null +++ b/src/lib/tauriApi.ts @@ -0,0 +1,129 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { ProjectMetadata } from "../types/project"; +import type { Effect } from "../types/effect"; +import type { StrokeInfo, StrokeStatusResponse } from "../types/stroke"; + +// Project commands +export async function newProject( + name: string, + imagePath: string +): Promise { + return invoke("new_project", { name, imagePath }); +} + +export async function openProject( + projectId: string +): Promise { + return invoke("open_project", { projectId }); +} + +export async function listProjects(): Promise { + return invoke("list_projects"); +} + +export async function deleteProject(projectId: string): Promise { + return invoke("delete_project", { projectId }); +} + +export async function exportImage( + projectId: string, + exportPath: string +): Promise { + return invoke("export_image", { projectId, exportPath }); +} + +export async function getCurrentImage(projectId: string): Promise { + return invoke("get_current_image", { projectId }); +} + +export async function setAppDir(dir: string): Promise { + return invoke("set_app_dir", { dir }); +} + +// Effect commands +export async function loadEffects(): Promise { + return invoke("load_effects"); +} + +// Python commands +export async function startPythonServer(): Promise { + return invoke("start_python_server"); +} + +export async function stopPythonServer(): Promise { + return invoke("stop_python_server"); +} + +export async function checkPythonServer(): Promise { + return invoke("check_python_server"); +} + +export async function detectPython(): Promise { + return invoke("detect_python"); +} + +// Stroke commands +export async function createStroke( + projectId: string, + effectId: string, + userInput: Record, + paths: number[][][], + clicks: number[][] +): Promise { + return invoke("create_stroke", { + projectId, + effectId, + userInput, + paths, + clicks, + }); +} + +export async function runStroke( + strokeId: string, + projectId: string +): Promise { + return invoke("run_stroke", { strokeId, projectId }); +} + +export async function applyStroke( + projectId: string, + strokeId: string +): Promise { + return invoke("apply_stroke", { projectId, strokeId }); +} + +export async function deleteStroke( + projectId: string, + strokeId: string +): Promise { + return invoke("delete_stroke", { projectId, strokeId }); +} + +export async function cancelStroke( + projectId: string, + strokeId: string +): Promise { + return invoke("cancel_stroke", { projectId, strokeId }); +} + +export async function getStrokeStatus( + projectId: string, + strokeId: string +): Promise { + return invoke("get_stroke_status", { projectId, strokeId }); +} + +export async function listStrokes( + projectId: string +): Promise { + return invoke("list_strokes", { projectId }); +} + +export async function updateStrokeParams( + projectId: string, + strokeId: string, + userInput: Record +): Promise { + return invoke("update_stroke_params", { projectId, strokeId, userInput }); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..d0d227f --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles/globals.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..9ae8fd6 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,14 @@ +import { create } from "zustand"; +import { createProjectSlice, type ProjectSlice } from "./slices/projectSlice"; +import { createCanvasSlice, type CanvasSlice } from "./slices/canvasSlice"; +import { createEffectSlice, type EffectSlice } from "./slices/effectSlice"; +import { createStrokeSlice, type StrokeSlice } from "./slices/strokeSlice"; + +export type AppStore = ProjectSlice & CanvasSlice & EffectSlice & StrokeSlice; + +export const useStore = create()((...a) => ({ + ...createProjectSlice(...a), + ...createCanvasSlice(...a), + ...createEffectSlice(...a), + ...createStrokeSlice(...a), +})); diff --git a/src/store/slices/canvasSlice.ts b/src/store/slices/canvasSlice.ts new file mode 100644 index 0000000..a495bed --- /dev/null +++ b/src/store/slices/canvasSlice.ts @@ -0,0 +1,104 @@ +import type { StateCreator } from "zustand"; + +export type ToolType = "brush" | "line" | "dot" | "eraser"; + +export interface CanvasPath { + points: [number, number][]; + clickPoint: [number, number]; + tool: ToolType; + color: string; + size: number; + opacity: number; +} + +export interface CanvasSlice { + // Tool state + activeTool: ToolType; + strokeColor: string; + strokeSize: number; + strokeOpacity: number; + // Paths + paths: CanvasPath[]; + currentPath: CanvasPath | null; + zoom: number; + panX: number; + panY: number; + imageWidth: number; + imageHeight: number; + // Undo/redo + undoStack: string[]; + redoStack: string[]; + // Actions + setActiveTool: (tool: ToolType) => void; + setStrokeColor: (color: string) => void; + setStrokeSize: (size: number) => void; + setStrokeOpacity: (opacity: number) => void; + addPath: (path: CanvasPath) => void; + setCurrentPath: (path: CanvasPath | null) => void; + clearPaths: () => void; + removePath: (index: number) => void; + setZoom: (zoom: number) => void; + setPan: (x: number, y: number) => void; + setImageDimensions: (width: number, height: number) => void; + pushUndo: (image: string) => void; + popUndo: () => string | undefined; + pushRedo: (image: string) => void; + popRedo: () => string | undefined; + clearRedo: () => void; +} + +const MAX_UNDO = 20; + +export const createCanvasSlice: StateCreator = (set, get) => ({ + activeTool: "brush", + strokeColor: "#3b82f6", + strokeSize: 4, + strokeOpacity: 1, + paths: [], + currentPath: null, + zoom: 1, + panX: 0, + panY: 0, + imageWidth: 0, + imageHeight: 0, + undoStack: [], + redoStack: [], + + setActiveTool: (tool) => set({ activeTool: tool }), + setStrokeColor: (color) => set({ strokeColor: color }), + setStrokeSize: (size) => set({ strokeSize: Math.max(1, Math.min(200, size)) }), + setStrokeOpacity: (opacity) => set({ strokeOpacity: Math.max(0, Math.min(1, opacity)) }), + addPath: (path) => + set((state) => ({ paths: [...state.paths, path] })), + setCurrentPath: (path) => set({ currentPath: path }), + clearPaths: () => set({ paths: [], currentPath: null }), + removePath: (index) => + set((state) => ({ + paths: state.paths.filter((_, i) => i !== index), + })), + setZoom: (zoom) => set({ zoom: Math.max(0.1, Math.min(10, zoom)) }), + setPan: (x, y) => set({ panX: x, panY: y }), + setImageDimensions: (width, height) => + set({ imageWidth: width, imageHeight: height }), + pushUndo: (image) => + set((state) => ({ + undoStack: [...state.undoStack.slice(-MAX_UNDO + 1), image], + })), + popUndo: () => { + const stack = get().undoStack; + if (stack.length === 0) return undefined; + const last = stack[stack.length - 1]; + set({ undoStack: stack.slice(0, -1) }); + return last; + }, + pushRedo: (image) => + set((state) => ({ redoStack: [...state.redoStack, image] })), + popRedo: () => { + const stack = get().redoStack; + if (stack.length === 0) return undefined; + const last = stack[stack.length - 1]; + set({ redoStack: stack.slice(0, -1) }); + return last; + }, + clearRedo: () => set({ redoStack: [] }), +}); diff --git a/src/store/slices/effectSlice.ts b/src/store/slices/effectSlice.ts new file mode 100644 index 0000000..9cf2f69 --- /dev/null +++ b/src/store/slices/effectSlice.ts @@ -0,0 +1,47 @@ +import type { StateCreator } from "zustand"; +import type { Effect } from "../../types/effect"; + +export interface EffectSlice { + effects: Effect[]; + selectedEffect: Effect | null; + paramValues: Record; + setEffects: (effects: Effect[]) => void; + setSelectedEffect: (effect: Effect | null) => void; + setParamValues: (values: Record) => void; + setParamValue: (key: string, value: unknown) => void; + resetParamDefaults: () => void; +} + +export const createEffectSlice: StateCreator = (set, get) => ({ + effects: [], + selectedEffect: null, + paramValues: {}, + + setEffects: (effects) => set({ effects }), + setSelectedEffect: (effect) => { + if (effect) { + const defaults: Record = {}; + for (const [key, spec] of Object.entries(effect.user_input)) { + defaults[key] = spec.default; + } + set({ selectedEffect: effect, paramValues: defaults }); + } else { + set({ selectedEffect: null, paramValues: {} }); + } + }, + setParamValues: (values) => set({ paramValues: values }), + setParamValue: (key, value) => + set((state) => ({ + paramValues: { ...state.paramValues, [key]: value }, + })), + resetParamDefaults: () => { + const effect = get().selectedEffect; + if (effect) { + const defaults: Record = {}; + for (const [key, spec] of Object.entries(effect.user_input)) { + defaults[key] = spec.default; + } + set({ paramValues: defaults }); + } + }, +}); diff --git a/src/store/slices/projectSlice.ts b/src/store/slices/projectSlice.ts new file mode 100644 index 0000000..2f8f46a --- /dev/null +++ b/src/store/slices/projectSlice.ts @@ -0,0 +1,20 @@ +import type { StateCreator } from "zustand"; +import type { ProjectMetadata } from "../../types/project"; + +export interface ProjectSlice { + currentProject: ProjectMetadata | null; + projects: ProjectMetadata[]; + currentImage: string | null; // base64 data URL + setCurrentProject: (project: ProjectMetadata | null) => void; + setProjects: (projects: ProjectMetadata[]) => void; + setCurrentImage: (image: string | null) => void; +} + +export const createProjectSlice: StateCreator = (set) => ({ + currentProject: null, + projects: [], + currentImage: null, + setCurrentProject: (project) => set({ currentProject: project }), + setProjects: (projects) => set({ projects }), + setCurrentImage: (image) => set({ currentImage: image }), +}); diff --git a/src/store/slices/strokeSlice.ts b/src/store/slices/strokeSlice.ts new file mode 100644 index 0000000..56fc8d0 --- /dev/null +++ b/src/store/slices/strokeSlice.ts @@ -0,0 +1,40 @@ +import type { StateCreator } from "zustand"; +import type { StrokeInfo } from "../../types/stroke"; + +export interface StrokeSlice { + strokes: StrokeInfo[]; + selectedStroke: StrokeInfo | null; + setStrokes: (strokes: StrokeInfo[]) => void; + addStroke: (stroke: StrokeInfo) => void; + updateStroke: (strokeId: string, updates: Partial) => void; + removeStroke: (strokeId: string) => void; + setSelectedStroke: (stroke: StrokeInfo | null) => void; +} + +export const createStrokeSlice: StateCreator = (set) => ({ + strokes: [], + selectedStroke: null, + + setStrokes: (strokes) => set({ strokes }), + addStroke: (stroke) => + set((state) => ({ strokes: [...state.strokes, stroke] })), + updateStroke: (strokeId, updates) => + set((state) => ({ + strokes: state.strokes.map((s) => + s.stroke_id === strokeId ? { ...s, ...updates } : s + ), + selectedStroke: + state.selectedStroke?.stroke_id === strokeId + ? { ...state.selectedStroke, ...updates } + : state.selectedStroke, + })), + removeStroke: (strokeId) => + set((state) => ({ + strokes: state.strokes.filter((s) => s.stroke_id !== strokeId), + selectedStroke: + state.selectedStroke?.stroke_id === strokeId + ? null + : state.selectedStroke, + })), + setSelectedStroke: (stroke) => set({ selectedStroke: stroke }), +}); diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..665111e --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,65 @@ +@import "tailwindcss"; + +@theme { + --color-bg-primary: #1e1e22; + --color-bg-secondary: #2a2a2e; + --color-bg-tertiary: #353539; + --color-bg-surface: #2f2f33; + --color-bg-hover: #3a3a3f; + --color-bg-panel: #28282c; + --color-bg-panel-header: #333337; + --color-text-primary: #d4d4d8; + --color-text-secondary: #a1a1aa; + --color-text-muted: #71717a; + --color-accent: #3b82f6; + --color-accent-hover: #2563eb; + --color-accent-dim: #3b82f633; + --color-border: #3f3f46; + --color-border-light: #52525b; + --color-success: #22c55e; + --color-error: #ef4444; + --color-warning: #f59e0b; + --color-info: #3b82f6; + --color-running: #f97316; + --color-pending: #6b7280; +} + +:root { + font-family: Inter, system-ui, -apple-system, sans-serif; + font-size: 13px; + line-height: 1.5; + color: var(--color-text-primary); + background-color: var(--color-bg-primary); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, #root { + height: 100%; + overflow: hidden; +} + +::-webkit-scrollbar { + width: 5px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--color-border-light); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-text-muted); +} + +.cursor-crosshair { cursor: crosshair; } +.cursor-grab { cursor: grab; } +.cursor-grabbing { cursor: grabbing; } diff --git a/src/types/effect.ts b/src/types/effect.ts new file mode 100644 index 0000000..a14cd1b --- /dev/null +++ b/src/types/effect.ts @@ -0,0 +1,18 @@ +export interface ParamSpec { + type: "int" | "float" | "bool" | "color" | "string"; + min?: number; + max?: number; + default: number | boolean | string; +} + +export interface Effect { + name: string; + id: string; + author: string; + version: string; + description: string; + dependencies: Record; + user_input: Record; + stroke_input: Record; + flags: Record; +} diff --git a/src/types/project.ts b/src/types/project.ts new file mode 100644 index 0000000..76dabad --- /dev/null +++ b/src/types/project.ts @@ -0,0 +1,7 @@ +export interface ProjectMetadata { + project_name: string; + project_id: string; + created_time: number; + modified_time: number; + status: string; +} diff --git a/src/types/stroke.ts b/src/types/stroke.ts new file mode 100644 index 0000000..b2cd611 --- /dev/null +++ b/src/types/stroke.ts @@ -0,0 +1,13 @@ +export interface StrokeInfo { + stroke_id: string; + project_id: string; + effect_id: string; + user_input: Record; + processing_status: "pending" | "running" | "completed" | "failed" | "canceled"; + has_output: boolean; +} + +export interface StrokeStatusResponse { + stroke_id: string; + status: string; +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..ab4554d --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9448108 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +const host = process.env.TAURI_DEV_HOST; + +export default defineConfig(async () => ({ + plugins: [react(), tailwindcss()], + + clearScreen: false, + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + ignored: ["**/src-tauri/**"], + }, + }, +}));